diff --git a/.github/actions/build-helm/action.yaml b/.github/actions/build-helm/action.yaml index b7f0ba3a58..63fdcbf495 100644 --- a/.github/actions/build-helm/action.yaml +++ b/.github/actions/build-helm/action.yaml @@ -21,7 +21,7 @@ runs: using: "composite" steps: - name: Set up Helm - uses: azure/setup-helm@fe7b79cd5ee1e45176fcad797de68ecaf3ca4814 # v4.2.0 + uses: azure/setup-helm@b9e51907a09c216f16ebe8536097933489208112 # v4.3.0 with: token: ${{ inputs.github-token }} - name: Generate helm-package diff --git a/.github/actions/build-image/action.yaml b/.github/actions/build-image/action.yaml deleted file mode 100644 index 76ae31bd49..0000000000 --- a/.github/actions/build-image/action.yaml +++ /dev/null @@ -1,53 +0,0 @@ -name: Build Docker Image -description: Builds the operator docker image -inputs: - platform: - description: The platform for which the image will be built - required: true - labels: - description: The labels for the built image - required: true - image-tag: - description: The tag of the built image - required: true -runs: - using: "composite" - steps: - - name: Set up QEMU - uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1 - - name: Set up Golang - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 - with: - go-version-file: "${{ github.workspace }}/go.mod" - - name: Prepare build parameters - id: prep - shell: bash - run: | - hack/build/ci/prepare-build-variables.sh - - name: Download third party licenses - shell: bash - run: | - hack/build/ci/third-party-licenses.sh - - name: Build target - uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0 - with: - builder: ${{ steps.buildx.outputs.name }} - build-args: | - GO_LINKER_ARGS=${{ steps.prep.outputs.go_linker_args }} - GO_BUILD_TAGS=${{ steps.prep.outputs.go_build_tags }} - context: . - file: ./Dockerfile - platforms: linux/${{ inputs.platform }} - push: false - tags: operator-${{ inputs.platform }}:${{ inputs.image-tag }} - labels: ${{ inputs.labels }} - outputs: type=docker,dest=/tmp/operator-${{ inputs.platform }}.tar - - name: Upload artifact - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 - with: - name: operator-${{ inputs.platform }} - path: /tmp/operator-${{ inputs.platform }}.tar - retention-days: 1 - diff --git a/.github/actions/build-push-image/action.yaml b/.github/actions/build-push-image/action.yaml new file mode 100644 index 0000000000..f5b0f06c11 --- /dev/null +++ b/.github/actions/build-push-image/action.yaml @@ -0,0 +1,71 @@ +name: Build and Push Docker Image +description: Builds and pushes the operator docker image +inputs: + platforms: + description: The platforms for which the image will be built + defaults: linux/amd64,linux/arm64 + required: true + labels: + description: The labels for the built image + required: true + tags: + description: The tags of the built image. Should be the whole image URIs, like docker.io/dynatrace/dynatrace-operator:snapshot,quay.io/dynatrace/dynatrace-operator:snapshot + required: true + annotation: + description: The annotation added to the built image +outputs: + digest: + description: The digest of the built image + value: ${{ steps.build-target.outputs.digest }} +runs: + using: "composite" + steps: + - name: Set up QEMU + uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 + - name: Set up Golang + uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 + with: + go-version-file: "${{ github.workspace }}/go.mod" + - name: Prepare build parameters + id: prep + shell: bash + run: | + hack/build/ci/prepare-build-variables.sh + - name: Download third party licenses + shell: bash + run: | + hack/build/ci/third-party-licenses.sh + - name: Create empty SBOM file + shell: bash + run: | + touch dynatrace-operator-bin-sbom.cdx.json + - name: Docker metadata + uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0 + id: meta + env: + DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index + with: + images: ${{ inputs.image }} + - name: Build target + id: build-target + uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0 + with: + builder: ${{ steps.buildx.outputs.name }} + build-args: | + GO_LINKER_ARGS=${{ steps.prep.outputs.go_linker_args }} + GO_BUILD_TAGS=${{ steps.prep.outputs.go_build_tags }} + context: . + file: ./Dockerfile + provenance: false + platforms: ${{ inputs.platforms }} + push: true + tags: ${{ inputs.tags }} + labels: | + ${{ inputs.labels }} + ${{ steps.meta.outputs.labels }} + annotations: | + ${{ inputs.annotation }} + ${{ steps.meta.outputs.annotations }} + diff --git a/.github/actions/create-manifests/action.yaml b/.github/actions/create-manifests/action.yaml deleted file mode 100644 index 96f4b90c0b..0000000000 --- a/.github/actions/create-manifests/action.yaml +++ /dev/null @@ -1,29 +0,0 @@ -name: Create Manifests -description: Creates the manifests for the images -inputs: - version: - description: The version the manifests are for - required: true - registry: - description: The registry where the manifests are pushed - required: true - repository: - description: The repository in the registry where the manifests are pushed - required: true - combined: - description: Should it create a combined manifests for amd64, arm64 and ppc64le builds - required: true -outputs: - digest: - description: The digest of the created manifest - value: ${{ steps.create-manifest.outputs.digest }} -runs: - using: "composite" - steps: - - name: Create manifest - id: create-manifest - env: - IMAGE: ${{ inputs.registry }}/${{ inputs.repository }} - shell: bash - run: | - hack/build/ci/create-manifest.sh "${IMAGE}" "${{ inputs.version }}" ${{ inputs.combined }} diff --git a/.github/actions/preflight/action.yaml b/.github/actions/preflight/action.yaml index 232a386734..c243dc44ce 100644 --- a/.github/actions/preflight/action.yaml +++ b/.github/actions/preflight/action.yaml @@ -29,12 +29,12 @@ runs: RHCC_APITOKEN: ${{ inputs.pyxis-api-token }} RHCC_PROJECT_ID: ${{ inputs.redhat-project-id }} # renovate depName=redhat-openshift-ecosystem/openshift-preflight - PREFLIGHT_VERSION: 1.10.2 + PREFLIGHT_VERSION: 1.12.1 IMAGE_URI: ${{ inputs.registry }}/${{ inputs.repository }}:${{ inputs.version }} run: | hack/build/ci/preflight.sh "${{ env.PREFLIGHT_VERSION }}" "${{ env.IMAGE_URI}}" "${{ inputs.report-name }}" - name: Upload report - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: preflight-report path: ${{ inputs.report-name }} diff --git a/.github/actions/run-e2e/action.yaml b/.github/actions/run-e2e/action.yaml index 75c2eb3a51..51cac21920 100644 --- a/.github/actions/run-e2e/action.yaml +++ b/.github/actions/run-e2e/action.yaml @@ -17,6 +17,9 @@ inputs: tenant1-apitoken: description: The API token of Tenant 1 required: true + tenant1-dataingesttoken: + description: The data ingest token of Tenant 1 + required: true tenant1-oauth-client-id: description: The OAuth client ID of Tenant 1 required: true @@ -32,6 +35,9 @@ inputs: tenant2-apitoken: description: The API token of Tenant 2 required: true + tenant2-dataingesttoken: + description: The data ingest token of Tenant 2 + required: true github-token: description: The GitHub token required: true @@ -50,11 +56,11 @@ runs: - name: Set up kubectl uses: azure/setup-kubectl@3e0aec4d80787158d308d7b364cb1b702e7feb7f # v4.0.0 - name: Set up go - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 with: go-version-file: "${{ github.workspace }}/target/go.mod" - name: Set up helm - uses: azure/setup-helm@fe7b79cd5ee1e45176fcad797de68ecaf3ca4814 # v4.2.0 + uses: azure/setup-helm@b9e51907a09c216f16ebe8536097933489208112 # v4.3.0 with: token: ${{ inputs.github-token }} - name: Install gotestsum @@ -75,11 +81,13 @@ runs: TARGET_BRANCH: ${{ inputs.target-branch || 'main' }} TENANT1_NAME: ${{ inputs.tenant1-name }} TENANT1_APITOKEN: ${{ inputs.tenant1-apitoken }} + TENANT1_DATAINGESTTOKEN: ${{ inputs.tenant1-dataingesttoken }} TENANT1_OAUTH_CLIENT_ID: ${{ inputs.tenant1-oauth-client-id }} TENANT1_OAUTH_SECRET: ${{ inputs.tenant1-oauth-secret }} TENANT1_OAUTH_URN: ${{ inputs.tenant1-oauth-urn }} TENANT2_NAME: ${{ inputs.tenant2-name }} TENANT2_APITOKEN: ${{ inputs.tenant2-apitoken }} + TENANT2_DATAINGESTTOKEN: ${{ inputs.tenant2-dataingesttoken }} - name: Destroy cluster shell: bash run: ref/.github/scripts/destroy-cluster.sh diff --git a/.github/actions/sign-image/action.yaml b/.github/actions/sign-image/action.yaml index 36c6e88fe9..1440c183be 100644 --- a/.github/actions/sign-image/action.yaml +++ b/.github/actions/sign-image/action.yaml @@ -14,17 +14,17 @@ runs: using: "composite" steps: - name: Install Cosign - uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0 + uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2 - name: Sign image with a key shell: bash run: | - cosign sign -y --key env://COSIGN_PRIVATE_KEY ${IMAGE} + cosign sign -y --key env://COSIGN_PRIVATE_KEY --recursive ${IMAGE} env: IMAGE: ${{ inputs.image }} COSIGN_PRIVATE_KEY: ${{ inputs.signing-key }} COSIGN_PASSWORD: ${{ inputs.signing-password }} - name: Sign the images with GitHub OIDC Token shell: bash - run: cosign sign -y ${IMAGE} + run: cosign sign -y --recursive ${IMAGE} env: IMAGE: ${{ inputs.image }} diff --git a/.github/actions/upload-image/action.yaml b/.github/actions/upload-image/action.yaml deleted file mode 100644 index 56ea77ecc4..0000000000 --- a/.github/actions/upload-image/action.yaml +++ /dev/null @@ -1,42 +0,0 @@ -name: Upload Docker Image -description: Uploads the operator docker image to a registry -inputs: - platform: - description: The platform of the uploaded image - required: true - labels: - description: The labels for the uploaded image - required: true - version: - description: The version the image is for - required: true - registry: - description: The registry where the image is uploaded - required: true - repository: - description: The repository in the registry where the image is uploaded - required: true - skip-platform-suffix: - description: Set if platform suffix should be skipped for image - required: false - default: "" -outputs: - digest: - description: The digest of the pushed image - value: ${{ steps.push-image.outputs.digest }} - -runs: - using: "composite" - steps: - - name: Download artifact - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 - with: - name: operator-${{ inputs.platform }} - path: /tmp - - name: Upload image to Registry - id: push-image - shell: bash - env: - IMAGE: "${{ inputs.registry }}/${{ inputs.repository }}:${{ inputs.version }}" - run: | - hack/build/ci/upload-docker-image.sh "${{ inputs.platform }}" "${{ env.IMAGE }}" "${{ inputs.skip-platform-suffix }}" diff --git a/.github/actions/upload-sbom/action.yaml b/.github/actions/upload-sbom/action.yaml index 27e7a4788e..c0713c6ba3 100644 --- a/.github/actions/upload-sbom/action.yaml +++ b/.github/actions/upload-sbom/action.yaml @@ -17,7 +17,7 @@ runs: using: "composite" steps: - name: Install Cosign - uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0 + uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2 - name: Attach sbom attestation to image shell: bash run: | diff --git a/.github/renovate.json5 b/.github/renovate.json5 index a2a18f2afe..593a7677e9 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -11,9 +11,9 @@ }, baseBranches: [ "$default", - "release-1.1", "release-1.2", "release-1.3", + "release-1.4", ], enabledManagers: [ "regex", @@ -62,9 +62,9 @@ { matchBaseBranches: [ "$default", - "release-1.1", "release-1.2", "release-1.3", + "release-1.4", ], matchUpdateTypes: [ "major", diff --git a/.github/scripts/run-e2e-tests.sh b/.github/scripts/run-e2e-tests.sh index 82360335db..16b8fb360b 100755 --- a/.github/scripts/run-e2e-tests.sh +++ b/.github/scripts/run-e2e-tests.sh @@ -26,6 +26,7 @@ cat << EOF > single-tenant.yaml tenantUid: $TENANT1_NAME apiUrl: https://$TENANT1_NAME.dev.dynatracelabs.com/api apiToken: $TENANT1_APITOKEN +dataIngestToken: $TENANT1_DATAINGESTTOKEN EOF cat << EOF > multi-tenant.yaml @@ -33,9 +34,11 @@ tenants: - tenantUid: $TENANT1_NAME apiUrl: https://$TENANT1_NAME.dev.dynatracelabs.com/api apiToken: $TENANT1_APITOKEN + dataIngestToken: $TENANT1_DATAINGESTTOKEN - tenantUid: $TENANT2_NAME apiUrl: https://$TENANT2_NAME.dev.dynatracelabs.com/api apiToken: $TENANT2_APITOKEN + dataIngestToken: $TENANT2_DATAINGESTTOKEN EOF cat << EOF > edgeconnect-tenant.yaml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d7ac32cd34..ccae198dcf 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -13,8 +13,10 @@ permissions: contents: read env: - DOCKER_REGISTRY: "quay.io" - DOCKER_REPOSITORY: "dynatrace/dynatrace-operator" + DOCKER_REGISTRY: quay.io + DOCKER_REPOSITORY: dynatrace/dynatrace-operator + PLATFORMS: linux/amd64,linux/arm64,linux/ppc64le,linux/s390x + PR_PLATFORMS: linux/amd64,linux/arm64 jobs: helm-test: @@ -24,9 +26,7 @@ jobs: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Helm - uses: azure/setup-helm@fe7b79cd5ee1e45176fcad797de68ecaf3ca4814 # v4.2.0 - with: - token: ${{ secrets.GITHUB_TOKEN }} + uses: azure/setup-helm@b9e51907a09c216f16ebe8536097933489208112 # v4.3.0 - name: Run Unit tests id: helm-unittest run: | @@ -39,9 +39,7 @@ jobs: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Helm - uses: azure/setup-helm@fe7b79cd5ee1e45176fcad797de68ecaf3ca4814 # v4.2.0 - with: - token: ${{ secrets.GITHUB_TOKEN }} + uses: azure/setup-helm@b9e51907a09c216f16ebe8536097933489208112 # v4.3.0 - name: Run Linting id: helm-linting run: | @@ -54,14 +52,10 @@ jobs: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Golang - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 with: go-version-file: "${{ github.workspace }}/go.mod" - - name: Download dependencies - id: depdownload - run: | - hack/build/ci/install-cgo-dependencies.sh - - name: Run unit tests and integration tests + - name: Run Unit tests and Integration tests id: unittest run: | make go/test @@ -70,15 +64,6 @@ jobs: id: check-code-coverage run: | make go/check-coverage - - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@015f24e6818733317a2da2edd6290ab26238649a # v5.0.7 - with: - fail_ci_if_error: true - verbose: true - files: ./coverage.txt - flags: unittests # optional - name: codecov-umbrella # optional - token: ${{ secrets.CODECOV_TOKEN }} linting: name: Run linting @@ -86,19 +71,15 @@ jobs: steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 with: go-version-file: "${{ github.workspace }}/go.mod" - - name: Download dependencies - id: depdownload - run: | - hack/build/ci/install-cgo-dependencies.sh - name: Run golangci-lint - uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6.1.1 + uses: golangci/golangci-lint-action@1481404843c368bc19ca9406f87d6e0fc97bdcfd # v7.0.0 with: # renovate depName=github.com/golangci/golangci-lint - version: v1.62.2 - args: --build-tags e2e --timeout 300s --out-${NO_FUTURE}format colored-line-number + version: v2.0.2 + args: --build-tags e2e --timeout 300s - name: Run deadcode id: deadcode run: | @@ -110,14 +91,28 @@ jobs: steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 with: go-version-file: "${{ github.workspace }}/go.mod" - - name: Check deepcopy files + - name: Set up Helm + uses: azure/setup-helm@b9e51907a09c216f16ebe8536097933489208112 # v4.3.0 + - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 + - name: Check deepcopy files are up-to-date id: deepcopy run: | make manifests/deepcopy git diff --exit-code + - name: Check automatic generated docs are up-to-date + id: doc + run: | + make doc + git diff --exit-code + - name: Check mocks are up-to-date + id: mockery + run: | + make prerequisites/mockery + make go/gen_mocks + git diff --exit-code security: name: Code security scanning alerts @@ -143,26 +138,7 @@ jobs: with: config: .markdownlint.json # renovate depName=github.com/igorshubovych/markdownlint-cli - version: v0.43.0 - - check-uncommitted-doc-changes: - name: Check uncommitted changes in api docs action - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 - with: - go-version-file: "${{ github.workspace }}/go.mod" - - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 - - name: Run make doc to see uncommitted changes - run: make doc - - name: Check for uncommitted changes - id: check-changes - uses: mskri/check-uncommitted-changes-action@2b152539dd033c3a26e0dd1d8b9a0c8e4d3a8a19 # v1.0.1 - - name: Evaluate if there are changes - if: steps.check-changes.outputs.outcome == failure() - run: echo "There are uncommitted changes" + version: v0.44.0 prepare: name: Prepare properties @@ -174,95 +150,27 @@ jobs: id: prep run: | hack/build/ci/prepare-build-variables.sh - - name: Docker metadata - uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5.6.1 - id: meta - with: - images: dynatrace/dynatrace-operator - tags: ${{ steps.prep.outputs.docker_image_tag }} - labels: | - ${{ steps.prep.outputs.docker_image_labels }} - vcs-ref=${{ github.sha }} - - name: Prepare build parameters - id: prepenv - run: | - # Set output parameters. - # Reason: global envs do not work in workflow calls - # More info: https://github.com/actions/runner/issues/480#issuecomment-1021278915 - - echo "registry=${{ env.DOCKER_REGISTRY }}" >> "$GITHUB_OUTPUT" - echo "repository=${{ env.DOCKER_REPOSITORY }}" >> "$GITHUB_OUTPUT" outputs: - labels: ${{ steps.meta.outputs.labels }} + labels: ${{ steps.prep.outputs.docker_image_labels }} version: ${{ steps.prep.outputs.docker_image_tag }} - registry: ${{ steps.prepenv.outputs.registry }} - repository: ${{ steps.prepenv.outputs.repository }} - build: + build-push: name: Build images runs-on: ubuntu-latest needs: [prepare, tests, linting, generated-files] - strategy: - matrix: - platform: [amd64, arm64, ppc64le, s390x] - steps: - - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Build image - if: matrix.platform == 'amd64' || github.ref_protected - uses: ./.github/actions/build-image - with: - platform: ${{ matrix.platform }} - labels: ${{ needs.prepare.outputs.labels }} - image-tag: ${{ needs.prepare.outputs.version }} - - push: - name: Push images - runs-on: ubuntu-latest - needs: [prepare, build] - strategy: - matrix: - platform: [amd64, arm64, ppc64le, s390x] - if: ${{ !github.event.pull_request.head.repo.fork }} steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Login to Registry - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: registry: quay.io username: ${{ secrets.QUAY_USERNAME }} password: ${{ secrets.QUAY_PASSWORD }} - - name: Upload Image - if: matrix.platform == 'amd64' || github.ref_protected - uses: ./.github/actions/upload-image + - name: Build image + uses: ./.github/actions/build-push-image with: - platform: ${{ matrix.platform }} + platforms: ${{github.ref_protected && env.PLATFORMS || env.PR_PLATFORMS }} labels: ${{ needs.prepare.outputs.labels }} - version: ${{ needs.prepare.outputs.version }} - registry: ${{ needs.prepare.outputs.registry }} - repository: ${{ needs.prepare.outputs.repository }} - - manifest: - name: Create manifest - needs: [prepare, push] - runs-on: ubuntu-latest - env: - COMBINED: ${{ github.ref_protected }} - if: ${{ !github.event.pull_request.head.repo.fork }} - steps: - - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Login to Registry - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 - with: - registry: quay.io - username: ${{ secrets.QUAY_USERNAME }} - password: ${{ secrets.QUAY_PASSWORD }} - - name: Create Manifests - uses: ./.github/actions/create-manifests - with: - version: ${{ needs.prepare.outputs.version }} - registry: ${{ needs.prepare.outputs.registry }} - repository: ${{ needs.prepare.outputs.repository }} - combined: ${{ env.COMBINED }} + tags: ${{ env.DOCKER_REGISTRY }}/${{ env.DOCKER_REPOSITORY }}:${{ needs.prepare.outputs.version }} + annotation: "version=${{ needs.prepare.outputs.version }}" diff --git a/.github/workflows/codeql-analysis.yaml b/.github/workflows/codeql-analysis.yaml index 88e6b4dca9..02adc8aba2 100644 --- a/.github/workflows/codeql-analysis.yaml +++ b/.github/workflows/codeql-analysis.yaml @@ -34,12 +34,12 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5 + uses: github/codeql-action/init@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16 with: languages: ${{ matrix.language }} - name: Autobuild - uses: github/codeql-action/autobuild@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5 + uses: github/codeql-action/autobuild@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5 + uses: github/codeql-action/analyze@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16 diff --git a/.github/workflows/e2e-tests-ondemand.yaml b/.github/workflows/e2e-tests-ondemand.yaml index e65fbe13dc..f45bede8ea 100644 --- a/.github/workflows/e2e-tests-ondemand.yaml +++ b/.github/workflows/e2e-tests-ondemand.yaml @@ -1,12 +1,13 @@ name: E2E tests ondemand on: + workflow_dispatch: schedule: # At 03:00 UTC on Monday, Wednesday, and Friday. - cron: 0 3 * * 1,3,5 env: - branch: 'release-1.3' + branch: release-1.4 permissions: checks: write @@ -23,8 +24,6 @@ jobs: max-parallel: 4 matrix: include: - - version: 1-25 - platform: k8s - version: 1-26 platform: k8s - version: 1-27 @@ -33,6 +32,10 @@ jobs: platform: k8s - version: 1-29 platform: k8s + - version: 1-30 + platform: k8s + - version: 1-31 + platform: k8s - version: 4-10 platform: ocp - version: 4-11 @@ -45,6 +48,10 @@ jobs: platform: ocp - version: 4-15 platform: ocp + - version: 4-16 + platform: ocp + - version: 4-17 + platform: ocp environment: E2E runs-on: - self-hosted @@ -60,14 +67,15 @@ jobs: target-branch: ${{ env.branch }} tenant1-name: ${{ secrets.TENANT1_NAME }} tenant1-apitoken: ${{ secrets.TENANT1_APITOKEN }} + tenant1-dataingesttoken: ${{ secrets.TENANT1_DATAINGESTTOKEN }} tenant1-oauth-client-id: ${{ secrets.TENANT1_OAUTH_CLIENT_ID }} tenant1-oauth-secret: ${{ secrets.TENANT1_OAUTH_SECRET }} tenant1-oauth-urn: ${{ secrets.TENANT1_OAUTH_URN }} tenant2-name: ${{ secrets.TENANT2_NAME }} tenant2-apitoken: ${{ secrets.TENANT2_APITOKEN }} + tenant2-dataingesttoken: ${{ secrets.TENANT2_DATAINGESTTOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }} - notify-failure: name: Notify failure in Slack environment: E2E @@ -78,8 +86,9 @@ jobs: - name: Notify failure in Slack uses: slackapi/slack-github-action@485a9d42d3a73031f12ec201c457e2162c45d02d # v2.0.0 with: - payload: | - message: ":x: E2E ondemand tests failed on ${{ env.branch }} branch (${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})", - run_id: "${{ github.run_id }}" webhook: ${{ secrets.SLACK_WEBHOOK_URL }} webhook-type: webhook-trigger + payload-templated: true + payload: | + "message": ":red_circle: E2E ondemand tests failed on ${{ env.branch }} branch (${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" + "run_id": "${{ github.run_id }}" diff --git a/.github/workflows/e2e-tests.yaml b/.github/workflows/e2e-tests.yaml index 9cb9b003be..06513209cf 100644 --- a/.github/workflows/e2e-tests.yaml +++ b/.github/workflows/e2e-tests.yaml @@ -32,11 +32,13 @@ jobs: target-branch: ${{ github.event.inputs.target }} tenant1-name: ${{ secrets.TENANT1_NAME }} tenant1-apitoken: ${{ secrets.TENANT1_APITOKEN }} + tenant1-dataingesttoken: ${{ secrets.TENANT1_DATAINGESTTOKEN }} tenant1-oauth-client-id: ${{ secrets.TENANT1_OAUTH_CLIENT_ID }} tenant1-oauth-secret: ${{ secrets.TENANT1_OAUTH_SECRET }} tenant1-oauth-urn: ${{ secrets.TENANT1_OAUTH_URN }} tenant2-name: ${{ secrets.TENANT2_NAME }} tenant2-apitoken: ${{ secrets.TENANT2_APITOKEN }} + tenant2-dataingesttoken: ${{ secrets.TENANT2_DATAINGESTTOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }} run-in-ocp: name: Run in OpenShift latest (${{ github.event.inputs.target || 'main' }}) @@ -55,11 +57,13 @@ jobs: target-branch: ${{ github.event.inputs.target }} tenant1-name: ${{ secrets.TENANT1_NAME }} tenant1-apitoken: ${{ secrets.TENANT1_APITOKEN }} + tenant1-dataingesttoken: ${{ secrets.TENANT1_DATAINGESTTOKEN }} tenant1-oauth-client-id: ${{ secrets.TENANT1_OAUTH_CLIENT_ID }} tenant1-oauth-secret: ${{ secrets.TENANT1_OAUTH_SECRET }} tenant1-oauth-urn: ${{ secrets.TENANT1_OAUTH_URN }} tenant2-name: ${{ secrets.TENANT2_NAME }} tenant2-apitoken: ${{ secrets.TENANT2_APITOKEN }} + tenant2-dataingesttoken: ${{ secrets.TENANT2_DATAINGESTTOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }} notify-slack: name: Notify test results in Slack diff --git a/.github/workflows/openssf-scorecards.yaml b/.github/workflows/openssf-scorecards.yaml index f73085f765..7daf79f501 100644 --- a/.github/workflows/openssf-scorecards.yaml +++ b/.github/workflows/openssf-scorecards.yaml @@ -29,7 +29,7 @@ jobs: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0 + uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1 with: results_file: results.sarif results_format: sarif @@ -48,7 +48,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: SARIF file path: results.sarif @@ -56,6 +56,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5 + uses: github/codeql-action/upload-sarif@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16 with: sarif_file: results.sarif diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index d35b008e8c..bacd873556 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -5,6 +5,10 @@ on: - v[0-9]+.[0-9]+.[0-9]+ - v[0-9]+.[0-9]+.[0-9]+-rc.[0-9]+ # include prerelease tags too +env: + GOOGLE_MARKETPLACE_ANNOTATION: com.googleapis.cloudmarketplace.product.service.name=services/dynatrace-operator-dynatrace-marketplace-prod.cloudpartnerservices.goog + PLATFORMS: linux/amd64,linux/arm64,linux/ppc64le,linux/s390x + DEPLOYER_PLATFORMS: linux/amd64 jobs: prepare: @@ -20,116 +24,82 @@ jobs: id: prep run: | hack/build/ci/prepare-build-variables.sh - - name: Docker metadata - uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5.6.1 - id: meta - with: - images: dynatrace/dynatrace-operator - tags: ${{ steps.prep.outputs.docker_image_tag }} - labels: | - ${{ steps.prep.outputs.docker_image_labels }} - vcs-ref=${{ github.sha }} outputs: - labels: ${{ steps.meta.outputs.labels }} + labels: ${{ steps.prep.outputs.docker_image_labels }} version: ${{ steps.prep.outputs.docker_image_tag }} version_without_prefix: ${{ steps.prep.outputs.docker_image_tag_without_prefix }} - build: + build-push: name: Build images - runs-on: ubuntu-latest - needs: [prepare] - strategy: - matrix: - platform: [amd64, arm64, ppc64le, s390x] - steps: - - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Build image - uses: ./.github/actions/build-image - with: - platform: ${{ matrix.platform }} - labels: ${{ needs.prepare.outputs.labels }} - image-tag: ${{ needs.prepare.outputs.version }} - - push: - name: Push images environment: Release - needs: [prepare, build] runs-on: ubuntu-latest permissions: id-token: write - strategy: - matrix: - platform: [amd64, arm64, ppc64le, s390x] - registry: [gcr, dockerhub, amazon-ecr, rhcc] - include: - - registry: gcr - url: gcr.io - repository: GCR_REPOSITORY - username: GCR_USERNAME - password: GCR_JSON_KEY - - registry: dockerhub - url: docker.io - repository: DOCKERHUB_REPOSITORY - username: DOCKERHUB_USERNAME - password: DOCKERHUB_PASSWORD - - registry: amazon-ecr - url: public.ecr.aws - repository: ECR_REPOSITORY - - registry: rhcc - url: quay.io - username: RHCC_USERNAME - password: RHCC_PASSWORD - repository: RHCC_REPOSITORY + needs: [prepare] + env: + GCR_IMAGE: gcr.io/${{ secrets.GCR_REPOSITORY }}:${{ needs.prepare.outputs.version }} + ECR_IMAGE: public.ecr.aws/${{ secrets.ECR_REPOSITORY }}:${{ needs.prepare.outputs.version }} + RHCC_IMAGE: quay.io/${{ secrets.RHCC_REPOSITORY }}:${{ needs.prepare.outputs.version }} + DOCKER_IMAGE: docker.io/${{ secrets.DOCKERHUB_REPOSITORY }}:${{ needs.prepare.outputs.version }} steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Login to Registry - if: ${{ matrix.registry != 'amazon-ecr' }} - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 + - name: Setup Golang + uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 with: - registry: ${{ matrix.url }} - username: ${{ secrets[matrix.username] }} - password: ${{ secrets[matrix.password] }} + go-version-file: "${{ github.workspace }}/go.mod" + - name: Prepare SBOM # Needs setup-go, uses a binary installed via `go install` + id: sbom + run: | + make release/gen-sbom + - name: Login to GCR + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + with: + registry: gcr.io + username: ${{ secrets.GCR_USERNAME }} + password: ${{ secrets.GCR_JSON_KEY }} + - name: Login to RHCC + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + with: + registry: quay.io + username: ${{ secrets.RHCC_USERNAME }} + password: ${{ secrets.RHCC_PASSWORD }} + - name: Login to Docker + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + with: + registry: docker.io + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} - name: Configure aws credentials - if: ${{ matrix.registry == 'amazon-ecr' }} - uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2 + uses: aws-actions/configure-aws-credentials@ececac1a45f3b08a01d2dd070d28d111c5fe6722 # v4.1.0 with: role-to-assume: ${{ secrets.ECR_IMAGEPUSH_ROLE }} aws-region: us-east-1 - name: Login to Amazon ECR - if: ${{ matrix.registry == 'amazon-ecr' }} uses: aws-actions/amazon-ecr-login@062b18b96a7aff071d4dc91bc00c4c1a7945b076 # v2.0.1 with: registry-type: public - - name: Push ${{matrix.platform}} to ${{matrix.registry}} - id: push-image - uses: ./.github/actions/upload-image + - name: Build image + id: build-image + uses: ./.github/actions/build-push-image with: - platform: ${{ matrix.platform }} + platforms: ${{ env.PLATFORMS }} labels: ${{ needs.prepare.outputs.labels }} - version: ${{ needs.prepare.outputs.version }} - registry: ${{ matrix.url }} - repository: ${{ secrets[matrix.repository] }} - - name: Sign image for ${{matrix.registry}} - uses: ./.github/actions/sign-image - with: - image: ${{ matrix.url }}/${{ secrets[matrix.repository] }}:${{ needs.prepare.outputs.version }}-${{ matrix.platform }}@${{steps.push-image.outputs.digest}} - signing-key: ${{ secrets.COSIGN_PRIVATE_KEY }} - signing-password: ${{ secrets.COSIGN_PASSWORD }} + tags: ${{ env.RHCC_IMAGE }},${{ env.GCR_IMAGE }},${{ env.ECR_IMAGE }},${{ env.DOCKER_IMAGE }} + annotation: ${{ env.GOOGLE_MARKETPLACE_ANNOTATION }} + outputs: + digest: ${{steps.build-image.outputs.digest}} - manifest: - name: Create Docker manifests + signing: + name: Sign the image index/manifests + Add SBOM environment: Release - needs: [prepare, push] + needs: [prepare, build-push] runs-on: ubuntu-latest permissions: id-token: write - outputs: - digest: ${{ steps.create-manifests.outputs.digest }} strategy: matrix: - registry: [gcr, dockerhub, amazon-ecr, rhcc] + registry: [gcr, dockerhub, rhcc, amazon-ecr] include: - registry: gcr url: gcr.io @@ -154,14 +124,14 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Login to Registry if: ${{ matrix.registry != 'amazon-ecr' }} - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: registry: ${{ matrix.url }} username: ${{ secrets[matrix.username] }} password: ${{ secrets[matrix.password] }} - name: Configure aws credentials if: ${{ matrix.registry == 'amazon-ecr' }} - uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2 + uses: aws-actions/configure-aws-credentials@ececac1a45f3b08a01d2dd070d28d111c5fe6722 # v4.1.0 with: role-to-assume: ${{ secrets.ECR_IMAGEPUSH_ROLE }} aws-region: us-east-1 @@ -170,86 +140,66 @@ jobs: uses: aws-actions/amazon-ecr-login@062b18b96a7aff071d4dc91bc00c4c1a7945b076 # v2.0.1 with: registry-type: public - - name: Create manifests for ${{matrix.registry}} - uses: ./.github/actions/create-manifests - id: create-manifests - with: - version: ${{ needs.prepare.outputs.version }} - registry: ${{ matrix.url }} - repository: ${{ secrets[matrix.repository] }} - combined: true - name: Sign images for ${{matrix.registry}} uses: ./.github/actions/sign-image with: - image: ${{ matrix.url }}/${{ secrets[matrix.repository] }}:${{ needs.prepare.outputs.version }}@${{ steps.create-manifests.outputs.digest }} + image: ${{ matrix.url }}/${{ secrets[matrix.repository] }}:${{ needs.prepare.outputs.version }}@${{ needs.build-push.outputs.digest }} signing-key: ${{ secrets.COSIGN_PRIVATE_KEY }} signing-password: ${{ secrets.COSIGN_PASSWORD }} - - attach-sbom: - name: Attach sbom - environment: Release - needs: [ prepare, push, manifest ] - runs-on: ubuntu-latest - permissions: - id-token: write - strategy: - matrix: - registry: [gcr, dockerhub, amazon-ecr] - include: - - registry: gcr - url: gcr.io - repository: GCR_REPOSITORY - username: GCR_USERNAME - password: GCR_JSON_KEY - - registry: dockerhub - url: docker.io - repository: DOCKERHUB_REPOSITORY - username: DOCKERHUB_USERNAME - password: DOCKERHUB_PASSWORD - - registry: amazon-ecr - url: public.ecr.aws - repository: ECR_REPOSITORY - steps: - - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Login to Registry - if: ${{ matrix.registry != 'amazon-ecr' }} - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 - with: - registry: ${{ matrix.url }} - username: ${{ secrets[matrix.username] }} - password: ${{ secrets[matrix.password] }} - - name: Configure aws credentials - if: ${{ matrix.registry == 'amazon-ecr' }} - uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2 - with: - role-to-assume: ${{ secrets.ECR_IMAGEPUSH_ROLE }} - aws-region: us-east-1 - - name: Login to Amazon ECR - if: ${{ matrix.registry == 'amazon-ecr' }} - uses: aws-actions/amazon-ecr-login@062b18b96a7aff071d4dc91bc00c4c1a7945b076 # v2.0.1 - with: - registry-type: public - name: Create sbom for ${{matrix.registry}} id: sbom - uses: aquasecurity/trivy-action@18f2510ee396bbf400402947b394f2dd8c87dbb0 # 0.29.0 + uses: aquasecurity/trivy-action@6c175e9c4083a92bbca2f9724c8a5e33bc2d97a5 # 0.30.0 with: - image-ref: ${{ matrix.url }}/${{ secrets[matrix.repository] }}:${{ needs.prepare.outputs.version }}@${{ needs.manifest.outputs.digest }} + image-ref: ${{ matrix.url }}/${{ secrets[matrix.repository] }}:${{ needs.prepare.outputs.version }}@${{ needs.build-push.outputs.digest }} format: 'cyclonedx' output: 'result.json' skip-dirs: '/usr/share/dynatrace-operator/third_party_licenses' + skip-files: '/usr/local/bin/dynatrace-operator' - name: Upload sbom to ${{matrix.registry}} uses: ./.github/actions/upload-sbom with: - image: ${{ matrix.url }}/${{ secrets[matrix.repository] }}:${{ needs.prepare.outputs.version }}@${{ needs.manifest.outputs.digest }} + image: ${{ matrix.url }}/${{ secrets[matrix.repository] }}:${{ needs.prepare.outputs.version }}@${{ needs.build-push.outputs.digest }} sbom: 'result.json' signing-key: ${{ secrets.COSIGN_PRIVATE_KEY }} signing-password: ${{ secrets.COSIGN_PASSWORD }} + build-gcr-deployer: + name: Build GCR deployer image + environment: Release + runs-on: ubuntu-latest + needs: [ prepare ] + env: + IMAGE: gcr.io/${{ secrets.GCR_REPOSITORY_DEPLOYER }}:${{ needs.prepare.outputs.version }} + IMAGE_NO_PREFIX: gcr.io/${{ secrets.GCR_REPOSITORY_DEPLOYER }}:${{ needs.prepare.outputs.version_without_prefix }} + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Login to Docker Hub + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + with: + registry: gcr.io + username: ${{ secrets.GCR_USERNAME }} + password: ${{ secrets.GCR_JSON_KEY }} + - name: Set up QEMU + uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # 3.10.0 + - name: Build and push + uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # 6.15.0 + with: + platforms: ${{ env.DEPLOYER_PLATFORMS }} + provenance: false + context: ./config/helm + file: ./config/helm/Dockerfile + push: true + tags: ${{ env.IMAGE }},${{ env.IMAGE_NO_PREFIX }} + annotations: | + ${{ env.GOOGLE_MARKETPLACE_ANNOTATION }} + run-preflight-rhcc: name: Run preflight for rhcc environment: Release - needs: [ prepare, push, manifest] + needs: [ prepare, build-push] runs-on: ubuntu-latest env: SCAN_REGISTRY: "quay.io" @@ -257,7 +207,7 @@ jobs: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Login to Registry - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: registry: ${{ env.SCAN_REGISTRY }} username: ${{ secrets.RHCC_USERNAME }} @@ -274,7 +224,7 @@ jobs: release: name: Create release - needs: [prepare, build, attach-sbom, manifest, run-preflight-rhcc] + needs: [prepare, build-push, signing, run-preflight-rhcc] environment: Release permissions: contents: write @@ -285,13 +235,9 @@ jobs: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Golang - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 with: go-version-file: "${{ github.workspace }}/go.mod" - - name: Download dependencies - id: depdownload - run: | - hack/build/ci/install-cgo-dependencies.sh - name: Generate release notes shell: bash env: @@ -306,10 +252,10 @@ jobs: run: | make manifests/crd/release CHART_VERSION="${VERSION_WITHOUT_PREFIX}" - make manifests/kubernetes/olm IMAGE="public.ecr.aws/dynatrace/dynatrace-operator" TAG="${VERSION}@${{needs.manifest.outputs.digest}}" - make manifests/kubernetes IMAGE="public.ecr.aws/dynatrace/dynatrace-operator" TAG="${VERSION}@${{needs.manifest.outputs.digest}}" - make manifests/openshift/olm IMAGE="registry.connect.redhat.com/dynatrace/dynatrace-operator" TAG="${VERSION}@${{needs.manifest.outputs.digest}}" - make manifests/openshift IMAGE="registry.connect.redhat.com/dynatrace/dynatrace-operator" TAG="${VERSION}@${{needs.manifest.outputs.digest}}" + make manifests/kubernetes/olm IMAGE="public.ecr.aws/dynatrace/dynatrace-operator" TAG="${VERSION}@${{needs.build-push.outputs.digest}}" + make manifests/kubernetes IMAGE="public.ecr.aws/dynatrace/dynatrace-operator" TAG="${VERSION}@${{needs.build-push.outputs.digest}}" + make manifests/openshift/olm IMAGE="registry.connect.redhat.com/dynatrace/dynatrace-operator" TAG="${VERSION}@${{needs.build-push.outputs.digest}}" + make manifests/openshift IMAGE="registry.connect.redhat.com/dynatrace/dynatrace-operator" TAG="${VERSION}@${{needs.build-push.outputs.digest}}" cp config/deploy/kubernetes/kubernetes.yaml config/deploy/kubernetes/gke-autopilot.yaml - name: Build helm packages uses: ./.github/actions/build-helm @@ -324,7 +270,7 @@ jobs: run: | helm registry login -u "${{ secrets.DOCKERHUB_USERNAME }}" -p "${{ secrets.DOCKERHUB_PASSWORD }}" "registry.hub.docker.com" - name: Login Docker to dockerhub - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: registry: docker.io username: ${{ secrets.DOCKERHUB_USERNAME }} @@ -337,7 +283,7 @@ jobs: cosign-private-key: ${{ secrets.COSIGN_PRIVATE_KEY }} cosign-password: ${{ secrets.COSIGN_PASSWORD }} - name: Configure aws credentials - uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2 + uses: aws-actions/configure-aws-credentials@ececac1a45f3b08a01d2dd070d28d111c5fe6722 # v4.1.0 with: role-to-assume: ${{ secrets.ECR_IMAGEPUSH_ROLE }} aws-region: us-east-1 @@ -361,7 +307,7 @@ jobs: mkdir -p tmp echo ${COSIGN_PUBLIC_KEY} | base64 -d > tmp/cosign.pub - name: Create pre-release - uses: softprops/action-gh-release@01570a1f39cb168c169c802c3bceb9e93fb10974 # v2.1.0 + uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2.2.2 if: ${{ contains(github.ref, '-rc.') }} with: body_path: ./CHANGELOG.md @@ -377,7 +323,7 @@ jobs: draft: true fail_on_unmatched_files: true - name: Create release - uses: softprops/action-gh-release@01570a1f39cb168c169c802c3bceb9e93fb10974 # v2.1.0 + uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2.2.2 if: ${{ !contains(github.ref, '-rc.') }} with: body_path: ./CHANGELOG.md @@ -407,7 +353,7 @@ jobs: hack/build/ci/generate-new-helm-index-yaml.sh "helm-pkg" ${{ needs.prepare.outputs.version_without_prefix }} - name: Create pull request for adding helm index to main branch if: ${{ !contains(github.ref, '-rc.') }} - uses: peter-evans/create-pull-request@5e914681df9dc83aa4e4905692ca88beb2f9e91f # v7.0.5 + uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 with: base: main delete-branch: true diff --git a/.github/workflows/self-update-on-new-release-version.yaml b/.github/workflows/self-update-on-new-release-version.yaml index ceb3d3f7d4..741aace543 100644 --- a/.github/workflows/self-update-on-new-release-version.yaml +++ b/.github/workflows/self-update-on-new-release-version.yaml @@ -17,9 +17,9 @@ jobs: with: ref: main - name: Install Python - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 + uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 with: - python-version: '3.12' + python-version: '3.13' - name: Install python requirements run: make prerequisites/python - name: Find last 3 release branches diff --git a/.gitignore b/.gitignore index 603b886772..9be34577e4 100644 --- a/.gitignore +++ b/.gitignore @@ -106,3 +106,4 @@ test/testdata/secrets/* local/ permissions.md +dynatrace-operator-bin-sbom.cdx.json diff --git a/.golangci.yml b/.golangci.yml index 84bc92a34c..147b0bb46e 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,342 +1,319 @@ -linters-settings: - misspell: - locale: US - staticcheck: - checks: - - all - govet: - enable: - - fieldalignment - disable: - # Disable checking for copied locks since it causes a lot of annoying false-positives - - copylocks - goconst: - ignore-tests: true - gosec: - excludes: - - G101 # "pattern": "(?i)passwd|pass|password|pwd|secret|private_key|token" - - G305 # File traversal when extracting zip/tar archive - revive: - enable-all-rules: true - rules: - - name: cognitive-complexity - arguments: [20] # max allowed cognitive complexity factor - - name: function-result-limit - arguments: [3] - - name: function-length - # following values set to avoid further regressions: - arguments: [52, 468] # the maximum allowed statements and lines. 0 disables the check - - name: cyclomatic - arguments: [12] - - name: banned-characters - disabled: true - - name: file-header - disabled: true - - name: max-public-structs - disabled: true - - name: argument-limit - arguments: [5] - - name: line-length-limit - disabled: true - - name: add-constant - disabled: true - - name: var-naming - disabled: true - - name: unused-receiver - disabled: true - - name: import-shadowing - disabled: true - - name: modifies-value-receiver - disabled: true - - name: bare-return - disabled: true - - name: bare-return - disabled: true - - name: if-return - disabled: true - - name: redefines-builtin-id - disabled: true - - name: context-keys-type - disabled: true - - name: unused-parameter - disabled: true - - name: time-naming - disabled: true - - name: errorf - disabled: true - - name: unexported-return - disabled: true - - name: unhandled-error - disabled: true - - name: confusing-naming - disabled: true - - name: indent-error-flow - disabled: true - - name: early-return - disabled: true - - name: bool-literal-in-expr - disabled: true - - name: error-strings - disabled: true - - name: empty-lines - disabled: true - - name: flag-parameter - disabled: true - - name: blank-imports - disabled: true - - name: increment-decrement - disabled: true - - name: context-as-argument - disabled: true - - name: confusing-results - disabled: true - - name: receiver-naming - disabled: true - - name: nested-structs - disabled: true - - name: struct-tag - disabled: true - - name: error-naming - disabled: true - - name: range-val-address - disabled: true - - name: import-alias-naming - arguments: ["^[a-z][\\w]{0,}$"] - - name: unchecked-type-assertion - disabled: true - depguard: - rules: - all: - files: - - $all - allow: - - $gostd - # Approved orgs. - - "github.com/Dynatrace" - - "github.com/container-storage-interface" - - "github.com/containers" - - "github.com/klauspost" - - "github.com/opencontainers" - - "github.com/prometheus" - - "istio.io" - - "k8s.io" - - "sigs.k8s.io" - - # Approved packages. - - "github.com/mattn/go-sqlite3" - - "github.com/pkg/errors" - - "github.com/spf13/afero" - - "github.com/spf13/cobra" # For CLI - - "github.com/evanphx/json-patch" - - "github.com/go-logr/logr" - - "github.com/stretchr/testify" - - "github.com/google/go-containerregistry" - - "github.com/docker/cli" - - "github.com/go-gormigrate/gormigrate" - - "github.com/google/uuid" - - # Allowed packages in container-based builder. - deny: - # TODO: (andrii) Potentially uncomment it in future, but requires some refactoring - # - pkg: "reflect" - # desc: Please don't use reflect package - - pkg: "unsafe" - desc: Please don't use unsafe package - - main: - files: - - $all - # Don't allow go-cmp in non-test code. - # NOTE: test code is allowed to use github.com/google/go-cmp (there is no - # deny for it) but non-test code is not. - - "!$test" - - "!test/**/*.go" - - "!**/testing/**" - - "!**/*mock*/**/.go" - deny: - - pkg: "github.com/google/go-cmp" - desc: Please don't use go-cmp for non-test code. - mnd: - checks: - - argument - - case - - condition - - operation - - return - ignored-files: - - test*.go,testing.go - ignored-functions: - - '^time\.' - - strings.SplitN - - '^wait\.' - - rand.WithLength - - '^require\.' - - WaitForCondition - - '^int*' - ignored-numbers: - - '0666' - - '0644' - - '0755' - - '0770' - - '0755' - - '0000' - - '1001' - - '1000' - - '1234' - dupl: - threshold: 150 - godot: - exclude: - - '^\ \+' +version: "2" linters: - disable-all: true + default: none + enable: + - asasalint + - asciicheck + - bidichk + - copyloopvar + - decorder + - depguard + - dogsled + - dupl + - durationcheck + - errorlint + - forbidigo + - gocheckcompilerdirectives + - gochecksumtype + - gocognit + - goconst + - gocritic + - gocyclo + - godot + - goheader + - gomoddirectives + - gomodguard + - goprintffuncname + - gosec + - gosmopolitan + - govet + - grouper + - importas + - ineffassign + - intrange + - loggercheck + - makezero + - mirror + - misspell + - mnd + - nakedret + - nilerr + - nilnil + - nlreturn + - noctx + - nolintlint + - nosprintfhostport + - perfsprint + - prealloc + - predeclared + - protogetter + - reassign + - revive + - sloglint + - staticcheck + - testableexamples + - testifylint + - thelper + - tparallel + - unconvert + - unparam + - unused + - usestdlibvars + - wastedassign + - whitespace + - wsl + - zerologlint + settings: + depguard: + rules: + all: + files: + - $all + allow: + - $gostd + - github.com/Dynatrace + - github.com/container-storage-interface + - github.com/containers + - github.com/klauspost + - github.com/opencontainers + - github.com/prometheus + - istio.io + - k8s.io + - sigs.k8s.io + - golang.org + - go.opentelemetry.io + - go.uber.org + - gopkg.in + - google.golang.org + - github.com/mattn/go-sqlite3 + - github.com/pkg/errors + - github.com/spf13/afero + - github.com/spf13/cobra + - github.com/evanphx/json-patch + - github.com/go-logr/logr + - github.com/stretchr/testify + - github.com/google/go-containerregistry + - github.com/docker/cli + - github.com/go-gormigrate/gormigrate + - github.com/google/uuid + deny: + - pkg: unsafe + desc: Please don't use unsafe package + main: + files: + - $all + - '!$test' + - '!test/**/*.go' + - '!**/testing/**' + - '!**/*mock*/**/.go' + deny: + - pkg: github.com/google/go-cmp + desc: Please don't use go-cmp for non-test code. + dupl: + threshold: 150 + godot: + exclude: + - ^\ \+ + gosec: + excludes: + - G101 + - G305 + govet: + enable: + - fieldalignment + disable: + - copylocks + misspell: + locale: US + mnd: + checks: + - argument + - case + - condition + - operation + - return + ignored-numbers: + - "0666" + - "0644" + - "0755" + - "0770" + - "0755" + - "0000" + - "1001" + - "1000" + - "1234" + ignored-files: + - test*.go,testing.go + ignored-functions: + - ^time\. + - strings.SplitN + - ^wait\. + - rand.WithLength + - ^require\. + - WaitForCondition + - ^int* + revive: + enable-all-rules: true + rules: + - name: cognitive-complexity + arguments: + - 20 + - name: function-result-limit + arguments: + - 3 + - name: function-length + arguments: + - 52 + - 468 + - name: cyclomatic + arguments: + - 12 + - name: banned-characters + disabled: true + - name: file-header + disabled: true + - name: max-public-structs + disabled: true + - name: argument-limit + arguments: + - 5 + - name: line-length-limit + disabled: true + - name: add-constant + disabled: true + - name: var-naming + disabled: true + - name: unused-receiver + disabled: true + - name: import-shadowing + disabled: true + - name: modifies-value-receiver + disabled: true + - name: bare-return + disabled: true + - name: bare-return + disabled: true + - name: if-return + disabled: true + - name: redefines-builtin-id + disabled: true + - name: context-keys-type + disabled: true + - name: unused-parameter + disabled: true + - name: time-naming + disabled: true + - name: errorf + disabled: true + - name: unexported-return + disabled: true + - name: unhandled-error + disabled: true + - name: confusing-naming + disabled: true + - name: indent-error-flow + disabled: true + - name: early-return + disabled: true + - name: bool-literal-in-expr + disabled: true + - name: error-strings + disabled: true + - name: empty-lines + disabled: true + - name: flag-parameter + disabled: true + - name: blank-imports + disabled: true + - name: increment-decrement + disabled: true + - name: context-as-argument + disabled: true + - name: confusing-results + disabled: true + - name: receiver-naming + disabled: true + - name: nested-structs + disabled: true + - name: struct-tag + disabled: true + - name: error-naming + disabled: true + - name: range-val-address + disabled: true + - name: import-alias-naming + arguments: + - ^[a-z][\w]{0,}$ + - name: unchecked-type-assertion + disabled: true + staticcheck: + checks: + - all + - "-ST*" # this is stylecheck, but has been rolled into staticcheck + - "-QF1008" # could remove embedded field from selector, example: dk.ObjectMeta.Labels == dk.Labels + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + rules: + - linters: + - dupl + - perfsprint + - wsl + path: _(test|gen)\.go + - linters: + - govet + - noctx + - perfsprint + - thelper + - wsl + path: (test/*) + - linters: + - dupl + - govet + path: _(test|gen)\.go + - linters: + - gosec + path: pkg/webhook/validation/proxy_url_test.go + - linters: + - gosec + path: pkg/ingestendpoint/secret_test.go + - linters: + - unparam + text: always receives + - linters: + - revive + path: pkg/clients/dynatrace + - linters: + - godot + path-except: pkg/api/(.+)\.go + - linters: + - goconst + path: (.+)_test\.go + paths: + - pkg/api/v1alpha1/dynakube + - pkg/api/v1alpha1/edgeconnect + - pkg/api/v1beta1/dynakube + - pkg/api/v1beta2/dynakube + - pkg/api/v1beta3/dynakube + - third_party$ + - builtin$ + - examples$ +formatters: enable: - - asasalint # Check for pass []any as any in variadic func(...any). - - asciicheck # Checks that all code identifiers does not have non-ASCII symbols in the name. - - bidichk # Checks for dangerous unicode character sequences. - - copyloopvar # Copyloopvar is a linter detects places where loop variables are copied. - - decorder # Check declaration order and count of types, constants, variables and functions. - - depguard # Go linter that checks if package imports are in a list of acceptable packages - - dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) - - dupl # Tool for code clone detection. - - durationcheck # Check for two durations multiplied together. - - errorlint # Errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13. - - forbidigo # Forbids identifiers. - - gci # Gci controls Go package import order and makes it always deterministic. - - gocheckcompilerdirectives # Checks that go compiler directive comments (//go:) are valid. - - gochecksumtype # Run exhaustiveness checks on Go "sum types" - - gocognit # Computes and checks the cognitive complexity of functions. - - goconst # Finds repeated strings that could be replaced by a constant - - gocritic # Provides diagnostics that check for bugs, performance and style issues. - - gocyclo # Computes and checks the cyclomatic complexity of functions. complexity. - - godot # Check if comments end in a period. - - gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification - - goheader # Checks is file header matches to pattern. - - mnd # An analyzer to detect magic numbers. - - gomoddirectives # Manage the use of 'replace', 'retract', and 'excludes' directives in go.mod. - - gomodguard # Allow and block list linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations. - - goprintffuncname # Checks that printf-like functions are named with f at the end. - - gosec # Inspects source code for security problems - - gosimple # Linter for Go source code that specializes in simplifying code. - - gosmopolitan # Report certain i18n/l10n anti-patterns in your Go codebase. - - govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string - - grouper # Analyze expression groups. - - importas # Enforces consistent import aliases. - - ineffassign # Detects when assignments to existing variables are not used - - intrange # Intrange is a linter to find places where for loops could make use of an integer range. - - loggercheck # Checks key value pairs for common logger libraries (kitlog,klog,logr,zap). - - makezero # Finds slice declarations with non-zero initial length. - - mirror # Reports wrong mirror patterns of bytes/strings usage. - - misspell # Finds commonly misspelled English words in comments. - - nakedret # Checks that functions with naked returns are not longer than a maximum size (can be zero). - - nilerr # Finds the code that returns nil even if it checks that the error is not nil. - - nilnil # Checks that there is no simultaneous return of nil error and an invalid value. - - nlreturn # Nlreturn checks for a new line before return and branch statements to increase code clarity. - - noctx # Finds sending http request without context.Context. - - nolintlint # Reports ill-formed or insufficient nolint directives. - - nosprintfhostport # Checks for misuse of Sprintf to construct a host with port in a URL. - - perfsprint # Checks that fmt.Sprintf can be replaced with a faster alternative. - - prealloc # Finds slice declarations that could potentially be pre-allocated - - predeclared # Find code that shadows one of Go's predeclared identifiers. - - protogetter # Reports direct reads from proto message fields when getters should be used. - - reassign # Checks that package variables are not reassigned. - - revive # Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint. - - sloglint # Ensure consistent code style when using log/slog. - - staticcheck # It's a set of rules from staticcheck. It's not the same thing as the staticcheck binary. The author of staticcheck doesn't support or approve the use of staticcheck as a library inside golangci-lint. - - tenv # tenv is analyzer that detects using os.Setenv instead of t.Setenv since Go1.17 - - testableexamples # Linter checks if examples are testable (have an expected output). - - testifylint # Checks usage of github.com/stretchr/testify. - - thelper # Thelper detects tests helpers which is not start with t.Helper() method. - - tparallel # tparallel detects inappropriate usage of t.Parallel() method in your Go test codes - - unconvert # Remove unnecessary type conversions - - unparam # Reports unused function parameters - - unused # Checks Go code for unused constants, variables, functions and types - - usestdlibvars # A linter that detect the possibility to use variables/constants from the Go standard library. - - wastedassign # wastedassign finds wasted assignment statements. - - whitespace # Tool for detection of leading and trailing whitespace. - - wsl # Add or remove empty lines. - - zerologlint # Detects the wrong usage of zerolog that a user forgets to dispatch with Send or Msg. - -# Blocked: -# false positive -> https://github.com/timakin/bodyclose/issues/51 -# - bodyclose # Checks whether HTTP response body is closed successfully. - -# TODO: Requires some code changes before enabling: -# - contextcheck # Check whether the function uses a non-inherited context. -# - cyclop # Checks function and package cyclomatic complexity. -# - dupword # Checks for duplicate words in the source code. comment -# - errcheck # enabled by default and recommended -# - errchkjson # Checks types passed to the json encoding functions. Reports unsupported types and reports occations, where the check for the returned error can be omitted. -# - errname # Checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error. -# - execinquery # Execinquery is a linter about query string checker in Query function which reads your Go src files and warning it finds. -# - exhaustruct # Checks if all structure fields are initialized. -# - exportloopref # Checks for pointers to enclosing loop variables. -# - forcetypeassert # Finds forced type assertions. -# - funlen # Tool for detection of long functions. -# - gochecknoglobals # Check that no global variables exist. -# - gochecknoinits # Checks that no init functions are present in Go code. -# - goerr113 # Go linter to check the errors handling expressions. -# - gofumpt # Gofumpt checks whether code was gofumpt-ed. -# - inamedparam # reports interfaces with unnamed method parameters. -# - ireturn # Accept Interfaces, Return Concrete Types. -# - maintidx # Maintidx measures the maintainability index of each function. -# - musttag # Enforce field tags in (un)marshaled structs. -# - nestif # Reports deeply nested if statements. -# - nonamedreturns # Reports all named returns. -# - paralleltest # Detects missing usage of t.Parallel() method in your Go test. -# - promlinter # Check Prometheus metrics naming via promlint. -# - rowserrcheck # Checks whether Rows.Err of rows is checked successfully. -# - sqlclosecheck # Checks that sql.Rows and sql.Stmt are closed. -# - wrapcheck # Checks that errors returned from external packages are wrapped. -# TODO: Need to discuss/decide/remove -# - exhaustive # Check exhaustiveness of enum switch statements. -# - godox # Tool for detection of FIXME, TODO and other comment keywords. -# - inamedparam # reports interfaces with unnamed method parameters. -# - interfacebloat # A linter that checks the number of methods inside an interface. -# - lll # Reports long lines. -# - stylecheck # Stylecheck is a replacement for golint. -# - tagliatelle # Checks the struct tags. -# - testpackage # Linter that makes you use a separate _test package. - - -service: - golangci-lint-version: 1.55.x # use the fixed version to not introduce new linters unexpectedly - -issues: - exclude-dirs: - - pkg/api/v1alpha1/dynakube # legacy version, should not be changed - exclude-rules: - # Exclude duplicate code and function length and complexity checking in test - # files (due to common repeats and long functions in test code) - - path: _(test|gen)\.go - linters: - - dupl - - wsl - - perfsprint - - path: (test/*) - linters: - - noctx - - wsl - - thelper - - perfsprint - - govet - - path: _(test|gen)\.go - linters: - - govet - - dupl - - linters: - - gosec - path: pkg/webhook/validation/proxy_url_test.go - - linters: - - gosec - path: pkg/ingestendpoint/secret_test.go - - linters: - - unparam - text: always receives - - linters: - - revive - path: pkg/clients/dynatrace # it's awaiting refactoring - # Run some linter only for test files by excluding its issues for everything else. - - path-except: 'pkg/api/(.+)\.go' - linters: - - godot + - gci + - gofmt + exclusions: + generated: lax + paths: + - pkg/api/v1alpha1/dynakube + - pkg/api/v1alpha1/edgeconnect + - pkg/api/v1beta1/dynakube + - pkg/api/v1beta2/dynakube + - pkg/api/v1beta3/dynakube + - third_party$ + - builtin$ + - examples$ diff --git a/.mockery.yaml b/.mockery.yaml index 5b7a2e4aa2..a21124e478 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -1,6 +1,7 @@ quiet: False disable-version-string: True with-expecter: True +issue-845-fix: True mockname: "{{.InterfaceName}}" filename: "{{.InterfaceName | snakecase}}.go" outpkg: mocks @@ -57,9 +58,13 @@ packages: github.com/Dynatrace/dynatrace-operator/pkg/webhook: interfaces: PodMutator: + PodInjector: sigs.k8s.io/controller-runtime/pkg/manager: interfaces: Manager: sigs.k8s.io/controller-runtime/pkg/reconcile: interfaces: Reconciler: + k8s.io/client-go/kubernetes/typed/core/v1: + interfaces: + PodInterface: diff --git a/.testcoverage.yml b/.testcoverage.yml index ba73e57bb6..7ab3bb7d5a 100644 --- a/.testcoverage.yml +++ b/.testcoverage.yml @@ -24,7 +24,7 @@ threshold: # (optional; default 0) # The minimum total coverage project should have - total: 71 + total: 69 # Holds regexp rules which will override thresholds for matched files or packages # using their paths. diff --git a/Dockerfile b/Dockerfile index f1bf6ffc31..e01d0b34ef 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,6 @@ +# check=skip=RedundantTargetPlatform # setup build image -FROM golang:1.23.3@sha256:d56c3e08fe5b27729ee3834854ae8f7015af48fd651cd25d1e3bcf3c19830174 AS operator-build - -RUN --mount=type=cache,target=/var/cache/apt \ - apt-get update && apt-get install -y libbtrfs-dev libdevmapper-dev +FROM --platform=$BUILDPLATFORM golang:1.24.2@sha256:991aa6a6e4431f2f01e869a812934bd60fbc87fb939e4a1ea54b8494ab9d2fc6 AS operator-build WORKDIR /app @@ -14,17 +12,23 @@ RUN if [ "$DEBUG_TOOLS" = "true" ]; then \ COPY go.mod go.sum ./ RUN go mod download -x +COPY pkg ./pkg +COPY cmd ./cmd + ARG GO_LINKER_ARGS ARG GO_BUILD_TAGS +ARG TARGETARCH +ARG TARGETOS -COPY pkg ./pkg -COPY cmd ./cmd -RUN --mount=type=cache,target="/root/.cache/go-build" CGO_ENABLED=1 CGO_CFLAGS="-O2 -Wno-return-local-addr" \ +RUN --mount=type=cache,target="/root/.cache/go-build" \ + --mount=type=cache,target="/go/pkg" \ + CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \ go build -tags "${GO_BUILD_TAGS}" -trimpath -ldflags="${GO_LINKER_ARGS}" \ -o ./build/_output/bin/dynatrace-operator ./cmd/ -FROM registry.access.redhat.com/ubi9-micro:9.5-1731934928@sha256:31f00ba1d79523e182624c96e05b2f5ca66ea35d64959d84acdc8b670429415f AS base -FROM registry.access.redhat.com/ubi9:9.5-1731517889@sha256:2bae9062eddbbc18e76555972e7026ffe02cef560a0076e6d7f72bed2c05723f AS dependency +# platform is required, otherwise the copy command will copy the wrong architecture files, don't trust GitHub Actions linting warnings +FROM --platform=$TARGETPLATFORM registry.access.redhat.com/ubi9-micro:9.5-1744118077@sha256:dca8bc186bb579f36414c6ad28f1dbeda33e5cf0bd5fc1c51430cc578e25f819 AS base +FROM --platform=$TARGETPLATFORM registry.access.redhat.com/ubi9:9.5-1744101466@sha256:ea57285741f007e83f2ee20423c20b0cbcce0b59cc3da027c671692cc7efe4dd AS dependency RUN mkdir -p /tmp/rootfs-dependency COPY --from=base / /tmp/rootfs-dependency RUN dnf install --installroot /tmp/rootfs-dependency \ @@ -38,7 +42,8 @@ RUN dnf install --installroot /tmp/rootfs-dependency \ /tmp/rootfs-dependency/var/log/dnf* \ /tmp/rootfs-dependency/var/log/yum.* -FROM base +# platform is required, otherwise the copy command will copy the wrong architecture files, don't trust GitHub Actions linting warnings +FROM --platform=$TARGETPLATFORM base COPY --from=dependency /tmp/rootfs-dependency / @@ -46,12 +51,14 @@ COPY --from=dependency /tmp/rootfs-dependency / COPY --from=operator-build /app/build/_output/bin /usr/local/bin # csi binaries -COPY --from=registry.k8s.io/sig-storage/csi-node-driver-registrar:v2.12.0@sha256:0d23a6fd60c421054deec5e6d0405dc3498095a5a597e175236c0692f4adee0f /csi-node-driver-registrar /usr/local/bin -COPY --from=registry.k8s.io/sig-storage/livenessprobe:v2.14.0@sha256:33692aed26aaf105b4d6e66280cceca9e0463f500c81b5d8c955428a75438f32 /livenessprobe /usr/local/bin +COPY --from=registry.k8s.io/sig-storage/csi-node-driver-registrar:v2.13.0@sha256:d7138bcc3aa5f267403d45ad4292c95397e421ea17a0035888850f424c7de25d /csi-node-driver-registrar /usr/local/bin +COPY --from=registry.k8s.io/sig-storage/livenessprobe:v2.15.0@sha256:2c5f9dc4ea5ac5509d93c664ae7982d4ecdec40ca7b0638c24e5b16243b8360f /livenessprobe /usr/local/bin COPY ./third_party_licenses /usr/share/dynatrace-operator/third_party_licenses COPY LICENSE /licenses/ +COPY ./dynatrace-operator-bin-sbom.cdx.json ./dynatrace-operator-bin-sbom.cdx.json + # custom scripts COPY hack/build/bin /usr/local/bin diff --git a/HACKING.md b/HACKING.md index 1345d12a7d..53a96489e6 100644 --- a/HACKING.md +++ b/HACKING.md @@ -7,19 +7,7 @@ ### Installation -There are automatic builds from the master branch. The latest development build can be installed as follows: - -#### Kubernetes - -```sh -make deploy/kubernetes -``` - -#### OpenShift - -```sh -make deploy/openshift -``` +There are automatic builds from the main branch. The latest development build can be installed using `make deploy`. #### Tests diff --git a/Makefile b/Makefile index 34900aaa6e..4ac1a8744e 100644 --- a/Makefile +++ b/Makefile @@ -52,12 +52,10 @@ SHELL ?= bash -include hack/make/helm/*.mk -include hack/make/manifests/*.mk -include hack/make/tests/*.mk +-include hack/make/release/*.mk ## Builds the operator image and pushes it to quay with a snapshot tag build: images/build/push -## Installs (deploys) the operator on a k8s/openshift cluster -install: deploy/helm - ## Installs prerequisites, builds and pushes a tagged operator image, and deploys the operator on a cluster all: prerequisites build install diff --git a/README.md b/README.md index de23687579..a184798ca1 100644 --- a/README.md +++ b/README.md @@ -39,71 +39,9 @@ objects like permissions, custom resources and corresponding StatefulSets. ### Installation -> For install instructions on Openshift, head to the +> For install instructions, head to the > [official help page](https://www.dynatrace.com/support/help/shortlink/full-stack-dto-k8) -To create the namespace and apply the operator run the following commands - -```sh -kubectl create namespace dynatrace -kubectl apply -f https://github.com/Dynatrace/dynatrace-operator/releases/latest/download/kubernetes.yaml -``` - -If using `cloudNativeFullStack` or `applicationMonitoring` with CSI driver, the following command is required as well: - -```sh -kubectl apply -f https://github.com/Dynatrace/dynatrace-operator/releases/latest/download/kubernetes-csi.yaml -``` - -A secret holding tokens for authenticating to the Dynatrace cluster needs to be created upfront. Create access tokens of -type *Dynatrace API* and use its values in the following commands respectively. For -assistance please refer -to [Create user-generated access tokens](https://www.dynatrace.com/support/help/shortlink/token#create-api-token). - -The token scopes required by the Dynatrace Operator are documented on our [official help page](https://www.dynatrace.com/support/help/shortlink/full-stack-dto-k8#tokens) - -```sh -kubectl -n dynatrace create secret generic dynakube --from-literal="apiToken=DYNATRACE_API_TOKEN" --from-literal="dataIngestToken=DATA_INGEST_TOKEN" -``` - -#### Create `DynaKube` custom resource for ActiveGate and OneAgent rollout - -The rollout of the Dynatrace components is governed by a custom resource of type `DynaKube`. This custom resource will -contain parameters for various Dynatrace capabilities (OneAgent deployment mode, ActiveGate capabilities, etc.) - -> Note: `.spec.tokens` denotes the name of the secret holding access tokens. -> -> If not specified Dynatrace Operator searches for a secret called like the DynaKube custom resource `.metadata.name`. - -The recommended approach is using classic Fullstack injection to roll out Dynatrace to your cluster, available as [classicFullStack sample](assets/samples/dynakube/v1beta2/classicFullStack.yaml). -In case you want to have adjustments please have a look at [our DynaKube Custom Resource examples](assets/samples/dynakube). - -Save one of the sample configurations, change the API url to your environment and apply it to your cluster. - -```sh -kubectl apply -f cr.yaml -``` - -For detailed instructions see -our [official help page](https://www.dynatrace.com/support/help/shortlink/full-stack-dto-k8). - -## Uninstall dynatrace-operator - -> For instructions on how to uninstall the dynatrace-operator on Openshift, -> head to the [official help page](https://docs.dynatrace.com/docs/setup-and-configuration/setup-on-k8s/guides/operation/update-uninstall-operator#uninstall-dynatrace-operator) - -Clean-up all Dynatrace Operator specific objects: - -```sh -kubectl delete -f https://github.com/Dynatrace/dynatrace-operator/releases/latest/download/kubernetes.yaml -``` - -If the CSI driver was installed, the following command is required as well: - -```sh -kubectl delete -f https://github.com/Dynatrace/dynatrace-operator/releases/latest/download/kubernetes-csi.yaml -``` - ## Hacking See [HACKING](HACKING.md) for details on how to get started enhancing Dynatrace Operator. diff --git a/SECURITY.md b/SECURITY.md index 5ffe18286f..f60ace8325 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,14 +1,6 @@ # Security Policy -## Supported Versions +Security related information can be found in the official docs: -The dynatrace-operator has not been released yet. -No version exists to be supported. - -| Version | Supported | -| ------- | ------------------ | - -## Reporting a Vulnerability - -This project is not yet released. -Currently no reporting policy exists. +- Network-traffic: https://docs.dynatrace.com/docs/ingest-from/setup-on-k8s/reference/security +- Permissions/Security-Benchmarks: https://docs.dynatrace.com/docs/ingest-from/setup-on-k8s/reference/network diff --git a/assets/samples/dynakube/v1beta3/applicationMonitoring.yaml b/assets/samples/dynakube/v1beta3/applicationMonitoring.yaml new file mode 100644 index 0000000000..e6499a5d50 --- /dev/null +++ b/assets/samples/dynakube/v1beta3/applicationMonitoring.yaml @@ -0,0 +1,177 @@ +apiVersion: dynatrace.com/v1beta3 +kind: DynaKube +metadata: + name: dynakube + namespace: dynatrace +spec: + # Link to api reference for further information: https://docs.dynatrace.com/docs/ingest-from/setup-on-k8s/reference/dynakube-parameters + + # Required: Dynatrace apiUrl including the `/api` path at the end. + apiUrl: https://ENVIRONMENTID.live.dynatrace.com/api + + # Optional: Name of the secret holding the tokens used for connecting to Dynatrace. + # + # tokens: "" + + # Optional: Defines a custom pull secret in case you use a private registry when pulling images from the Dynatrace environment + # + # customPullSecret: "custom-pull-secret" + + # Optional: Disable certificate check for the connection between Dynatrace Operator and the Dynatrace Cluster. + # + # skipCertCheck: false + + # Optional: Set custom proxy settings either directly or from a secret with the field 'proxy' + # + # proxy: + # value: my-proxy-url.com + # valueFrom: name-of-my-proxy-secret + + # Optional: Add custom RootCAs from a configmap. + # + # trustedCAs: name-of-my-ca-configmap + + # Optional: Set a network zone for the OneAgent and ActiveGate pods. + # + # networkZone: name-of-my-network-zone + + # Optional: Configure istio to allow access to the Dynatrace Cluster from the OneAgent or ActiveGate pods. + # + # enableIstio: true + + # Optional: Minimum minutes between Dynatrace API requests. + # + # dynatraceApiRequestThreshold: 15 + + # Configuration for Metadata Enrichment. + # + metadataEnrichment: + enabled: true + + # Optional: The namespaces where you want Dynatrace Operator to inject + # + # namespaceSelector: + # matchLabels: + # app: my-app + # matchExpressions: + # - key: app + # operator: In + # values: [my-frontend, my-backend, my-database] + + # Configuration for OneAgent + # + oneAgent: + # Optional: Set a host group for OneAgent + # + # hostGroup: "" + + applicationMonitoring: {} + # Optional: The namespaces where you want Dynatrace Operator to inject + # + # namespaceSelector: + # matchLabels: + # app: my-app + # matchExpressions: + # - key: app + # operator: In + # values: [my-frontend, my-backend, my-database] + + # Optional: The OneAgent version to be used. + # Example: ..., e.g. 1.200.0.20240101-000000 + # + # version: "" + + # Optional: Define resources requests and limits for the initContainer. + # + # initResources: + # requests: + # cpu: 30m + # memory: 30Mi + # limits: + # cpu: 100m + # memory: 60Mi + + # Optional: The OneAgent image that is used to inject into pods + # + # codeModulesImage: "" + + # Configuration for ActiveGate instances. + # + activeGate: + # Defines the ActiveGate capabilities + # + capabilities: + - routing + - kubernetes-monitoring + - dynatrace-api + + # Optional: Amount of replicas of ActiveGate pods. + # + # replicas: 1 + + # Optional: Use a custom ActiveGate image + # + # image: "" + + # Optional: Set the ActiveGate group + # + # group: "" + + # Optional: Add a custom properties file by providing it as a value or by referencing it from a secret. + # + # customProperties: + # value: | + # [connectivity] + # networkZone= + # valueFrom: myCustomPropertiesSecret + + # Optional: Resource settings for ActiveGate container. + # + resources: + requests: + cpu: 500m + memory: 1.5Gi + limits: + cpu: 1000m + memory: 1.5Gi + + # Optional: Specify the node selector that controls on which nodes ActiveGate will be deployed. + # + # nodeSelector: {} + + # Optional: Set tolerations for the ActiveGate pods. + # + # tolerations: + # - effect: NoSchedule + # key: node-role.kubernetes.io/master + # operator: Exists + + # Optional: Add custom labels to ActiveGate pods + # + # labels: + # custom: label + + # Optional: Add custom environment variables to ActiveGate pods + # + # env: [] + + # Optional: Name of a secret containing ActiveGate TLS certificate, key, and password. + # + # tlsSecretName: "my-tls-secret" + + # Optional: Set the DNS policy for ActiveGate pods. + # + # dnsPolicy: "Default" + + # Optional: Assign a priority class to the ActiveGate pods. + # + # priorityClassName: priority-class + + # Optional: Add custom annotations to ActiveGate pods + # + # annotations: + # custom: annotation + + # Optional: Add TopologySpreadConstraints to the ActiveGate pods + # + # topologySpreadConstraints: [] diff --git a/assets/samples/dynakube/v1beta3/classicFullStack.yaml b/assets/samples/dynakube/v1beta3/classicFullStack.yaml new file mode 100644 index 0000000000..7c39b32002 --- /dev/null +++ b/assets/samples/dynakube/v1beta3/classicFullStack.yaml @@ -0,0 +1,232 @@ +apiVersion: dynatrace.com/v1beta3 +kind: DynaKube +metadata: + name: dynakube + namespace: dynatrace +spec: + # Link to api reference for further information: https://docs.dynatrace.com/docs/ingest-from/setup-on-k8s/reference/dynakube-parameters + + # Required: Dynatrace apiUrl including the `/api` path at the end. + apiUrl: https://ENVIRONMENTID.live.dynatrace.com/api + + # Optional: Name of the secret holding the tokens used for connecting to Dynatrace. + # + # tokens: "" + + # Optional: Defines a custom pull secret in case you use a private registry when pulling images from the Dynatrace environment + # + # customPullSecret: "custom-pull-secret" + + # Optional: Disable certificate check for the connection between Dynatrace Operator and the Dynatrace Cluster. + # + # skipCertCheck: false + + # Optional: Set custom proxy settings either directly or from a secret with the field 'proxy' + # + # proxy: + # value: my-proxy-url.com + # valueFrom: name-of-my-proxy-secret + + # Optional: Add custom RootCAs from a configmap. + # + # trustedCAs: name-of-my-ca-configmap + + # Optional: Set a network zone for the OneAgent and ActiveGate pods. + # + # networkZone: name-of-my-network-zone + + # Optional: Configure istio to allow access to the Dynatrace Cluster from the OneAgent or ActiveGate pods. + # + # enableIstio: true + + # Optional: Minimum minutes between Dynatrace API requests. + # + # dynatraceApiRequestThreshold: 15 + + # Configuration for Metadata Enrichment. + # + # metadataEnrichment: + # enabled: true + + # Optional: The namespaces where you want Dynatrace Operator to inject + # + # namespaceSelector: + # matchLabels: + # app: my-app + # matchExpressions: + # - key: app + # operator: In + # values: [my-frontend, my-backend, my-database] + + # Configuration for Log monitoring. + # + # logMonitoring: {} + + # Optional: Specifies the rules and conditions for matching ingest attributes. + # + # ingestRuleMatchers: + # - attribute: "k8s.namespace.name" + # values: + # - "kube-system" + # - "dynatrace" + # - "default" + # - attribute: "k8s.pod.annotation", + # values: + # - "logs.dynatrace.com/ingest=true" + # - "category=security" + + # Configuration for OneAgent + # + oneAgent: + # Optional: Set a host group for OneAgent + # + # hostGroup: "" + + classicFullStack: + # Optional: Specify the node selector that controls on which nodes OneAgent will be deployed. + # + # nodeSelector: {} + + # Optional: Assign a priority class to the OneAgent pods. + # + # priorityClassName: priority-class + + # Optional: Tolerations to include with the OneAgent DaemonSet. + # + tolerations: + - effect: NoSchedule + key: node-role.kubernetes.io/master + operator: Exists + - effect: NoSchedule + key: node-role.kubernetes.io/control-plane + operator: Exists + + # Optional: Resource settings for OneAgent container. + # + # oneAgentResources: + # requests: + # cpu: 100m + # memory: 512Mi + # limits: + # cpu: 300m + # memory: 1.5Gi + + # Optional: Set additional arguments to the OneAgent installer. + # + # args: [] + + # Optional: Set additional environment variables for the OneAgent pods. + # + # env: [] + + # Optional: Disable automatic restarts of OneAgent pods in case a new version is available + # + # autoUpdate: true + + # Optional: Set the DNS Policy for OneAgent pods. + # + # dnsPolicy: "ClusterFirstWithHostNet" + + # Optional: Add custom OneAgent annotations. + # + # annotations: + # custom: annotation + + # Optional: Add custom labels to OneAgent pods + # + # labels: + # custom: label + + # Optional: Use a custom OneAgent image. + # + # image: "" + + # Optional: The OneAgent version to be used. + # Example: ..., e.g. 1.200.0.20240101-000000 + # + # version: "" + + # Optional: The SecComp Profile that will be configured in order to run in secure computing mode. + # + # secCompProfile: "" + + # Configuration for ActiveGate instances. + # + activeGate: + # Defines the ActiveGate capabilities + # + capabilities: + - routing + - kubernetes-monitoring + - dynatrace-api + + # Optional: Amount of replicas of ActiveGate pods. + # + # replicas: 1 + + # Optional: Use a custom ActiveGate image + # + # image: "" + + # Optional: Set the ActiveGate group + # + # group: "" + + # Optional: Add a custom properties file by providing it as a value or by referencing it from a secret. + # + # customProperties: + # value: | + # [connectivity] + # networkZone= + # valueFrom: myCustomPropertiesSecret + + # Optional: Resource settings for ActiveGate container. + # + resources: + requests: + cpu: 500m + memory: 1.5Gi + limits: + cpu: 1000m + memory: 1.5Gi + + # Optional: Specify the node selector that controls on which nodes ActiveGate will be deployed. + # + # nodeSelector: {} + + # Optional: Set tolerations for the ActiveGate pods. + # + # tolerations: + # - effect: NoSchedule + # key: node-role.kubernetes.io/master + # operator: Exists + + # Optional: Add custom labels to ActiveGate pods + # + # labels: + # custom: label + + # Optional: Add custom environment variables to ActiveGate pods + # + # env: [] + + # Optional: Name of a secret containing ActiveGate TLS certificate, key, and password. + # + # tlsSecretName: "my-tls-secret" + + # Optional: Set the DNS policy for ActiveGate pods. + # + # dnsPolicy: "Default" + + # Optional: Assign a priority class to the ActiveGate pods. + # + # priorityClassName: priority-class + + # Optional: Add custom annotations to ActiveGate pods + # + # annotations: + # custom: annotation + + # Optional: Add TopologySpreadConstraints to the ActiveGate pods + # + # topologySpreadConstraints: [] diff --git a/assets/samples/dynakube/v1beta3/cloudNativeFullStack.yaml b/assets/samples/dynakube/v1beta3/cloudNativeFullStack.yaml new file mode 100644 index 0000000000..daf9c1fe9e --- /dev/null +++ b/assets/samples/dynakube/v1beta3/cloudNativeFullStack.yaml @@ -0,0 +1,257 @@ +apiVersion: dynatrace.com/v1beta3 +kind: DynaKube +metadata: + name: dynakube + namespace: dynatrace +spec: + # Link to api reference for further information: https://docs.dynatrace.com/docs/ingest-from/setup-on-k8s/reference/dynakube-parameters + + # Required: Dynatrace apiUrl including the `/api` path at the end. + apiUrl: https://ENVIRONMENTID.live.dynatrace.com/api + + # Optional: Name of the secret holding the tokens used for connecting to Dynatrace. + # + # tokens: "" + + # Optional: Defines a custom pull secret in case you use a private registry when pulling images from the Dynatrace environment + # + # customPullSecret: "custom-pull-secret" + + # Optional: Disable certificate check for the connection between Dynatrace Operator and the Dynatrace Cluster. + # + # skipCertCheck: false + + # Optional: Set custom proxy settings either directly or from a secret with the field 'proxy' + # + # proxy: + # value: my-proxy-url.com + # valueFrom: name-of-my-proxy-secret + + # Optional: Add custom RootCAs from a configmap. + # + # trustedCAs: name-of-my-ca-configmap + + # Optional: Set a network zone for the OneAgent and ActiveGate pods. + # + # networkZone: name-of-my-network-zone + + # Optional: Configure istio to allow access to the Dynatrace Cluster from the OneAgent or ActiveGate pods. + # + # enableIstio: true + + # Optional: Minimum minutes between Dynatrace API requests. + # + # dynatraceApiRequestThreshold: 15 + + # Configuration for Metadata Enrichment. + # + metadataEnrichment: + enabled: true + + # Optional: The namespaces where you want Dynatrace Operator to inject + # + # namespaceSelector: + # matchLabels: + # app: my-app + # matchExpressions: + # - key: app + # operator: In + # values: [my-frontend, my-backend, my-database] + + # Configuration for Log monitoring. + # + # logMonitoring: {} + + # Optional: Specifies the rules and conditions for matching ingest attributes. + # + # ingestRuleMatchers: + # - attribute: "k8s.namespace.name" + # values: + # - "kube-system" + # - "dynatrace" + # - "default" + # - attribute: "k8s.pod.annotation", + # values: + # - "logs.dynatrace.com/ingest=true" + # - "category=security" + + # Configuration for OneAgent + # + oneAgent: + # Optional: Set a host group for OneAgent + # + # hostGroup: "" + + cloudNativeFullStack: + + # Optional: The namespaces where you want Dynatrace Operator to inject + # + # namespaceSelector: + # matchLabels: + # app: my-app + # matchExpressions: + # - key: app + # operator: In + # values: [my-frontend, my-backend, my-database] + + # Optional: Specify the node selector that controls on which nodes OneAgent will be deployed. + # + # nodeSelector: {} + + # Optional: Assign a priority class to the OneAgent pods. + # + # priorityClassName: priority-class + + # Optional: Tolerations to include with the OneAgent DaemonSet. + # + tolerations: + - effect: NoSchedule + key: node-role.kubernetes.io/master + operator: Exists + - effect: NoSchedule + key: node-role.kubernetes.io/control-plane + operator: Exists + + # Optional: Resource settings for OneAgent container. + # + # oneAgentResources: + # requests: + # cpu: 100m + # memory: 512Mi + # limits: + # cpu: 300m + # memory: 1.5Gi + + # Optional: Set additional arguments to the OneAgent installer. + # + # args: [] + + # Optional: Set additional environment variables for the OneAgent pods. + # + # env: [] + + # Optional: Disable automatic restarts of OneAgent pods in case a new version is available + # + # autoUpdate: true + + # Optional: Set the DNS Policy for OneAgent pods. + # + # dnsPolicy: "ClusterFirstWithHostNet" + + # Optional: Add custom OneAgent annotations. + # + # annotations: + # custom: annotation + + # Optional: Add custom labels to OneAgent pods + # + # labels: + # custom: label + + # Optional: Use a custom OneAgent image. + # + # image: "" + + # Optional: The OneAgent version to be used. + # Example: ..., e.g. 1.200.0.20240101-000000 + # + # version: "" + + # Optional: Define resources requests and limits for the initContainer. + # + # initResources: + # requests: + # cpu: 30m + # memory: 30Mi + # limits: + # cpu: 100m + # memory: 60Mi + + # Optional: The OneAgent image that is used to inject into pods + # + # codeModulesImage: "" + + # Optional: The SecComp Profile that will be configured in order to run in secure computing mode. + # + # secCompProfile: "" + + # Configuration for ActiveGate instances. + # + activeGate: + # Defines the ActiveGate capabilities + # + capabilities: + - routing + - kubernetes-monitoring + - dynatrace-api + + # Optional: Amount of replicas of ActiveGate pods. + # + # replicas: 1 + + # Optional: Use a custom ActiveGate image + # + # image: "" + + # Optional: Set the ActiveGate group + # + # group: "" + + # Optional: Add a custom properties file by providing it as a value or by referencing it from a secret. + # + # customProperties: + # value: | + # [connectivity] + # networkZone= + # valueFrom: myCustomPropertiesSecret + + # Optional: Resource settings for ActiveGate container. + # + resources: + requests: + cpu: 500m + memory: 1.5Gi + limits: + cpu: 1000m + memory: 1.5Gi + + # Optional: Specify the node selector that controls on which nodes ActiveGate will be deployed. + # + # nodeSelector: {} + + # Optional: Set tolerations for the ActiveGate pods. + # + # tolerations: + # - effect: NoSchedule + # key: node-role.kubernetes.io/master + # operator: Exists + + # Optional: Add custom labels to ActiveGate pods + # + # labels: + # custom: label + + # Optional: Add custom environment variables to ActiveGate pods + # + # env: [] + + # Optional: Name of a secret containing ActiveGate TLS certificate, key, and password. + # + # tlsSecretName: "my-tls-secret" + + # Optional: Set the DNS policy for ActiveGate pods. + # + # dnsPolicy: "Default" + + # Optional: Assign a priority class to the ActiveGate pods. + # + # priorityClassName: priority-class + + # Optional: Add custom annotations to ActiveGate pods + # + # annotations: + # custom: annotation + + # Optional: Add TopologySpreadConstraints to the ActiveGate pods + # + # topologySpreadConstraints: [] diff --git a/assets/samples/dynakube/v1beta3/hostMonitoring.yaml b/assets/samples/dynakube/v1beta3/hostMonitoring.yaml new file mode 100644 index 0000000000..d56932b255 --- /dev/null +++ b/assets/samples/dynakube/v1beta3/hostMonitoring.yaml @@ -0,0 +1,232 @@ +apiVersion: dynatrace.com/v1beta3 +kind: DynaKube +metadata: + name: dynakube + namespace: dynatrace +spec: + # Link to api reference for further information: https://docs.dynatrace.com/docs/ingest-from/setup-on-k8s/reference/dynakube-parameters + + # Required: Dynatrace apiUrl including the `/api` path at the end. + apiUrl: https://ENVIRONMENTID.live.dynatrace.com/api + + # Optional: Name of the secret holding the tokens used for connecting to Dynatrace. + # + # tokens: "" + + # Optional: Defines a custom pull secret in case you use a private registry when pulling images from the Dynatrace environment + # + # customPullSecret: "custom-pull-secret" + + # Optional: Disable certificate check for the connection between Dynatrace Operator and the Dynatrace Cluster. + # + # skipCertCheck: false + + # Optional: Set custom proxy settings either directly or from a secret with the field 'proxy' + # + # proxy: + # value: my-proxy-url.com + # valueFrom: name-of-my-proxy-secret + + # Optional: Add custom RootCAs from a configmap. + # + # trustedCAs: name-of-my-ca-configmap + + # Optional: Set a network zone for the OneAgent and ActiveGate pods. + # + # networkZone: name-of-my-network-zone + + # Optional: Configure istio to allow access to the Dynatrace Cluster from the OneAgent or ActiveGate pods. + # + # enableIstio: true + + # Optional: Minimum minutes between Dynatrace API requests. + # + # dynatraceApiRequestThreshold: 15 + + # Configuration for Metadata Enrichment. + # + # metadataEnrichment: + # enabled: true + + # Optional: The namespaces where you want Dynatrace Operator to inject + # + # namespaceSelector: + # matchLabels: + # app: my-app + # matchExpressions: + # - key: app + # operator: In + # values: [my-frontend, my-backend, my-database] + + # Configuration for Log monitoring. + # + # logMonitoring: {} + + # Optional: Specifies the rules and conditions for matching ingest attributes. + # + # ingestRuleMatchers: + # - attribute: "k8s.namespace.name" + # values: + # - "kube-system" + # - "dynatrace" + # - "default" + # - attribute: "k8s.pod.annotation", + # values: + # - "logs.dynatrace.com/ingest=true" + # - "category=security" + + # Configuration for OneAgent + # + oneAgent: + # Optional: Set a host group for OneAgent + # + # hostGroup: "" + + hostMonitoring: + # Optional: Specify the node selector that controls on which nodes OneAgent will be deployed. + # + # nodeSelector: {} + + # Optional: Assign a priority class to the OneAgent pods. + # + # priorityClassName: priority-class + + # Optional: Tolerations to include with the OneAgent DaemonSet. + # + tolerations: + - effect: NoSchedule + key: node-role.kubernetes.io/master + operator: Exists + - effect: NoSchedule + key: node-role.kubernetes.io/control-plane + operator: Exists + + # Optional: Resource settings for OneAgent container. + # + # oneAgentResources: + # requests: + # cpu: 100m + # memory: 512Mi + # limits: + # cpu: 300m + # memory: 1.5Gi + + # Optional: Set additional arguments to the OneAgent installer. + # + # args: [] + + # Optional: Set additional environment variables for the OneAgent pods. + # + # env: [] + + # Optional: Disable automatic restarts of OneAgent pods in case a new version is available + # + # autoUpdate: true + + # Optional: Set the DNS Policy for OneAgent pods. + # + # dnsPolicy: "ClusterFirstWithHostNet" + + # Optional: Add custom OneAgent annotations. + # + # annotations: + # custom: annotation + + # Optional: Add custom labels to OneAgent pods + # + # labels: + # custom: label + + # Optional: Use a custom OneAgent image. + # + # image: "" + + # Optional: The OneAgent version to be used. + # Example: ..., e.g. 1.200.0.20240101-000000 + # + # version: "" + + # Optional: The SecComp Profile that will be configured in order to run in secure computing mode. + # + # secCompProfile: "" + + # Configuration for ActiveGate instances. + # + activeGate: + # Defines the ActiveGate capabilities + # + capabilities: + - routing + - kubernetes-monitoring + - dynatrace-api + + # Optional: Amount of replicas of ActiveGate pods. + # + # replicas: 1 + + # Optional: Use a custom ActiveGate image + # + # image: "" + + # Optional: Set the ActiveGate group + # + # group: "" + + # Optional: Add a custom properties file by providing it as a value or by referencing it from a secret. + # + # customProperties: + # value: | + # [connectivity] + # networkZone= + # valueFrom: myCustomPropertiesSecret + + # Optional: Resource settings for ActiveGate container. + # + resources: + requests: + cpu: 500m + memory: 1.5Gi + limits: + cpu: 1000m + memory: 1.5Gi + + # Optional: Specify the node selector that controls on which nodes ActiveGate will be deployed. + # + # nodeSelector: {} + + # Optional: Set tolerations for the ActiveGate pods. + # + # tolerations: + # - effect: NoSchedule + # key: node-role.kubernetes.io/master + # operator: Exists + + # Optional: Add custom labels to ActiveGate pods + # + # labels: + # custom: label + + # Optional: Add custom environment variables to ActiveGate pods + # + # env: [] + + # Optional: Name of a secret containing ActiveGate TLS certificate, key, and password. + # + # tlsSecretName: "my-tls-secret" + + # Optional: Set the DNS policy for ActiveGate pods. + # + # dnsPolicy: "Default" + + # Optional: Assign a priority class to the ActiveGate pods. + # + # priorityClassName: priority-class + + # Optional: Add custom annotations to ActiveGate pods + # + # annotations: + # custom: annotation + + # Optional: Add TopologySpreadConstraints to the ActiveGate pods + # + # topologySpreadConstraints: [] diff --git a/assets/samples/dynakube/v1beta3/kubernetesObservability.yaml b/assets/samples/dynakube/v1beta3/kubernetesObservability.yaml new file mode 100644 index 0000000000..de0320f8dd --- /dev/null +++ b/assets/samples/dynakube/v1beta3/kubernetesObservability.yaml @@ -0,0 +1,140 @@ +apiVersion: dynatrace.com/v1beta3 +kind: DynaKube +metadata: + name: dynakube + namespace: dynatrace + # annotations: + # feature.dynatrace.com/k8s-app-enabled: "true" +spec: + # Link to api reference for further information: https://docs.dynatrace.com/docs/ingest-from/setup-on-k8s/reference/dynakube-parameters + + # Required: Dynatrace apiUrl including the `/api` path at the end. + apiUrl: https://ENVIRONMENTID.live.dynatrace.com/api + + # Optional: Name of the secret holding the tokens used for connecting to Dynatrace. + # + # tokens: "" + + # Optional: Defines a custom pull secret in case you use a private registry when pulling images from the Dynatrace environment + # + # customPullSecret: "custom-pull-secret" + + # Optional: Disable certificate check for the connection between Dynatrace Operator and the Dynatrace Cluster. + # + # skipCertCheck: false + + # Optional: Set custom proxy settings either directly or from a secret with the field 'proxy' + # + # proxy: + # value: my-proxy-url.com + # valueFrom: name-of-my-proxy-secret + + # Optional: Add custom RootCAs from a configmap. + # + # trustedCAs: name-of-my-ca-configmap + + # Optional: Set a network zone for the OneAgent and ActiveGate pods. + # + # networkZone: name-of-my-network-zone + + # Optional: Configure istio to allow access to the Dynatrace Cluster from the OneAgent or ActiveGate pods. + # + # enableIstio: true + + # Optional: Minimum minutes between Dynatrace API requests. + # + # dynatraceApiRequestThreshold: 15 + + # Configuration for Metadata Enrichment. + # + # metadataEnrichment: + # enabled: true + + # Optional: The namespaces where you want Dynatrace Operator to inject + # + # namespaceSelector: + # matchLabels: + # app: my-app + # matchExpressions: + # - key: app + # operator: In + # values: [my-frontend, my-backend, my-database] + + # Configuration for ActiveGate instances. + # + activeGate: + # Defines the ActiveGate capabilities + # + capabilities: + - kubernetes-monitoring + + # Optional: Amount of replicas of ActiveGate pods. + # + # replicas: 1 + + # Optional: Use a custom ActiveGate image + # + # image: "" + + # Optional: Set the ActiveGate group + # + # group: "" + + # Optional: Add a custom properties file by providing it as a value or by referencing it from a secret. + # + # customProperties: + # value: | + # [connectivity] + # networkZone= + # valueFrom: myCustomPropertiesSecret + + # Optional: Resource settings for ActiveGate container. + # + resources: + requests: + cpu: 500m + memory: 1.5Gi + limits: + cpu: 1000m + memory: 1.5Gi + + # Optional: Specify the node selector that controls on which nodes ActiveGate will be deployed. + # + # nodeSelector: {} + + # Optional: Set tolerations for the ActiveGate pods. + # + # tolerations: + # - effect: NoSchedule + # key: node-role.kubernetes.io/master + # operator: Exists + + # Optional: Add custom labels to ActiveGate pods + # + # labels: + # custom: label + + # Optional: Add custom environment variables to ActiveGate pods + # + # env: [] + + # Optional: Name of a secret containing ActiveGate TLS certificate, key, and password. + # + # tlsSecretName: "my-tls-secret" + + # Optional: Set the DNS policy for ActiveGate pods. + # + # dnsPolicy: "Default" + + # Optional: Assign a priority class to the ActiveGate pods. + # + # priorityClassName: priority-class + + # Optional: Add custom annotations to ActiveGate pods + # + # annotations: + # custom: annotation + + # Optional: Add TopologySpreadConstraints to the ActiveGate pods + # + # topologySpreadConstraints: [] diff --git a/assets/samples/dynakube/v1beta3/kubernetesSecurityPostureManagement.yaml b/assets/samples/dynakube/v1beta3/kubernetesSecurityPostureManagement.yaml new file mode 100644 index 0000000000..1176eed337 --- /dev/null +++ b/assets/samples/dynakube/v1beta3/kubernetesSecurityPostureManagement.yaml @@ -0,0 +1,195 @@ +apiVersion: dynatrace.com/v1beta3 +kind: DynaKube +metadata: + name: dynakube + namespace: dynatrace +spec: + # Link to api reference for further information: https://docs.dynatrace.com/docs/ingest-from/setup-on-k8s/reference/dynakube-parameters + + # Required: Dynatrace apiUrl including the `/api` path at the end. + apiUrl: https://ENVIRONMENTID.live.dynatrace.com/api + + # Optional: Name of the secret holding the tokens used for connecting to Dynatrace. + # + # tokens: "" + + # Optional: Defines a custom pull secret in case you use a private registry when pulling images from the Dynatrace environment + # + # customPullSecret: "custom-pull-secret" + + # Optional: Disable certificate check for the connection between Dynatrace Operator and the Dynatrace Cluster. + # + # skipCertCheck: false + + # Optional: Set custom proxy settings either directly or from a secret with the field 'proxy' + # + # proxy: + # value: my-proxy-url.com + # valueFrom: name-of-my-proxy-secret + + # Optional: Add custom RootCAs from a configmap. + # + # trustedCAs: name-of-my-ca-configmap + + # Optional: Set a network zone for the OneAgent and ActiveGate pods. + # + # networkZone: name-of-my-network-zone + + # Optional: Configure istio to allow access to the Dynatrace Cluster from the OneAgent or ActiveGate pods. + # + # enableIstio: true + + # Optional: Minimum minutes between Dynatrace API requests. + # + # dynatraceApiRequestThreshold: 15 + + # Configuration for Metadata Enrichment. + # + # metadataEnrichment: + # enabled: true + + # Optional: The namespaces where you want Dynatrace Operator to inject + # + # namespaceSelector: + # matchLabels: + # app: my-app + # matchExpressions: + # - key: app + # operator: In + # values: [my-frontend, my-backend, my-database] + + kspm: {} + + # Configuration for ActiveGate instances. + # + activeGate: + # Defines the ActiveGate capabilities + # + capabilities: + - kubernetes-monitoring + + # Optional: Amount of replicas of ActiveGate pods. + # + # replicas: 1 + + # Optional: Use a custom ActiveGate image + # + # image: "" + + # Optional: Set the ActiveGate group + # + # group: "" + + # Optional: Add a custom properties file by providing it as a value or by referencing it from a secret. + # + # customProperties: + # value: | + # [kubernetes_monitoring] + # kubernetes_configuration_dataset_pipeline_enabled = true + # kubernetes_configuration_dataset_pipeline_include_node_config = true + + # Optional: Resource settings for ActiveGate container. + # + resources: + requests: + cpu: 500m + memory: 1.5Gi + limits: + cpu: 1000m + memory: 1.5Gi + + # Optional: Specify the node selector that controls on which nodes ActiveGate will be deployed. + # + # nodeSelector: {} + + # Optional: Set tolerations for the ActiveGate pods. + # + # tolerations: + # - effect: NoSchedule + # key: node-role.kubernetes.io/master + # operator: Exists + + # Optional: Add custom labels to ActiveGate pods + # + # labels: + # custom: label + + # Optional: Add custom environment variables to ActiveGate pods + # + # env: [] + + # Optional: Name of a secret containing ActiveGate TLS certificate, key, and password. + # + # tlsSecretName: "my-tls-secret" + + # Optional: Set the DNS policy for ActiveGate pods. + # + # dnsPolicy: "Default" + + # Optional: Assign a priority class to the ActiveGate pods. + # + # priorityClassName: priority-class + + # Optional: Add custom annotations to ActiveGate pods + # + # annotations: + # custom: annotation + + # Optional: Add TopologySpreadConstraints to the ActiveGate pods + # + # topologySpreadConstraints: [] + + templates: + kspmNodeConfigurationCollector: + # Required: Configure the image for the Node Configuration Collector. + imageRef: + repository: public.ecr.aws/dynatrace/dynatrace-k8s-node-config-collector + tag: + + # Optional: Define the update strategy for the Node Configuration Collector daemonSet + # + # updateStrategy: {} + + # Optional: Add custom labels to NNode Configuration Collector pods + # + # labels: + # custom: label + + # Optional: Add custom annotations to Node Configuration Collector pods. + # + # annotations: + # custom: annotation + + # Optional: Specify the node selector that controls on which nodes OneAgent will be deployed. + # + # nodeSelector: {} + + # Optional: Define the nodeAffinity for the DaemonSet of the Node Configuration Collector + # + # nodeAffinity: {} + + # Optional: Assign a priority class to the Node Configuration Collector pods. + # + # priorityClassName: priority-class + + # Optional: Tolerations to include with the Node Configuration Collector DaemonSet. + # + tolerations: + - effect: NoSchedule + key: node-role.kubernetes.io/master + operator: Exists + - effect: NoSchedule + key: node-role.kubernetes.io/control-plane + operator: Exists + + # Optional: Resource settings for Node Configuration Collector container. + # + # resources: {} + + # Optional: Set additional arguments to the Node Configuration Collector. + # + # args: [] + + # Optional: Set additional environment variables for the Node Configuration Collector pods. + # + # env: [] diff --git a/assets/samples/dynakube/v1beta3/logMonitoring.yaml b/assets/samples/dynakube/v1beta3/logMonitoring.yaml new file mode 100644 index 0000000000..5f4549899d --- /dev/null +++ b/assets/samples/dynakube/v1beta3/logMonitoring.yaml @@ -0,0 +1,211 @@ +apiVersion: dynatrace.com/v1beta3 +kind: DynaKube +metadata: + name: dynakube + namespace: dynatrace +# annotations: +# feature.dynatrace.com/oneagent-privileged: "true" # Required on Openshift +spec: + # Link to api reference for further information: https://docs.dynatrace.com/docs/ingest-from/setup-on-k8s/reference/dynakube-parameters + + # Required: Dynatrace apiUrl including the `/api` path at the end. + apiUrl: https://ENVIRONMENTID.live.dynatrace.com/api + + # Optional: Name of the secret holding the tokens used for connecting to Dynatrace. + # + # tokens: "" + + # Optional: Defines a custom pull secret in case you use a private registry when pulling images from the Dynatrace environment + # + # customPullSecret: "custom-pull-secret" + + # Optional: Disable certificate check for the connection between Dynatrace Operator and the Dynatrace Cluster. + # + # skipCertCheck: false + + # Optional: Set custom proxy settings either directly or from a secret with the field 'proxy' + # + # proxy: + # value: my-proxy-url.com + # valueFrom: name-of-my-proxy-secret + + # Optional: Add custom RootCAs from a configmap. + # + # trustedCAs: name-of-my-ca-configmap + + # Optional: Set a network zone for the OneAgent and ActiveGate pods. + # + # networkZone: name-of-my-network-zone + + # Optional: Configure istio to allow access to the Dynatrace Cluster from the OneAgent or ActiveGate pods. + # + # enableIstio: true + + # Optional: Minimum minutes between Dynatrace API requests. + # + # dynatraceApiRequestThreshold: 15 + + # Configuration for Metadata Enrichment. + # + metadataEnrichment: + enabled: true + + # Optional: The namespaces where you want Dynatrace Operator to inject + # + # namespaceSelector: + # matchLabels: + # app: my-app + # matchExpressions: + # - key: app + # operator: In + # values: [my-frontend, my-backend, my-database] + + # Configuration for Log monitoring. + # + logMonitoring: {} + + # Optional: Specifies the rules and conditions for matching ingest attributes. + # + # ingestRuleMatchers: + # - attribute: "k8s.namespace.name" + # values: + # - "kube-system" + # - "dynatrace" + # - "default" + # - attribute: "k8s.pod.annotation", + # values: + # - "logs.dynatrace.com/ingest=true" + # - "category=security" + + # Configuration for ActiveGate instances. + # + activeGate: + # Defines the ActiveGate capabilities + # + capabilities: + - kubernetes-monitoring + + # Optional: Amount of replicas of ActiveGate pods. + # + # replicas: 1 + + # Optional: Use a custom ActiveGate image + # + # image: "" + + # Optional: Set the ActiveGate group + # + # group: "" + + # Optional: Add a custom properties file by providing it as a value or by referencing it from a secret. + # + # customProperties: + # value: | + # [kubernetes_monitoring] + # kubernetes_configuration_dataset_pipeline_enabled = true + # kubernetes_configuration_dataset_pipeline_include_node_config = true + + # Optional: Resource settings for ActiveGate container. + # + resources: + requests: + cpu: 500m + memory: 1.5Gi + limits: + cpu: 1000m + memory: 1.5Gi + + # Optional: Specify the node selector that controls on which nodes ActiveGate will be deployed. + # + # nodeSelector: {} + + # Optional: Set tolerations for the ActiveGate pods. + # + # tolerations: + # - effect: NoSchedule + # key: node-role.kubernetes.io/master + # operator: Exists + + # Optional: Add custom labels to ActiveGate pods + # + # labels: + # custom: label + + # Optional: Add custom environment variables to ActiveGate pods + # + # env: [] + + # Optional: Name of a secret containing ActiveGate TLS certificate, key, and password. + # + # tlsSecretName: "my-tls-secret" + + # Optional: Set the DNS policy for ActiveGate pods. + # + # dnsPolicy: "Default" + + # Optional: Assign a priority class to the ActiveGate pods. + # + # priorityClassName: priority-class + + # Optional: Add custom annotations to ActiveGate pods + # + # annotations: + # custom: annotation + + # Optional: Add TopologySpreadConstraints to the ActiveGate pods + # + # topologySpreadConstraints: [] + + templates: + logMonitoring: + imageRef: + repository: public.ecr.aws/dynatrace/dynatrace-logmodule + tag: + + # Optional: Define the update strategy for the Log monitoring daemonSet + # + # updateStrategy: {} + + # Optional: Add custom labels to Log monitoring pods + # + # labels: + # custom: label + + # Optional: Add custom annotations to Log monitoring pods. + # + # annotations: + # custom: annotation + + # Optional: Specify the node selector that controls on which nodes Log monitoring will be deployed. + # + # nodeSelector: {} + + # Optional: Define the nodeAffinity for the Log monitoring DaemonSet + # + # nodeAffinity: {} + + # Optional: Assign a priority class to the Log monitoring pods. + # + # priorityClassName: priority-class + + # Optional: Tolerations to include with the Log monitoring DaemonSet. + # + tolerations: + - effect: NoSchedule + key: node-role.kubernetes.io/master + operator: Exists + - effect: NoSchedule + key: node-role.kubernetes.io/control-plane + operator: Exists + + # Optional: Resource settings for Log monitoring container. + # + # resources: {} + + # Optional: Set additional arguments to Log monitoring + # + # args: [] + + # Optional: Set additional environment variables for the Log monitoring pods. + # + # env: [] \ No newline at end of file diff --git a/assets/samples/dynakube/v1beta3/multipleDynakubes.yaml b/assets/samples/dynakube/v1beta3/multipleDynakubes.yaml new file mode 100644 index 0000000000..5b7d64fa1c --- /dev/null +++ b/assets/samples/dynakube/v1beta3/multipleDynakubes.yaml @@ -0,0 +1,49 @@ +apiVersion: dynatrace.com/v1beta3 +kind: DynaKube +metadata: + name: dynakube-application-monitoring + namespace: dynatrace +spec: + apiUrl: https://ENVIRONMENTID.live.dynatrace.com/api + + # Link to api reference for further information: https://docs.dynatrace.com/docs/ingest-from/setup-on-k8s/reference/dynakube-parameters + + oneAgent: + applicationMonitoring: + namespaceSelector: + matchLabels: + monitor: applicationMonitoring + + activeGate: + capabilities: + - kubernetes-monitoring +--- + +apiVersion: dynatrace.com/v1beta3 +kind: DynaKube +metadata: + name: dynakube-cloud-native + namespace: dynatrace +spec: + apiUrl: https://ENVIRONMENTID.live.dynatrace.com/api + + # Link to api reference for further information: https://docs.dynatrace.com/docs/ingest-from/setup-on-k8s/reference/dynakube-parameters + + oneAgent: + cloudNativeFullStack: + namespaceSelector: + matchLabels: + monitor: cloudNativeFullStack + + tolerations: + - effect: NoSchedule + key: node-role.kubernetes.io/master + operator: Exists + - effect: NoSchedule + key: node-role.kubernetes.io/control-plane + operator: Exists + + activeGate: + capabilities: + - routing + - dynatrace-api diff --git a/assets/samples/dynakube/v1beta4/applicationMonitoring.yaml b/assets/samples/dynakube/v1beta4/applicationMonitoring.yaml new file mode 100644 index 0000000000..06520bd65b --- /dev/null +++ b/assets/samples/dynakube/v1beta4/applicationMonitoring.yaml @@ -0,0 +1,177 @@ +apiVersion: dynatrace.com/v1beta4 +kind: DynaKube +metadata: + name: dynakube + namespace: dynatrace +spec: + # Link to api reference for further information: https://docs.dynatrace.com/docs/ingest-from/setup-on-k8s/reference/dynakube-parameters + + # Required: Dynatrace apiUrl including the `/api` path at the end. + apiUrl: https://ENVIRONMENTID.live.dynatrace.com/api + + # Optional: Name of the secret holding the tokens used for connecting to Dynatrace. + # + # tokens: "" + + # Optional: Defines a custom pull secret in case you use a private registry when pulling images from the Dynatrace environment + # + # customPullSecret: "custom-pull-secret" + + # Optional: Disable certificate check for the connection between Dynatrace Operator and the Dynatrace Cluster. + # + # skipCertCheck: false + + # Optional: Set custom proxy settings either directly or from a secret with the field 'proxy' + # + # proxy: + # value: my-proxy-url.com + # valueFrom: name-of-my-proxy-secret + + # Optional: Add custom RootCAs from a configmap. + # + # trustedCAs: name-of-my-ca-configmap + + # Optional: Set a network zone for the OneAgent and ActiveGate pods. + # + # networkZone: name-of-my-network-zone + + # Optional: Configure istio to allow access to the Dynatrace Cluster from the OneAgent or ActiveGate pods. + # + # enableIstio: true + + # Optional: Minimum minutes between Dynatrace API requests. + # + # dynatraceApiRequestThreshold: 15 + + # Configuration for Metadata Enrichment. + # + metadataEnrichment: + enabled: true + + # Optional: The namespaces where you want Dynatrace Operator to inject + # + # namespaceSelector: + # matchLabels: + # app: my-app + # matchExpressions: + # - key: app + # operator: In + # values: [my-frontend, my-backend, my-database] + + # Configuration for OneAgent + # + oneAgent: + # Optional: Set a host group for OneAgent + # + # hostGroup: "" + + applicationMonitoring: {} + # Optional: The namespaces where you want Dynatrace Operator to inject + # + # namespaceSelector: + # matchLabels: + # app: my-app + # matchExpressions: + # - key: app + # operator: In + # values: [my-frontend, my-backend, my-database] + + # Optional: The OneAgent version to be used. + # Example: ..., e.g. 1.200.0.20240101-000000 + # + # version: "" + + # Optional: Define resources requests and limits for the initContainer. + # + # initResources: + # requests: + # cpu: 30m + # memory: 30Mi + # limits: + # cpu: 100m + # memory: 60Mi + + # Optional: The OneAgent image that is used to inject into pods + # + # codeModulesImage: "" + + # Configuration for ActiveGate instances. + # + activeGate: + # Defines the ActiveGate capabilities + # + capabilities: + - routing + - kubernetes-monitoring + - dynatrace-api + + # Optional: Amount of replicas of ActiveGate pods. + # + # replicas: 1 + + # Optional: Use a custom ActiveGate image + # + # image: "" + + # Optional: Set the ActiveGate group + # + # group: "" + + # Optional: Add a custom properties file by providing it as a value or by referencing it from a secret. + # + # customProperties: + # value: | + # [connectivity] + # networkZone= + # valueFrom: myCustomPropertiesSecret + + # Optional: Resource settings for ActiveGate container. + # + resources: + requests: + cpu: 500m + memory: 1.5Gi + limits: + cpu: 1000m + memory: 1.5Gi + + # Optional: Specify the node selector that controls on which nodes ActiveGate will be deployed. + # + # nodeSelector: {} + + # Optional: Set tolerations for the ActiveGate pods. + # + # tolerations: + # - effect: NoSchedule + # key: node-role.kubernetes.io/master + # operator: Exists + + # Optional: Add custom labels to ActiveGate pods + # + # labels: + # custom: label + + # Optional: Add custom environment variables to ActiveGate pods + # + # env: [] + + # Optional: Name of a secret containing ActiveGate TLS certificate, key, and password. + # + # tlsSecretName: "my-tls-secret" + + # Optional: Set the DNS policy for ActiveGate pods. + # + # dnsPolicy: "Default" + + # Optional: Assign a priority class to the ActiveGate pods. + # + # priorityClassName: priority-class + + # Optional: Add custom annotations to ActiveGate pods + # + # annotations: + # custom: annotation + + # Optional: Add TopologySpreadConstraints to the ActiveGate pods + # + # topologySpreadConstraints: [] diff --git a/assets/samples/dynakube/v1beta4/classicFullStack.yaml b/assets/samples/dynakube/v1beta4/classicFullStack.yaml new file mode 100644 index 0000000000..bcf2962a88 --- /dev/null +++ b/assets/samples/dynakube/v1beta4/classicFullStack.yaml @@ -0,0 +1,232 @@ +apiVersion: dynatrace.com/v1beta4 +kind: DynaKube +metadata: + name: dynakube + namespace: dynatrace +spec: + # Link to api reference for further information: https://docs.dynatrace.com/docs/ingest-from/setup-on-k8s/reference/dynakube-parameters + + # Required: Dynatrace apiUrl including the `/api` path at the end. + apiUrl: https://ENVIRONMENTID.live.dynatrace.com/api + + # Optional: Name of the secret holding the tokens used for connecting to Dynatrace. + # + # tokens: "" + + # Optional: Defines a custom pull secret in case you use a private registry when pulling images from the Dynatrace environment + # + # customPullSecret: "custom-pull-secret" + + # Optional: Disable certificate check for the connection between Dynatrace Operator and the Dynatrace Cluster. + # + # skipCertCheck: false + + # Optional: Set custom proxy settings either directly or from a secret with the field 'proxy' + # + # proxy: + # value: my-proxy-url.com + # valueFrom: name-of-my-proxy-secret + + # Optional: Add custom RootCAs from a configmap. + # + # trustedCAs: name-of-my-ca-configmap + + # Optional: Set a network zone for the OneAgent and ActiveGate pods. + # + # networkZone: name-of-my-network-zone + + # Optional: Configure istio to allow access to the Dynatrace Cluster from the OneAgent or ActiveGate pods. + # + # enableIstio: true + + # Optional: Minimum minutes between Dynatrace API requests. + # + # dynatraceApiRequestThreshold: 15 + + # Configuration for Metadata Enrichment. + # + # metadataEnrichment: + # enabled: true + + # Optional: The namespaces where you want Dynatrace Operator to inject + # + # namespaceSelector: + # matchLabels: + # app: my-app + # matchExpressions: + # - key: app + # operator: In + # values: [my-frontend, my-backend, my-database] + + # Configuration for Log monitoring. + # + # logMonitoring: {} + + # Optional: Specifies the rules and conditions for matching ingest attributes. + # + # ingestRuleMatchers: + # - attribute: "k8s.namespace.name" + # values: + # - "kube-system" + # - "dynatrace" + # - "default" + # - attribute: "k8s.pod.annotation", + # values: + # - "logs.dynatrace.com/ingest=true" + # - "category=security" + + # Configuration for OneAgent + # + oneAgent: + # Optional: Set a host group for OneAgent + # + # hostGroup: "" + + classicFullStack: + # Optional: Specify the node selector that controls on which nodes OneAgent will be deployed. + # + # nodeSelector: {} + + # Optional: Assign a priority class to the OneAgent pods. + # + # priorityClassName: priority-class + + # Optional: Tolerations to include with the OneAgent DaemonSet. + # + tolerations: + - effect: NoSchedule + key: node-role.kubernetes.io/master + operator: Exists + - effect: NoSchedule + key: node-role.kubernetes.io/control-plane + operator: Exists + + # Optional: Resource settings for OneAgent container. + # + # oneAgentResources: + # requests: + # cpu: 100m + # memory: 512Mi + # limits: + # cpu: 300m + # memory: 1.5Gi + + # Optional: Set additional arguments to the OneAgent installer. + # + # args: [] + + # Optional: Set additional environment variables for the OneAgent pods. + # + # env: [] + + # Optional: Disable automatic restarts of OneAgent pods in case a new version is available + # + # autoUpdate: true + + # Optional: Set the DNS Policy for OneAgent pods. + # + # dnsPolicy: "ClusterFirstWithHostNet" + + # Optional: Add custom OneAgent annotations. + # + # annotations: + # custom: annotation + + # Optional: Add custom labels to OneAgent pods + # + # labels: + # custom: label + + # Optional: Use a custom OneAgent image. + # + # image: "" + + # Optional: The OneAgent version to be used. + # Example: ..., e.g. 1.200.0.20240101-000000 + # + # version: "" + + # Optional: The SecComp Profile that will be configured in order to run in secure computing mode. + # + # secCompProfile: "" + + # Configuration for ActiveGate instances. + # + activeGate: + # Defines the ActiveGate capabilities + # + capabilities: + - routing + - kubernetes-monitoring + - dynatrace-api + + # Optional: Amount of replicas of ActiveGate pods. + # + # replicas: 1 + + # Optional: Use a custom ActiveGate image + # + # image: "" + + # Optional: Set the ActiveGate group + # + # group: "" + + # Optional: Add a custom properties file by providing it as a value or by referencing it from a secret. + # + # customProperties: + # value: | + # [connectivity] + # networkZone= + # valueFrom: myCustomPropertiesSecret + + # Optional: Resource settings for ActiveGate container. + # + resources: + requests: + cpu: 500m + memory: 1.5Gi + limits: + cpu: 1000m + memory: 1.5Gi + + # Optional: Specify the node selector that controls on which nodes ActiveGate will be deployed. + # + # nodeSelector: {} + + # Optional: Set tolerations for the ActiveGate pods. + # + # tolerations: + # - effect: NoSchedule + # key: node-role.kubernetes.io/master + # operator: Exists + + # Optional: Add custom labels to ActiveGate pods + # + # labels: + # custom: label + + # Optional: Add custom environment variables to ActiveGate pods + # + # env: [] + + # Optional: Name of a secret containing ActiveGate TLS certificate, key, and password. + # + # tlsSecretName: "my-tls-secret" + + # Optional: Set the DNS policy for ActiveGate pods. + # + # dnsPolicy: "Default" + + # Optional: Assign a priority class to the ActiveGate pods. + # + # priorityClassName: priority-class + + # Optional: Add custom annotations to ActiveGate pods + # + # annotations: + # custom: annotation + + # Optional: Add TopologySpreadConstraints to the ActiveGate pods + # + # topologySpreadConstraints: [] diff --git a/assets/samples/dynakube/v1beta4/cloudNativeFullStack.yaml b/assets/samples/dynakube/v1beta4/cloudNativeFullStack.yaml new file mode 100644 index 0000000000..e246b9975f --- /dev/null +++ b/assets/samples/dynakube/v1beta4/cloudNativeFullStack.yaml @@ -0,0 +1,257 @@ +apiVersion: dynatrace.com/v1beta4 +kind: DynaKube +metadata: + name: dynakube + namespace: dynatrace +spec: + # Link to api reference for further information: https://docs.dynatrace.com/docs/ingest-from/setup-on-k8s/reference/dynakube-parameters + + # Required: Dynatrace apiUrl including the `/api` path at the end. + apiUrl: https://ENVIRONMENTID.live.dynatrace.com/api + + # Optional: Name of the secret holding the tokens used for connecting to Dynatrace. + # + # tokens: "" + + # Optional: Defines a custom pull secret in case you use a private registry when pulling images from the Dynatrace environment + # + # customPullSecret: "custom-pull-secret" + + # Optional: Disable certificate check for the connection between Dynatrace Operator and the Dynatrace Cluster. + # + # skipCertCheck: false + + # Optional: Set custom proxy settings either directly or from a secret with the field 'proxy' + # + # proxy: + # value: my-proxy-url.com + # valueFrom: name-of-my-proxy-secret + + # Optional: Add custom RootCAs from a configmap. + # + # trustedCAs: name-of-my-ca-configmap + + # Optional: Set a network zone for the OneAgent and ActiveGate pods. + # + # networkZone: name-of-my-network-zone + + # Optional: Configure istio to allow access to the Dynatrace Cluster from the OneAgent or ActiveGate pods. + # + # enableIstio: true + + # Optional: Minimum minutes between Dynatrace API requests. + # + # dynatraceApiRequestThreshold: 15 + + # Configuration for Metadata Enrichment. + # + metadataEnrichment: + enabled: true + + # Optional: The namespaces where you want Dynatrace Operator to inject + # + # namespaceSelector: + # matchLabels: + # app: my-app + # matchExpressions: + # - key: app + # operator: In + # values: [my-frontend, my-backend, my-database] + + # Configuration for Log monitoring. + # + # logMonitoring: {} + + # Optional: Specifies the rules and conditions for matching ingest attributes. + # + # ingestRuleMatchers: + # - attribute: "k8s.namespace.name" + # values: + # - "kube-system" + # - "dynatrace" + # - "default" + # - attribute: "k8s.pod.annotation", + # values: + # - "logs.dynatrace.com/ingest=true" + # - "category=security" + + # Configuration for OneAgent + # + oneAgent: + # Optional: Set a host group for OneAgent. + # + # hostGroup: "" + + cloudNativeFullStack: + + # Optional: The namespaces where you want Dynatrace Operator to inject + # + # namespaceSelector: + # matchLabels: + # app: my-app + # matchExpressions: + # - key: app + # operator: In + # values: [my-frontend, my-backend, my-database] + + # Optional: Specify the node selector that controls on which nodes OneAgent will be deployed. + # + # nodeSelector: {} + + # Optional: Assign a priority class to the OneAgent pods. + # + # priorityClassName: priority-class + + # Optional: Tolerations to include with the OneAgent DaemonSet. + # + tolerations: + - effect: NoSchedule + key: node-role.kubernetes.io/master + operator: Exists + - effect: NoSchedule + key: node-role.kubernetes.io/control-plane + operator: Exists + + # Optional: Resource settings for OneAgent container. + # + # oneAgentResources: + # requests: + # cpu: 100m + # memory: 512Mi + # limits: + # cpu: 300m + # memory: 1.5Gi + + # Optional: Set additional arguments to the OneAgent installer. + # + # args: [] + + # Optional: Set additional environment variables for the OneAgent pods. + # + # env: [] + + # Optional: Disable automatic restarts of OneAgent pods in case a new version is available + # + # autoUpdate: true + + # Optional: Set the DNS Policy for OneAgent pods. + # + # dnsPolicy: "ClusterFirstWithHostNet" + + # Optional: Add custom OneAgent annotations. + # + # annotations: + # custom: annotation + + # Optional: Add custom labels to OneAgent pods + # + # labels: + # custom: label + + # Optional: Use a custom OneAgent image. + # + # image: "" + + # Optional: The OneAgent version to be used. + # Example: ..., e.g. 1.200.0.20240101-000000 + # + # version: "" + + # Optional: Define resources requests and limits for the initContainer. + # + # initResources: + # requests: + # cpu: 30m + # memory: 30Mi + # limits: + # cpu: 100m + # memory: 60Mi + + # Optional: The OneAgent image that is used to inject into pods + # + # codeModulesImage: "" + + # Optional: The SecComp Profile that will be configured in order to run in secure computing mode. + # + # secCompProfile: "" + + # Configuration for ActiveGate instances. + # + activeGate: + # Defines the ActiveGate capabilities + # + capabilities: + - routing + - kubernetes-monitoring + - dynatrace-api + + # Optional: Amount of replicas of ActiveGate pods. + # + # replicas: 1 + + # Optional: Use a custom ActiveGate image + # + # image: "" + + # Optional: Set the ActiveGate group + # + # group: "" + + # Optional: Add a custom properties file by providing it as a value or by referencing it from a secret. + # + # customProperties: + # value: | + # [connectivity] + # networkZone= + # valueFrom: myCustomPropertiesSecret + + # Optional: Resource settings for ActiveGate container. + # + resources: + requests: + cpu: 500m + memory: 1.5Gi + limits: + cpu: 1000m + memory: 1.5Gi + + # Optional: Specify the node selector that controls on which nodes ActiveGate will be deployed. + # + # nodeSelector: {} + + # Optional: Set tolerations for the ActiveGate pods. + # + # tolerations: + # - effect: NoSchedule + # key: node-role.kubernetes.io/master + # operator: Exists + + # Optional: Add custom labels to ActiveGate pods + # + # labels: + # custom: label + + # Optional: Add custom environment variables to ActiveGate pods + # + # env: [] + + # Optional: Name of a secret containing ActiveGate TLS certificate, key, and password. + # + # tlsSecretName: "my-tls-secret" + + # Optional: Set the DNS policy for ActiveGate pods. + # + # dnsPolicy: "Default" + + # Optional: Assign a priority class to the ActiveGate pods. + # + # priorityClassName: priority-class + + # Optional: Add custom annotations to ActiveGate pods + # + # annotations: + # custom: annotation + + # Optional: Add TopologySpreadConstraints to the ActiveGate pods + # + # topologySpreadConstraints: [] diff --git a/assets/samples/dynakube/v1beta4/hostMonitoring.yaml b/assets/samples/dynakube/v1beta4/hostMonitoring.yaml new file mode 100644 index 0000000000..cc44ac8571 --- /dev/null +++ b/assets/samples/dynakube/v1beta4/hostMonitoring.yaml @@ -0,0 +1,232 @@ +apiVersion: dynatrace.com/v1beta4 +kind: DynaKube +metadata: + name: dynakube + namespace: dynatrace +spec: + # Link to api reference for further information: https://docs.dynatrace.com/docs/ingest-from/setup-on-k8s/reference/dynakube-parameters + + # Required: Dynatrace apiUrl including the `/api` path at the end. + apiUrl: https://ENVIRONMENTID.live.dynatrace.com/api + + # Optional: Name of the secret holding the tokens used for connecting to Dynatrace. + # + # tokens: "" + + # Optional: Defines a custom pull secret in case you use a private registry when pulling images from the Dynatrace environment + # + # customPullSecret: "custom-pull-secret" + + # Optional: Disable certificate check for the connection between Dynatrace Operator and the Dynatrace Cluster. + # + # skipCertCheck: false + + # Optional: Set custom proxy settings either directly or from a secret with the field 'proxy' + # + # proxy: + # value: my-proxy-url.com + # valueFrom: name-of-my-proxy-secret + + # Optional: Add custom RootCAs from a configmap. + # + # trustedCAs: name-of-my-ca-configmap + + # Optional: Set a network zone for the OneAgent and ActiveGate pods. + # + # networkZone: name-of-my-network-zone + + # Optional: Configure istio to allow access to the Dynatrace Cluster from the OneAgent or ActiveGate pods. + # + # enableIstio: true + + # Optional: Minimum minutes between Dynatrace API requests. + # + # dynatraceApiRequestThreshold: 15 + + # Configuration for Metadata Enrichment. + # + # metadataEnrichment: + # enabled: true + + # Optional: The namespaces where you want Dynatrace Operator to inject + # + # namespaceSelector: + # matchLabels: + # app: my-app + # matchExpressions: + # - key: app + # operator: In + # values: [my-frontend, my-backend, my-database] + + # Configuration for Log monitoring. + # + # logMonitoring: {} + + # Optional: Specifies the rules and conditions for matching ingest attributes. + # + # ingestRuleMatchers: + # - attribute: "k8s.namespace.name" + # values: + # - "kube-system" + # - "dynatrace" + # - "default" + # - attribute: "k8s.pod.annotation", + # values: + # - "logs.dynatrace.com/ingest=true" + # - "category=security" + + # Configuration for OneAgent + # + oneAgent: + # Optional: Set a host group for OneAgent + # + # hostGroup: "" + + hostMonitoring: + # Optional: Specify the node selector that controls on which nodes OneAgent will be deployed. + # + # nodeSelector: {} + + # Optional: Assign a priority class to the OneAgent pods. + # + # priorityClassName: priority-class + + # Optional: Tolerations to include with the OneAgent DaemonSet. + # + tolerations: + - effect: NoSchedule + key: node-role.kubernetes.io/master + operator: Exists + - effect: NoSchedule + key: node-role.kubernetes.io/control-plane + operator: Exists + + # Optional: Resource settings for OneAgent container. + # + # oneAgentResources: + # requests: + # cpu: 100m + # memory: 512Mi + # limits: + # cpu: 300m + # memory: 1.5Gi + + # Optional: Set additional arguments to the OneAgent installer. + # + # args: [] + + # Optional: Set additional environment variables for the OneAgent pods. + # + # env: [] + + # Optional: Disable automatic restarts of OneAgent pods in case a new version is available + # + # autoUpdate: true + + # Optional: Set the DNS Policy for OneAgent pods. + # + # dnsPolicy: "ClusterFirstWithHostNet" + + # Optional: Add custom OneAgent annotations. + # + # annotations: + # custom: annotation + + # Optional: Add custom labels to OneAgent pods + # + # labels: + # custom: label + + # Optional: Use a custom OneAgent image. + # + # image: "" + + # Optional: The OneAgent version to be used. + # Example: ..., e.g. 1.200.0.20240101-000000 + # + # version: "" + + # Optional: The SecComp Profile that will be configured in order to run in secure computing mode. + # + # secCompProfile: "" + + # Configuration for ActiveGate instances. + # + activeGate: + # Defines the ActiveGate capabilities + # + capabilities: + - routing + - kubernetes-monitoring + - dynatrace-api + + # Optional: Amount of replicas of ActiveGate pods. + # + # replicas: 1 + + # Optional: Use a custom ActiveGate image + # + # image: "" + + # Optional: Set the ActiveGate group + # + # group: "" + + # Optional: Add a custom properties file by providing it as a value or by referencing it from a secret. + # + # customProperties: + # value: | + # [connectivity] + # networkZone= + # valueFrom: myCustomPropertiesSecret + + # Optional: Resource settings for ActiveGate container. + # + resources: + requests: + cpu: 500m + memory: 1.5Gi + limits: + cpu: 1000m + memory: 1.5Gi + + # Optional: Specify the node selector that controls on which nodes ActiveGate will be deployed. + # + # nodeSelector: {} + + # Optional: Set tolerations for the ActiveGate pods. + # + # tolerations: + # - effect: NoSchedule + # key: node-role.kubernetes.io/master + # operator: Exists + + # Optional: Add custom labels to ActiveGate pods + # + # labels: + # custom: label + + # Optional: Add custom environment variables to ActiveGate pods + # + # env: [] + + # Optional: Name of a secret containing ActiveGate TLS certificate, key, and password. + # + # tlsSecretName: "my-tls-secret" + + # Optional: Set the DNS policy for ActiveGate pods. + # + # dnsPolicy: "Default" + + # Optional: Assign a priority class to the ActiveGate pods. + # + # priorityClassName: priority-class + + # Optional: Add custom annotations to ActiveGate pods + # + # annotations: + # custom: annotation + + # Optional: Add TopologySpreadConstraints to the ActiveGate pods + # + # topologySpreadConstraints: [] diff --git a/assets/samples/dynakube/v1beta4/kubernetesObservability.yaml b/assets/samples/dynakube/v1beta4/kubernetesObservability.yaml new file mode 100644 index 0000000000..5393e867a7 --- /dev/null +++ b/assets/samples/dynakube/v1beta4/kubernetesObservability.yaml @@ -0,0 +1,140 @@ +apiVersion: dynatrace.com/v1beta4 +kind: DynaKube +metadata: + name: dynakube + namespace: dynatrace + # annotations: + # feature.dynatrace.com/k8s-app-enabled: "true" +spec: + # Link to api reference for further information: https://docs.dynatrace.com/docs/ingest-from/setup-on-k8s/reference/dynakube-parameters + + # Required: Dynatrace apiUrl including the `/api` path at the end. + apiUrl: https://ENVIRONMENTID.live.dynatrace.com/api + + # Optional: Name of the secret holding the tokens used for connecting to Dynatrace. + # + # tokens: "" + + # Optional: Defines a custom pull secret in case you use a private registry when pulling images from the Dynatrace environment + # + # customPullSecret: "custom-pull-secret" + + # Optional: Disable certificate check for the connection between Dynatrace Operator and the Dynatrace Cluster. + # + # skipCertCheck: false + + # Optional: Set custom proxy settings either directly or from a secret with the field 'proxy' + # + # proxy: + # value: my-proxy-url.com + # valueFrom: name-of-my-proxy-secret + + # Optional: Add custom RootCAs from a configmap. + # + # trustedCAs: name-of-my-ca-configmap + + # Optional: Set a network zone for the OneAgent and ActiveGate pods. + # + # networkZone: name-of-my-network-zone + + # Optional: Configure istio to allow access to the Dynatrace Cluster from the OneAgent or ActiveGate pods. + # + # enableIstio: true + + # Optional: Minimum minutes between Dynatrace API requests. + # + # dynatraceApiRequestThreshold: 15 + + # Configuration for Metadata Enrichment. + # + # metadataEnrichment: + # enabled: true + + # Optional: The namespaces where you want Dynatrace Operator to inject + # + # namespaceSelector: + # matchLabels: + # app: my-app + # matchExpressions: + # - key: app + # operator: In + # values: [my-frontend, my-backend, my-database] + + # Configuration for ActiveGate instances. + # + activeGate: + # Defines the ActiveGate capabilities + # + capabilities: + - kubernetes-monitoring + + # Optional: Amount of replicas of ActiveGate pods. + # + # replicas: 1 + + # Optional: Use a custom ActiveGate image + # + # image: "" + + # Optional: Set the ActiveGate group + # + # group: "" + + # Optional: Add a custom properties file by providing it as a value or by referencing it from a secret. + # + # customProperties: + # value: | + # [connectivity] + # networkZone= + # valueFrom: myCustomPropertiesSecret + + # Optional: Resource settings for ActiveGate container. + # + resources: + requests: + cpu: 500m + memory: 1.5Gi + limits: + cpu: 1000m + memory: 1.5Gi + + # Optional: Specify the node selector that controls on which nodes ActiveGate will be deployed. + # + # nodeSelector: {} + + # Optional: Set tolerations for the ActiveGate pods. + # + # tolerations: + # - effect: NoSchedule + # key: node-role.kubernetes.io/master + # operator: Exists + + # Optional: Add custom labels to ActiveGate pods + # + # labels: + # custom: label + + # Optional: Add custom environment variables to ActiveGate pods + # + # env: [] + + # Optional: Name of a secret containing ActiveGate TLS certificate, key, and password. + # + # tlsSecretName: "my-tls-secret" + + # Optional: Set the DNS policy for ActiveGate pods. + # + # dnsPolicy: "Default" + + # Optional: Assign a priority class to the ActiveGate pods. + # + # priorityClassName: priority-class + + # Optional: Add custom annotations to ActiveGate pods + # + # annotations: + # custom: annotation + + # Optional: Add TopologySpreadConstraints to the ActiveGate pods + # + # topologySpreadConstraints: [] diff --git a/assets/samples/dynakube/v1beta4/kubernetesSecurityPostureManagement.yaml b/assets/samples/dynakube/v1beta4/kubernetesSecurityPostureManagement.yaml new file mode 100644 index 0000000000..0a20caa7c3 --- /dev/null +++ b/assets/samples/dynakube/v1beta4/kubernetesSecurityPostureManagement.yaml @@ -0,0 +1,197 @@ +apiVersion: dynatrace.com/v1beta4 +kind: DynaKube +metadata: + name: dynakube + namespace: dynatrace + # annotations: + # feature.dynatrace.com/k8s-app-enabled: "true" +spec: + # Link to api reference for further information: https://docs.dynatrace.com/docs/ingest-from/setup-on-k8s/reference/dynakube-parameters + + # Required: Dynatrace apiUrl including the `/api` path at the end. + apiUrl: https://ENVIRONMENTID.live.dynatrace.com/api + + # Optional: Name of the secret holding the tokens used for connecting to Dynatrace. + # + # tokens: "" + + # Optional: Defines a custom pull secret in case you use a private registry when pulling images from the Dynatrace environment + # + # customPullSecret: "custom-pull-secret" + + # Optional: Disable certificate check for the connection between Dynatrace Operator and the Dynatrace Cluster. + # + # skipCertCheck: false + + # Optional: Set custom proxy settings either directly or from a secret with the field 'proxy' + # + # proxy: + # value: my-proxy-url.com + # valueFrom: name-of-my-proxy-secret + + # Optional: Add custom RootCAs from a configmap. + # + # trustedCAs: name-of-my-ca-configmap + + # Optional: Set a network zone for the OneAgent and ActiveGate pods. + # + # networkZone: name-of-my-network-zone + + # Optional: Configure istio to allow access to the Dynatrace Cluster from the OneAgent or ActiveGate pods. + # + # enableIstio: true + + # Optional: Minimum minutes between Dynatrace API requests. + # + # dynatraceApiRequestThreshold: 15 + + # Configuration for Metadata Enrichment. + # + # metadataEnrichment: + # enabled: true + + # Optional: The namespaces where you want Dynatrace Operator to inject + # + # namespaceSelector: + # matchLabels: + # app: my-app + # matchExpressions: + # - key: app + # operator: In + # values: [my-frontend, my-backend, my-database] + + kspm: {} + + # Configuration for ActiveGate instances. + # + activeGate: + # Defines the ActiveGate capabilities + # + capabilities: + - kubernetes-monitoring + + # Optional: Amount of replicas of ActiveGate pods. + # + # replicas: 1 + + # Optional: Use a custom ActiveGate image + # + # image: "" + + # Optional: Set the ActiveGate group + # + # group: "" + + # Optional: Add a custom properties file by providing it as a value or by referencing it from a secret. + # + # customProperties: + # value: | + # [kubernetes_monitoring] + # kubernetes_configuration_dataset_pipeline_enabled = true + # kubernetes_configuration_dataset_pipeline_include_node_config = true + + # Optional: Resource settings for ActiveGate container. + # + resources: + requests: + cpu: 500m + memory: 1.5Gi + limits: + cpu: 1000m + memory: 1.5Gi + + # Optional: Specify the node selector that controls on which nodes ActiveGate will be deployed. + # + # nodeSelector: {} + + # Optional: Set tolerations for the ActiveGate pods. + # + # tolerations: + # - effect: NoSchedule + # key: node-role.kubernetes.io/master + # operator: Exists + + # Optional: Add custom labels to ActiveGate pods + # + # labels: + # custom: label + + # Optional: Add custom environment variables to ActiveGate pods + # + # env: [] + + # Optional: Name of a secret containing ActiveGate TLS certificate, key, and password. + # + # tlsSecretName: "my-tls-secret" + + # Optional: Set the DNS policy for ActiveGate pods. + # + # dnsPolicy: "Default" + + # Optional: Assign a priority class to the ActiveGate pods. + # + # priorityClassName: priority-class + + # Optional: Add custom annotations to ActiveGate pods + # + # annotations: + # custom: annotation + + # Optional: Add TopologySpreadConstraints to the ActiveGate pods + # + # topologySpreadConstraints: [] + + templates: + kspmNodeConfigurationCollector: + # Required: Configure the image for the Node Configuration Collector. + imageRef: + repository: public.ecr.aws/dynatrace/dynatrace-k8s-node-config-collector + tag: + + # Optional: Define the update strategy for the Node Configuration Collector daemonSet + # + # updateStrategy: {} + + # Optional: Add custom labels to NNode Configuration Collector pods + # + # labels: + # custom: label + + # Optional: Add custom annotations to Node Configuration Collector pods. + # + # annotations: + # custom: annotation + + # Optional: Specify the node selector that controls on which nodes OneAgent will be deployed. + # + # nodeSelector: {} + + # Optional: Define the nodeAffinity for the DaemonSet of the Node Configuration Collector + # + # nodeAffinity: {} + + # Optional: Assign a priority class to the Node Configuration Collector pods. + # + # priorityClassName: priority-class + + # Optional: Tolerations to include with the Node Configuration Collector DaemonSet. + # + tolerations: + - effect: NoSchedule + key: node-role.kubernetes.io/master + operator: Exists + - effect: NoSchedule + key: node-role.kubernetes.io/control-plane + operator: Exists + + # Optional: Resource settings for Node Configuration Collector container. + # + # resources: {} + + # Optional: Set additional arguments to the Node Configuration Collector. + # + # args: [] + + # Optional: Set additional environment variables for the Node Configuration Collector pods. + # + # env: [] diff --git a/assets/samples/dynakube/v1beta4/logMonitoring.yaml b/assets/samples/dynakube/v1beta4/logMonitoring.yaml new file mode 100644 index 0000000000..6aff2039fe --- /dev/null +++ b/assets/samples/dynakube/v1beta4/logMonitoring.yaml @@ -0,0 +1,211 @@ +apiVersion: dynatrace.com/v1beta4 +kind: DynaKube +metadata: + name: dynakube + namespace: dynatrace + # annotations: + # feature.dynatrace.com/oneagent-privileged: "true" # Required on Openshift +spec: + # Link to api reference for further information: https://docs.dynatrace.com/docs/ingest-from/setup-on-k8s/reference/dynakube-parameters + + # Required: Dynatrace apiUrl including the `/api` path at the end. + apiUrl: https://ENVIRONMENTID.live.dynatrace.com/api + + # Optional: Name of the secret holding the tokens used for connecting to Dynatrace. + # + # tokens: "" + + # Optional: Defines a custom pull secret in case you use a private registry when pulling images from the Dynatrace environment + # + # customPullSecret: "custom-pull-secret" + + # Optional: Disable certificate check for the connection between Dynatrace Operator and the Dynatrace Cluster. + # + # skipCertCheck: false + + # Optional: Set custom proxy settings either directly or from a secret with the field 'proxy' + # + # proxy: + # value: my-proxy-url.com + # valueFrom: name-of-my-proxy-secret + + # Optional: Add custom RootCAs from a configmap. + # + # trustedCAs: name-of-my-ca-configmap + + # Optional: Set a network zone for the OneAgent and ActiveGate pods. + # + # networkZone: name-of-my-network-zone + + # Optional: Configure istio to allow access to the Dynatrace Cluster from the OneAgent or ActiveGate pods. + # + # enableIstio: true + + # Optional: Minimum minutes between Dynatrace API requests. + # + # dynatraceApiRequestThreshold: 15 + + # Configuration for Metadata Enrichment. + # + metadataEnrichment: + enabled: true + + # Optional: The namespaces where you want Dynatrace Operator to inject + # + # namespaceSelector: + # matchLabels: + # app: my-app + # matchExpressions: + # - key: app + # operator: In + # values: [my-frontend, my-backend, my-database] + + # Configuration for Log monitoring. + # + logMonitoring: {} + + # Optional: Specifies the rules and conditions for matching ingest attributes. + # + # ingestRuleMatchers: + # - attribute: "k8s.namespace.name" + # values: + # - "kube-system" + # - "dynatrace" + # - "default" + # - attribute: "k8s.pod.annotation", + # values: + # - "logs.dynatrace.com/ingest=true" + # - "category=security" + + # Configuration for ActiveGate instances. + # + activeGate: + # Defines the ActiveGate capabilities + # + capabilities: + - kubernetes-monitoring + + # Optional: Amount of replicas of ActiveGate pods. + # + # replicas: 1 + + # Optional: Use a custom ActiveGate image + # + # image: "" + + # Optional: Set the ActiveGate group + # + # group: "" + + # Optional: Add a custom properties file by providing it as a value or by referencing it from a secret. + # + # customProperties: + # value: | + # [kubernetes_monitoring] + # kubernetes_configuration_dataset_pipeline_enabled = true + # kubernetes_configuration_dataset_pipeline_include_node_config = true + + # Optional: Resource settings for ActiveGate container. + # + resources: + requests: + cpu: 500m + memory: 1.5Gi + limits: + cpu: 1000m + memory: 1.5Gi + + # Optional: Specify the node selector that controls on which nodes ActiveGate will be deployed. + # + # nodeSelector: {} + + # Optional: Set tolerations for the ActiveGate pods. + # + # tolerations: + # - effect: NoSchedule + # key: node-role.kubernetes.io/master + # operator: Exists + + # Optional: Add custom labels to ActiveGate pods + # + # labels: + # custom: label + + # Optional: Add custom environment variables to ActiveGate pods + # + # env: [] + + # Optional: Name of a secret containing ActiveGate TLS certificate, key, and password. + # + # tlsSecretName: "my-tls-secret" + + # Optional: Set the DNS policy for ActiveGate pods. + # + # dnsPolicy: "Default" + + # Optional: Assign a priority class to the ActiveGate pods. + # + # priorityClassName: priority-class + + # Optional: Add custom annotations to ActiveGate pods + # + # annotations: + # custom: annotation + + # Optional: Add TopologySpreadConstraints to the ActiveGate pods + # + # topologySpreadConstraints: [] + + templates: + logMonitoring: + imageRef: + repository: public.ecr.aws/dynatrace/dynatrace-logmodule + tag: + + # Optional: Define the update strategy for the Log monitoring daemonSet + # + # updateStrategy: {} + + # Optional: Add custom labels to Log monitoring pods + # + # labels: + # custom: label + + # Optional: Add custom annotations to Log monitoring pods. + # + # annotations: + # custom: annotation + + # Optional: Specify the node selector that controls on which nodes Log monitoring will be deployed. + # + # nodeSelector: {} + + # Optional: Define the nodeAffinity for the Log monitoring DaemonSet + # + # nodeAffinity: {} + + # Optional: Assign a priority class to the Log monitoring pods. + # + # priorityClassName: priority-class + + # Optional: Tolerations to include with the Log monitoring DaemonSet. + # + tolerations: + - effect: NoSchedule + key: node-role.kubernetes.io/master + operator: Exists + - effect: NoSchedule + key: node-role.kubernetes.io/control-plane + operator: Exists + + # Optional: Resource settings for Log monitoring container. + # + # resources: {} + + # Optional: Set additional arguments to Log monitoring + # + # args: [] + + # Optional: Set additional environment variables for the Log monitoring pods. + # + # env: [] diff --git a/assets/samples/dynakube/v1beta4/multipleDynakubes.yaml b/assets/samples/dynakube/v1beta4/multipleDynakubes.yaml new file mode 100644 index 0000000000..8faee91306 --- /dev/null +++ b/assets/samples/dynakube/v1beta4/multipleDynakubes.yaml @@ -0,0 +1,49 @@ +apiVersion: dynatrace.com/v1beta4 +kind: DynaKube +metadata: + name: dynakube-application-monitoring + namespace: dynatrace +spec: + apiUrl: https://ENVIRONMENTID.live.dynatrace.com/api + + # Link to api reference for further information: https://docs.dynatrace.com/docs/ingest-from/setup-on-k8s/reference/dynakube-parameters + + oneAgent: + applicationMonitoring: + namespaceSelector: + matchLabels: + monitor: applicationMonitoring + + activeGate: + capabilities: + - kubernetes-monitoring +--- + +apiVersion: dynatrace.com/v1beta4 +kind: DynaKube +metadata: + name: dynakube-cloud-native + namespace: dynatrace +spec: + apiUrl: https://ENVIRONMENTID.live.dynatrace.com/api + + # Link to api reference for further information: https://docs.dynatrace.com/docs/ingest-from/setup-on-k8s/reference/dynakube-parameters + + oneAgent: + cloudNativeFullStack: + namespaceSelector: + matchLabels: + monitor: cloudNativeFullStack + + tolerations: + - effect: NoSchedule + key: node-role.kubernetes.io/master + operator: Exists + - effect: NoSchedule + key: node-role.kubernetes.io/control-plane + operator: Exists + + activeGate: + capabilities: + - routing + - dynatrace-api diff --git a/cmd/config/config.go b/cmd/config/config.go deleted file mode 100644 index 14ca776ddb..0000000000 --- a/cmd/config/config.go +++ /dev/null @@ -1,24 +0,0 @@ -package config - -import ( - "github.com/pkg/errors" - "k8s.io/client-go/rest" - "sigs.k8s.io/controller-runtime/pkg/client/config" -) - -type Provider interface { - GetConfig() (*rest.Config, error) -} - -type KubeConfigProvider struct { -} - -func NewKubeConfigProvider() Provider { - return KubeConfigProvider{} -} - -func (provider KubeConfigProvider) GetConfig() (*rest.Config, error) { - cfg, err := config.GetConfig() - - return cfg, errors.WithStack(err) -} diff --git a/cmd/config/config_test.go b/cmd/config/config_test.go deleted file mode 100644 index 643cf72f50..0000000000 --- a/cmd/config/config_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package config - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestConfigProvider(t *testing.T) { - provider := NewKubeConfigProvider() - - assert.NotNil(t, provider) -} diff --git a/cmd/csi/init/builder.go b/cmd/csi/init/builder.go deleted file mode 100644 index 934921a257..0000000000 --- a/cmd/csi/init/builder.go +++ /dev/null @@ -1,101 +0,0 @@ -package init - -import ( - "github.com/Dynatrace/dynatrace-operator/cmd/config" - dtcsi "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi" - "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi/metadata" - "github.com/Dynatrace/dynatrace-operator/pkg/logd" - "github.com/Dynatrace/dynatrace-operator/pkg/version" - "github.com/pkg/errors" - "github.com/spf13/afero" - "github.com/spf13/cobra" - "golang.org/x/sys/unix" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -const use = "csi-init" - -var nodeId, endpoint string - -type CommandBuilder struct { - configProvider config.Provider - namespace string -} - -func NewCsiInitCommandBuilder() CommandBuilder { - return CommandBuilder{} -} - -func (builder CommandBuilder) SetConfigProvider(provider config.Provider) CommandBuilder { - builder.configProvider = provider - - return builder -} - -func (builder CommandBuilder) SetNamespace(namespace string) CommandBuilder { - builder.namespace = namespace - - return builder -} - -func (builder CommandBuilder) Build() *cobra.Command { - cmd := &cobra.Command{ - Use: use, - RunE: builder.buildRun(), - } - - return cmd -} - -func (builder CommandBuilder) buildRun() func(*cobra.Command, []string) error { - return func(cmd *cobra.Command, args []string) error { - unix.Umask(dtcsi.UnixUmask) - version.LogVersion() - logd.LogBaseLoggerSettings() - - kubeConfig, err := builder.configProvider.GetConfig() - if err != nil { - return err - } - - csiManager, err := createManager(builder.namespace, kubeConfig) - if err != nil { - log.Info("failed to create/configure kubernetes client, will only run non-network related corrections and checks", "err", err.Error()) - } - - err = createCsiDataPath(afero.NewOsFs()) - if err != nil { - return err - } - - signalHandler := ctrl.SetupSignalHandler() - - access, err := metadata.NewAccess(signalHandler, dtcsi.MetadataAccessPath) - if err != nil { - return err - } - - csiOptions := dtcsi.CSIOptions{ - NodeId: nodeId, - Endpoint: endpoint, - RootDir: dtcsi.DataPath, - } - - var apiReader client.Reader - if csiManager != nil { - apiReader = csiManager.GetAPIReader() - } - - err = metadata.NewCorrectnessChecker(apiReader, access, csiOptions).CorrectCSI(signalHandler) - if err != nil { - return err - } - - return nil - } -} - -func createCsiDataPath(fs afero.Fs) error { - return errors.WithStack(fs.MkdirAll(dtcsi.DataPath, 0770)) -} diff --git a/cmd/csi/init/cmd.go b/cmd/csi/init/cmd.go new file mode 100644 index 0000000000..1e10fbb673 --- /dev/null +++ b/cmd/csi/init/cmd.go @@ -0,0 +1,87 @@ +package init + +import ( + "os" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme" + dtcsi "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi" + "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi/metadata" + "github.com/Dynatrace/dynatrace-operator/pkg/logd" + "github.com/Dynatrace/dynatrace-operator/pkg/util/installconfig" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/env" + "github.com/Dynatrace/dynatrace-operator/pkg/version" + "github.com/pkg/errors" + "github.com/spf13/afero" + "github.com/spf13/cobra" + "golang.org/x/sys/unix" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/client/config" + "sigs.k8s.io/controller-runtime/pkg/manager" +) + +const use = "csi-init" + +var nodeId, endpoint string + +func New() *cobra.Command { + cmd := &cobra.Command{ + Use: use, + RunE: run(), + SilenceUsage: true, + } + + return cmd +} + +func run() func(*cobra.Command, []string) error { + return func(cmd *cobra.Command, args []string) error { + unix.Umask(dtcsi.UnixUmask) + installconfig.ReadModules() + version.LogVersion() + logd.LogBaseLoggerSettings() + + err := createCSIDataPath(afero.NewOsFs()) + if err != nil { + return err + } + + signalHandler := ctrl.SetupSignalHandler() + + csiOptions := dtcsi.CSIOptions{ + NodeId: nodeId, + Endpoint: endpoint, + RootDir: dtcsi.DataPath, + } + + managerOptions := ctrl.Options{ + Cache: cache.Options{ + DefaultNamespaces: map[string]cache.Config{ + env.DefaultNamespace(): {}, + }, + }, + Scheme: scheme.Scheme, + } + + kubeconfig, err := config.GetConfig() + if err != nil { + return errors.WithStack(err) + } + + mgr, err := manager.New(kubeconfig, managerOptions) + if err != nil { + return errors.WithStack(err) + } + + err = metadata.NewCorrectnessChecker(mgr.GetAPIReader(), csiOptions).CorrectCSI(signalHandler) + if err != nil { + return err + } + + return nil + } +} + +func createCSIDataPath(fs afero.Fs) error { + return errors.WithStack(fs.MkdirAll(dtcsi.DataPath, os.ModePerm)) +} diff --git a/cmd/csi/init/manager.go b/cmd/csi/init/manager.go deleted file mode 100644 index f70d8bca2f..0000000000 --- a/cmd/csi/init/manager.go +++ /dev/null @@ -1,30 +0,0 @@ -package init - -import ( - "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme" - "github.com/pkg/errors" - "k8s.io/client-go/rest" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/cache" - "sigs.k8s.io/controller-runtime/pkg/manager" -) - -func createManager(namespace string, config *rest.Config) (manager.Manager, error) { - mgr, err := manager.New(config, createManagerOptions(namespace)) - if err != nil { - return nil, errors.WithStack(err) - } - - return mgr, nil -} - -func createManagerOptions(namespace string) ctrl.Options { - return ctrl.Options{ - Cache: cache.Options{ - DefaultNamespaces: map[string]cache.Config{ - namespace: {}, - }, - }, - Scheme: scheme.Scheme, - } -} diff --git a/cmd/csi/provisioner/builder.go b/cmd/csi/provisioner/builder.go deleted file mode 100644 index 9be1f83bfe..0000000000 --- a/cmd/csi/provisioner/builder.go +++ /dev/null @@ -1,146 +0,0 @@ -package provisioner - -import ( - "github.com/Dynatrace/dynatrace-operator/cmd/config" - cmdManager "github.com/Dynatrace/dynatrace-operator/cmd/manager" - dtcsi "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi" - "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi/metadata" - csiprovisioner "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi/provisioner" - "github.com/Dynatrace/dynatrace-operator/pkg/logd" - "github.com/Dynatrace/dynatrace-operator/pkg/version" - "github.com/pkg/errors" - "github.com/spf13/afero" - "github.com/spf13/cobra" - "golang.org/x/sys/unix" - ctrl "sigs.k8s.io/controller-runtime" -) - -const use = "csi-provisioner" - -var probeAddress string - -type CommandBuilder struct { - configProvider config.Provider - managerProvider cmdManager.Provider - filesystem afero.Fs - csiOptions *dtcsi.CSIOptions - namespace string -} - -func NewCsiProvisionerCommandBuilder() CommandBuilder { - return CommandBuilder{} -} - -func (builder CommandBuilder) SetConfigProvider(provider config.Provider) CommandBuilder { - builder.configProvider = provider - - return builder -} - -func (builder CommandBuilder) setManagerProvider(provider cmdManager.Provider) CommandBuilder { - builder.managerProvider = provider - - return builder -} - -func (builder CommandBuilder) SetNamespace(namespace string) CommandBuilder { - builder.namespace = namespace - - return builder -} - -func (builder CommandBuilder) setCsiOptions(csiOptions dtcsi.CSIOptions) CommandBuilder { - builder.csiOptions = &csiOptions - - return builder -} - -func (builder CommandBuilder) setFilesystem(filesystem afero.Fs) CommandBuilder { - builder.filesystem = filesystem - - return builder -} - -func (builder CommandBuilder) getCsiOptions() dtcsi.CSIOptions { - if builder.csiOptions == nil { - builder.csiOptions = &dtcsi.CSIOptions{ - RootDir: dtcsi.DataPath, - } - } - - return *builder.csiOptions -} - -func (builder CommandBuilder) getManagerProvider() cmdManager.Provider { - if builder.managerProvider == nil { - builder.managerProvider = newCsiDriverManagerProvider(probeAddress) - } - - return builder.managerProvider -} - -func (builder CommandBuilder) getFilesystem() afero.Fs { - if builder.filesystem == nil { - builder.filesystem = afero.NewOsFs() - } - - return builder.filesystem -} - -func (builder CommandBuilder) Build() *cobra.Command { - cmd := &cobra.Command{ - Use: use, - RunE: builder.buildRun(), - } - - addFlags(cmd) - - return cmd -} - -func addFlags(cmd *cobra.Command) { - cmd.PersistentFlags().StringVar(&probeAddress, "health-probe-bind-address", ":10090", "The address the probe endpoint binds to.") -} - -func (builder CommandBuilder) buildRun() func(*cobra.Command, []string) error { - return func(cmd *cobra.Command, args []string) error { - unix.Umask(dtcsi.UnixUmask) - version.LogVersion() - logd.LogBaseLoggerSettings() - - kubeConfig, err := builder.configProvider.GetConfig() - if err != nil { - return err - } - - csiManager, err := builder.getManagerProvider().CreateManager(builder.namespace, kubeConfig) - if err != nil { - return err - } - - signalHandler := ctrl.SetupSignalHandler() - - err = createCsiDataPath(builder.getFilesystem()) - if err != nil { - return err - } - - access, err := metadata.NewAccess(signalHandler, dtcsi.MetadataAccessPath) - if err != nil { - return err - } - - err = csiprovisioner.NewOneAgentProvisioner(csiManager, builder.getCsiOptions(), access).SetupWithManager(csiManager) - if err != nil { - return err - } - - err = csiManager.Start(signalHandler) - - return errors.WithStack(err) - } -} - -func createCsiDataPath(fs afero.Fs) error { - return errors.WithStack(fs.MkdirAll(dtcsi.DataPath, 0770)) -} diff --git a/cmd/csi/provisioner/builder_test.go b/cmd/csi/provisioner/builder_test.go deleted file mode 100644 index 33be4c7374..0000000000 --- a/cmd/csi/provisioner/builder_test.go +++ /dev/null @@ -1,82 +0,0 @@ -package provisioner - -import ( - "io/fs" - "testing" - - dtcsi "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi" - configmock "github.com/Dynatrace/dynatrace-operator/test/mocks/cmd/config" - managermock "github.com/Dynatrace/dynatrace-operator/test/mocks/cmd/manager" - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestCsiCommandBuilder(t *testing.T) { - t.Run("build command", func(t *testing.T) { - builder := NewCsiProvisionerCommandBuilder() - csiCommand := builder.Build() - - assert.NotNil(t, csiCommand) - assert.Equal(t, use, csiCommand.Use) - assert.NotNil(t, csiCommand.RunE) - }) - t.Run("set config provider", func(t *testing.T) { - builder := NewCsiProvisionerCommandBuilder() - - assert.NotNil(t, builder) - - expectedProvider := &configmock.Provider{} - builder = builder.SetConfigProvider(expectedProvider) - - assert.Equal(t, expectedProvider, builder.configProvider) - }) - t.Run("set manager provider", func(t *testing.T) { - expectedProvider := managermock.NewProvider(t) - builder := NewCsiProvisionerCommandBuilder().setManagerProvider(expectedProvider) - - assert.Equal(t, expectedProvider, builder.managerProvider) - }) - t.Run("set namespace", func(t *testing.T) { - builder := NewCsiProvisionerCommandBuilder().SetNamespace("namespace") - - assert.Equal(t, "namespace", builder.namespace) - }) - t.Run("set filesystem", func(t *testing.T) { - expectedFs := afero.NewMemMapFs() - builder := NewCsiProvisionerCommandBuilder() - - assert.Equal(t, afero.NewOsFs(), builder.getFilesystem()) - - builder = builder.setFilesystem(expectedFs) - - assert.Equal(t, expectedFs, builder.getFilesystem()) - }) - t.Run("set csi options", func(t *testing.T) { - expectedOptions := dtcsi.CSIOptions{ - RootDir: dtcsi.DataPath, - } - builder := NewCsiProvisionerCommandBuilder(). - setCsiOptions(expectedOptions) - - assert.Equal(t, expectedOptions, builder.getCsiOptions()) - }) -} - -func TestCreateCsiRootPath(t *testing.T) { - memFs := afero.NewMemMapFs() - err := createCsiDataPath(memFs) - - require.NoError(t, err) - - exists, err := afero.Exists(memFs, dtcsi.DataPath) - - assert.True(t, exists) - require.NoError(t, err) - - stat, err := memFs.Stat(dtcsi.DataPath) - - require.NoError(t, err) - assert.Equal(t, fs.FileMode(0770), stat.Mode()&fs.FileMode(0770)) - assert.True(t, stat.IsDir()) -} diff --git a/cmd/csi/provisioner/cmd.go b/cmd/csi/provisioner/cmd.go new file mode 100644 index 0000000000..5fcf2c01b3 --- /dev/null +++ b/cmd/csi/provisioner/cmd.go @@ -0,0 +1,124 @@ +package provisioner + +import ( + "os" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme" + dtcsi "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi" + csiprovisioner "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi/provisioner" + "github.com/Dynatrace/dynatrace-operator/pkg/logd" + "github.com/Dynatrace/dynatrace-operator/pkg/util/installconfig" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/env" + "github.com/Dynatrace/dynatrace-operator/pkg/version" + "github.com/pkg/errors" + "github.com/spf13/afero" + "github.com/spf13/cobra" + "golang.org/x/sys/unix" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/client/config" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/metrics/server" +) + +const ( + use = "csi-provisioner" + + metricsBindAddress = ":8090" + defaultProbeAddress = ":8091" + livenessEndpointName = "/livez" + livezEndpointName = "livez" +) + +var probeAddress string + +func New() *cobra.Command { + cmd := &cobra.Command{ + Use: use, + RunE: run(), + SilenceUsage: true, + } + + addFlags(cmd) + + return cmd +} + +func addFlags(cmd *cobra.Command) { + cmd.PersistentFlags().StringVar(&probeAddress, "health-probe-bind-address", ":10090", "The address the probe endpoint binds to.") +} + +func run() func(*cobra.Command, []string) error { + return func(cmd *cobra.Command, args []string) error { + unix.Umask(dtcsi.UnixUmask) + installconfig.ReadModules() + version.LogVersion() + logd.LogBaseLoggerSettings() + + kubeConfig, err := config.GetConfig() + if err != nil { + return err + } + + csiManager, err := createManager(kubeConfig, env.DefaultNamespace()) + if err != nil { + return err + } + + signalHandler := ctrl.SetupSignalHandler() + + err = createCSIDataPath(afero.NewOsFs()) + if err != nil { + return err + } + + err = csiprovisioner.NewOneAgentProvisioner(csiManager, createCsiOptions()).SetupWithManager(csiManager) + if err != nil { + return err + } + + err = csiManager.Start(signalHandler) + + return errors.WithStack(err) + } +} + +func createCSIDataPath(fs afero.Fs) error { + return errors.WithStack(fs.MkdirAll(dtcsi.DataPath, os.ModePerm)) +} + +func createManager(config *rest.Config, namespace string) (manager.Manager, error) { + options := ctrl.Options{ + Cache: cache.Options{ + DefaultNamespaces: map[string]cache.Config{ + namespace: {}, + }, + }, + Scheme: scheme.Scheme, + Metrics: server.Options{ + BindAddress: metricsBindAddress, + }, + HealthProbeBindAddress: probeAddress, + LivenessEndpointName: livenessEndpointName, + } + + mgr, err := manager.New(config, options) + if err != nil { + return nil, errors.WithStack(err) + } + + err = mgr.AddHealthzCheck(livezEndpointName, healthz.Ping) + if err != nil { + return nil, errors.WithStack(err) + } + + return mgr, nil +} + +func createCsiOptions() dtcsi.CSIOptions { + return dtcsi.CSIOptions{ + RootDir: dtcsi.DataPath, + } +} diff --git a/cmd/csi/provisioner/command_test.go b/cmd/csi/provisioner/command_test.go deleted file mode 100644 index d91b9e2d2d..0000000000 --- a/cmd/csi/provisioner/command_test.go +++ /dev/null @@ -1,49 +0,0 @@ -package provisioner - -import ( - "testing" - - dtcsi "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi" - "github.com/Dynatrace/dynatrace-operator/pkg/logd" - configmock "github.com/Dynatrace/dynatrace-operator/test/mocks/cmd/config" - providermock "github.com/Dynatrace/dynatrace-operator/test/mocks/cmd/manager" - managermock "github.com/Dynatrace/dynatrace-operator/test/mocks/sigs.k8s.io/controller-runtime/pkg/manager" - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - "k8s.io/client-go/rest" -) - -func TestCsiCommand(t *testing.T) { - configProvider := configmock.NewProvider(t) - configProvider.On("GetConfig").Return(&rest.Config{}, nil) - - cmdMgr := managermock.NewManager(t) - - managerProvider := providermock.NewProvider(t) - managerProvider.On("CreateManager", mock.Anything, mock.Anything).Return(cmdMgr, nil) - - memFs := afero.NewMemMapFs() - builder := NewCsiProvisionerCommandBuilder(). - SetConfigProvider(configProvider). - setManagerProvider(managerProvider). - SetNamespace("test-namespace"). - setFilesystem(memFs) - command := builder.Build() - commandFn := builder.buildRun() - - err := commandFn(command, make([]string, 0)) - - // sqlite library does not use afero fs, so it throws an error because path does not exist - require.Error(t, err) - configProvider.AssertCalled(t, "GetConfig") - managerProvider.AssertCalled(t, "CreateManager", "test-namespace", &rest.Config{}) - - exists, err := afero.Exists(memFs, dtcsi.DataPath) - assert.True(t, exists) - require.NoError(t, err) - - // Logging a newline because otherwise `go test` doesn't recognize the result - logd.Get().WithName("csi command").Info("") -} diff --git a/cmd/csi/provisioner/manager.go b/cmd/csi/provisioner/manager.go deleted file mode 100644 index 4017b932ed..0000000000 --- a/cmd/csi/provisioner/manager.go +++ /dev/null @@ -1,69 +0,0 @@ -package provisioner - -import ( - cmdManager "github.com/Dynatrace/dynatrace-operator/cmd/manager" - "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme" - "github.com/pkg/errors" - "k8s.io/client-go/rest" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/cache" - "sigs.k8s.io/controller-runtime/pkg/healthz" - "sigs.k8s.io/controller-runtime/pkg/manager" - "sigs.k8s.io/controller-runtime/pkg/metrics/server" -) - -const ( - metricsBindAddress = ":8090" - defaultProbeAddress = ":8091" - livenessEndpointName = "/livez" - livezEndpointName = "livez" -) - -type csiDriverManagerProvider struct { - probeAddress string -} - -func newCsiDriverManagerProvider(probeAddress string) cmdManager.Provider { - return csiDriverManagerProvider{ - probeAddress: probeAddress, - } -} - -func (provider csiDriverManagerProvider) CreateManager(namespace string, config *rest.Config) (manager.Manager, error) { - mgr, err := manager.New(config, provider.createOptions(namespace)) - if err != nil { - return nil, errors.WithStack(err) - } - - err = provider.addHealthzCheck(mgr) - if err != nil { - return nil, err - } - - return mgr, nil -} - -func (provider csiDriverManagerProvider) addHealthzCheck(mgr manager.Manager) error { - err := mgr.AddHealthzCheck(livezEndpointName, healthz.Ping) - if err != nil { - return errors.WithStack(err) - } - - return nil -} - -func (provider csiDriverManagerProvider) createOptions(namespace string) ctrl.Options { - return ctrl.Options{ - Cache: cache.Options{ - DefaultNamespaces: map[string]cache.Config{ - namespace: {}, - }, - }, - Scheme: scheme.Scheme, - Metrics: server.Options{ - BindAddress: metricsBindAddress, - }, - HealthProbeBindAddress: provider.probeAddress, - LivenessEndpointName: livenessEndpointName, - } -} diff --git a/cmd/csi/provisioner/manager_test.go b/cmd/csi/provisioner/manager_test.go deleted file mode 100644 index 38152cfb04..0000000000 --- a/cmd/csi/provisioner/manager_test.go +++ /dev/null @@ -1,55 +0,0 @@ -package provisioner - -import ( - "testing" - - "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme" - managermock "github.com/Dynatrace/dynatrace-operator/test/mocks/sigs.k8s.io/controller-runtime/pkg/manager" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" -) - -func TestCsiDriverManagerProvider(t *testing.T) { - t.Run("is instantiable", func(t *testing.T) { - csiManagerProvider := newCsiDriverManagerProvider(defaultProbeAddress) - assert.NotNil(t, csiManagerProvider) - - csiManagerProviderImpl := csiManagerProvider.(csiDriverManagerProvider) - assert.Equal(t, defaultProbeAddress, csiManagerProviderImpl.probeAddress) - }) - t.Run("creates options", func(t *testing.T) { - csiManagerProvider := csiDriverManagerProvider{} - - options := csiManagerProvider.createOptions("namespace") - - assert.NotNil(t, options) - assert.Contains(t, options.Cache.DefaultNamespaces, "namespace") - assert.Equal(t, scheme.Scheme, options.Scheme) - assert.Equal(t, metricsBindAddress, options.Metrics.BindAddress) - assert.Equal(t, "", options.HealthProbeBindAddress) - assert.Equal(t, livenessEndpointName, options.LivenessEndpointName) - }) - t.Run("adds healthz check endpoint", func(t *testing.T) { - const addHealthzCheck = "AddHealthzCheck" - - operatorMgrProvider := csiDriverManagerProvider{} - mockMgr := managermock.NewManager(t) - mockMgr.On(addHealthzCheck, livezEndpointName, mock.AnythingOfType("healthz.Checker")).Return(nil) - - err := operatorMgrProvider.addHealthzCheck(mockMgr) - - require.NoError(t, err) - mockMgr.AssertCalled(t, addHealthzCheck, livezEndpointName, mock.AnythingOfType("healthz.Checker")) - - expectedError := errors.New("healthz error") - mockMgr = managermock.NewManager(t) - mockMgr.On(addHealthzCheck, mock.Anything, mock.Anything).Return(expectedError) - - err = operatorMgrProvider.addHealthzCheck(mockMgr) - - require.EqualError(t, err, expectedError.Error()) - mockMgr.AssertCalled(t, addHealthzCheck, mock.Anything, mock.Anything) - }) -} diff --git a/cmd/csi/server/builder.go b/cmd/csi/server/builder.go deleted file mode 100644 index 524bb5436a..0000000000 --- a/cmd/csi/server/builder.go +++ /dev/null @@ -1,149 +0,0 @@ -package server - -import ( - "github.com/Dynatrace/dynatrace-operator/cmd/config" - cmdManager "github.com/Dynatrace/dynatrace-operator/cmd/manager" - dtcsi "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi" - csidriver "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi/driver" - "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi/metadata" - "github.com/Dynatrace/dynatrace-operator/pkg/logd" - "github.com/Dynatrace/dynatrace-operator/pkg/version" - "github.com/pkg/errors" - "github.com/spf13/afero" - "github.com/spf13/cobra" - "golang.org/x/sys/unix" - ctrl "sigs.k8s.io/controller-runtime" -) - -const use = "csi-server" - -var nodeId, endpoint string - -type CommandBuilder struct { - configProvider config.Provider - managerProvider cmdManager.Provider - filesystem afero.Fs - csiOptions *dtcsi.CSIOptions - namespace string -} - -func NewCsiServerCommandBuilder() CommandBuilder { - return CommandBuilder{} -} - -func (builder CommandBuilder) SetConfigProvider(provider config.Provider) CommandBuilder { - builder.configProvider = provider - - return builder -} - -func (builder CommandBuilder) setManagerProvider(provider cmdManager.Provider) CommandBuilder { - builder.managerProvider = provider - - return builder -} - -func (builder CommandBuilder) SetNamespace(namespace string) CommandBuilder { - builder.namespace = namespace - - return builder -} - -func (builder CommandBuilder) setCsiOptions(csiOptions dtcsi.CSIOptions) CommandBuilder { - builder.csiOptions = &csiOptions - - return builder -} - -func (builder CommandBuilder) setFilesystem(filesystem afero.Fs) CommandBuilder { - builder.filesystem = filesystem - - return builder -} - -func (builder CommandBuilder) getCsiOptions() dtcsi.CSIOptions { - if builder.csiOptions == nil { - builder.csiOptions = &dtcsi.CSIOptions{ - NodeId: nodeId, - Endpoint: endpoint, - RootDir: dtcsi.DataPath, - } - } - - return *builder.csiOptions -} - -func (builder CommandBuilder) getManagerProvider() cmdManager.Provider { - if builder.managerProvider == nil { - builder.managerProvider = newCsiDriverManagerProvider() - } - - return builder.managerProvider -} - -func (builder CommandBuilder) getFilesystem() afero.Fs { - if builder.filesystem == nil { - builder.filesystem = afero.NewOsFs() - } - - return builder.filesystem -} - -func (builder CommandBuilder) Build() *cobra.Command { - cmd := &cobra.Command{ - Use: use, - RunE: builder.buildRun(), - } - - addFlags(cmd) - - return cmd -} - -func addFlags(cmd *cobra.Command) { - cmd.PersistentFlags().StringVar(&nodeId, "node-id", "", "node id") - cmd.PersistentFlags().StringVar(&endpoint, "endpoint", "unix:/tmp/csi.sock", "CSI endpoint") -} - -func (builder CommandBuilder) buildRun() func(*cobra.Command, []string) error { - return func(cmd *cobra.Command, args []string) error { - unix.Umask(dtcsi.UnixUmask) - version.LogVersion() - logd.LogBaseLoggerSettings() - - kubeConfig, err := builder.configProvider.GetConfig() - if err != nil { - return err - } - - csiManager, err := builder.getManagerProvider().CreateManager(builder.namespace, kubeConfig) - if err != nil { - return err - } - - signalHandler := ctrl.SetupSignalHandler() - - err = createCsiDataPath(builder.getFilesystem()) - if err != nil { - return err - } - - access, err := metadata.NewAccess(signalHandler, dtcsi.MetadataAccessPath) - if err != nil { - return err - } - - err = csidriver.NewServer(builder.getCsiOptions(), access).SetupWithManager(csiManager) - if err != nil { - return err - } - - err = csiManager.Start(signalHandler) - - return errors.WithStack(err) - } -} - -func createCsiDataPath(fs afero.Fs) error { - return errors.WithStack(fs.MkdirAll(dtcsi.DataPath, 0770)) -} diff --git a/cmd/csi/server/builder_test.go b/cmd/csi/server/builder_test.go deleted file mode 100644 index e2a263d4ce..0000000000 --- a/cmd/csi/server/builder_test.go +++ /dev/null @@ -1,84 +0,0 @@ -package server - -import ( - "io/fs" - "testing" - - dtcsi "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi" - configmock "github.com/Dynatrace/dynatrace-operator/test/mocks/cmd/config" - providermock "github.com/Dynatrace/dynatrace-operator/test/mocks/cmd/manager" - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestCsiCommandBuilder(t *testing.T) { - t.Run("build command", func(t *testing.T) { - builder := NewCsiServerCommandBuilder() - csiCommand := builder.Build() - - assert.NotNil(t, csiCommand) - assert.Equal(t, use, csiCommand.Use) - assert.NotNil(t, csiCommand.RunE) - }) - t.Run("set config provider", func(t *testing.T) { - builder := NewCsiServerCommandBuilder() - - assert.NotNil(t, builder) - - expectedProvider := &configmock.Provider{} - builder = builder.SetConfigProvider(expectedProvider) - - assert.Equal(t, expectedProvider, builder.configProvider) - }) - t.Run("set manager provider", func(t *testing.T) { - expectedProvider := providermock.NewProvider(t) - builder := NewCsiServerCommandBuilder().setManagerProvider(expectedProvider) - - assert.Equal(t, expectedProvider, builder.managerProvider) - }) - t.Run("set namespace", func(t *testing.T) { - builder := NewCsiServerCommandBuilder().SetNamespace("namespace") - - assert.Equal(t, "namespace", builder.namespace) - }) - t.Run("set filesystem", func(t *testing.T) { - expectedFs := afero.NewMemMapFs() - builder := NewCsiServerCommandBuilder() - - assert.Equal(t, afero.NewOsFs(), builder.getFilesystem()) - - builder = builder.setFilesystem(expectedFs) - - assert.Equal(t, expectedFs, builder.getFilesystem()) - }) - t.Run("set csi options", func(t *testing.T) { - expectedOptions := dtcsi.CSIOptions{ - NodeId: "test-node-id", - Endpoint: "test-endpoint", - RootDir: dtcsi.DataPath, - } - builder := NewCsiServerCommandBuilder(). - setCsiOptions(expectedOptions) - - assert.Equal(t, expectedOptions, builder.getCsiOptions()) - }) -} - -func TestCreateCsiRootPath(t *testing.T) { - memFs := afero.NewMemMapFs() - err := createCsiDataPath(memFs) - - require.NoError(t, err) - - exists, err := afero.Exists(memFs, dtcsi.DataPath) - - assert.True(t, exists) - require.NoError(t, err) - - stat, err := memFs.Stat(dtcsi.DataPath) - - require.NoError(t, err) - assert.Equal(t, fs.FileMode(0770), stat.Mode()&fs.FileMode(0770)) - assert.True(t, stat.IsDir()) -} diff --git a/cmd/csi/server/cmd.go b/cmd/csi/server/cmd.go new file mode 100644 index 0000000000..5222c6ce52 --- /dev/null +++ b/cmd/csi/server/cmd.go @@ -0,0 +1,116 @@ +package server + +import ( + "os" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme" + dtcsi "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi" + csidriver "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi/driver" + "github.com/Dynatrace/dynatrace-operator/pkg/logd" + "github.com/Dynatrace/dynatrace-operator/pkg/util/installconfig" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/env" + "github.com/Dynatrace/dynatrace-operator/pkg/version" + "github.com/pkg/errors" + "github.com/spf13/afero" + "github.com/spf13/cobra" + "golang.org/x/sys/unix" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/client/config" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/metrics/server" +) + +const ( + use = "csi-server" + + metricsBindAddress = ":8080" +) + +var nodeId, endpoint string + +func New() *cobra.Command { + cmd := &cobra.Command{ + Use: use, + RunE: run(), + SilenceUsage: true, + } + + addFlags(cmd) + + return cmd +} + +func addFlags(cmd *cobra.Command) { + cmd.PersistentFlags().StringVar(&nodeId, "node-id", "", "node id") + cmd.PersistentFlags().StringVar(&endpoint, "endpoint", "unix:///tmp/csi.sock", "CSI endpoint") +} + +func run() func(*cobra.Command, []string) error { + return func(cmd *cobra.Command, args []string) error { + unix.Umask(dtcsi.UnixUmask) + installconfig.ReadModules() + version.LogVersion() + logd.LogBaseLoggerSettings() + + kubeConfig, err := config.GetConfig() + if err != nil { + return err + } + + csiManager, err := createManager(kubeConfig, env.DefaultNamespace()) + if err != nil { + return err + } + + signalHandler := ctrl.SetupSignalHandler() + + err = createCSIDataPath(afero.NewOsFs()) + if err != nil { + return err + } + + err = csidriver.NewServer(createCsiOptions()).SetupWithManager(csiManager) + if err != nil { + return err + } + + err = csiManager.Start(signalHandler) + + return errors.WithStack(err) + } +} + +func createCSIDataPath(fs afero.Fs) error { + return errors.WithStack(fs.MkdirAll(dtcsi.DataPath, os.ModePerm)) +} + +func createManager(config *rest.Config, namespace string) (manager.Manager, error) { + options := ctrl.Options{ + Cache: cache.Options{ + DefaultNamespaces: map[string]cache.Config{ + namespace: {}, + }, + }, + Metrics: server.Options{ + BindAddress: metricsBindAddress, + }, + Scheme: scheme.Scheme, + } + + mgr, err := manager.New(config, options) + if err != nil { + return nil, errors.WithStack(err) + } + + return mgr, nil +} + +func createCsiOptions() dtcsi.CSIOptions { + return dtcsi.CSIOptions{ + NodeId: nodeId, + Endpoint: endpoint, + RootDir: dtcsi.DataPath, + } +} diff --git a/cmd/csi/server/command_test.go b/cmd/csi/server/command_test.go deleted file mode 100644 index 6f932b45a3..0000000000 --- a/cmd/csi/server/command_test.go +++ /dev/null @@ -1,49 +0,0 @@ -package server - -import ( - "testing" - - dtcsi "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi" - "github.com/Dynatrace/dynatrace-operator/pkg/logd" - configmock "github.com/Dynatrace/dynatrace-operator/test/mocks/cmd/config" - providermock "github.com/Dynatrace/dynatrace-operator/test/mocks/cmd/manager" - managermock "github.com/Dynatrace/dynatrace-operator/test/mocks/sigs.k8s.io/controller-runtime/pkg/manager" - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - "k8s.io/client-go/rest" -) - -func TestCsiCommand(t *testing.T) { - configProvider := &configmock.Provider{} - configProvider.On("GetConfig").Return(&rest.Config{}, nil) - - cmdMgr := managermock.NewManager(t) - - managerProvider := providermock.NewProvider(t) - managerProvider.On("CreateManager", mock.Anything, mock.Anything).Return(cmdMgr, nil) - - memFs := afero.NewMemMapFs() - builder := NewCsiServerCommandBuilder(). - SetConfigProvider(configProvider). - setManagerProvider(managerProvider). - SetNamespace("test-namespace"). - setFilesystem(memFs) - command := builder.Build() - commandFn := builder.buildRun() - - err := commandFn(command, make([]string, 0)) - - // sqlite library does not use afero fs, so it throws an error because path does not exist - require.Error(t, err) - configProvider.AssertCalled(t, "GetConfig") - managerProvider.AssertCalled(t, "CreateManager", "test-namespace", &rest.Config{}) - - exists, err := afero.Exists(memFs, dtcsi.DataPath) - assert.True(t, exists) - require.NoError(t, err) - - // Logging a newline because otherwise `go test` doesn't recognize the result - logd.Get().WithName("csi command").Info("") -} diff --git a/cmd/csi/server/manager.go b/cmd/csi/server/manager.go deleted file mode 100644 index 751db85d9f..0000000000 --- a/cmd/csi/server/manager.go +++ /dev/null @@ -1,46 +0,0 @@ -package server - -import ( - cmdManager "github.com/Dynatrace/dynatrace-operator/cmd/manager" - "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme" - "github.com/pkg/errors" - "k8s.io/client-go/rest" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/cache" - "sigs.k8s.io/controller-runtime/pkg/manager" - "sigs.k8s.io/controller-runtime/pkg/metrics/server" -) - -const ( - metricsBindAddress = ":8080" -) - -type csiDriverManagerProvider struct{} - -func newCsiDriverManagerProvider() cmdManager.Provider { - return csiDriverManagerProvider{} -} - -func (provider csiDriverManagerProvider) CreateManager(namespace string, config *rest.Config) (manager.Manager, error) { - mgr, err := manager.New(config, provider.createOptions(namespace)) - if err != nil { - return nil, errors.WithStack(err) - } - - return mgr, nil -} -func (provider csiDriverManagerProvider) createOptions(namespace string) ctrl.Options { - options := ctrl.Options{ - Cache: cache.Options{ - DefaultNamespaces: map[string]cache.Config{ - namespace: {}, - }, - }, - Metrics: server.Options{ - BindAddress: metricsBindAddress, - }, - Scheme: scheme.Scheme, - } - - return options -} diff --git a/cmd/csi/server/manager_test.go b/cmd/csi/server/manager_test.go deleted file mode 100644 index 0f20757c67..0000000000 --- a/cmd/csi/server/manager_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package server - -import ( - "testing" - - "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme" - "github.com/stretchr/testify/assert" -) - -func TestCsiDriverManagerProvider(t *testing.T) { - t.Run("is instantiable", func(t *testing.T) { - csiManagerProvider := newCsiDriverManagerProvider() - assert.NotNil(t, csiManagerProvider) - }) - t.Run("creates options", func(t *testing.T) { - csiManagerProvider := csiDriverManagerProvider{} - - options := csiManagerProvider.createOptions("namespace") - - assert.NotNil(t, options) - assert.Contains(t, options.Cache.DefaultNamespaces, "namespace") - assert.Equal(t, scheme.Scheme, options.Scheme) - assert.Equal(t, metricsBindAddress, options.Metrics.BindAddress) - - assert.Equal(t, "", options.HealthProbeBindAddress) - }) -} diff --git a/cmd/main.go b/cmd/main.go index bc5c6c3fed..7bd4e7f767 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -17,18 +17,16 @@ package main import ( "os" - cmdConfig "github.com/Dynatrace/dynatrace-operator/cmd/config" csiInit "github.com/Dynatrace/dynatrace-operator/cmd/csi/init" csiProvisioner "github.com/Dynatrace/dynatrace-operator/cmd/csi/provisioner" csiServer "github.com/Dynatrace/dynatrace-operator/cmd/csi/server" "github.com/Dynatrace/dynatrace-operator/cmd/operator" "github.com/Dynatrace/dynatrace-operator/cmd/standalone" - "github.com/Dynatrace/dynatrace-operator/cmd/startup_probe" - "github.com/Dynatrace/dynatrace-operator/cmd/support_archive" + startupProbe "github.com/Dynatrace/dynatrace-operator/cmd/startup_probe" + supportArchive "github.com/Dynatrace/dynatrace-operator/cmd/support_archive" "github.com/Dynatrace/dynatrace-operator/cmd/troubleshoot" "github.com/Dynatrace/dynatrace-operator/cmd/webhook" "github.com/Dynatrace/dynatrace-operator/pkg/logd" - "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/env" "github.com/pkg/errors" "github.com/spf13/cobra" ctrl "sigs.k8s.io/controller-runtime" @@ -47,52 +45,6 @@ func newRootCommand() *cobra.Command { return cmd } -func createWebhookCommandBuilder() webhook.CommandBuilder { - return webhook.NewWebhookCommandBuilder(). - SetNamespace(os.Getenv(env.PodNamespace)). - SetPodName(os.Getenv(env.PodName)). - SetConfigProvider(cmdConfig.NewKubeConfigProvider()) -} - -func createOperatorCommandBuilder() operator.CommandBuilder { - return operator.NewOperatorCommandBuilder(). - SetNamespace(os.Getenv(env.PodNamespace)). - SetPodName(os.Getenv(env.PodName)). - SetConfigProvider(cmdConfig.NewKubeConfigProvider()) -} - -func createCsiServerCommandBuilder() csiServer.CommandBuilder { - return csiServer.NewCsiServerCommandBuilder(). - SetNamespace(os.Getenv(env.PodNamespace)). - SetConfigProvider(cmdConfig.NewKubeConfigProvider()) -} - -func createCsiInitCommandBuilder() csiInit.CommandBuilder { - return csiInit.NewCsiInitCommandBuilder(). - SetNamespace(os.Getenv(env.PodNamespace)). - SetConfigProvider(cmdConfig.NewKubeConfigProvider()) -} - -func createCsiProvisionerCommandBuilder() csiProvisioner.CommandBuilder { - return csiProvisioner.NewCsiProvisionerCommandBuilder(). - SetNamespace(os.Getenv(env.PodNamespace)). - SetConfigProvider(cmdConfig.NewKubeConfigProvider()) -} - -func createTroubleshootCommandBuilder() troubleshoot.CommandBuilder { - return troubleshoot.NewTroubleshootCommandBuilder(). - SetConfigProvider(cmdConfig.NewKubeConfigProvider()) -} - -func createSupportArchiveCommandBuilder() support_archive.CommandBuilder { - return support_archive.NewCommandBuilder(). - SetConfigProvider(cmdConfig.NewKubeConfigProvider()) -} - -func createStartupProbe() startup_probe.CommandBuilder { - return startup_probe.NewCommandBuilder() -} - func rootCommand(_ *cobra.Command, _ []string) error { return errors.New("operator binary must be called with one of the subcommands") } @@ -103,15 +55,15 @@ func main() { cmd := newRootCommand() cmd.AddCommand( - createWebhookCommandBuilder().Build(), - createOperatorCommandBuilder().Build(), - createCsiServerCommandBuilder().Build(), - createCsiProvisionerCommandBuilder().Build(), + webhook.New(), + operator.New(), standalone.NewStandaloneCommand(), - createTroubleshootCommandBuilder().Build(), - createSupportArchiveCommandBuilder().Build(), - createStartupProbe().Build(), - createCsiInitCommandBuilder().Build(), + troubleshoot.New(), + supportArchive.New(), + startupProbe.New(), + csiInit.New(), + csiProvisioner.New(), + csiServer.New(), ) err := cmd.Execute() diff --git a/cmd/manager/manager.go b/cmd/manager/manager.go deleted file mode 100644 index 381ffefdaf..0000000000 --- a/cmd/manager/manager.go +++ /dev/null @@ -1,10 +0,0 @@ -package manager - -import ( - "k8s.io/client-go/rest" - "sigs.k8s.io/controller-runtime/pkg/manager" -) - -type Provider interface { - CreateManager(namespace string, config *rest.Config) (manager.Manager, error) -} diff --git a/cmd/manager/test.go b/cmd/manager/test.go deleted file mode 100644 index b9874cef25..0000000000 --- a/cmd/manager/test.go +++ /dev/null @@ -1,55 +0,0 @@ -package manager - -import ( - "context" - - "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme" - "github.com/Dynatrace/dynatrace-operator/pkg/logd" - "github.com/go-logr/logr" - "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/apimachinery/pkg/runtime" - "sigs.k8s.io/controller-runtime/pkg/cache" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/config" - "sigs.k8s.io/controller-runtime/pkg/manager" -) - -type TestManager struct { - manager.Manager -} - -func (mgr *TestManager) GetClient() client.Client { - return struct{ client.Client }{} -} - -func (mgr *TestManager) GetAPIReader() client.Reader { - return nil -} - -func (mgr *TestManager) GetControllerOptions() config.Controller { - return config.Controller{} -} - -func (mgr *TestManager) GetScheme() *runtime.Scheme { - return scheme.Scheme -} - -func (mgr *TestManager) GetCache() cache.Cache { - return nil -} - -func (mgr *TestManager) GetLogger() logr.Logger { - return logd.Get().WithName("test-manager").Logger -} - -func (mgr *TestManager) GetRESTMapper() meta.RESTMapper { - return nil -} - -func (mgr *TestManager) Add(manager.Runnable) error { - return nil -} - -func (mgr *TestManager) Start(_ context.Context) error { - return nil -} diff --git a/cmd/operator/builder.go b/cmd/operator/builder.go deleted file mode 100644 index f6b3ae1b0e..0000000000 --- a/cmd/operator/builder.go +++ /dev/null @@ -1,210 +0,0 @@ -package operator - -import ( - "context" - - "github.com/Dynatrace/dynatrace-operator/cmd/config" - cmdManager "github.com/Dynatrace/dynatrace-operator/cmd/manager" - "github.com/Dynatrace/dynatrace-operator/pkg/controllers/certificates" - "github.com/Dynatrace/dynatrace-operator/pkg/logd" - "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/pod" - "github.com/Dynatrace/dynatrace-operator/pkg/util/kubesystem" - "github.com/Dynatrace/dynatrace-operator/pkg/version" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "k8s.io/client-go/rest" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -const ( - use = "operator" -) - -type CommandBuilder struct { - configProvider config.Provider - bootstrapManagerProvider cmdManager.Provider - operatorManagerProvider cmdManager.Provider - signalHandler context.Context - client client.Client - namespace string - podName string -} - -func NewOperatorCommandBuilder() CommandBuilder { - return CommandBuilder{} -} - -func (builder CommandBuilder) SetConfigProvider(provider config.Provider) CommandBuilder { - builder.configProvider = provider - - return builder -} - -func (builder CommandBuilder) setOperatorManagerProvider(provider cmdManager.Provider) CommandBuilder { - builder.operatorManagerProvider = provider - - return builder -} - -func (builder CommandBuilder) setBootstrapManagerProvider(provider cmdManager.Provider) CommandBuilder { - builder.bootstrapManagerProvider = provider - - return builder -} - -func (builder CommandBuilder) SetNamespace(namespace string) CommandBuilder { - builder.namespace = namespace - - return builder -} - -func (builder CommandBuilder) SetPodName(podName string) CommandBuilder { - builder.podName = podName - - return builder -} - -func (builder CommandBuilder) setSignalHandler(ctx context.Context) CommandBuilder { - builder.signalHandler = ctx - - return builder -} - -func (builder CommandBuilder) setClient(client client.Client) CommandBuilder { - builder.client = client - - return builder -} - -func (builder CommandBuilder) getOperatorManagerProvider(isDeployedByOlm bool) cmdManager.Provider { - if builder.operatorManagerProvider == nil { - builder.operatorManagerProvider = NewOperatorManagerProvider(isDeployedByOlm) - } - - return builder.operatorManagerProvider -} - -func (builder CommandBuilder) getBootstrapManagerProvider() cmdManager.Provider { - if builder.bootstrapManagerProvider == nil { - builder.bootstrapManagerProvider = NewBootstrapManagerProvider() - } - - return builder.bootstrapManagerProvider -} - -// TODO: This can't be stateless (so pointer receiver needs to be used), because the ctrl.SetupSignalHandler() can only be called once in a process, otherwise we get a panic. This "builder" pattern has to be refactored. -func (builder *CommandBuilder) getSignalHandler() context.Context { - if builder.signalHandler == nil { - builder.signalHandler = ctrl.SetupSignalHandler() - } - - return builder.signalHandler -} - -func (builder CommandBuilder) Build() *cobra.Command { - return &cobra.Command{ - Use: use, - RunE: builder.buildRun(), - } -} - -func (builder CommandBuilder) setClientFromConfig(kubeCfg *rest.Config) (CommandBuilder, error) { - if builder.client == nil { - clt, err := client.New(kubeCfg, client.Options{}) - if err != nil { - return builder, err - } - - return builder.setClient(clt), nil - } - - return builder, nil -} - -func (builder CommandBuilder) buildRun() func(cmd *cobra.Command, args []string) error { - return func(cmd *cobra.Command, args []string) error { - version.LogVersion() - logd.LogBaseLoggerSettings() - - kubeCfg, err := builder.configProvider.GetConfig() - if err != nil { - return err - } - - builder, err = builder.setClientFromConfig(kubeCfg) - if err != nil { - return err - } - - if kubesystem.IsRunLocally() { - log.Info("running locally in debug mode") - - return builder.runLocally(kubeCfg) - } - - return builder.runInPod(kubeCfg) - } -} - -func (builder CommandBuilder) runInPod(kubeCfg *rest.Config) error { - operatorPod, err := pod.Get(context.TODO(), builder.client, builder.podName, builder.namespace) - if err != nil { - return err - } - - isDeployedViaOlm := kubesystem.IsDeployedViaOlm(*operatorPod) - if !isDeployedViaOlm { - err = builder.runBootstrapper(kubeCfg) - if err != nil { - return err - } - } - - return builder.runOperatorManager(kubeCfg, isDeployedViaOlm) -} - -func (builder CommandBuilder) runLocally(kubeCfg *rest.Config) error { - err := builder.runBootstrapper(kubeCfg) - if err != nil { - return err - } - - return builder.runOperatorManager(kubeCfg, false) -} - -func (builder CommandBuilder) runBootstrapper(kubeCfg *rest.Config) error { - bootstrapManager, err := builder.getBootstrapManagerProvider().CreateManager(builder.namespace, kubeCfg) - if err != nil { - return err - } - - return startBootstrapperManager(bootstrapManager, builder.namespace) -} - -func (builder CommandBuilder) runOperatorManager(kubeCfg *rest.Config, isDeployedViaOlm bool) error { - operatorManager, err := builder.getOperatorManagerProvider(isDeployedViaOlm).CreateManager(builder.namespace, kubeCfg) - if err != nil { - return err - } - - err = operatorManager.Start(builder.getSignalHandler()) - - return errors.WithStack(err) -} - -func startBootstrapperManager(bootstrapManager ctrl.Manager, namespace string) error { - ctx, cancelFn := context.WithCancel(context.Background()) - - err := certificates.AddBootstrap(bootstrapManager, namespace, cancelFn) - if err != nil { - return errors.WithStack(err) - } - - err = bootstrapManager.Start(ctx) - if err != nil { - return errors.WithStack(err) - } - - return nil -} diff --git a/cmd/operator/builder_test.go b/cmd/operator/builder_test.go deleted file mode 100644 index d693577db3..0000000000 --- a/cmd/operator/builder_test.go +++ /dev/null @@ -1,257 +0,0 @@ -package operator - -import ( - "context" - "testing" - - "github.com/Dynatrace/dynatrace-operator/cmd/manager" - dtfake "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" - "github.com/Dynatrace/dynatrace-operator/pkg/util/address" - configmock "github.com/Dynatrace/dynatrace-operator/test/mocks/cmd/config" - providermock "github.com/Dynatrace/dynatrace-operator/test/mocks/cmd/manager" - managermock "github.com/Dynatrace/dynatrace-operator/test/mocks/sigs.k8s.io/controller-runtime/pkg/manager" - "github.com/go-logr/logr" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes/scheme" - "k8s.io/client-go/rest" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/client/fake" - "sigs.k8s.io/controller-runtime/pkg/config" -) - -const ( - testNamespace = "test-namespace" - testPod = "test-pod-name" -) - -func TestCommandBuilder(t *testing.T) { - t.Run("build command", func(t *testing.T) { - builder := NewOperatorCommandBuilder() - operatorCommand := builder.Build() - - assert.NotNil(t, operatorCommand) - assert.Equal(t, use, operatorCommand.Use) - assert.NotNil(t, operatorCommand.RunE) - }) - t.Run("set config provider", func(t *testing.T) { - builder := NewOperatorCommandBuilder() - - assert.NotNil(t, builder) - - expectedProvider := &configmock.Provider{} - builder = builder.SetConfigProvider(expectedProvider) - - assert.Equal(t, expectedProvider, builder.configProvider) - }) - t.Run("set operator manager provider", func(t *testing.T) { - expectedProvider := providermock.NewProvider(t) - builder := NewOperatorCommandBuilder().setOperatorManagerProvider(expectedProvider) - - assert.Equal(t, expectedProvider, builder.operatorManagerProvider) - }) - t.Run("set bootstrap manager provider", func(t *testing.T) { - expectedProvider := providermock.NewProvider(t) - builder := NewOperatorCommandBuilder().setBootstrapManagerProvider(expectedProvider) - - assert.Equal(t, expectedProvider, builder.bootstrapManagerProvider) - }) - t.Run("set namespace", func(t *testing.T) { - builder := NewOperatorCommandBuilder().SetNamespace("namespace") - - assert.Equal(t, "namespace", builder.namespace) - }) - t.Run("set context", func(t *testing.T) { - // If ctrl.SetupSignalHandler() is used multiple times during a test suit, it will panic - // Therefore it is necessary to set a custom context to unit test properly - ctx := context.TODO() - builder := NewOperatorCommandBuilder().setSignalHandler(ctx) - - assert.Equal(t, ctx, builder.signalHandler) - }) -} - -func TestOperatorCommand(t *testing.T) { - t.Run("operator command exists", func(t *testing.T) { - operatorCommand := NewOperatorCommandBuilder().Build() - - assert.Equal(t, "operator", operatorCommand.Use) - assert.NotNil(t, operatorCommand.RunE) - }) - t.Run("kubernetes config provider is called", func(t *testing.T) { - mockCfgProvider := &configmock.Provider{} - mockCfgProvider.On("GetConfig").Return(&rest.Config{}, nil) - - mockMgrProvider := providermock.NewProvider(t) - - builder := NewOperatorCommandBuilder(). - SetNamespace(testNamespace). - setOperatorManagerProvider(mockMgrProvider). - setBootstrapManagerProvider(mockMgrProvider). - SetConfigProvider(mockCfgProvider) - operatorCommand := builder.Build() - - _ = operatorCommand.RunE(operatorCommand, make([]string, 0)) - - mockCfgProvider.AssertCalled(t, "GetConfig") - }) - t.Run("exit on config provider error", func(t *testing.T) { - mockCfgProvider := &configmock.Provider{} - mockCfgProvider.On("GetConfig").Return(&rest.Config{}, errors.New("config provider error")) - builder := NewOperatorCommandBuilder(). - SetConfigProvider(mockCfgProvider) - operatorCommand := builder.Build() - - err := operatorCommand.RunE(operatorCommand, make([]string, 0)) - - require.EqualError(t, err, "config provider error") - }) - t.Run("create manager if not in OLM", func(t *testing.T) { - mockCfgProvider := &configmock.Provider{} - mockCfgProvider.On("GetConfig").Return(&rest.Config{}, nil) - - mockMgrProvider := providermock.NewProvider(t) - mockMgrProvider. - On("CreateManager", mock.AnythingOfType("string"), &rest.Config{}). - Return(&manager.TestManager{}, nil) - - builder := NewOperatorCommandBuilder(). - SetNamespace(testNamespace). - SetPodName(testPod). - setOperatorManagerProvider(mockMgrProvider). - setBootstrapManagerProvider(mockMgrProvider). - SetConfigProvider(mockCfgProvider). - setSignalHandler(context.TODO()). - setClient(createFakeClient(false)) - operatorCommand := builder.Build() - - err := operatorCommand.RunE(operatorCommand, make([]string, 0)) - - require.NoError(t, err) - }) - t.Run("exit on manager error", func(t *testing.T) { - mockCfgProvider := &configmock.Provider{} - mockCfgProvider.On("GetConfig").Return(&rest.Config{}, nil) - - mockMgrProvider := providermock.NewProvider(t) - mockMgrProvider. - On("CreateManager", mock.AnythingOfType("string"), &rest.Config{}). - Return(&manager.TestManager{}, errors.New("create manager error")) - - builder := NewOperatorCommandBuilder(). - SetNamespace(testNamespace). - SetPodName(testPod). - setBootstrapManagerProvider(mockMgrProvider). - SetConfigProvider(mockCfgProvider). - setClient(createFakeClient(false)) - operatorCommand := builder.Build() - - err := operatorCommand.RunE(operatorCommand, make([]string, 0)) - - require.EqualError(t, err, "create manager error") - }) - t.Run("bootstrap manager is started", func(t *testing.T) { - mockCfgProvider := &configmock.Provider{} - mockCfgProvider.On("GetConfig").Return(&rest.Config{}, nil) - - mockMgr := managermock.NewManager(t) - mockMgr.On("Start", mock.Anything).Return(nil) - - clt := dtfake.NewClient() - - mockMgr.On("GetScheme").Return(scheme.Scheme) - mockMgr.On("GetClient").Return(clt) - mockMgr.On("GetAPIReader").Return(clt) - mockMgr.On("GetControllerOptions").Return(config.Controller{SkipNameValidation: address.Of(true)}) - mockMgr.On("GetLogger").Return(logr.Logger{}) - mockMgr.On("Add", mock.AnythingOfType("*controller.Controller[sigs.k8s.io/controller-runtime/pkg/reconcile.Request]")).Return(nil) - mockMgr.On("GetCache").Return(nil) - - mockMgrProvider := providermock.NewProvider(t) - mockMgrProvider. - On("CreateManager", mock.AnythingOfType("string"), &rest.Config{}). - Return(mockMgr, nil) - - builder := NewOperatorCommandBuilder(). - SetNamespace(testNamespace). - SetPodName(testPod). - setOperatorManagerProvider(mockMgrProvider). - setBootstrapManagerProvider(mockMgrProvider). - SetConfigProvider(mockCfgProvider). - setSignalHandler(context.TODO()). - setClient(createFakeClient(false)) - operatorCommand := builder.Build() - - err := operatorCommand.RunE(operatorCommand, make([]string, 0)) - - require.NoError(t, err) - mockMgr.AssertCalled(t, "Start", mock.Anything) - }) - t.Run("operator manager is started", func(t *testing.T) { - mockCfgProvider := &configmock.Provider{} - mockCfgProvider.On("GetConfig").Return(&rest.Config{}, nil) - - bootstrapMockMgr := managermock.NewManager(t) - bootstrapMockMgr.On("Start", mock.Anything).Return(nil).Maybe() - - mockBootstrapMgrProvider := providermock.NewProvider(t) - mockBootstrapMgrProvider. - On("CreateManager", mock.AnythingOfType("string"), &rest.Config{}). - Return(bootstrapMockMgr, nil).Maybe() - - operatorMockMgr := managermock.NewManager(t) - operatorMockMgr.On("Start", mock.Anything).Return(nil) - - mockOperatorMgrProvider := providermock.NewProvider(t) - mockOperatorMgrProvider. - On("CreateManager", mock.AnythingOfType("string"), &rest.Config{}). - Return(operatorMockMgr, nil) - - builder := NewOperatorCommandBuilder(). - SetNamespace(testNamespace). - SetPodName(testPod). - setOperatorManagerProvider(mockOperatorMgrProvider). - setBootstrapManagerProvider(mockBootstrapMgrProvider). - SetConfigProvider(mockCfgProvider). - setSignalHandler(context.TODO()). - setClient(createFakeClient(true)) - operatorCommand := builder.Build() - - err := operatorCommand.RunE(operatorCommand, make([]string, 0)) - - require.NoError(t, err) - mockBootstrapMgrProvider.AssertNotCalled(t, "CreateManager", mock.AnythingOfType("string"), &rest.Config{}) - bootstrapMockMgr.AssertNotCalled(t, "Start", mock.Anything) - operatorMockMgr.AssertCalled(t, "Start", mock.Anything) - }) -} - -func createFakeClient(isDeployedViaOlm bool) client.WithWatch { - annotations := map[string]string{} - if isDeployedViaOlm { - annotations = map[string]string{ - "olm.operatorNamespace": "operators", - } - } - - return fake.NewClientBuilder(). - WithScheme(scheme.Scheme). - WithObjects( - &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: testNamespace, - }, - }, - &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: testPod, - Namespace: testNamespace, - Annotations: annotations, - }, - }, - ).Build() -} diff --git a/cmd/operator/certs.go b/cmd/operator/certs.go new file mode 100644 index 0000000000..5488694662 --- /dev/null +++ b/cmd/operator/certs.go @@ -0,0 +1,59 @@ +package operator + +import ( + "context" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme" + "github.com/Dynatrace/dynatrace-operator/pkg/controllers/certificates" + "github.com/pkg/errors" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/manager" +) + +func runCertInit(cfg *rest.Config, namespace string) error { + certInitManager, err := createCertInitManager(cfg, namespace) + if err != nil { + return err + } + + ctx, cancelFn := context.WithCancel(context.Background()) + + err = certificates.AddInit(certInitManager, namespace, cancelFn) + if err != nil { + return errors.WithStack(err) + } + + err = certInitManager.Start(ctx) + if err != nil { + return errors.WithStack(err) + } + + return nil +} + +func createCertInitManager(cfg *rest.Config, namespace string) (manager.Manager, error) { + controlManager, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme.Scheme, + Cache: cache.Options{ + DefaultNamespaces: map[string]cache.Config{ + namespace: {}, + }, + }, + HealthProbeBindAddress: healthProbeBindAddress, + LivenessEndpointName: livenessEndpointName, + }) + + if err != nil { + return nil, errors.WithStack(err) + } + + err = controlManager.AddHealthzCheck(livezEndpointName, healthz.Ping) + if err != nil { + return nil, errors.WithStack(err) + } + + return controlManager, errors.WithStack(err) +} diff --git a/cmd/operator/cmd.go b/cmd/operator/cmd.go new file mode 100644 index 0000000000..f371252dd6 --- /dev/null +++ b/cmd/operator/cmd.go @@ -0,0 +1,100 @@ +package operator + +import ( + "context" + "os" + + "github.com/Dynatrace/dynatrace-operator/pkg/logd" + "github.com/Dynatrace/dynatrace-operator/pkg/util/installconfig" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/env" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/pod" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubesystem" + "github.com/Dynatrace/dynatrace-operator/pkg/version" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/config" +) + +const ( + use = "operator" +) + +func New() *cobra.Command { + return &cobra.Command{ + Use: use, + RunE: run(), + SilenceUsage: true, + } +} + +func run() func(cmd *cobra.Command, args []string) error { + return func(cmd *cobra.Command, args []string) error { + installconfig.ReadModules() + version.LogVersion() + logd.LogBaseLoggerSettings() + + kubeCfg, err := config.GetConfig() + if err != nil { + return err + } + + if kubesystem.IsRunLocally() { + log.Info("running locally in debug mode") + + return runLocally(kubeCfg) + } + + return runInPod(kubeCfg) + } +} + +func runInPod(kubeCfg *rest.Config) error { + clt, err := client.New(kubeCfg, client.Options{}) + if err != nil { + return err + } + + podName := os.Getenv(env.PodName) + namespace := os.Getenv(env.PodNamespace) + + operatorPod, err := pod.Get(context.Background(), clt, podName, namespace) + if err != nil { + return err + } + + isOLM := kubesystem.IsDeployedViaOlm(*operatorPod) + if !isOLM { + err = runCertInit(kubeCfg, namespace) + if err != nil { + return err + } + } + + return runOperator(kubeCfg, namespace, isOLM) +} + +func runLocally(kubeCfg *rest.Config) error { + namespace := os.Getenv(env.PodNamespace) + + err := runCertInit(kubeCfg, namespace) + if err != nil { + return err + } + + return runOperator(kubeCfg, namespace, false) +} + +func runOperator(kubeCfg *rest.Config, namespace string, isOLM bool) error { + operatorManager, err := createOperatorManager(kubeCfg, namespace, isOLM) + if err != nil { + return err + } + + ctx := ctrl.SetupSignalHandler() + err = operatorManager.Start(ctx) + + return errors.WithStack(err) +} diff --git a/cmd/operator/config.go b/cmd/operator/config.go index d5714fc1ca..d752aed4b7 100644 --- a/cmd/operator/config.go +++ b/cmd/operator/config.go @@ -4,4 +4,22 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/logd" ) +const ( + metricsBindAddress = ":8080" + healthProbeBindAddress = ":10080" + + leaderElectionId = "dynatrace-operator-lock" + leaderElectionResourceLock = "leases" + leaderElectionEnvVarRenewDeadline = "LEADER_ELECTION_RENEW_DEADLINE" + leaderElectionEnvVarRetryPeriod = "LEADER_ELECTION_RETRY_PERIOD" + leaderElectionEnvVarLeaseDuration = "LEADER_ELECTION_LEASE_DURATION" + + livezEndpointName = "livez" + livenessEndpointName = "/" + livezEndpointName + + defaultLeaseDuration = int64(30) + defaultRenewDeadline = int64(20) + defaultRetryPeriod = int64(6) +) + var log = logd.Get().WithName("operator-command") diff --git a/cmd/operator/manager.go b/cmd/operator/manager.go index fa71a84e63..9da99488ef 100644 --- a/cmd/operator/manager.go +++ b/cmd/operator/manager.go @@ -5,7 +5,6 @@ import ( "strconv" "time" - cmdManager "github.com/Dynatrace/dynatrace-operator/cmd/manager" "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/certificates" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube" @@ -21,76 +20,22 @@ import ( "sigs.k8s.io/controller-runtime/pkg/metrics/server" ) -const ( - metricsBindAddress = ":8080" - healthProbeBindAddress = ":10080" - - leaderElectionId = "dynatrace-operator-lock" - leaderElectionResourceLock = "leases" - leaderElectionEnvVarRenewDeadline = "LEADER_ELECTION_RENEW_DEADLINE" - leaderElectionEnvVarRetryPeriod = "LEADER_ELECTION_RETRY_PERIOD" - leaderElectionEnvVarLeaseDuration = "LEADER_ELECTION_LEASE_DURATION" - - livezEndpointName = "livez" - livenessEndpointName = "/" + livezEndpointName - - defaultLeaseDuration = int64(30) - defaultRenewDeadline = int64(20) - defaultRetryPeriod = int64(6) -) - -type bootstrapManagerProvider struct { - managerBuilder -} - -func NewBootstrapManagerProvider() cmdManager.Provider { - return bootstrapManagerProvider{} -} - -func (provider bootstrapManagerProvider) CreateManager(namespace string, config *rest.Config) (manager.Manager, error) { - controlManager, err := provider.getManager(config, ctrl.Options{ - Scheme: scheme.Scheme, - Cache: cache.Options{ - DefaultNamespaces: map[string]cache.Config{ - namespace: {}, - }, - }, - HealthProbeBindAddress: healthProbeBindAddress, - LivenessEndpointName: livenessEndpointName, - }) - - if err != nil { - return nil, errors.WithStack(err) - } - - err = controlManager.AddHealthzCheck(livezEndpointName, healthz.Ping) +func createOperatorManager(cfg *rest.Config, namespace string, isOLM bool) (manager.Manager, error) { + mgr, err := ctrl.NewManager(cfg, createOptions(namespace)) if err != nil { return nil, errors.WithStack(err) } - return controlManager, errors.WithStack(err) -} - -type operatorManagerProvider struct { - managerBuilder - deployedViaOlm bool -} - -func NewOperatorManagerProvider(deployedViaOlm bool) cmdManager.Provider { - return operatorManagerProvider{ - deployedViaOlm: deployedViaOlm, - } -} - -func (provider operatorManagerProvider) CreateManager(namespace string, cfg *rest.Config) (manager.Manager, error) { - mgr, err := provider.getManager(cfg, provider.createOptions(namespace)) + err = mgr.AddHealthzCheck(livezEndpointName, healthz.Ping) if err != nil { return nil, errors.WithStack(err) } - err = mgr.AddHealthzCheck(livezEndpointName, healthz.Ping) - if err != nil { - return nil, errors.WithStack(err) + if isOLM { + err = certificates.Add(mgr, namespace) + if err != nil { + return nil, err + } } err = dynakube.Add(mgr, namespace) @@ -103,11 +48,6 @@ func (provider operatorManagerProvider) CreateManager(namespace string, cfg *res return nil, err } - err = provider.addCertificateController(mgr, namespace) - if err != nil { - return nil, err - } - err = edgeconnect.Add(mgr, namespace) if err != nil { return nil, err @@ -116,15 +56,7 @@ func (provider operatorManagerProvider) CreateManager(namespace string, cfg *res return mgr, nil } -func (provider operatorManagerProvider) addCertificateController(mgr manager.Manager, namespace string) error { - if !provider.deployedViaOlm { - return certificates.Add(mgr, namespace) - } - - return nil -} - -func (provider operatorManagerProvider) createOptions(namespace string) ctrl.Options { +func createOptions(namespace string) ctrl.Options { return ctrl.Options{ Cache: cache.Options{ DefaultNamespaces: map[string]cache.Config{ @@ -147,24 +79,6 @@ func (provider operatorManagerProvider) createOptions(namespace string) ctrl.Opt } } -// managerBuilder is used for testing the createManager functions in the providers. -type managerBuilder struct { - mgr manager.Manager -} - -func (builder *managerBuilder) getManager(config *rest.Config, options manager.Options) (manager.Manager, error) { - var err error - if builder.mgr == nil { - builder.mgr, err = ctrl.NewManager(config, options) - } - - return builder.mgr, err -} - -func (builder *managerBuilder) setManager(mgr manager.Manager) { - builder.mgr = mgr -} - func getTimeFromEnvWithDefault(envName string, defaultValue int64) *time.Duration { duration := time.Duration(defaultValue) * time.Second diff --git a/cmd/operator/manager_test.go b/cmd/operator/manager_test.go deleted file mode 100644 index e2b92d90ac..0000000000 --- a/cmd/operator/manager_test.go +++ /dev/null @@ -1,120 +0,0 @@ -package operator - -import ( - "errors" - "testing" - - "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme" - "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" - "github.com/Dynatrace/dynatrace-operator/pkg/util/address" - managermock "github.com/Dynatrace/dynatrace-operator/test/mocks/sigs.k8s.io/controller-runtime/pkg/manager" - "github.com/go-logr/logr" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/rest" - "sigs.k8s.io/controller-runtime/pkg/config" -) - -const ( - addHealthzCheckMethodName = "AddHealthzCheck" - checkerArgumentType = "healthz.Checker" -) - -func TestOperatorManagerProvider(t *testing.T) { - t.Run("creates correct options", func(t *testing.T) { - operatorMgrProvider := operatorManagerProvider{} - options := operatorMgrProvider.createOptions("namespace") - - assert.NotNil(t, options) - - assert.Contains(t, options.Cache.DefaultNamespaces, "namespace") - assert.Equal(t, scheme.Scheme, options.Scheme) - assert.Equal(t, metricsBindAddress, options.Metrics.BindAddress) - - assert.True(t, options.LeaderElection) - assert.Equal(t, leaderElectionId, options.LeaderElectionID) - assert.Equal(t, leaderElectionResourceLock, options.LeaderElectionResourceLock) - assert.Equal(t, "namespace", options.LeaderElectionNamespace) - assert.Equal(t, healthProbeBindAddress, options.HealthProbeBindAddress) - assert.Equal(t, livenessEndpointName, options.LivenessEndpointName) - }) - t.Run("check if healthz/readyz checks are added", func(t *testing.T) { - testHealthzAndReadyz(t, func(mockMgr *managermock.Manager) error { - var controlManagerProvider = NewOperatorManagerProvider(false).(operatorManagerProvider) - - controlManagerProvider.setManager(mockMgr) - _, err := controlManagerProvider.CreateManager("namespace", &rest.Config{}) - - return err - }) - }) -} - -func TestBootstrapManagerProvider(t *testing.T) { - t.Run("implements interface", func(t *testing.T) { - bootstrapProvider := NewBootstrapManagerProvider() - _, _ = bootstrapProvider.CreateManager("namespace", &rest.Config{}) - }) - t.Run("check if healthz/readyz checks are added", func(t *testing.T) { - testBootstrapHealthzAndReadyz(t, func(mockMgr *managermock.Manager) error { - bootstrapProvider, _ := NewBootstrapManagerProvider().(bootstrapManagerProvider) - bootstrapProvider.setManager(mockMgr) - _, err := bootstrapProvider.CreateManager("namespace", &rest.Config{}) - - return err - }) - }) -} - -func testHealthzAndReadyz(t *testing.T, createProviderAndRunManager func(mockMgr *managermock.Manager) error) { - mockMgr := managermock.NewManager(t) - mockMgr.On(addHealthzCheckMethodName, livezEndpointName, mock.AnythingOfType(checkerArgumentType)).Return(nil) - - client := fake.NewClient(&corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "kube-system"}}) - - mockMgr.On("GetConfig").Return(&rest.Config{}) - mockMgr.On("GetScheme").Return(scheme.Scheme) - mockMgr.On("GetClient").Return(client) - mockMgr.On("GetAPIReader").Return(client) - mockMgr.On("GetControllerOptions").Return(config.Controller{SkipNameValidation: address.Of(true)}) - mockMgr.On("GetLogger").Return(logr.Logger{}) - mockMgr.On("Add", mock.AnythingOfType("*controller.Controller[sigs.k8s.io/controller-runtime/pkg/reconcile.Request]")).Return(nil) - mockMgr.On("GetCache").Return(nil) - mockMgr.On("GetRESTMapper").Return(nil) - - err := createProviderAndRunManager(mockMgr) - - require.NoError(t, err) - mockMgr.AssertCalled(t, addHealthzCheckMethodName, livezEndpointName, mock.AnythingOfType(checkerArgumentType)) - - expectedHealthzError := errors.New("healthz error") - mockMgr = managermock.NewManager(t) - mockMgr.On(addHealthzCheckMethodName, mock.Anything, mock.Anything).Return(expectedHealthzError) - - err = createProviderAndRunManager(mockMgr) - - require.EqualError(t, err, expectedHealthzError.Error()) - mockMgr.AssertCalled(t, addHealthzCheckMethodName, mock.Anything, mock.Anything) -} - -func testBootstrapHealthzAndReadyz(t *testing.T, createProviderAndRunManager func(mockMgr *managermock.Manager) error) { - mockMgr := managermock.NewManager(t) - mockMgr.On(addHealthzCheckMethodName, livezEndpointName, mock.AnythingOfType(checkerArgumentType)).Return(nil) - - err := createProviderAndRunManager(mockMgr) - - require.NoError(t, err) - mockMgr.AssertCalled(t, addHealthzCheckMethodName, livezEndpointName, mock.AnythingOfType(checkerArgumentType)) - - expectedHealthzError := errors.New("healthz error") - mockMgr = managermock.NewManager(t) - mockMgr.On(addHealthzCheckMethodName, mock.Anything, mock.Anything).Return(expectedHealthzError) - - err = createProviderAndRunManager(mockMgr) - - require.EqualError(t, err, expectedHealthzError.Error()) - mockMgr.AssertCalled(t, addHealthzCheckMethodName, mock.Anything, mock.Anything) -} diff --git a/cmd/standalone/standalone.go b/cmd/standalone/cmd.go similarity index 82% rename from cmd/standalone/standalone.go rename to cmd/standalone/cmd.go index 6c87bd5e33..15729fc0e7 100644 --- a/cmd/standalone/standalone.go +++ b/cmd/standalone/cmd.go @@ -14,12 +14,13 @@ const ( func NewStandaloneCommand() *cobra.Command { return &cobra.Command{ - Use: use, - RunE: startStandAloneInit, + Use: use, + RunE: run, + SilenceUsage: true, } } -func startStandAloneInit(_ *cobra.Command, _ []string) error { +func run(_ *cobra.Command, _ []string) error { unix.Umask(0000) signalHandler := ctrl.SetupSignalHandler() diff --git a/cmd/standalone/standalone_test.go b/cmd/standalone/cmd_test.go similarity index 100% rename from cmd/standalone/standalone_test.go rename to cmd/standalone/cmd_test.go diff --git a/cmd/startup_probe/builder.go b/cmd/startup_probe/cmd.go similarity index 81% rename from cmd/startup_probe/builder.go rename to cmd/startup_probe/cmd.go index 5ca0ffa006..a3ea9a399a 100644 --- a/cmd/startup_probe/builder.go +++ b/cmd/startup_probe/cmd.go @@ -21,18 +21,11 @@ var ( timeoutFlagValue = 5 ) -type CommandBuilder struct { -} - -func NewCommandBuilder() CommandBuilder { - return CommandBuilder{} -} - -func (builder CommandBuilder) Build() *cobra.Command { +func New() *cobra.Command { cmd := &cobra.Command{ Use: use, Long: "query DNS server about " + hostname, - RunE: builder.buildRun(), + RunE: run(), } cmd.PersistentFlags().IntVar(&timeoutFlagValue, "timeout", defaultTimeout, "specify a different timeout [s]") @@ -43,7 +36,7 @@ func (builder CommandBuilder) Build() *cobra.Command { return cmd } -func (builder CommandBuilder) buildRun() func(*cobra.Command, []string) error { +func run() func(*cobra.Command, []string) error { return func(cmd *cobra.Command, args []string) error { f := func(_ *cobra.Command, _ []string) error { ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutFlagValue)*time.Second) diff --git a/cmd/support_archive/builder.go b/cmd/support_archive/cmd.go similarity index 88% rename from cmd/support_archive/builder.go rename to cmd/support_archive/cmd.go index 297e05de7b..0dc2c1d522 100644 --- a/cmd/support_archive/builder.go +++ b/cmd/support_archive/cmd.go @@ -3,14 +3,15 @@ package support_archive import ( "bytes" "context" + "fmt" "io" "os" "time" - "github.com/Dynatrace/dynatrace-operator/cmd/config" - "github.com/Dynatrace/dynatrace-operator/cmd/remote_command" + "github.com/Dynatrace/dynatrace-operator/cmd/support_archive/remote_command" "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme" "github.com/Dynatrace/dynatrace-operator/pkg/logd" + "github.com/Dynatrace/dynatrace-operator/pkg/util/installconfig" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/env" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/labels" "github.com/Dynatrace/dynatrace-operator/pkg/version" @@ -22,6 +23,7 @@ import ( clientgocorev1 "k8s.io/client-go/kubernetes/typed/core/v1" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/config" "sigs.k8s.io/controller-runtime/pkg/cluster" ) @@ -35,7 +37,9 @@ const ( loadsimFileSizeFlagName = "loadsim-file-size" loadsimFilesFlagName = "loadsim-files" collectManagedLogsFlagName = "managed-logs" + numEventsFlagName = "num-events" defaultSimFileSize = 10 + DefaultNumEvents = 300 ) const ( @@ -51,31 +55,14 @@ var ( loadsimFileSizeFlagValue int collectManagedLogsFlagValue bool delayFlagValue int + NumEventsFlagValue int ) -type CommandBuilder struct { - configProvider config.Provider -} - -func NewCommandBuilder() CommandBuilder { - return CommandBuilder{} -} - -func (builder CommandBuilder) SetConfigProvider(provider config.Provider) CommandBuilder { - builder.configProvider = provider - - return builder -} - -func clusterOptions(opts *cluster.Options) { - opts.Scheme = scheme.Scheme -} - -func (builder CommandBuilder) Build() *cobra.Command { +func New() *cobra.Command { cmd := &cobra.Command{ Use: use, Long: "Pack logs and manifests useful for troubleshooting into single tarball", - RunE: builder.buildRun(), + RunE: run(), } addFlags(cmd) @@ -89,14 +76,16 @@ func addFlags(cmd *cobra.Command) { cmd.PersistentFlags().IntVar(&loadsimFilesFlagValue, loadsimFilesFlagName, 0, "Number of simulated log files (default 0)") cmd.PersistentFlags().BoolVar(&collectManagedLogsFlagValue, collectManagedLogsFlagName, true, "Add logs from rolled out pods to the support archive.") cmd.PersistentFlags().IntVar(&delayFlagValue, delayFlagName, 0, "Delay start of support-archive collection. Useful for standalone execution with 'kubectl run'") + cmd.PersistentFlags().IntVar(&NumEventsFlagValue, numEventsFlagName, DefaultNumEvents, fmt.Sprintf("Number of events to be fetched (default %d)", DefaultNumEvents)) } -func (builder CommandBuilder) buildRun() func(*cobra.Command, []string) error { +func run() func(*cobra.Command, []string) error { return func(cmd *cobra.Command, args []string) error { time.Sleep(time.Duration(delayFlagValue) * time.Second) logBuffer := bytes.Buffer{} log := newSupportArchiveLogger(getLogOutput(archiveToStdoutFlagValue, &logBuffer)) + installconfig.ReadModulesToLogger(log) version.LogVersionToLogger(log) archiveTargetFile, err := createZipArchiveTargetFile(archiveToStdoutFlagValue, defaultSupportArchiveTargetDir) @@ -109,7 +98,7 @@ func (builder CommandBuilder) buildRun() func(*cobra.Command, []string) error { defer archiveTargetFile.Close() defer supportArchive.Close() - err = builder.runCollectors(log, supportArchive) + err = runCollectors(log, supportArchive) if err != nil { return err } @@ -148,10 +137,10 @@ func getAppNameLabel(ctx context.Context, pods clientgocorev1.PodInterface) stri return defaultOperatorAppName } -func (builder CommandBuilder) runCollectors(log logd.Logger, supportArchive archiver) error { +func runCollectors(log logd.Logger, supportArchive archiver) error { ctx := context.Background() - kubeConfig, err := builder.configProvider.GetConfig() + kubeConfig, err := config.GetConfig() if err != nil { return err } @@ -206,6 +195,10 @@ func getK8sClients(kubeConfig *rest.Config) (*kubernetes.Clientset, client.Reade return clientSet, apiReader, nil } +func clusterOptions(opts *cluster.Options) { + opts.Scheme = scheme.Scheme +} + func printCopyCommand(log logd.Logger, tarballToStdout bool, tarFileName string) { podNamespace := os.Getenv(env.PodNamespace) podName := os.Getenv(env.PodName) diff --git a/cmd/support_archive/builder_test.go b/cmd/support_archive/cmd_test.go similarity index 100% rename from cmd/support_archive/builder_test.go rename to cmd/support_archive/cmd_test.go diff --git a/cmd/support_archive/eec_fs_logs.go b/cmd/support_archive/eec_fs_logs.go index 02848c014d..3e7ac54564 100644 --- a/cmd/support_archive/eec_fs_logs.go +++ b/cmd/support_archive/eec_fs_logs.go @@ -6,7 +6,7 @@ import ( "io" "strings" - "github.com/Dynatrace/dynatrace-operator/cmd/remote_command" + "github.com/Dynatrace/dynatrace-operator/cmd/support_archive/remote_command" "github.com/Dynatrace/dynatrace-operator/pkg/logd" "github.com/Dynatrace/dynatrace-operator/pkg/util/installconfig" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/labels" diff --git a/cmd/support_archive/logs_test.go b/cmd/support_archive/logs_test.go index bc3ec74fcd..604cd40935 100644 --- a/cmd/support_archive/logs_test.go +++ b/cmd/support_archive/logs_test.go @@ -10,7 +10,7 @@ import ( "testing" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/labels" - corev1mock "github.com/Dynatrace/dynatrace-operator/pkg/util/testing/mocks/k8s.io/client-go/kubernetes/typed/core/v1" + corev1mock "github.com/Dynatrace/dynatrace-operator/test/mocks/k8s.io/client-go/kubernetes/typed/core/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -97,7 +97,6 @@ func testLogCollection(t *testing.T, collectManagedLogs bool) { } } -//go:generate mockery --case=snake --srcpkg=k8s.io/client-go/kubernetes/typed/core/v1 --with-expecter --name=PodInterface --output ../../mocks/k8s.io/client-go/kubernetes/typed/core/v1 func TestLogCollectorPodListError(t *testing.T) { ctx := context.Background() logBuffer := bytes.Buffer{} diff --git a/cmd/remote_command/command.go b/cmd/support_archive/remote_command/command.go similarity index 100% rename from cmd/remote_command/command.go rename to cmd/support_archive/remote_command/command.go diff --git a/cmd/support_archive/resource_query.go b/cmd/support_archive/resource_query.go index 2ebe502c2e..1bb1eb1d00 100644 --- a/cmd/support_archive/resource_query.go +++ b/cmd/support_archive/resource_query.go @@ -5,8 +5,8 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/api/v1alpha2" "github.com/Dynatrace/dynatrace-operator/pkg/api/v1alpha2/edgeconnect" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/labels" "github.com/Dynatrace/dynatrace-operator/pkg/webhook" appsv1 "k8s.io/api/apps/v1" @@ -16,6 +16,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) +var DefaultEventFieldSelector = fields.OneTermEqualSelector("type", corev1.EventTypeWarning) + type resourceQueryGroup struct { resources []schema.GroupVersionKind filters []client.ListOption @@ -34,6 +36,7 @@ func getQueries(namespace string, appName string) []resourceQuery { allQueries = append(allQueries, getComponentsQueryGroup(namespace, appName, labels.AppManagedByLabel).getQueries()...) allQueries = append(allQueries, getCustomResourcesQueryGroup(namespace).getQueries()...) allQueries = append(allQueries, getConfigMapQueryGroup(namespace).getQueries()...) + allQueries = append(allQueries, getEventsQueryGroup(namespace).getQueries()...) return allQueries } @@ -86,7 +89,7 @@ func getComponentsQueryGroup(namespace string, appName string, labelKey string) func getCustomResourcesQueryGroup(namespace string) resourceQueryGroup { return resourceQueryGroup{ resources: []schema.GroupVersionKind{ - toGroupVersionKind(v1beta3.GroupVersion, dynakube.DynaKube{}), + toGroupVersionKind(v1beta4.GroupVersion, dynakube.DynaKube{}), toGroupVersionKind(v1alpha2.GroupVersion, edgeconnect.EdgeConnect{}), }, filters: []client.ListOption{ @@ -106,6 +109,21 @@ func getConfigMapQueryGroup(namespace string) resourceQueryGroup { } } +func getEventsQueryGroup(namespace string) resourceQueryGroup { + return resourceQueryGroup{ + resources: []schema.GroupVersionKind{ + toGroupVersionKind(corev1.SchemeGroupVersion, corev1.Event{}), + }, + filters: []client.ListOption{ + client.InNamespace(namespace), + client.Limit(NumEventsFlagValue), + &client.ListOptions{ + FieldSelector: DefaultEventFieldSelector, + }, + }, + } +} + func toGroupVersionKind(groupVersion schema.GroupVersion, resource any) schema.GroupVersionKind { typ := reflect.TypeOf(resource) typ.Name() diff --git a/cmd/support_archive/resource_query_test.go b/cmd/support_archive/resource_query_test.go index 27c6049227..a5c75edc17 100644 --- a/cmd/support_archive/resource_query_test.go +++ b/cmd/support_archive/resource_query_test.go @@ -10,7 +10,7 @@ const namespace = "dynatrace" func TestObjectQuerySyntax(t *testing.T) { queries := getQueries(namespace, defaultOperatorAppName) - assert.Len(t, queries, 17) + assert.Len(t, queries, 18) for _, query := range queries { assert.NotEmpty(t, query.groupVersionKind.Kind) diff --git a/cmd/support_archive/resources_test.go b/cmd/support_archive/resources_test.go index 7fdbbf9c1b..5d45d31a42 100644 --- a/cmd/support_archive/resources_test.go +++ b/cmd/support_archive/resources_test.go @@ -11,7 +11,7 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" "github.com/Dynatrace/dynatrace-operator/pkg/api/v1alpha2/edgeconnect" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/labels" "github.com/Dynatrace/dynatrace-operator/pkg/webhook" "github.com/stretchr/testify/assert" @@ -199,7 +199,7 @@ func TestManifestCollector_PartialCollectionOnMissingResources(t *testing.T) { log := newSupportArchiveLogger(&logBuffer) queries := getQueries(testOperatorNamespace, defaultOperatorAppName) - require.Len(t, queries, 17) + require.Len(t, queries, 18) clt := fake.NewClientWithIndex( &appsv1.StatefulSet{ diff --git a/cmd/support_archive/troubleshoot_test.go b/cmd/support_archive/troubleshoot_test.go index 5d3eabfc65..4e78966d6f 100644 --- a/cmd/support_archive/troubleshoot_test.go +++ b/cmd/support_archive/troubleshoot_test.go @@ -10,7 +10,7 @@ import ( "testing" "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" appsv1 "k8s.io/api/apps/v1" diff --git a/cmd/troubleshoot/builder_test.go b/cmd/troubleshoot/builder_test.go deleted file mode 100644 index 19b4f3b9aa..0000000000 --- a/cmd/troubleshoot/builder_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package troubleshoot - -import ( - "context" - "testing" - - "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func TestTroubleshootCommandBuilder(t *testing.T) { - t.Run("build command", func(t *testing.T) { - builder := NewTroubleshootCommandBuilder() - csiCommand := builder.Build() - - assert.NotNil(t, csiCommand) - assert.Equal(t, use, csiCommand.Use) - assert.NotNil(t, csiCommand.RunE) - }) - - t.Run("getAllDynakubesInNamespace", func(t *testing.T) { - dk := buildTestDynakube() - clt := fake.NewClient(&dk) - - dynakubes, err := getAllDynakubesInNamespace(context.Background(), getNullLogger(t), clt, testNamespace) - require.NoError(t, err) - assert.Len(t, dynakubes, 1) - assert.Equal(t, dk.Name, dynakubes[0].Name) - }) - - t.Run("getDynakube - only check one dynakube if set", func(t *testing.T) { - dk := buildTestDynakube() - clt := fake.NewClient(&dk) - dynakubes, err := getDynakubes(context.Background(), getNullLogger(t), clt, testNamespace, testDynakube) - require.NoError(t, err) - assert.Len(t, dynakubes, 1) - assert.Equal(t, testDynakube, dynakubes[0].Name) - }) -} - -func buildTestDynakube() dynakube.DynaKube { - return dynakube.DynaKube{ - TypeMeta: metav1.TypeMeta{}, - ObjectMeta: metav1.ObjectMeta{ - Name: testDynakube, - Namespace: testNamespace, - }, - } -} diff --git a/cmd/troubleshoot/builder.go b/cmd/troubleshoot/cmd.go similarity index 89% rename from cmd/troubleshoot/builder.go rename to cmd/troubleshoot/cmd.go index 26cc1eefcd..7e638f69a9 100644 --- a/cmd/troubleshoot/builder.go +++ b/cmd/troubleshoot/cmd.go @@ -5,9 +5,8 @@ import ( "net/http" "os" - "github.com/Dynatrace/dynatrace-operator/cmd/config" "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/logd" "github.com/Dynatrace/dynatrace-operator/pkg/oci/dockerkeychain" "github.com/Dynatrace/dynatrace-operator/pkg/oci/registry" @@ -17,6 +16,7 @@ import ( "github.com/spf13/cobra" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/config" "sigs.k8s.io/controller-runtime/pkg/cluster" ) @@ -33,24 +33,10 @@ var ( namespaceFlagValue string ) -type CommandBuilder struct { - configProvider config.Provider -} - -func NewTroubleshootCommandBuilder() CommandBuilder { - return CommandBuilder{} -} - -func (builder CommandBuilder) SetConfigProvider(provider config.Provider) CommandBuilder { - builder.configProvider = provider - - return builder -} - -func (builder CommandBuilder) Build() *cobra.Command { +func New() *cobra.Command { cmd := &cobra.Command{ Use: use, - RunE: builder.buildRun(), + RunE: run(), } addFlags(cmd) @@ -67,12 +53,12 @@ func clusterOptions(opts *cluster.Options) { opts.Scheme = scheme.Scheme } -func (builder CommandBuilder) buildRun() func(*cobra.Command, []string) error { +func run() func(*cobra.Command, []string) error { return func(cmd *cobra.Command, args []string) error { version.LogVersion() logd.LogBaseLoggerSettings() - kubeConfig, err := builder.configProvider.GetConfig() + kubeConfig, err := config.GetConfig() if err != nil { return err } diff --git a/cmd/troubleshoot/component.go b/cmd/troubleshoot/component.go index e6aa42b3f7..1000cf899b 100644 --- a/cmd/troubleshoot/component.go +++ b/cmd/troubleshoot/component.go @@ -1,7 +1,7 @@ package troubleshoot import ( - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" ) type component string @@ -37,13 +37,13 @@ func (c component) getImage(dk *dynakube.DynaKube) (string, bool) { switch c { case componentOneAgent: - if dk.CustomOneAgentImage() != "" { - return dk.CustomOneAgentImage(), true + if dk.OneAgent().GetCustomImage() != "" { + return dk.OneAgent().GetCustomImage(), true } - return dk.OneAgentImage(), false + return dk.OneAgent().GetImage(), false case componentCodeModules: - return dk.CustomCodeModulesImage(), true + return dk.OneAgent().GetCustomCodeModulesImage(), true case componentActiveGate: if dk.ActiveGate().GetCustomImage() != "" { return dk.ActiveGate().GetCustomImage(), true diff --git a/cmd/troubleshoot/dynakube.go b/cmd/troubleshoot/dynakube.go index 1f60f66a21..3322db3155 100644 --- a/cmd/troubleshoot/dynakube.go +++ b/cmd/troubleshoot/dynakube.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/dtpullsecret" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/dynatraceclient" diff --git a/cmd/troubleshoot/dynakube_test.go b/cmd/troubleshoot/dynakube_test.go index d9771a88a3..74cc1210d2 100644 --- a/cmd/troubleshoot/dynakube_test.go +++ b/cmd/troubleshoot/dynakube_test.go @@ -6,8 +6,9 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/value" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" "github.com/pkg/errors" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" @@ -295,24 +296,24 @@ func (builder *testDynaKubeBuilder) withActiveGateCustomImage(image string) *tes } func (builder *testDynaKubeBuilder) withCloudNativeFullStack() *testDynaKubeBuilder { - builder.dynakube.Spec.OneAgent.CloudNativeFullStack = &dynakube.CloudNativeFullStackSpec{ - HostInjectSpec: dynakube.HostInjectSpec{}, + builder.dynakube.Spec.OneAgent.CloudNativeFullStack = &oneagent.CloudNativeFullStackSpec{ + HostInjectSpec: oneagent.HostInjectSpec{}, } - builder.dynakube.Status.OneAgent.ImageID = builder.dynakube.DefaultOneAgentImage(testVersion) + builder.dynakube.Status.OneAgent.ImageID = builder.dynakube.OneAgent().GetDefaultImage(testVersion) return builder } func (builder *testDynaKubeBuilder) withClassicFullStack() *testDynaKubeBuilder { - builder.dynakube.Spec.OneAgent.ClassicFullStack = &dynakube.HostInjectSpec{} - builder.dynakube.Status.OneAgent.ImageID = builder.dynakube.DefaultOneAgentImage(testVersion) + builder.dynakube.Spec.OneAgent.ClassicFullStack = &oneagent.HostInjectSpec{} + builder.dynakube.Status.OneAgent.ImageID = builder.dynakube.OneAgent().GetDefaultImage(testVersion) return builder } func (builder *testDynaKubeBuilder) withHostMonitoring() *testDynaKubeBuilder { - builder.dynakube.Spec.OneAgent.HostMonitoring = &dynakube.HostInjectSpec{} - builder.dynakube.Status.OneAgent.ImageID = builder.dynakube.DefaultOneAgentImage(testVersion) + builder.dynakube.Spec.OneAgent.HostMonitoring = &oneagent.HostInjectSpec{} + builder.dynakube.Status.OneAgent.ImageID = builder.dynakube.OneAgent().GetDefaultImage(testVersion) return builder } @@ -321,7 +322,7 @@ func (builder *testDynaKubeBuilder) withClassicFullStackCustomImage(image string if builder.dynakube.Spec.OneAgent.ClassicFullStack != nil { builder.dynakube.Spec.OneAgent.ClassicFullStack.Image = image } else { - builder.dynakube.Spec.OneAgent.ClassicFullStack = &dynakube.HostInjectSpec{ + builder.dynakube.Spec.OneAgent.ClassicFullStack = &oneagent.HostInjectSpec{ Image: image, } } @@ -334,8 +335,8 @@ func (builder *testDynaKubeBuilder) withCloudNativeFullStackCustomImage(image st if builder.dynakube.Spec.OneAgent.CloudNativeFullStack != nil { builder.dynakube.Spec.OneAgent.CloudNativeFullStack.Image = image } else { - builder.dynakube.Spec.OneAgent.CloudNativeFullStack = &dynakube.CloudNativeFullStackSpec{ - HostInjectSpec: dynakube.HostInjectSpec{ + builder.dynakube.Spec.OneAgent.CloudNativeFullStack = &oneagent.CloudNativeFullStackSpec{ + HostInjectSpec: oneagent.HostInjectSpec{ Image: image, }, } @@ -349,7 +350,7 @@ func (builder *testDynaKubeBuilder) withHostMonitoringCustomImage(image string) if builder.dynakube.Spec.OneAgent.HostMonitoring != nil { builder.dynakube.Spec.OneAgent.HostMonitoring.Image = image } else { - builder.dynakube.Spec.OneAgent.HostMonitoring = &dynakube.HostInjectSpec{ + builder.dynakube.Spec.OneAgent.HostMonitoring = &oneagent.HostInjectSpec{ Image: image, } } @@ -362,8 +363,8 @@ func (builder *testDynaKubeBuilder) withCloudNativeCodeModulesImage(image string if builder.dynakube.Spec.OneAgent.CloudNativeFullStack != nil { builder.dynakube.Spec.OneAgent.CloudNativeFullStack.CodeModulesImage = image } else { - builder.dynakube.Spec.OneAgent.CloudNativeFullStack = &dynakube.CloudNativeFullStackSpec{ - AppInjectionSpec: dynakube.AppInjectionSpec{ + builder.dynakube.Spec.OneAgent.CloudNativeFullStack = &oneagent.CloudNativeFullStackSpec{ + AppInjectionSpec: oneagent.AppInjectionSpec{ InitResources: &corev1.ResourceRequirements{}, CodeModulesImage: image, }, @@ -378,8 +379,8 @@ func (builder *testDynaKubeBuilder) withApplicationMonitoringCodeModulesImage(im if builder.dynakube.Spec.OneAgent.ApplicationMonitoring != nil { builder.dynakube.Spec.OneAgent.ApplicationMonitoring.CodeModulesImage = image } else { - builder.dynakube.Spec.OneAgent.ApplicationMonitoring = &dynakube.ApplicationMonitoringSpec{ - AppInjectionSpec: dynakube.AppInjectionSpec{ + builder.dynakube.Spec.OneAgent.ApplicationMonitoring = &oneagent.ApplicationMonitoringSpec{ + AppInjectionSpec: oneagent.AppInjectionSpec{ InitResources: &corev1.ResourceRequirements{}, CodeModulesImage: image, }, diff --git a/cmd/troubleshoot/image.go b/cmd/troubleshoot/image.go index efa7a3b8b6..0f0082964f 100644 --- a/cmd/troubleshoot/image.go +++ b/cmd/troubleshoot/image.go @@ -4,7 +4,7 @@ import ( "context" "net/http" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/arch" "github.com/Dynatrace/dynatrace-operator/pkg/logd" "github.com/google/go-containerregistry/pkg/authn" @@ -35,7 +35,7 @@ func verifyAllImagesAvailable(ctx context.Context, baseLog logd.Logger, keychain imagePullFunc := CreateImagePullFunc(ctx, keychain, transport) - if dk.NeedsOneAgent() { + if dk.OneAgent().IsDaemonsetRequired() { verifyImageIsAvailable(log, imagePullFunc, dk, componentOneAgent, false) verifyImageIsAvailable(log, imagePullFunc, dk, componentCodeModules, true) } diff --git a/cmd/troubleshoot/image_test.go b/cmd/troubleshoot/image_test.go index f472bddff1..005831b509 100644 --- a/cmd/troubleshoot/image_test.go +++ b/cmd/troubleshoot/image_test.go @@ -3,7 +3,6 @@ package troubleshoot import ( "context" "encoding/json" - "fmt" "net/http" "net/http/httptest" "net/url" @@ -11,11 +10,13 @@ import ( "testing" "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/dtpullsecret" "github.com/Dynatrace/dynatrace-operator/pkg/logd" "github.com/Dynatrace/dynatrace-operator/pkg/oci/dockerkeychain" + "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" @@ -80,7 +81,7 @@ func TestImagePullable(t *testing.T) { dockerServer, secret, server, err := setupDockerMocker( []string{ "/v2/", - "/v2" + dynakube.DefaultOneAgentImageRegistrySubPath + "/manifests/" + testVersion + "-raw", + "/v2" + oneagent.DefaultOneAgentImageRegistrySubPath + "/manifests/" + testVersion + "-raw", "/v2/" + testCustomOneAgentImage + "/manifests/" + testVersion, "/v2/" + testOneAgentCodeModulesImage + "/manifests/" + testVersion, "/v2" + activegate.DefaultImageRegistrySubPath + "/manifests/" + testVersion + "-raw", @@ -365,7 +366,7 @@ func TestImagePullablePullSecret(t *testing.T) { func getPullSecretToken(pullSecret *corev1.Secret) (string, error) { secretBytes, hasPullSecret := pullSecret.Data[dtpullsecret.DockerConfigJson] if !hasPullSecret { - return "", fmt.Errorf("token .dockerconfigjson does not exist in secret '%s'", pullSecret.Name) + return "", errors.Errorf("token .dockerconfigjson does not exist in secret '%s'", pullSecret.Name) } secretStr := string(secretBytes) diff --git a/cmd/troubleshoot/proxy.go b/cmd/troubleshoot/proxy.go index eb0e1e8a70..94f3da3c75 100644 --- a/cmd/troubleshoot/proxy.go +++ b/cmd/troubleshoot/proxy.go @@ -3,7 +3,7 @@ package troubleshoot import ( "context" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/logd" "golang.org/x/net/http/httpproxy" "sigs.k8s.io/controller-runtime/pkg/client" diff --git a/cmd/troubleshoot/proxy_test.go b/cmd/troubleshoot/proxy_test.go index 5b18f027ea..fae53a0da5 100644 --- a/cmd/troubleshoot/proxy_test.go +++ b/cmd/troubleshoot/proxy_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/logd" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/cmd/webhook/builder_test.go b/cmd/webhook/builder_test.go deleted file mode 100644 index 36cfdaba55..0000000000 --- a/cmd/webhook/builder_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package webhook - -import ( - "testing" - - configmock "github.com/Dynatrace/dynatrace-operator/test/mocks/cmd/config" - providermock "github.com/Dynatrace/dynatrace-operator/test/mocks/cmd/manager" - "github.com/stretchr/testify/assert" -) - -func TestWebhookCommandBuilder(t *testing.T) { - t.Run("build command", func(t *testing.T) { - builder := NewWebhookCommandBuilder() - csiCommand := builder.Build() - - assert.NotNil(t, csiCommand) - assert.Equal(t, use, csiCommand.Use) - assert.NotNil(t, csiCommand.RunE) - }) - t.Run("set config provider", func(t *testing.T) { - builder := NewWebhookCommandBuilder() - - assert.NotNil(t, builder) - - expectedProvider := &configmock.Provider{} - builder = builder.SetConfigProvider(expectedProvider) - - assert.Equal(t, expectedProvider, builder.configProvider) - }) - t.Run("set manager provider", func(t *testing.T) { - expectedProvider := providermock.NewProvider(t) - builder := NewWebhookCommandBuilder().SetManagerProvider(expectedProvider) - - assert.Equal(t, expectedProvider, builder.managerProvider) - }) - t.Run("set namespace", func(t *testing.T) { - builder := NewWebhookCommandBuilder().SetNamespace("namespace") - - assert.Equal(t, "namespace", builder.namespace) - }) -} diff --git a/cmd/certificates/config.go b/cmd/webhook/certificates/config.go similarity index 100% rename from cmd/certificates/config.go rename to cmd/webhook/certificates/config.go diff --git a/cmd/certificates/watcher.go b/cmd/webhook/certificates/watcher.go similarity index 97% rename from cmd/certificates/watcher.go rename to cmd/webhook/certificates/watcher.go index 21f6c5a772..894075bd38 100644 --- a/cmd/certificates/watcher.go +++ b/cmd/webhook/certificates/watcher.go @@ -3,7 +3,6 @@ package certificates import ( "bytes" "context" - "fmt" "os" "path/filepath" "time" @@ -77,7 +76,7 @@ func (watcher *CertificateWatcher) updateCertificatesFromSecret() (bool, error) if _, err = watcher.fs.Stat(watcher.certificateDirectory); os.IsNotExist(err) { err = watcher.fs.MkdirAll(watcher.certificateDirectory, permDirUser) if err != nil { - return false, fmt.Errorf("could not create cert directory: %w", err) + return false, errors.WithMessage(err, "could not create cert directory") } } diff --git a/cmd/webhook/builder.go b/cmd/webhook/cmd.go similarity index 57% rename from cmd/webhook/builder.go rename to cmd/webhook/cmd.go index 9ca9d7eb93..ef0d786a79 100644 --- a/cmd/webhook/builder.go +++ b/cmd/webhook/cmd.go @@ -2,18 +2,20 @@ package webhook import ( "context" + "os" - "github.com/Dynatrace/dynatrace-operator/cmd/certificates" - "github.com/Dynatrace/dynatrace-operator/cmd/config" - cmdManager "github.com/Dynatrace/dynatrace-operator/cmd/manager" + "github.com/Dynatrace/dynatrace-operator/cmd/webhook/certificates" edgeconnectv1alpha1 "github.com/Dynatrace/dynatrace-operator/pkg/api/v1alpha1/edgeconnect" edgeconnectv1alpha2 "github.com/Dynatrace/dynatrace-operator/pkg/api/v1alpha2/edgeconnect" dynakubev1beta1 "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta1/dynakube" //nolint:staticcheck dynakubev1beta2 "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta2/dynakube" //nolint:staticcheck dynakubev1beta3 "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + dynakubev1beta4 "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" dynakubevalidation "github.com/Dynatrace/dynatrace-operator/pkg/api/validation/dynakube" edgeconnectvalidation "github.com/Dynatrace/dynatrace-operator/pkg/api/validation/edgeconnect" "github.com/Dynatrace/dynatrace-operator/pkg/logd" + "github.com/Dynatrace/dynatrace-operator/pkg/util/installconfig" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/env" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/pod" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubesystem" "github.com/Dynatrace/dynatrace-operator/pkg/version" @@ -23,6 +25,7 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client/config" "sigs.k8s.io/controller-runtime/pkg/manager" ) @@ -39,148 +42,133 @@ var ( certificateKeyFileName string ) -type CommandBuilder struct { - configProvider config.Provider - managerProvider cmdManager.Provider - namespace string - podName string -} - -func NewWebhookCommandBuilder() CommandBuilder { - return CommandBuilder{} -} - -func (builder CommandBuilder) SetConfigProvider(provider config.Provider) CommandBuilder { - builder.configProvider = provider - - return builder -} - -func (builder CommandBuilder) SetManagerProvider(provider cmdManager.Provider) CommandBuilder { - builder.managerProvider = provider - - return builder -} - -func (builder CommandBuilder) GetManagerProvider() cmdManager.Provider { - if builder.managerProvider == nil { - builder.managerProvider = NewProvider(certificateDirectory, certificateKeyFileName, certificateFileName) - } - - return builder.managerProvider -} - -func (builder CommandBuilder) SetNamespace(namespace string) CommandBuilder { - builder.namespace = namespace - - return builder -} - -func (builder CommandBuilder) SetPodName(podName string) CommandBuilder { - builder.podName = podName - - return builder -} - -func (builder CommandBuilder) Build() *cobra.Command { - cmd := &cobra.Command{ - Use: use, - RunE: builder.buildRun(), - } - - addFlags(cmd) - - return cmd -} - func addFlags(cmd *cobra.Command) { cmd.PersistentFlags().StringVar(&certificateDirectory, FlagCertificateDirectory, "/tmp/webhook/certs", "Directory to look certificates for.") cmd.PersistentFlags().StringVar(&certificateFileName, FlagCertificateFileName, "tls.crt", "File name for the public certificate.") cmd.PersistentFlags().StringVar(&certificateKeyFileName, FlagCertificateKeyFileName, "tls.key", "File name for the private key.") } -func startCertificateWatcher(webhookManager manager.Manager, namespace string, podName string) error { - webhookPod, err := pod.Get(context.TODO(), webhookManager.GetAPIReader(), podName, namespace) - if err != nil { - return err +func New() *cobra.Command { + cmd := &cobra.Command{ + Use: use, + RunE: run(), + SilenceUsage: true, } - isDeployedViaOLM := kubesystem.IsDeployedViaOlm(*webhookPod) - if !isDeployedViaOLM { - watcher, err := certificates.NewCertificateWatcher(webhookManager, namespace, webhook.SecretCertsName) - if err != nil { - return err - } - - watcher.WaitForCertificates() - } + addFlags(cmd) - return nil + return cmd } -func (builder CommandBuilder) buildRun() func(*cobra.Command, []string) error { +func run() func(*cobra.Command, []string) error { return func(cmd *cobra.Command, args []string) error { + installconfig.ReadModules() version.LogVersion() logd.LogBaseLoggerSettings() - kubeConfig, err := builder.configProvider.GetConfig() + podName := os.Getenv(env.PodName) + namespace := os.Getenv(env.PodNamespace) + + kubeConfig, err := config.GetConfig() if err != nil { return err } - webhookManager, err := builder.GetManagerProvider().CreateManager(builder.namespace, kubeConfig) + webhookManager, err := createManager(kubeConfig, namespace, certificateDirectory, certificateFileName, certificateKeyFileName) if err != nil { return err } signalHandler := ctrl.SetupSignalHandler() - err = startCertificateWatcher(webhookManager, builder.namespace, builder.podName) + err = startCertificateWatcher(webhookManager, namespace, podName) if err != nil { return err } - err = namespacemutator.AddWebhookToManager(webhookManager, builder.namespace) + err = namespacemutator.AddWebhookToManager(webhookManager, namespace) if err != nil { return err } - err = podmutator.AddWebhookToManager(signalHandler, webhookManager, builder.namespace) + err = podmutator.AddWebhookToManager(signalHandler, webhookManager, namespace) if err != nil { return err } - dkValidator := dynakubevalidation.New(webhookManager.GetAPIReader(), webhookManager.GetConfig()) - - err = dynakubev1beta1.SetupWebhookWithManager(webhookManager, dkValidator) + err = setupDynakubeValidation(webhookManager) if err != nil { return err } - err = dynakubev1beta2.SetupWebhookWithManager(webhookManager, dkValidator) + err = setupEdgeconnectValidation(webhookManager) if err != nil { return err } - err = dynakubev1beta3.SetupWebhookWithManager(webhookManager, dkValidator) - if err != nil { - return err - } + err = webhookManager.Start(signalHandler) - ecValidator := edgeconnectvalidation.New(webhookManager.GetAPIReader(), webhookManager.GetConfig()) + return errors.WithStack(err) + } +} - err = edgeconnectv1alpha1.SetupWebhookWithManager(webhookManager, ecValidator) - if err != nil { - return err - } +func startCertificateWatcher(webhookManager manager.Manager, namespace string, podName string) error { + webhookPod, err := pod.Get(context.TODO(), webhookManager.GetAPIReader(), podName, namespace) + if err != nil { + return err + } - err = edgeconnectv1alpha2.SetupWebhookWithManager(webhookManager, ecValidator) + isDeployedViaOLM := kubesystem.IsDeployedViaOlm(*webhookPod) + if !isDeployedViaOLM { + watcher, err := certificates.NewCertificateWatcher(webhookManager, namespace, webhook.SecretCertsName) if err != nil { return err } - err = webhookManager.Start(signalHandler) + watcher.WaitForCertificates() + } - return errors.WithStack(err) + return nil +} + +func setupDynakubeValidation(webhookManager manager.Manager) error { + dkValidator := dynakubevalidation.New(webhookManager.GetAPIReader(), webhookManager.GetConfig()) + + err := dynakubev1beta1.SetupWebhookWithManager(webhookManager, dkValidator) + if err != nil { + return err + } + + err = dynakubev1beta2.SetupWebhookWithManager(webhookManager, dkValidator) + if err != nil { + return err + } + + err = dynakubev1beta3.SetupWebhookWithManager(webhookManager, dkValidator) + if err != nil { + return err } + + err = dynakubev1beta4.SetupWebhookWithManager(webhookManager, dkValidator) + if err != nil { + return err + } + + return nil +} + +func setupEdgeconnectValidation(webhookManager manager.Manager) error { + ecValidator := edgeconnectvalidation.New(webhookManager.GetAPIReader(), webhookManager.GetConfig()) + + err := edgeconnectv1alpha1.SetupWebhookWithManager(webhookManager, ecValidator) + if err != nil { + return err + } + + err = edgeconnectv1alpha2.SetupWebhookWithManager(webhookManager, ecValidator) + if err != nil { + return err + } + + return nil } diff --git a/cmd/webhook/manager.go b/cmd/webhook/manager.go index 0041660144..b394ddc8b3 100644 --- a/cmd/webhook/manager.go +++ b/cmd/webhook/manager.go @@ -17,31 +17,17 @@ import ( ) const ( - metricsBindAddress = ":8383" - healthProbeBindAddress = ":10080" - defaultPort = 8443 - livezEndpointName = "livez" - livenessEndpointName = "/" + livezEndpointName - readyzEndpointName = "readyz" - readinessEndpointName = "/" + readyzEndpointName + defaultMetricsBindAddress = ":8383" + defaultHealthProbeBindAddress = ":10080" + defaultPort = 8443 + livezEndpointName = "livez" + livenessEndpointName = "/" + livezEndpointName + readyzEndpointName = "readyz" + readinessEndpointName = "/" + readyzEndpointName ) -type Provider struct { - certificateDirectory string - certificateFileName string - keyFileName string -} - -func NewProvider(certificateDirectory string, keyFileName string, certificateFileName string) Provider { - return Provider{ - certificateDirectory: certificateDirectory, - certificateFileName: certificateFileName, - keyFileName: keyFileName, - } -} - -func (provider Provider) CreateManager(namespace string, config *rest.Config) (manager.Manager, error) { - controlManager, err := ctrl.NewManager(config, provider.createOptions(namespace)) +func createManager(config *rest.Config, namespace, certificateDirectory, certificateFileName, keyFileName string) (manager.Manager, error) { + controlManager, err := ctrl.NewManager(config, createOptions(namespace)) if err != nil { return nil, errors.WithStack(err) } @@ -56,10 +42,10 @@ func (provider Provider) CreateManager(namespace string, config *rest.Config) (m return nil, errors.WithStack(err) } - return provider.setupWebhookServer(controlManager) + return setupWebhookServer(controlManager, certificateDirectory, certificateFileName, keyFileName) } -func (provider Provider) createOptions(namespace string) ctrl.Options { +func createOptions(namespace string) ctrl.Options { port := defaultPort webhookPortEnv := os.Getenv("WEBHOOK_PORT") @@ -67,6 +53,20 @@ func (provider Provider) createOptions(namespace string) ctrl.Options { port = parsedWebhookPort } + metricsBindAddress := defaultMetricsBindAddress + + metricsBindAddressEnv := os.Getenv("METRICS_BIND_ADDRESS") + if metricsBindAddressEnv != "" { + metricsBindAddress = metricsBindAddressEnv + } + + healthProbeBindAddress := defaultHealthProbeBindAddress + + healthProbeBindAddressEnv := os.Getenv("HEALTH_PROBE_BIND_ADDRESS") + if healthProbeBindAddressEnv != "" { + healthProbeBindAddress = healthProbeBindAddressEnv + } + return ctrl.Options{ Scheme: scheme.Scheme, ReadinessEndpointName: readinessEndpointName, @@ -86,7 +86,7 @@ func (provider Provider) createOptions(namespace string) ctrl.Options { } } -func (provider Provider) setupWebhookServer(mgr manager.Manager) (manager.Manager, error) { +func setupWebhookServer(mgr manager.Manager, certificateDirectory, certificateFileName, keyFileName string) (manager.Manager, error) { tlsConfig := func(config *tls.Config) { config.MinVersion = tls.VersionTLS13 } @@ -96,9 +96,9 @@ func (provider Provider) setupWebhookServer(mgr manager.Manager) (manager.Manage return nil, errors.WithStack(errors.New("Unable to cast webhook server")) } - webhookServer.Options.CertDir = provider.certificateDirectory - webhookServer.Options.KeyName = provider.keyFileName - webhookServer.Options.CertName = provider.certificateFileName + webhookServer.Options.CertDir = certificateDirectory + webhookServer.Options.KeyName = keyFileName + webhookServer.Options.CertName = certificateFileName webhookServer.Options.TLSOpts = []func(*tls.Config){tlsConfig} return mgr, nil diff --git a/cmd/webhook/manager_test.go b/cmd/webhook/manager_test.go deleted file mode 100644 index 6c316c27dc..0000000000 --- a/cmd/webhook/manager_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package webhook - -import ( - "testing" - - "github.com/Dynatrace/dynatrace-operator/cmd/manager" - "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme" - managermock "github.com/Dynatrace/dynatrace-operator/test/mocks/sigs.k8s.io/controller-runtime/pkg/manager" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "k8s.io/client-go/rest" - "sigs.k8s.io/controller-runtime/pkg/webhook" -) - -func TestCreateOptions(t *testing.T) { - t.Run("implements interface", func(t *testing.T) { - var provider manager.Provider = NewProvider("certs-dir", "key-file", "cert-file") - _, _ = provider.CreateManager("namespace", &rest.Config{}) - - providerImpl := provider.(Provider) - assert.Equal(t, "certs-dir", providerImpl.certificateDirectory) - assert.Equal(t, "key-file", providerImpl.keyFileName) - assert.Equal(t, "cert-file", providerImpl.certificateFileName) - }) - t.Run("creates options", func(t *testing.T) { - provider := Provider{} - options := provider.createOptions("test-namespace") - - assert.NotNil(t, options) - assert.Contains(t, options.Cache.DefaultNamespaces, "test-namespace") - assert.Equal(t, scheme.Scheme, options.Scheme) - assert.Equal(t, metricsBindAddress, options.Metrics.BindAddress) - - webhookServer, ok := options.WebhookServer.(*webhook.DefaultServer) - require.True(t, ok) - assert.Equal(t, defaultPort, webhookServer.Options.Port) - }) - - t.Run("creates options with configured webhook port", func(t *testing.T) { - t.Setenv("WEBHOOK_PORT", "6443") - - provider := Provider{} - options := provider.createOptions("test-namespace") - - assert.NotNil(t, options) - assert.Contains(t, options.Cache.DefaultNamespaces, "test-namespace") - assert.Equal(t, scheme.Scheme, options.Scheme) - assert.Equal(t, metricsBindAddress, options.Metrics.BindAddress) - - webhookServer, ok := options.WebhookServer.(*webhook.DefaultServer) - require.True(t, ok) - assert.Equal(t, 6443, webhookServer.Options.Port) - }) - t.Run("configures webhooks server", func(t *testing.T) { - provider := NewProvider("certs-dir", "key-file", "cert-file") - expectedWebhookServer := &webhook.DefaultServer{} - - mockedMgr := managermock.NewManager(t) - mockedMgr.On("GetWebhookServer").Return(expectedWebhookServer) - - mgrWithWebhookServer, err := provider.setupWebhookServer(mockedMgr) - require.NoError(t, err) - - mgrWebhookServer, ok := mgrWithWebhookServer.GetWebhookServer().(*webhook.DefaultServer) - require.True(t, ok) - assert.Equal(t, "certs-dir", mgrWebhookServer.Options.CertDir) - assert.Equal(t, "key-file", mgrWebhookServer.Options.KeyName) - assert.Equal(t, "cert-file", mgrWebhookServer.Options.CertName) - }) -} diff --git a/codecov.yml b/codecov.yml index 89afc8d3f9..8ac6f6a035 100644 --- a/codecov.yml +++ b/codecov.yml @@ -2,7 +2,7 @@ codecov: require_ci_to_pass: false coverage: - range: 63..100 + range: 62..100 round: down precision: 2 @@ -11,16 +11,16 @@ coverage: default: # context, you can create multiple ones with custom titles enabled: yes # must be yes|true to enable this status target: - 63 # specify the target coverage for each commit status + 62 # specify the target coverage for each commit status # option: "auto" (must increase from parent commit or pull request base) # option: "X%" a static target percentage to hit if_not_found: success # if parent is not found report status as success, error, or failure if_ci_failed: error # if ci fails report status as success, error, or failure - patch: - default: - enabled: yes - target: auto - threshold: 1% # allowed to drop X% and still result in a "success" commit status - base: auto + patch: off ignore: + - "./cmd/csi/*.go" + - "./cmd/operator/*.go" + - "./cmd/standalone/*.go" + - "./cmd/webhook/*.go" + - "./cmd/startup_probe/*.go" - "./test/mocks/*.go" # ignore test mocks. diff --git a/config/crd/bases/dynatrace.com_dynakubes.yaml b/config/crd/bases/dynatrace.com_dynakubes.yaml index 0568dc3943..088bcaceab 100644 --- a/config/crd/bases/dynatrace.com_dynakubes.yaml +++ b/config/crd/bases/dynatrace.com_dynakubes.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.16.5 + controller-gen.kubebuilder.io/version: v0.17.2 name: dynakubes.dynatrace.com spec: group: dynatrace.com @@ -35,19 +35,12 @@ spec: description: DynaKube is the Schema for the DynaKube API properties: apiVersion: - 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 + description: APIVersion defines the versioned schema of this representation + of an object. type: string kind: - 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 + description: Kind is a string value representing the REST resource this + object represents. type: string metadata: type: object @@ -71,7 +64,7 @@ spec: customProperties: description: |- Add a custom properties file by providing it as a value or reference it from a secret - If referenced from a secret, make sure the key is called 'customProperties' + If referenced... properties: value: description: Custom properties value. @@ -98,9 +91,7 @@ spec: value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. + using the previously defined environment variables in... type: string valueFrom: description: Source for the environment variable's value. @@ -114,12 +105,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the ConfigMap or its @@ -130,9 +116,8 @@ spec: type: object x-kubernetes-map-type: atomic fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + description: 'Selects a field of the pod: supports metadata.name, + metadata.namespace, `metadata.' properties: apiVersion: description: Version of the schema the FieldPath @@ -149,7 +134,7 @@ spec: resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + (limits.cpu, limits. properties: containerName: description: 'Container name: required for volumes, @@ -180,12 +165,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the Secret or its key @@ -204,8 +184,7 @@ spec: description: Set activation group for ActiveGate type: string image: - description: The ActiveGate container image. Defaults to the latest - ActiveGate image provided by the registry on the tenant + description: The ActiveGate container image. type: string labels: additionalProperties: @@ -218,9 +197,7 @@ spec: description: Node selector to control the selection of nodes type: object priorityClassName: - description: |- - If specified, indicates the pod's priority. Name must be defined by creating a PriorityClass object with that - name. If not specified the setting will be removed from the StatefulSet. + description: If specified, indicates the pod's priority. type: string replicas: description: Amount of replicas for your ActiveGates @@ -231,28 +208,18 @@ spec: pods properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry in + pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -270,7 +237,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -279,54 +246,43 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of compute + resources required. type: object type: object tlsSecretName: - description: |- - The name of a secret containing ActiveGate TLS cert+key and password. If not set, self-signed certificate is used. - server.p12: certificate+key pair in pkcs12 format - password: passphrase to read server.p12 + description: The name of a secret containing ActiveGate TLS cert+key + and password. type: string tolerations: description: Set tolerations for the ActiveGate pods items: description: |- The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . + the triple... properties: effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + description: Effect indicates the taint effect to match. + Empty means match all taint effects. type: string key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. + description: Key is the taint key that the toleration applies + to. Empty means match all taint keys. type: string operator: description: |- Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. + Valid operators are Exists and Equal. type: string tolerationSeconds: description: |- TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). + of effect NoExecute,... format: int64 type: integer value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. + description: Value is the taint value the toleration matches + to. type: string type: object type: array @@ -338,34 +294,25 @@ spec: matching pods among the given topology. properties: labelSelector: - description: |- - LabelSelector is used to find matching pods. - Pods that match this label selector are counted to determine the number of pods - in their corresponding topology domain. + description: LabelSelector is used to find matching pods. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. + description: A label selector requirement is a selector + that contains values, a key, and an operator that... properties: key: description: key is the label key that the selector applies to. type: string operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. + description: operator represents a key's relationship + to a set of values. type: string values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. + description: values is an array of string values. items: type: string type: array @@ -379,62 +326,45 @@ spec: matchLabels: additionalProperties: type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. + description: matchLabels is a map of {key,value} pairs. type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: description: |- MatchLabelKeys is a set of pod label keys to select the pods over which - spreading will be calculated. + spreading will be... items: type: string type: array x-kubernetes-list-type: atomic maxSkew: - description: |- - MaxSkew describes the degree to which pods may be unevenly distributed. - When `whenUnsatisfiable=DoNotSchedule`, it is the maximum permitted difference - between the number of matching pods in the target topology and the global minimum. + description: MaxSkew describes the degree to which pods + may be unevenly distributed. format: int32 type: integer minDomains: - description: |- - MinDomains indicates a minimum number of eligible domains. - When the number of eligible domains with matching topology keys is less than minDomains, - Pod Topology Spread treats "global minimum" as 0, and then the calculation of Skew is performed. + description: MinDomains indicates a minimum number of eligible + domains. format: int32 type: integer nodeAffinityPolicy: description: |- NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector - when calculating pod topology spread skew. Options are: - - Honor: only nodes matching nodeAffinity/nodeSelector are included in the calculations. - - Ignore: nodeAffinity/nodeSelector are ignored. + when calculating pod... type: string nodeTaintsPolicy: description: |- NodeTaintsPolicy indicates how we will treat node taints when calculating - pod topology spread skew. Options are: - - Honor: nodes without taints, along with tainted nodes for which the incoming pod - has a toleration, are included. - - Ignore: node taints are ignored. All nodes are included. + pod topology spread skew. type: string topologyKey: - description: |- - TopologyKey is the key of node labels. Nodes that have a label with this key - and identical values are considered to be in the same topology. - We consider each as a "bucket", and try to put balanced number - of pods into each bucket. + description: TopologyKey is the key of node labels. type: string whenUnsatisfiable: description: |- WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy the spread constraint. - - DoNotSchedule (default) tells the scheduler not to schedule it. type: string required: - maxSkew @@ -444,21 +374,15 @@ spec: type: array type: object apiUrl: - description: |- - Dynatrace apiUrl, including the /api path at the end. For SaaS, set YOUR_ENVIRONMENT_ID to your environment ID. For Managed, change the apiUrl address. - For instructions on how to determine the environment ID and how to configure the apiUrl address, see Environment ID (https://www.dynatrace. + description: Dynatrace apiUrl, including the /api path at the end. type: string customPullSecret: - description: |- - Defines a custom pull secret in case you use a private registry when pulling images from the Dynatrace environment. - To define a custom pull secret and learn about the expected behavior, see Configure customPullSecret - (https://www.dynatrace. + description: Defines a custom pull secret in case you use a private + registry when pulling images from the... type: string enableIstio: - description: |- - When enabled, and if Istio is installed on the Kubernetes environment, Dynatrace Operator will create the corresponding - VirtualService and ServiceEntry objects to allow access to the Dynatrace Cluster from the OneAgent or ActiveGate. - Disabled by default. + description: When enabled, and if Istio is installed on the Kubernetes + environment, Dynatrace Operator will... type: boolean kubernetesMonitoring: description: Configuration for Kubernetes Monitoring @@ -466,7 +390,7 @@ spec: customProperties: description: |- Add a custom properties file by providing it as a value or reference it from a secret - If referenced from a secret, make sure the key is called 'customProperties' + If referenced... properties: value: description: Custom properties value. @@ -493,9 +417,7 @@ spec: value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. + using the previously defined environment variables in... type: string valueFrom: description: Source for the environment variable's value. @@ -509,12 +431,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the ConfigMap or its @@ -525,9 +442,8 @@ spec: type: object x-kubernetes-map-type: atomic fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + description: 'Selects a field of the pod: supports metadata.name, + metadata.namespace, `metadata.' properties: apiVersion: description: Version of the schema the FieldPath @@ -544,7 +460,7 @@ spec: resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + (limits.cpu, limits. properties: containerName: description: 'Container name: required for volumes, @@ -575,12 +491,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the Secret or its key @@ -599,8 +510,7 @@ spec: description: Set activation group for ActiveGate type: string image: - description: The ActiveGate container image. Defaults to the latest - ActiveGate image provided by the registry on the tenant + description: The ActiveGate container image. type: string labels: additionalProperties: @@ -621,28 +531,18 @@ spec: pods properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry in + pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -660,7 +560,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -669,11 +569,8 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of compute + resources required. type: object type: object tolerations: @@ -681,36 +578,30 @@ spec: items: description: |- The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . + the triple... properties: effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + description: Effect indicates the taint effect to match. + Empty means match all taint effects. type: string key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. + description: Key is the taint key that the toleration applies + to. Empty means match all taint keys. type: string operator: description: |- Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. + Valid operators are Exists and Equal. type: string tolerationSeconds: description: |- TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). + of effect NoExecute,... format: int64 type: integer value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. + description: Value is the taint value the toleration matches + to. type: string type: object type: array @@ -722,34 +613,25 @@ spec: matching pods among the given topology. properties: labelSelector: - description: |- - LabelSelector is used to find matching pods. - Pods that match this label selector are counted to determine the number of pods - in their corresponding topology domain. + description: LabelSelector is used to find matching pods. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. + description: A label selector requirement is a selector + that contains values, a key, and an operator that... properties: key: description: key is the label key that the selector applies to. type: string operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. + description: operator represents a key's relationship + to a set of values. type: string values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. + description: values is an array of string values. items: type: string type: array @@ -763,62 +645,45 @@ spec: matchLabels: additionalProperties: type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. + description: matchLabels is a map of {key,value} pairs. type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: description: |- MatchLabelKeys is a set of pod label keys to select the pods over which - spreading will be calculated. + spreading will be... items: type: string type: array x-kubernetes-list-type: atomic maxSkew: - description: |- - MaxSkew describes the degree to which pods may be unevenly distributed. - When `whenUnsatisfiable=DoNotSchedule`, it is the maximum permitted difference - between the number of matching pods in the target topology and the global minimum. + description: MaxSkew describes the degree to which pods + may be unevenly distributed. format: int32 type: integer minDomains: - description: |- - MinDomains indicates a minimum number of eligible domains. - When the number of eligible domains with matching topology keys is less than minDomains, - Pod Topology Spread treats "global minimum" as 0, and then the calculation of Skew is performed. + description: MinDomains indicates a minimum number of eligible + domains. format: int32 type: integer nodeAffinityPolicy: description: |- NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector - when calculating pod topology spread skew. Options are: - - Honor: only nodes matching nodeAffinity/nodeSelector are included in the calculations. - - Ignore: nodeAffinity/nodeSelector are ignored. + when calculating pod... type: string nodeTaintsPolicy: description: |- NodeTaintsPolicy indicates how we will treat node taints when calculating - pod topology spread skew. Options are: - - Honor: nodes without taints, along with tainted nodes for which the incoming pod - has a toleration, are included. - - Ignore: node taints are ignored. All nodes are included. + pod topology spread skew. type: string topologyKey: - description: |- - TopologyKey is the key of node labels. Nodes that have a label with this key - and identical values are considered to be in the same topology. - We consider each as a "bucket", and try to put balanced number - of pods into each bucket. + description: TopologyKey is the key of node labels. type: string whenUnsatisfiable: description: |- WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy the spread constraint. - - DoNotSchedule (default) tells the scheduler not to schedule it. type: string required: - maxSkew @@ -828,33 +693,26 @@ spec: type: array type: object namespaceSelector: - description: |- - Applicable only for applicationMonitoring or cloudNativeFullStack configuration types. The namespaces where you want Dynatrace Operator to inject. - For more information, see Configure monitoring for namespaces and pods (https://www.dynatrace. + description: Applicable only for applicationMonitoring or cloudNativeFullStack + configuration types. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. + description: A label selector requirement is a selector that + contains values, a key, and an operator that... properties: key: description: key is the label key that the selector applies to. type: string operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. + description: operator represents a key's relationship to + a set of values. type: string values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. + description: values is an array of string values. items: type: string type: array @@ -868,10 +726,7 @@ spec: matchLabels: additionalProperties: type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. + description: matchLabels is a map of {key,value} pairs. type: object type: object x-kubernetes-map-type: atomic @@ -879,14 +734,11 @@ spec: description: Sets a network zone for the OneAgent and ActiveGate pods. type: string oneAgent: - description: |- - General configuration about OneAgent instances. - You can't enable more than one module (classicFullStack, cloudNativeFullStack, hostMonitoring, or applicationMonitoring). + description: General configuration about OneAgent instances. properties: applicationMonitoring: - description: |- - dynatrace-webhook injects into application pods based on labeled namespaces. - Has an optional CSI driver per node via DaemonSet to provide binaries to pods. + description: dynatrace-webhook injects into application pods based + on labeled namespaces. nullable: true properties: codeModulesImage: @@ -894,33 +746,22 @@ spec: Pods. type: string initResources: - description: |- - Define resources requests and limits for the initContainer. For details, see Managing resources for containers - (https://kubernetes.io/docs/concepts/configuration/manage-resources-containers). + description: Define resources requests and limits for the + initContainer. properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry + in pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -938,7 +779,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -947,26 +788,19 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of + compute resources required. type: object type: object useCSIDriver: - description: Set if you want to use the CSIDriver. Don't enable - it if you do not have access to Kubernetes nodes or if you - lack privileges. + description: Set if you want to use the CSIDriver. type: boolean version: description: The OneAgent version to be used. type: string type: object classicFullStack: - description: |- - Has a single OneAgent per node via DaemonSet. - Injection is performed via the same OneAgent DaemonSet. + description: Has a single OneAgent per node via DaemonSet. nullable: true properties: annotations: @@ -975,21 +809,18 @@ spec: description: Add custom OneAgent annotations. type: object args: - description: |- - Set additional arguments to the OneAgent installer. - For available options, see Linux custom installation (https://www.dynatrace.com/support/help/setup-and-configuration/dynatrace-oneagent/installation-and-operation/linux/installation/customize-oneagent-installation-on-linux). + description: Set additional arguments to the OneAgent installer. items: type: string type: array x-kubernetes-list-type: set autoUpdate: - description: |- - Disables automatic restarts of OneAgent pods in case a new version is available (https://www.dynatrace.com/support/help/setup-and-configuration/setup-on-container-platforms/kubernetes/get-started-with-kubernetes-monitoring#disable-auto). - Enabled by default. + description: Disables automatic restarts of OneAgent pods + in case a new version is available (https://www. type: boolean dnsPolicy: description: Set the DNS Policy for OneAgent pods. For details, - see Pods DNS Policy (https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-s-dns-policy). + see Pods DNS Policy (https://kubernetes. type: string env: description: Set additional environment variables for the @@ -1005,9 +836,7 @@ spec: value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. + using the previously defined environment variables in... type: string valueFrom: description: Source for the environment variable's value. @@ -1021,12 +850,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the ConfigMap or @@ -1037,9 +861,8 @@ spec: type: object x-kubernetes-map-type: atomic fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.' properties: apiVersion: description: Version of the schema the FieldPath @@ -1056,7 +879,7 @@ spec: resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + (limits.cpu, limits. properties: containerName: description: 'Container name: required for volumes, @@ -1087,12 +910,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the Secret or its @@ -1124,33 +942,21 @@ spec: nodes OneAgent will be deployed. type: object oneAgentResources: - description: |- - Resource settings for OneAgent container. Consumption of the OneAgent heavily depends on the workload to monitor. You can use the default settings in the CR. - Note: resource.requests shows the values needed to run; resource.limits shows the maximum limits for the pod. + description: Resource settings for OneAgent container. properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry + in pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -1168,7 +974,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -1177,54 +983,43 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of + compute resources required. type: object type: object priorityClassName: - description: |- - Assign a priority class to the OneAgent pods. By default, no class is set. - For details, see Pod Priority and Preemption (https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/). + description: Assign a priority class to the OneAgent pods. + By default, no class is set. type: string tolerations: description: Tolerations to include with the OneAgent DaemonSet. - For details, see Taints and Tolerations (https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/). items: description: |- The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . + the triple... properties: effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + description: Effect indicates the taint effect to match. + Empty means match all taint effects. type: string key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. + description: Key is the taint key that the toleration + applies to. Empty means match all taint keys. type: string operator: description: |- Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. + Valid operators are Exists and Equal. type: string tolerationSeconds: description: |- TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). + of effect NoExecute,... format: int64 type: integer value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. + description: Value is the taint value the toleration + matches to. type: string type: object type: array @@ -1233,10 +1028,7 @@ spec: type: string type: object cloudNativeFullStack: - description: |- - Has a single OneAgent per node via DaemonSet. - dynatrace-webhook injects into application pods based on labeled namespaces. - Has a CSI driver per node via DaemonSet to provide binaries to pods. + description: Has a single OneAgent per node via DaemonSet. nullable: true properties: annotations: @@ -1245,17 +1037,14 @@ spec: description: Add custom OneAgent annotations. type: object args: - description: |- - Set additional arguments to the OneAgent installer. - For available options, see Linux custom installation (https://www.dynatrace.com/support/help/setup-and-configuration/dynatrace-oneagent/installation-and-operation/linux/installation/customize-oneagent-installation-on-linux). + description: Set additional arguments to the OneAgent installer. items: type: string type: array x-kubernetes-list-type: set autoUpdate: - description: |- - Disables automatic restarts of OneAgent pods in case a new version is available (https://www.dynatrace.com/support/help/setup-and-configuration/setup-on-container-platforms/kubernetes/get-started-with-kubernetes-monitoring#disable-auto). - Enabled by default. + description: Disables automatic restarts of OneAgent pods + in case a new version is available (https://www. type: boolean codeModulesImage: description: The OneAgent image that is used to inject into @@ -1263,7 +1052,7 @@ spec: type: string dnsPolicy: description: Set the DNS Policy for OneAgent pods. For details, - see Pods DNS Policy (https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-s-dns-policy). + see Pods DNS Policy (https://kubernetes. type: string env: description: Set additional environment variables for the @@ -1279,9 +1068,7 @@ spec: value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. + using the previously defined environment variables in... type: string valueFrom: description: Source for the environment variable's value. @@ -1295,12 +1082,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the ConfigMap or @@ -1311,9 +1093,8 @@ spec: type: object x-kubernetes-map-type: atomic fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.' properties: apiVersion: description: Version of the schema the FieldPath @@ -1330,7 +1111,7 @@ spec: resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + (limits.cpu, limits. properties: containerName: description: 'Container name: required for volumes, @@ -1361,12 +1142,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the Secret or its @@ -1386,33 +1162,22 @@ spec: to the image from the Dynatrace cluster. type: string initResources: - description: |- - Define resources requests and limits for the initContainer. For details, see Managing resources for containers - (https://kubernetes.io/docs/concepts/configuration/manage-resources-containers). + description: Define resources requests and limits for the + initContainer. properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry + in pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -1430,7 +1195,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -1439,11 +1204,8 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of + compute resources required. type: object type: object labels: @@ -1459,33 +1221,21 @@ spec: nodes OneAgent will be deployed. type: object oneAgentResources: - description: |- - Resource settings for OneAgent container. Consumption of the OneAgent heavily depends on the workload to monitor. You can use the default settings in the CR. - Note: resource.requests shows the values needed to run; resource.limits shows the maximum limits for the pod. + description: Resource settings for OneAgent container. properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry + in pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -1503,7 +1253,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -1512,54 +1262,43 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of + compute resources required. type: object type: object priorityClassName: - description: |- - Assign a priority class to the OneAgent pods. By default, no class is set. - For details, see Pod Priority and Preemption (https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/). + description: Assign a priority class to the OneAgent pods. + By default, no class is set. type: string tolerations: description: Tolerations to include with the OneAgent DaemonSet. - For details, see Taints and Tolerations (https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/). items: description: |- The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . + the triple... properties: effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + description: Effect indicates the taint effect to match. + Empty means match all taint effects. type: string key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. + description: Key is the taint key that the toleration + applies to. Empty means match all taint keys. type: string operator: description: |- Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. + Valid operators are Exists and Equal. type: string tolerationSeconds: description: |- TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). + of effect NoExecute,... format: int64 type: integer value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. + description: Value is the taint value the toleration + matches to. type: string type: object type: array @@ -1582,21 +1321,18 @@ spec: description: Add custom OneAgent annotations. type: object args: - description: |- - Set additional arguments to the OneAgent installer. - For available options, see Linux custom installation (https://www.dynatrace.com/support/help/setup-and-configuration/dynatrace-oneagent/installation-and-operation/linux/installation/customize-oneagent-installation-on-linux). + description: Set additional arguments to the OneAgent installer. items: type: string type: array x-kubernetes-list-type: set autoUpdate: - description: |- - Disables automatic restarts of OneAgent pods in case a new version is available (https://www.dynatrace.com/support/help/setup-and-configuration/setup-on-container-platforms/kubernetes/get-started-with-kubernetes-monitoring#disable-auto). - Enabled by default. + description: Disables automatic restarts of OneAgent pods + in case a new version is available (https://www. type: boolean dnsPolicy: description: Set the DNS Policy for OneAgent pods. For details, - see Pods DNS Policy (https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-s-dns-policy). + see Pods DNS Policy (https://kubernetes. type: string env: description: Set additional environment variables for the @@ -1612,9 +1348,7 @@ spec: value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. + using the previously defined environment variables in... type: string valueFrom: description: Source for the environment variable's value. @@ -1628,12 +1362,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the ConfigMap or @@ -1644,9 +1373,8 @@ spec: type: object x-kubernetes-map-type: atomic fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.' properties: apiVersion: description: Version of the schema the FieldPath @@ -1663,7 +1391,7 @@ spec: resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + (limits.cpu, limits. properties: containerName: description: 'Container name: required for volumes, @@ -1694,12 +1422,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the Secret or its @@ -1731,33 +1454,21 @@ spec: nodes OneAgent will be deployed. type: object oneAgentResources: - description: |- - Resource settings for OneAgent container. Consumption of the OneAgent heavily depends on the workload to monitor. You can use the default settings in the CR. - Note: resource.requests shows the values needed to run; resource.limits shows the maximum limits for the pod. + description: Resource settings for OneAgent container. properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry + in pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -1775,7 +1486,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -1784,54 +1495,43 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of + compute resources required. type: object type: object priorityClassName: - description: |- - Assign a priority class to the OneAgent pods. By default, no class is set. - For details, see Pod Priority and Preemption (https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/). + description: Assign a priority class to the OneAgent pods. + By default, no class is set. type: string tolerations: description: Tolerations to include with the OneAgent DaemonSet. - For details, see Taints and Tolerations (https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/). items: description: |- The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . + the triple... properties: effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + description: Effect indicates the taint effect to match. + Empty means match all taint effects. type: string key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. + description: Key is the taint key that the toleration + applies to. Empty means match all taint keys. type: string operator: description: |- Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. + Valid operators are Exists and Equal. type: string tolerationSeconds: description: |- TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). + of effect NoExecute,... format: int64 type: integer value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. + description: Value is the taint value the toleration + matches to. type: string type: object type: array @@ -1841,9 +1541,8 @@ spec: type: object type: object proxy: - description: |- - Set custom proxy settings either directly or from a secret with the field proxy. - Note: Applies to Dynatrace Operator, ActiveGate, and OneAgents. + description: Set custom proxy settings either directly or from a secret + with the field proxy. properties: value: description: Proxy URL. It has preference over ValueFrom. @@ -1860,7 +1559,7 @@ spec: customProperties: description: |- Add a custom properties file by providing it as a value or reference it from a secret - If referenced from a secret, make sure the key is called 'customProperties' + If referenced... properties: value: description: Custom properties value. @@ -1887,9 +1586,7 @@ spec: value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. + using the previously defined environment variables in... type: string valueFrom: description: Source for the environment variable's value. @@ -1903,12 +1600,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the ConfigMap or its @@ -1919,9 +1611,8 @@ spec: type: object x-kubernetes-map-type: atomic fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + description: 'Selects a field of the pod: supports metadata.name, + metadata.namespace, `metadata.' properties: apiVersion: description: Version of the schema the FieldPath @@ -1938,7 +1629,7 @@ spec: resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + (limits.cpu, limits. properties: containerName: description: 'Container name: required for volumes, @@ -1969,12 +1660,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the Secret or its key @@ -1993,8 +1679,7 @@ spec: description: Set activation group for ActiveGate type: string image: - description: The ActiveGate container image. Defaults to the latest - ActiveGate image provided by the registry on the tenant + description: The ActiveGate container image. type: string labels: additionalProperties: @@ -2015,28 +1700,18 @@ spec: pods properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry in + pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -2054,7 +1729,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -2063,11 +1738,8 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of compute + resources required. type: object type: object tolerations: @@ -2075,36 +1747,30 @@ spec: items: description: |- The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . + the triple... properties: effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + description: Effect indicates the taint effect to match. + Empty means match all taint effects. type: string key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. + description: Key is the taint key that the toleration applies + to. Empty means match all taint keys. type: string operator: description: |- Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. + Valid operators are Exists and Equal. type: string tolerationSeconds: description: |- TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). + of effect NoExecute,... format: int64 type: integer value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. + description: Value is the taint value the toleration matches + to. type: string type: object type: array @@ -2116,34 +1782,25 @@ spec: matching pods among the given topology. properties: labelSelector: - description: |- - LabelSelector is used to find matching pods. - Pods that match this label selector are counted to determine the number of pods - in their corresponding topology domain. + description: LabelSelector is used to find matching pods. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. + description: A label selector requirement is a selector + that contains values, a key, and an operator that... properties: key: description: key is the label key that the selector applies to. type: string operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. + description: operator represents a key's relationship + to a set of values. type: string values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. + description: values is an array of string values. items: type: string type: array @@ -2157,62 +1814,45 @@ spec: matchLabels: additionalProperties: type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. + description: matchLabels is a map of {key,value} pairs. type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: description: |- MatchLabelKeys is a set of pod label keys to select the pods over which - spreading will be calculated. + spreading will be... items: type: string type: array x-kubernetes-list-type: atomic maxSkew: - description: |- - MaxSkew describes the degree to which pods may be unevenly distributed. - When `whenUnsatisfiable=DoNotSchedule`, it is the maximum permitted difference - between the number of matching pods in the target topology and the global minimum. + description: MaxSkew describes the degree to which pods + may be unevenly distributed. format: int32 type: integer minDomains: - description: |- - MinDomains indicates a minimum number of eligible domains. - When the number of eligible domains with matching topology keys is less than minDomains, - Pod Topology Spread treats "global minimum" as 0, and then the calculation of Skew is performed. + description: MinDomains indicates a minimum number of eligible + domains. format: int32 type: integer nodeAffinityPolicy: description: |- NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector - when calculating pod topology spread skew. Options are: - - Honor: only nodes matching nodeAffinity/nodeSelector are included in the calculations. - - Ignore: nodeAffinity/nodeSelector are ignored. + when calculating pod... type: string nodeTaintsPolicy: description: |- NodeTaintsPolicy indicates how we will treat node taints when calculating - pod topology spread skew. Options are: - - Honor: nodes without taints, along with tainted nodes for which the incoming pod - has a toleration, are included. - - Ignore: node taints are ignored. All nodes are included. + pod topology spread skew. type: string topologyKey: - description: |- - TopologyKey is the key of node labels. Nodes that have a label with this key - and identical values are considered to be in the same topology. - We consider each as a "bucket", and try to put balanced number - of pods into each bucket. + description: TopologyKey is the key of node labels. type: string whenUnsatisfiable: description: |- WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy the spread constraint. - - DoNotSchedule (default) tells the scheduler not to schedule it. type: string required: - maxSkew @@ -2222,18 +1862,16 @@ spec: type: array type: object skipCertCheck: - description: |- - Disable certificate check for the connection between Dynatrace Operator and the Dynatrace Cluster. - Set to true if you want to skip certification validation checks. + description: Disable certificate check for the connection between + Dynatrace Operator and the Dynatrace Cluster. type: boolean tokens: description: Name of the secret holding the tokens used for connecting to Dynatrace. type: string trustedCAs: - description: |- - Adds custom RootCAs from a configmap. Put the certificate under certs within your configmap. - Note: Applies to Dynatrace Operator, OneAgent and ActiveGate. + description: Adds custom RootCAs from a configmap. Put the certificate + under certs within your configmap. type: string required: - apiUrl @@ -2254,6 +1892,9 @@ spec: description: Time of the last connection request format: date-time type: string + tenantTokenHash: + description: Hash of the tenant token + type: string tenantUUID: description: UUID of the tenant, received from the tenant type: string @@ -2313,31 +1954,24 @@ spec: state of this API Resource. properties: lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + description: lastTransitionTime is the last time the condition + transitioned from one status to another. format: date-time type: string message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. + description: message is a human readable message indicating + details about the transition. maxLength: 32768 type: string observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. format: int64 minimum: 0 type: integer reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. maxLength: 1024 minLength: 1 pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ @@ -2408,6 +2042,9 @@ spec: description: Time of the last connection request format: date-time type: string + tenantTokenHash: + description: Hash of the tenant token + type: string tenantUUID: description: UUID of the tenant, received from the tenant type: string @@ -2482,19 +2119,12 @@ spec: description: DynaKube is the Schema for the DynaKube API properties: apiVersion: - 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 + description: APIVersion defines the versioned schema of this representation + of an object. type: string kind: - 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 + description: Kind is a string value representing the REST resource this + object represents. type: string metadata: type: object @@ -2511,17 +2141,14 @@ spec: type: object capabilities: description: "Defines the ActiveGate pod capabilities\nPossible - values:\n\t- `routing` enables OneAgent routing.\n\t- `kubernetes-monitoring` - enables Kubernetes API monitoring.\n\t- `metrics-ingest` opens - the metrics ingest endpoint on the DynaKube ActiveGate and redirects - all pods to it." + values:\n\t- `routing` enables OneAgent routing." items: type: string type: array customProperties: description: |- Add a custom properties file by providing it as a value or reference it from a secret - If referenced from a secret, make sure the key is called `customProperties` + If referenced... properties: value: description: Custom properties value. @@ -2548,9 +2175,7 @@ spec: value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. + using the previously defined environment variables in... type: string valueFrom: description: Source for the environment variable's value. @@ -2564,12 +2189,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the ConfigMap or its @@ -2580,9 +2200,8 @@ spec: type: object x-kubernetes-map-type: atomic fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + description: 'Selects a field of the pod: supports metadata.name, + metadata.namespace, `metadata.' properties: apiVersion: description: Version of the schema the FieldPath @@ -2599,7 +2218,7 @@ spec: resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + (limits.cpu, limits. properties: containerName: description: 'Container name: required for volumes, @@ -2630,12 +2249,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the Secret or its key @@ -2654,8 +2268,7 @@ spec: description: Set activation group for ActiveGate type: string image: - description: Use a custom ActiveGate image. Defaults to the latest - ActiveGate image provided by the registry on the tenant + description: Use a custom ActiveGate image. type: string labels: additionalProperties: @@ -2670,9 +2283,8 @@ spec: nodes ActiveGate will be deployed. type: object priorityClassName: - description: |- - Assign a priority class to the ActiveGate pods. By default, no class is set. - For details, see Pod Priority and Preemption. (https://dt-url.net/n8437bl) + description: Assign a priority class to the ActiveGate pods. By + default, no class is set. type: string replicas: default: 1 @@ -2680,33 +2292,21 @@ spec: format: int32 type: integer resources: - description: |- - Resource settings for ActiveGate container. - Consumption of the ActiveGate heavily depends on the workload to monitor. Adjust values accordingly. + description: Resource settings for ActiveGate container. properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry in + pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -2724,7 +2324,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -2733,54 +2333,43 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of compute + resources required. type: object type: object tlsSecretName: - description: |- - The name of a secret containing ActiveGate TLS cert+key and password. If not set, self-signed certificate is used. - `server.p12`: certificate+key pair in pkcs12 format - `password`: passphrase to read server.p12 + description: The name of a secret containing ActiveGate TLS cert+key + and password. type: string tolerations: description: Set tolerations for the ActiveGate pods items: description: |- The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . + the triple... properties: effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + description: Effect indicates the taint effect to match. + Empty means match all taint effects. type: string key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. + description: Key is the taint key that the toleration applies + to. Empty means match all taint keys. type: string operator: description: |- Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. + Valid operators are Exists and Equal. type: string tolerationSeconds: description: |- TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). + of effect NoExecute,... format: int64 type: integer value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. + description: Value is the taint value the toleration matches + to. type: string type: object type: array @@ -2792,34 +2381,25 @@ spec: matching pods among the given topology. properties: labelSelector: - description: |- - LabelSelector is used to find matching pods. - Pods that match this label selector are counted to determine the number of pods - in their corresponding topology domain. + description: LabelSelector is used to find matching pods. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. + description: A label selector requirement is a selector + that contains values, a key, and an operator that... properties: key: description: key is the label key that the selector applies to. type: string operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. + description: operator represents a key's relationship + to a set of values. type: string values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. + description: values is an array of string values. items: type: string type: array @@ -2833,62 +2413,45 @@ spec: matchLabels: additionalProperties: type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. + description: matchLabels is a map of {key,value} pairs. type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: description: |- MatchLabelKeys is a set of pod label keys to select the pods over which - spreading will be calculated. + spreading will be... items: type: string type: array x-kubernetes-list-type: atomic maxSkew: - description: |- - MaxSkew describes the degree to which pods may be unevenly distributed. - When `whenUnsatisfiable=DoNotSchedule`, it is the maximum permitted difference - between the number of matching pods in the target topology and the global minimum. + description: MaxSkew describes the degree to which pods + may be unevenly distributed. format: int32 type: integer minDomains: - description: |- - MinDomains indicates a minimum number of eligible domains. - When the number of eligible domains with matching topology keys is less than minDomains, - Pod Topology Spread treats "global minimum" as 0, and then the calculation of Skew is performed. + description: MinDomains indicates a minimum number of eligible + domains. format: int32 type: integer nodeAffinityPolicy: description: |- NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector - when calculating pod topology spread skew. Options are: - - Honor: only nodes matching nodeAffinity/nodeSelector are included in the calculations. - - Ignore: nodeAffinity/nodeSelector are ignored. + when calculating pod... type: string nodeTaintsPolicy: description: |- NodeTaintsPolicy indicates how we will treat node taints when calculating - pod topology spread skew. Options are: - - Honor: nodes without taints, along with tainted nodes for which the incoming pod - has a toleration, are included. - - Ignore: node taints are ignored. All nodes are included. + pod topology spread skew. type: string topologyKey: - description: |- - TopologyKey is the key of node labels. Nodes that have a label with this key - and identical values are considered to be in the same topology. - We consider each as a "bucket", and try to put balanced number - of pods into each bucket. + description: TopologyKey is the key of node labels. type: string whenUnsatisfiable: description: |- WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy the spread constraint. - - DoNotSchedule (default) tells the scheduler not to schedule it. type: string required: - maxSkew @@ -2898,26 +2461,20 @@ spec: type: array type: object apiUrl: - description: |- - Dynatrace `apiUrl`, including the `/api` path at the end. - - For SaaS, set `YOUR_ENVIRONMENT_ID` to your environment ID. - - For Managed, change the `apiUrl` address. - For instructions on how to determine the environment ID and how to configure the apiUrl address, see Environment ID (https://www. + description: Dynatrace `apiUrl`, including the `/api` path at the + end. type: string customPullSecret: - description: |- - Defines a custom pull secret in case you use a private registry when pulling images from the Dynatrace environment. - To define a custom pull secret and learn about the expected behavior, see Configure customPullSecret - (https://www.dynatrace. + description: Defines a custom pull secret in case you use a private + registry when pulling images from the... type: string dynatraceApiRequestThreshold: default: 15 description: Minimum minutes between Dynatrace API requests. type: integer enableIstio: - description: |- - When enabled, and if Istio is installed in the Kubernetes environment, Dynatrace Operator will create the corresponding VirtualService and ServiceEntry objects to allow access to the Dynatrace Cluster from the OneAgent or ActiveGate. - Disabled by default. + description: When enabled, and if Istio is installed in the Kubernetes + environment, Dynatrace Operator will... type: boolean metadataEnrichment: description: Configuration for Metadata Enrichment. @@ -2934,25 +2491,19 @@ spec: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. + description: A label selector requirement is a selector + that contains values, a key, and an operator that... properties: key: description: key is the label key that the selector applies to. type: string operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. + description: operator represents a key's relationship + to a set of values. type: string values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. + description: values is an array of string values. items: type: string type: array @@ -2966,10 +2517,7 @@ spec: matchLabels: additionalProperties: type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. + description: matchLabels is a map of {key,value} pairs. type: object type: object x-kubernetes-map-type: atomic @@ -2980,14 +2528,11 @@ spec: description: Sets a network zone for the OneAgent and ActiveGate pods. type: string oneAgent: - description: |- - General configuration about OneAgent instances. - You can't enable more than one module (classicFullStack, cloudNativeFullStack, hostMonitoring, or applicationMonitoring). + description: General configuration about OneAgent instances. properties: applicationMonitoring: - description: |- - dynatrace-webhook injects into application pods based on labeled namespaces. - Has an optional CSI driver per node via DaemonSet to provide binaries to pods. + description: dynatrace-webhook injects into application pods based + on labeled namespaces. nullable: true properties: codeModulesImage: @@ -2995,33 +2540,22 @@ spec: Pods. type: string initResources: - description: |- - Define resources requests and limits for the initContainer. For details, see Managing resources for containers - (https://kubernetes.io/docs/concepts/configuration/manage-resources-containers). + description: Define resources requests and limits for the + initContainer. properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry + in pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -3039,7 +2573,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -3048,41 +2582,31 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of + compute resources required. type: object type: object namespaceSelector: - description: |- - Applicable only for applicationMonitoring or cloudNativeFullStack configuration types. The namespaces where you want Dynatrace Operator to inject. - For more information, see Configure monitoring for namespaces and pods (https://www.dynatrace. + description: Applicable only for applicationMonitoring or + cloudNativeFullStack configuration types. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. + description: A label selector requirement is a selector + that contains values, a key, and an operator that... properties: key: description: key is the label key that the selector applies to. type: string operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. + description: operator represents a key's relationship + to a set of values. type: string values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. + description: values is an array of string values. items: type: string type: array @@ -3096,27 +2620,20 @@ spec: matchLabels: additionalProperties: type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. + description: matchLabels is a map of {key,value} pairs. type: object type: object x-kubernetes-map-type: atomic useCSIDriver: default: false - description: Set if you want to use the CSIDriver. Don't enable - it if you do not have access to Kubernetes nodes or if you - lack privileges. + description: Set if you want to use the CSIDriver. type: boolean version: description: The OneAgent version to be used. type: string type: object classicFullStack: - description: |- - Has a single OneAgent per node via DaemonSet. - Injection is performed via the same OneAgent DaemonSet. + description: Has a single OneAgent per node via DaemonSet. nullable: true properties: annotations: @@ -3125,22 +2642,19 @@ spec: description: Add custom OneAgent annotations. type: object args: - description: |- - Set additional arguments to the OneAgent installer. - For available options, see Linux custom installation (https://www.dynatrace.com/support/help/setup-and-configuration/dynatrace-oneagent/installation-and-operation/linux/installation/customize-oneagent-installation-on-linux). + description: Set additional arguments to the OneAgent installer. items: type: string type: array x-kubernetes-list-type: set autoUpdate: default: true - description: |- - Disables automatic restarts of OneAgent pods in case a new version is available (https://www.dynatrace.com/support/help/setup-and-configuration/setup-on-container-platforms/kubernetes/get-started-with-kubernetes-monitoring#disable-auto). - Enabled by default. + description: Disables automatic restarts of OneAgent pods + in case a new version is available (https://www. type: boolean dnsPolicy: description: Set the DNS Policy for OneAgent pods. For details, - see Pods DNS Policy (https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-s-dns-policy). + see Pods DNS Policy (https://kubernetes. type: string env: description: Set additional environment variables for the @@ -3156,9 +2670,7 @@ spec: value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. + using the previously defined environment variables in... type: string valueFrom: description: Source for the environment variable's value. @@ -3172,12 +2684,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the ConfigMap or @@ -3188,9 +2695,8 @@ spec: type: object x-kubernetes-map-type: atomic fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.' properties: apiVersion: description: Version of the schema the FieldPath @@ -3207,7 +2713,7 @@ spec: resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + (limits.cpu, limits. properties: containerName: description: 'Container name: required for volumes, @@ -3238,12 +2744,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the Secret or its @@ -3275,34 +2776,21 @@ spec: nodes OneAgent will be deployed. type: object oneAgentResources: - description: |- - Resource settings for OneAgent container. Consumption of the OneAgent heavily depends on the workload to monitor. You can use the default settings in the CR. - - `resource.requests` shows the values needed to run - - `resource.limits` shows the maximum limits for the pod + description: Resource settings for OneAgent container. properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry + in pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -3320,7 +2808,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -3329,17 +2817,13 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of + compute resources required. type: object type: object priorityClassName: - description: |- - Assign a priority class to the OneAgent pods. By default, no class is set. - For details, see Pod Priority and Preemption (https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/). + description: Assign a priority class to the OneAgent pods. + By default, no class is set. type: string secCompProfile: description: The SecComp Profile that will be configured in @@ -3347,54 +2831,43 @@ spec: type: string tolerations: description: Tolerations to include with the OneAgent DaemonSet. - For details, see Taints and Tolerations (https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/). items: description: |- The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . + the triple... properties: effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + description: Effect indicates the taint effect to match. + Empty means match all taint effects. type: string key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. + description: Key is the taint key that the toleration + applies to. Empty means match all taint keys. type: string operator: description: |- Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. + Valid operators are Exists and Equal. type: string tolerationSeconds: description: |- TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). + of effect NoExecute,... format: int64 type: integer value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. + description: Value is the taint value the toleration + matches to. type: string type: object type: array version: description: The OneAgent version to be used for OneAgents - running in the dedicated pod. This setting doesn't affect - the OneAgent version used for application monitoring. + running in the dedicated pod. type: string type: object cloudNativeFullStack: - description: |- - Has a single OneAgent per node via DaemonSet. - dynatrace-webhook injects into application pods based on labeled namespaces. - Has a CSI driver per node via DaemonSet to provide binaries to pods. + description: Has a single OneAgent per node via DaemonSet. nullable: true properties: annotations: @@ -3403,18 +2876,15 @@ spec: description: Add custom OneAgent annotations. type: object args: - description: |- - Set additional arguments to the OneAgent installer. - For available options, see Linux custom installation (https://www.dynatrace.com/support/help/setup-and-configuration/dynatrace-oneagent/installation-and-operation/linux/installation/customize-oneagent-installation-on-linux). + description: Set additional arguments to the OneAgent installer. items: type: string type: array x-kubernetes-list-type: set autoUpdate: default: true - description: |- - Disables automatic restarts of OneAgent pods in case a new version is available (https://www.dynatrace.com/support/help/setup-and-configuration/setup-on-container-platforms/kubernetes/get-started-with-kubernetes-monitoring#disable-auto). - Enabled by default. + description: Disables automatic restarts of OneAgent pods + in case a new version is available (https://www. type: boolean codeModulesImage: description: The OneAgent image that is used to inject into @@ -3422,7 +2892,7 @@ spec: type: string dnsPolicy: description: Set the DNS Policy for OneAgent pods. For details, - see Pods DNS Policy (https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-s-dns-policy). + see Pods DNS Policy (https://kubernetes. type: string env: description: Set additional environment variables for the @@ -3438,9 +2908,7 @@ spec: value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. + using the previously defined environment variables in... type: string valueFrom: description: Source for the environment variable's value. @@ -3454,12 +2922,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the ConfigMap or @@ -3470,9 +2933,8 @@ spec: type: object x-kubernetes-map-type: atomic fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.' properties: apiVersion: description: Version of the schema the FieldPath @@ -3489,7 +2951,7 @@ spec: resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + (limits.cpu, limits. properties: containerName: description: 'Container name: required for volumes, @@ -3520,12 +2982,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the Secret or its @@ -3545,33 +3002,22 @@ spec: to the image from the Dynatrace cluster. type: string initResources: - description: |- - Define resources requests and limits for the initContainer. For details, see Managing resources for containers - (https://kubernetes.io/docs/concepts/configuration/manage-resources-containers). + description: Define resources requests and limits for the + initContainer. properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry + in pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -3589,7 +3035,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -3598,11 +3044,8 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of + compute resources required. type: object type: object labels: @@ -3612,33 +3055,26 @@ spec: to structure workloads as desired. type: object namespaceSelector: - description: |- - Applicable only for applicationMonitoring or cloudNativeFullStack configuration types. The namespaces where you want Dynatrace Operator to inject. - For more information, see Configure monitoring for namespaces and pods (https://www.dynatrace. + description: Applicable only for applicationMonitoring or + cloudNativeFullStack configuration types. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. + description: A label selector requirement is a selector + that contains values, a key, and an operator that... properties: key: description: key is the label key that the selector applies to. type: string operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. + description: operator represents a key's relationship + to a set of values. type: string values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. + description: values is an array of string values. items: type: string type: array @@ -3652,10 +3088,7 @@ spec: matchLabels: additionalProperties: type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. + description: matchLabels is a map of {key,value} pairs. type: object type: object x-kubernetes-map-type: atomic @@ -3666,34 +3099,21 @@ spec: nodes OneAgent will be deployed. type: object oneAgentResources: - description: |- - Resource settings for OneAgent container. Consumption of the OneAgent heavily depends on the workload to monitor. You can use the default settings in the CR. - - `resource.requests` shows the values needed to run - - `resource.limits` shows the maximum limits for the pod + description: Resource settings for OneAgent container. properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry + in pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -3711,7 +3131,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -3720,17 +3140,13 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of + compute resources required. type: object type: object priorityClassName: - description: |- - Assign a priority class to the OneAgent pods. By default, no class is set. - For details, see Pod Priority and Preemption (https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/). + description: Assign a priority class to the OneAgent pods. + By default, no class is set. type: string secCompProfile: description: The SecComp Profile that will be configured in @@ -3738,54 +3154,44 @@ spec: type: string tolerations: description: Tolerations to include with the OneAgent DaemonSet. - For details, see Taints and Tolerations (https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/). items: description: |- The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . + the triple... properties: effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + description: Effect indicates the taint effect to match. + Empty means match all taint effects. type: string key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. + description: Key is the taint key that the toleration + applies to. Empty means match all taint keys. type: string operator: description: |- Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. + Valid operators are Exists and Equal. type: string tolerationSeconds: description: |- TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). + of effect NoExecute,... format: int64 type: integer value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. + description: Value is the taint value the toleration + matches to. type: string type: object type: array version: description: The OneAgent version to be used for OneAgents - running in the dedicated pod. This setting doesn't affect - the OneAgent version used for application monitoring. + running in the dedicated pod. type: string type: object hostGroup: - description: |- - Specify the name of the group to which you want to assign the host. - This method is preferred over the now obsolete `--set-host-group` argument. - If both settings are used, this field takes precedence over the `--set-host-group` argument. + description: Specify the name of the group to which you want to + assign the host. type: string hostMonitoring: description: |- @@ -3799,22 +3205,19 @@ spec: description: Add custom OneAgent annotations. type: object args: - description: |- - Set additional arguments to the OneAgent installer. - For available options, see Linux custom installation (https://www.dynatrace.com/support/help/setup-and-configuration/dynatrace-oneagent/installation-and-operation/linux/installation/customize-oneagent-installation-on-linux). + description: Set additional arguments to the OneAgent installer. items: type: string type: array x-kubernetes-list-type: set autoUpdate: default: true - description: |- - Disables automatic restarts of OneAgent pods in case a new version is available (https://www.dynatrace.com/support/help/setup-and-configuration/setup-on-container-platforms/kubernetes/get-started-with-kubernetes-monitoring#disable-auto). - Enabled by default. + description: Disables automatic restarts of OneAgent pods + in case a new version is available (https://www. type: boolean dnsPolicy: description: Set the DNS Policy for OneAgent pods. For details, - see Pods DNS Policy (https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-s-dns-policy). + see Pods DNS Policy (https://kubernetes. type: string env: description: Set additional environment variables for the @@ -3830,9 +3233,7 @@ spec: value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. + using the previously defined environment variables in... type: string valueFrom: description: Source for the environment variable's value. @@ -3846,12 +3247,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the ConfigMap or @@ -3862,9 +3258,8 @@ spec: type: object x-kubernetes-map-type: atomic fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.' properties: apiVersion: description: Version of the schema the FieldPath @@ -3881,7 +3276,7 @@ spec: resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + (limits.cpu, limits. properties: containerName: description: 'Container name: required for volumes, @@ -3912,12 +3307,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the Secret or its @@ -3949,34 +3339,21 @@ spec: nodes OneAgent will be deployed. type: object oneAgentResources: - description: |- - Resource settings for OneAgent container. Consumption of the OneAgent heavily depends on the workload to monitor. You can use the default settings in the CR. - - `resource.requests` shows the values needed to run - - `resource.limits` shows the maximum limits for the pod + description: Resource settings for OneAgent container. properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry + in pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -3994,7 +3371,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -4003,17 +3380,13 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of + compute resources required. type: object type: object priorityClassName: - description: |- - Assign a priority class to the OneAgent pods. By default, no class is set. - For details, see Pod Priority and Preemption (https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/). + description: Assign a priority class to the OneAgent pods. + By default, no class is set. type: string secCompProfile: description: The SecComp Profile that will be configured in @@ -4021,54 +3394,45 @@ spec: type: string tolerations: description: Tolerations to include with the OneAgent DaemonSet. - For details, see Taints and Tolerations (https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/). items: description: |- The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . + the triple... properties: effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + description: Effect indicates the taint effect to match. + Empty means match all taint effects. type: string key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. + description: Key is the taint key that the toleration + applies to. Empty means match all taint keys. type: string operator: description: |- Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. + Valid operators are Exists and Equal. type: string tolerationSeconds: description: |- TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). + of effect NoExecute,... format: int64 type: integer value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. + description: Value is the taint value the toleration + matches to. type: string type: object type: array version: description: The OneAgent version to be used for OneAgents - running in the dedicated pod. This setting doesn't affect - the OneAgent version used for application monitoring. + running in the dedicated pod. type: string type: object type: object proxy: - description: |- - Set custom proxy settings either directly or from a secret with the field `proxy`. - Applies to Dynatrace Operator, ActiveGate, and OneAgents. + description: Set custom proxy settings either directly or from a secret + with the field `proxy`. properties: value: description: Proxy URL. It has preference over ValueFrom. @@ -4080,9 +3444,8 @@ spec: type: string type: object skipCertCheck: - description: |- - Disable certificate check for the connection between Dynatrace Operator and the Dynatrace Cluster. - Set to `true` if you want to skip certification validation checks. + description: Disable certificate check for the connection between + Dynatrace Operator and the Dynatrace Cluster. type: boolean tokens: description: Name of the secret holding the tokens used for connecting @@ -4092,7 +3455,6 @@ spec: description: |- Adds custom RootCAs from a configmap. The key to the data must be `certs`. - This applies to both the Dynatrace Operator and the OneAgent. Doesn't apply to ActiveGate. type: string required: - apiUrl @@ -4113,6 +3475,9 @@ spec: description: Time of the last connection request format: date-time type: string + tenantTokenHash: + description: Hash of the tenant token + type: string tenantUUID: description: UUID of the tenant, received from the tenant type: string @@ -4172,31 +3537,24 @@ spec: state of this API Resource. properties: lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + description: lastTransitionTime is the last time the condition + transitioned from one status to another. format: date-time type: string message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. + description: message is a human readable message indicating + details about the transition. maxLength: 32768 type: string observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. format: int64 minimum: 0 type: integer reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. maxLength: 1024 minLength: 1 pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ @@ -4239,8 +3597,7 @@ spec: type: string kubernetesClusterName: description: KubernetesClusterName contains the display name (also - know as label) of the monitored entity that points to the Kubernetes - cluster + know as label) of the monitored entity that... type: string metadataEnrichment: description: Observed state of Metadata-Enrichment @@ -4288,6 +3645,9 @@ spec: description: Time of the last connection request format: date-time type: string + tenantTokenHash: + description: Hash of the tenant token + type: string tenantUUID: description: UUID of the tenant, received from the tenant type: string @@ -4362,19 +3722,12 @@ spec: description: DynaKube is the Schema for the DynaKube API properties: apiVersion: - 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 + description: APIVersion defines the versioned schema of this representation + of an object. type: string kind: - 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 + description: Kind is a string value representing the REST resource this + object represents. type: string metadata: type: object @@ -4398,7 +3751,7 @@ spec: customProperties: description: |- Add a custom properties file by providing it as a value or reference it from a secret - If referenced from a secret, make sure the key is called 'customProperties' + If referenced... properties: value: description: Raw value for given property. @@ -4425,9 +3778,7 @@ spec: value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. + using the previously defined environment variables in... type: string valueFrom: description: Source for the environment variable's value. @@ -4441,12 +3792,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the ConfigMap or its @@ -4457,9 +3803,8 @@ spec: type: object x-kubernetes-map-type: atomic fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + description: 'Selects a field of the pod: supports metadata.name, + metadata.namespace, `metadata.' properties: apiVersion: description: Version of the schema the FieldPath @@ -4476,7 +3821,7 @@ spec: resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + (limits.cpu, limits. properties: containerName: description: 'Container name: required for volumes, @@ -4507,12 +3852,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the Secret or its key @@ -4531,8 +3871,7 @@ spec: description: Set activation group for ActiveGate type: string image: - description: The ActiveGate container image. Defaults to the latest - ActiveGate image provided by the registry on the tenant + description: The ActiveGate container image. type: string labels: additionalProperties: @@ -4545,9 +3884,7 @@ spec: description: Node selector to control the selection of nodes type: object priorityClassName: - description: |- - If specified, indicates the pod's priority. Name must be defined by creating a PriorityClass object with that - name. If not specified the setting will be removed from the StatefulSet. + description: If specified, indicates the pod's priority. type: string replicas: description: Amount of replicas for your ActiveGates @@ -4558,28 +3895,18 @@ spec: pods properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry in + pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -4597,9 +3924,2853 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount of compute + resources required. + type: object + type: object + tlsSecretName: + description: The name of a secret containing ActiveGate TLS cert+key + and password. + type: string + tolerations: + description: Set tolerations for the ActiveGate pods + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple... + properties: + effect: + description: Effect indicates the taint effect to match. + Empty means match all taint effects. + type: string + key: + description: Key is the taint key that the toleration applies + to. Empty means match all taint keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute,... + format: int64 + type: integer + value: + description: Value is the taint value the toleration matches + to. + type: string + type: object + type: array + topologySpreadConstraints: + description: Adds TopologySpreadConstraints for the ActiveGate + pods + items: + description: TopologySpreadConstraint specifies how to spread + matching pods among the given topology. + properties: + labelSelector: + description: LabelSelector is used to find matching pods. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that... + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. + type: string + values: + description: values is an array of string values. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select the pods over which + spreading will be... + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + description: MaxSkew describes the degree to which pods + may be unevenly distributed. + format: int32 + type: integer + minDomains: + description: MinDomains indicates a minimum number of eligible + domains. + format: int32 + type: integer + nodeAffinityPolicy: + description: |- + NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector + when calculating pod... + type: string + nodeTaintsPolicy: + description: |- + NodeTaintsPolicy indicates how we will treat node taints when calculating + pod topology spread skew. + type: string + topologyKey: + description: TopologyKey is the key of node labels. + type: string + whenUnsatisfiable: + description: |- + WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy + the spread constraint. + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + type: object + apiUrl: + description: Dynatrace apiUrl, including the /api path at the end. + maxLength: 128 + type: string + customPullSecret: + description: Defines a custom pull secret in case you use a private + registry when pulling images from the... + type: string + dynatraceApiRequestThreshold: + description: Configuration for thresholding Dynatrace API requests. + type: integer + enableIstio: + description: When enabled, and if Istio is installed on the Kubernetes + environment, Dynatrace Operator will... + type: boolean + extensions: + description: When an (empty) ExtensionsSpec is provided, the extensions + related components (extensions... + type: object + kspm: + description: General configuration about the KSPM feature. + type: object + logMonitoring: + description: General configuration about the LogMonitoring feature. + properties: + ingestRuleMatchers: + items: + properties: + attribute: + type: string + values: + items: + type: string + type: array + type: object + type: array + type: object + metadataEnrichment: + description: Configuration for Metadata Enrichment. + properties: + enabled: + description: Enables MetadataEnrichment, `false` by default. + type: boolean + namespaceSelector: + description: The namespaces where you want Dynatrace Operator + to inject enrichment. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that... + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. + type: string + values: + description: values is an array of string values. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + type: object + networkZone: + description: Sets a network zone for the OneAgent and ActiveGate pods. + type: string + oneAgent: + description: General configuration about OneAgent instances. + properties: + applicationMonitoring: + description: dynatrace-webhook injects into application pods based + on labeled namespaces. + nullable: true + properties: + codeModulesImage: + description: Use a custom OneAgent CodeModule image to download + binaries. + type: string + initResources: + description: Define resources requests and limits for the + initContainer. + properties: + claims: + description: Claims lists the names of resources, defined + in spec. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: Name must match the name of one entry + in pod.spec. + type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount of + compute resources required. + type: object + type: object + namespaceSelector: + description: Applicable only for applicationMonitoring or + cloudNativeFullStack configuration types. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that... + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. + type: string + values: + description: values is an array of string values. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + version: + description: Use a specific OneAgent CodeModule version. + type: string + type: object + classicFullStack: + description: Has a single OneAgent per node via DaemonSet. + nullable: true + properties: + annotations: + additionalProperties: + type: string + description: Add custom OneAgent annotations. + type: object + args: + description: Set additional arguments to the OneAgent installer. + items: + type: string + type: array + x-kubernetes-list-type: set + autoUpdate: + description: Disables automatic restarts of OneAgent pods + in case a new version is available (https://www. + type: boolean + dnsPolicy: + description: Set the DNS Policy for OneAgent pods. For details, + see Pods DNS Policy (https://kubernetes. + type: string + env: + description: Set additional environment variables for the + OneAgent pods. + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must + be a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in... + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: Name of the referent. + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in + the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of + the exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: Name of the referent. + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + description: Use a custom OneAgent image. Defaults to the + latest image from the Dynatrace cluster. + type: string + labels: + additionalProperties: + type: string + description: Your defined labels for OneAgent pods in order + to structure workloads as desired. + type: object + nodeSelector: + additionalProperties: + type: string + description: Specify the node selector that controls on which + nodes OneAgent will be deployed. + type: object + oneAgentResources: + description: Resource settings for OneAgent container. + properties: + claims: + description: Claims lists the names of resources, defined + in spec. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: Name must match the name of one entry + in pod.spec. + type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount of + compute resources required. + type: object + type: object + priorityClassName: + description: Assign a priority class to the OneAgent pods. + By default, no class is set. + type: string + secCompProfile: + description: The SecComp Profile that will be configured in + order to run in secure computing mode. + type: string + tolerations: + description: Tolerations to include with the OneAgent DaemonSet. + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple... + properties: + effect: + description: Effect indicates the taint effect to match. + Empty means match all taint effects. + type: string + key: + description: Key is the taint key that the toleration + applies to. Empty means match all taint keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute,... + format: int64 + type: integer + value: + description: Value is the taint value the toleration + matches to. + type: string + type: object + type: array + version: + description: Use a specific OneAgent version. Defaults to + the latest version from the Dynatrace cluster. + type: string + type: object + cloudNativeFullStack: + description: Has a single OneAgent per node via DaemonSet. + nullable: true + properties: + annotations: + additionalProperties: + type: string + description: Add custom OneAgent annotations. + type: object + args: + description: Set additional arguments to the OneAgent installer. + items: + type: string + type: array + x-kubernetes-list-type: set + autoUpdate: + description: Disables automatic restarts of OneAgent pods + in case a new version is available (https://www. + type: boolean + codeModulesImage: + description: Use a custom OneAgent CodeModule image to download + binaries. + type: string + dnsPolicy: + description: Set the DNS Policy for OneAgent pods. For details, + see Pods DNS Policy (https://kubernetes. + type: string + env: + description: Set additional environment variables for the + OneAgent pods. + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must + be a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in... + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: Name of the referent. + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in + the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of + the exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: Name of the referent. + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + description: Use a custom OneAgent image. Defaults to the + latest image from the Dynatrace cluster. + type: string + initResources: + description: Define resources requests and limits for the + initContainer. + properties: + claims: + description: Claims lists the names of resources, defined + in spec. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: Name must match the name of one entry + in pod.spec. + type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount of + compute resources required. + type: object + type: object + labels: + additionalProperties: + type: string + description: Your defined labels for OneAgent pods in order + to structure workloads as desired. + type: object + namespaceSelector: + description: Applicable only for applicationMonitoring or + cloudNativeFullStack configuration types. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that... + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. + type: string + values: + description: values is an array of string values. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + nodeSelector: + additionalProperties: + type: string + description: Specify the node selector that controls on which + nodes OneAgent will be deployed. + type: object + oneAgentResources: + description: Resource settings for OneAgent container. + properties: + claims: + description: Claims lists the names of resources, defined + in spec. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: Name must match the name of one entry + in pod.spec. + type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount of + compute resources required. + type: object + type: object + priorityClassName: + description: Assign a priority class to the OneAgent pods. + By default, no class is set. + type: string + secCompProfile: + description: The SecComp Profile that will be configured in + order to run in secure computing mode. + type: string + tolerations: + description: Tolerations to include with the OneAgent DaemonSet. + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple... + properties: + effect: + description: Effect indicates the taint effect to match. + Empty means match all taint effects. + type: string + key: + description: Key is the taint key that the toleration + applies to. Empty means match all taint keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute,... + format: int64 + type: integer + value: + description: Value is the taint value the toleration + matches to. + type: string + type: object + type: array + version: + description: Use a specific OneAgent version. Defaults to + the latest version from the Dynatrace cluster. + type: string + type: object + hostGroup: + description: Sets a host group for OneAgent. + type: string + hostMonitoring: + description: |- + Has a single OneAgent per node via DaemonSet. + Doesn't inject into application pods. + nullable: true + properties: + annotations: + additionalProperties: + type: string + description: Add custom OneAgent annotations. + type: object + args: + description: Set additional arguments to the OneAgent installer. + items: + type: string + type: array + x-kubernetes-list-type: set + autoUpdate: + description: Disables automatic restarts of OneAgent pods + in case a new version is available (https://www. + type: boolean + dnsPolicy: + description: Set the DNS Policy for OneAgent pods. For details, + see Pods DNS Policy (https://kubernetes. + type: string + env: + description: Set additional environment variables for the + OneAgent pods. + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must + be a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in... + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: Name of the referent. + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in + the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of + the exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: Name of the referent. + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + description: Use a custom OneAgent image. Defaults to the + latest image from the Dynatrace cluster. + type: string + labels: + additionalProperties: + type: string + description: Your defined labels for OneAgent pods in order + to structure workloads as desired. + type: object + nodeSelector: + additionalProperties: + type: string + description: Specify the node selector that controls on which + nodes OneAgent will be deployed. + type: object + oneAgentResources: + description: Resource settings for OneAgent container. + properties: + claims: + description: Claims lists the names of resources, defined + in spec. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: Name must match the name of one entry + in pod.spec. + type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount of + compute resources required. + type: object + type: object + priorityClassName: + description: Assign a priority class to the OneAgent pods. + By default, no class is set. + type: string + secCompProfile: + description: The SecComp Profile that will be configured in + order to run in secure computing mode. + type: string + tolerations: + description: Tolerations to include with the OneAgent DaemonSet. + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple... + properties: + effect: + description: Effect indicates the taint effect to match. + Empty means match all taint effects. + type: string + key: + description: Key is the taint key that the toleration + applies to. Empty means match all taint keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute,... + format: int64 + type: integer + value: + description: Value is the taint value the toleration + matches to. + type: string + type: object + type: array + version: + description: Use a specific OneAgent version. Defaults to + the latest version from the Dynatrace cluster. + type: string + type: object + type: object + proxy: + description: Set custom proxy settings either directly or from a secret + with the field proxy. + properties: + value: + description: Raw value for given property. + nullable: true + type: string + valueFrom: + description: Name of the secret to get the property from. + nullable: true + type: string + type: object + skipCertCheck: + description: Disable certificate check for the connection between + Dynatrace Operator and the Dynatrace Cluster. + type: boolean + templates: + properties: + extensionExecutionController: + properties: + annotations: + additionalProperties: + type: string + description: Adds additional annotations to the ExtensionExecutionController + pods + type: object + customConfig: + description: Defines name of ConfigMap containing custom configuration + file + type: string + customExtensionCertificates: + description: Defines name of Secret containing certificates + for custom extensions signature validation + type: string + imageRef: + description: Overrides the default image + properties: + repository: + description: Custom image repository + example: docker.io/dynatrace/image-name + type: string + tag: + description: Indicates a tag of the image to use + type: string + type: object + labels: + additionalProperties: + type: string + description: Adds additional labels for the ExtensionExecutionController + pods + type: object + persistentVolumeClaim: + description: Defines storage device + properties: + accessModes: + description: |- + accessModes contains the desired access modes the volume should have. + More info: https://kubernetes. + items: + type: string + type: array + x-kubernetes-list-type: atomic + dataSource: + description: |- + dataSource field can be used to specify either: + * An existing VolumeSnapshot object (snapshot. + properties: + apiGroup: + description: APIGroup is the group for the resource + being referenced. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + description: dataSourceRef specifies the object from which + to populate the volume with data, if a non-empty... + properties: + apiGroup: + description: APIGroup is the group for the resource + being referenced. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + namespace: + description: |- + Namespace is the namespace of resource being referenced + Note that when a namespace is specified, a... + type: string + required: + - kind + - name + type: object + resources: + description: resources represents the minimum resources + the volume should have. + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount + of compute resources required. + type: object + type: object + selector: + description: selector is a label query over volumes to + consider for binding. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that... + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. + type: string + values: + description: values is an array of string values. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + description: storageClassName is the name of the StorageClass + required by the claim. + type: string + volumeAttributesClassName: + description: volumeAttributesClassName may be used to + set the VolumeAttributesClass used by this claim. + type: string + volumeMode: + description: volumeMode defines what type of volume is + required by the claim. + type: string + volumeName: + description: volumeName is the binding reference to the + PersistentVolume backing this claim. + type: string + type: object + resources: + description: Define resources' requests and limits for single + ExtensionExecutionController pod + properties: + claims: + description: Claims lists the names of resources, defined + in spec. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: Name must match the name of one entry + in pod.spec. + type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount of + compute resources required. + type: object + type: object + tlsRefName: + type: string + tolerations: + description: Set tolerations for the ExtensionExecutionController + pods + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple... + properties: + effect: + description: Effect indicates the taint effect to match. + Empty means match all taint effects. + type: string + key: + description: Key is the taint key that the toleration + applies to. Empty means match all taint keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute,... + format: int64 + type: integer + value: + description: Value is the taint value the toleration + matches to. + type: string + type: object + type: array + topologySpreadConstraints: + description: Adds TopologySpreadConstraints for the ExtensionExecutionController + pods + items: + description: TopologySpreadConstraint specifies how to spread + matching pods among the given topology. + properties: + labelSelector: + description: LabelSelector is used to find matching + pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: A label selector requirement is a + selector that contains values, a key, and an + operator that... + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. + type: string + values: + description: values is an array of string + values. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select the pods over which + spreading will be... + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + description: MaxSkew describes the degree to which pods + may be unevenly distributed. + format: int32 + type: integer + minDomains: + description: MinDomains indicates a minimum number of + eligible domains. + format: int32 + type: integer + nodeAffinityPolicy: + description: |- + NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector + when calculating pod... + type: string + nodeTaintsPolicy: + description: |- + NodeTaintsPolicy indicates how we will treat node taints when calculating + pod topology spread skew. + type: string + topologyKey: + description: TopologyKey is the key of node labels. + type: string + whenUnsatisfiable: + description: |- + WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy + the spread constraint. + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + useEphemeralVolume: + description: Selects EmptyDir volume to be storage device + type: boolean + type: object + kspmNodeConfigurationCollector: + properties: + annotations: + additionalProperties: + type: string + description: Adds additional annotations for the NodeConfigurationCollector + pods + type: object + args: + description: Set additional arguments to the NodeConfigurationCollector + pods + items: + type: string + type: array + env: + description: Set additional environment variables for the + NodeConfigurationCollector pods + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must + be a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in... + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: Name of the referent. + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in + the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of + the exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: Name of the referent. + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + imageRef: + description: Overrides the default image + properties: + repository: + description: Custom image repository + example: docker.io/dynatrace/image-name + type: string + tag: + description: Indicates a tag of the image to use + type: string + type: object + labels: + additionalProperties: + type: string + description: Adds additional labels for the NodeConfigurationCollector + pods + type: object + nodeAffinity: + description: Define the nodeAffinity for the DaemonSet of + the NodeConfigurationCollector + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified... + items: + description: |- + An empty preferred scheduling term matches all objects with implicit weight 0 + (i.e. it's a no-op). + properties: + preference: + description: A node selector term, associated with + the corresponding weight. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates... + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: Represents a key's relationship + to a set of values. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates... + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: Represents a key's relationship + to a set of values. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + description: Weight associated with matching the + corresponding nodeSelectorTerm, in the range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will... + properties: + nodeSelectorTerms: + description: Required. A list of node selector terms. + The terms are ORed. + items: + description: |- + A null or empty node selector term matches no objects. The requirements of + them are ANDed. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates... + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: Represents a key's relationship + to a set of values. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates... + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: Represents a key's relationship + to a set of values. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + nodeSelector: + additionalProperties: + type: string + description: Specify the node selector that controls on which + nodes NodeConfigurationCollector pods will be... + type: object + priorityClassName: + description: If specified, indicates the pod's priority. + type: string + resources: + description: Define resources' requests and limits for single + NodeConfigurationCollector pod + properties: + claims: + description: Claims lists the names of resources, defined + in spec. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: Name must match the name of one entry + in pod.spec. + type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount of + compute resources required. + type: object + type: object + tolerations: + description: Set tolerations for the NodeConfigurationCollector + pods + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple... + properties: + effect: + description: Effect indicates the taint effect to match. + Empty means match all taint effects. + type: string + key: + description: Key is the taint key that the toleration + applies to. Empty means match all taint keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute,... + format: int64 + type: integer + value: + description: Value is the taint value the toleration + matches to. + type: string + type: object + type: array + updateStrategy: + description: Define the NodeConfigurationCollector daemonSet + updateStrategy + properties: + rollingUpdate: + description: Rolling update config params. Present only + if type = "RollingUpdate". + properties: + maxSurge: + anyOf: + - type: integer + - type: string + description: |- + The maximum number of nodes with an existing available DaemonSet pod that + can have an updated... + x-kubernetes-int-or-string: true + maxUnavailable: + anyOf: + - type: integer + - type: string + description: |- + The maximum number of DaemonSet pods that can be unavailable during the + update. + x-kubernetes-int-or-string: true + type: object + type: + description: Type of daemon set update. Can be "RollingUpdate" + or "OnDelete". Default is RollingUpdate. + type: string + type: object + type: object + logMonitoring: + description: Low-level configuration options for the LogMonitoring + feature. + properties: + annotations: + additionalProperties: + type: string + description: Add custom annotations to the LogMonitoring pods + type: object + args: + description: Set additional arguments to the LogMonitoring + main container + items: + type: string + type: array + dnsPolicy: + description: Sets DNS Policy for the LogMonitoring pods + type: string + imageRef: + description: Overrides the default image for the LogMonitoring + pods + properties: + repository: + description: Custom image repository + example: docker.io/dynatrace/image-name + type: string + tag: + description: Indicates a tag of the image to use + type: string + type: object + labels: + additionalProperties: + type: string + description: Add custom labels to the LogMonitoring pods + type: object + nodeSelector: + additionalProperties: + type: string + description: Node selector to control the selection of nodes + for the LogMonitoring pods + type: object + priorityClassName: + description: Assign a priority class to the LogMonitoring + pods. By default, no class is set + type: string + resources: + description: Define resources' requests and limits for all + the LogMonitoring pods + properties: + claims: + description: Claims lists the names of resources, defined + in spec. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: Name must match the name of one entry + in pod.spec. + type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount of + compute resources required. + type: object + type: object + secCompProfile: + description: The SecComp Profile that will be configured in + order to run in secure computing mode for the... + type: string + tolerations: + description: Set tolerations for the LogMonitoring pods + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple... + properties: + effect: + description: Effect indicates the taint effect to match. + Empty means match all taint effects. + type: string + key: + description: Key is the taint key that the toleration + applies to. Empty means match all taint keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute,... + format: int64 + type: integer + value: + description: Value is the taint value the toleration + matches to. + type: string + type: object + type: array + type: object + otelCollector: + properties: + annotations: + additionalProperties: + type: string + description: Adds additional annotations to the OtelCollector + pods + type: object + imageRef: + description: Overrides the default image + properties: + repository: + description: Custom image repository + example: docker.io/dynatrace/image-name + type: string + tag: + description: Indicates a tag of the image to use + type: string + type: object + labels: + additionalProperties: + type: string + description: Adds additional labels for the OtelCollector + pods + type: object + replicas: + description: Number of replicas for your OtelCollector + format: int32 + type: integer + resources: + description: Define resources' requests and limits for single + OtelCollector pod + properties: + claims: + description: Claims lists the names of resources, defined + in spec. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: Name must match the name of one entry + in pod.spec. + type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount of + compute resources required. + type: object + type: object + tlsRefName: + type: string + tolerations: + description: Set tolerations for the OtelCollector pods + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple... + properties: + effect: + description: Effect indicates the taint effect to match. + Empty means match all taint effects. + type: string + key: + description: Key is the taint key that the toleration + applies to. Empty means match all taint keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute,... + format: int64 + type: integer + value: + description: Value is the taint value the toleration + matches to. + type: string + type: object + type: array + topologySpreadConstraints: + description: Adds TopologySpreadConstraints for the OtelCollector + pods + items: + description: TopologySpreadConstraint specifies how to spread + matching pods among the given topology. + properties: + labelSelector: + description: LabelSelector is used to find matching + pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: A label selector requirement is a + selector that contains values, a key, and an + operator that... + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. + type: string + values: + description: values is an array of string + values. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select the pods over which + spreading will be... + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + description: MaxSkew describes the degree to which pods + may be unevenly distributed. + format: int32 + type: integer + minDomains: + description: MinDomains indicates a minimum number of + eligible domains. + format: int32 + type: integer + nodeAffinityPolicy: + description: |- + NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector + when calculating pod... + type: string + nodeTaintsPolicy: + description: |- + NodeTaintsPolicy indicates how we will treat node taints when calculating + pod topology spread skew. + type: string + topologyKey: + description: TopologyKey is the key of node labels. + type: string + whenUnsatisfiable: + description: |- + WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy + the spread constraint. + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + type: object + type: object + tokens: + description: Name of the secret holding the tokens used for connecting + to Dynatrace. + type: string + trustedCAs: + description: Adds custom RootCAs from a configmap. Put the certificate + under certs within your configmap. + type: string + required: + - apiUrl + type: object + status: + description: DynaKubeStatus defines the observed state of DynaKube + properties: + activeGate: + description: Observed state of ActiveGate + properties: + connectionInfoStatus: + description: Information about Active Gate's connections + properties: + endpoints: + description: Available connection endpoints + type: string + lastRequest: + description: Time of the last connection request + format: date-time + type: string + tenantTokenHash: + description: Hash of the tenant token + type: string + tenantUUID: + description: UUID of the tenant, received from the tenant + type: string + type: object + imageID: + description: Image ID + type: string + lastProbeTimestamp: + description: Indicates when the last check for a new version was + performed + format: date-time + type: string + serviceIPs: + description: The ClusterIPs set by Kubernetes on the ActiveGate + Service created by the Operator + items: + type: string + type: array + source: + description: Source of the image (tenant-registry, public-registry, + ...) + type: string + type: + description: Image type + type: string + version: + description: Image version + type: string + type: object + codeModules: + description: Observed state of Code Modules + properties: + imageID: + description: Image ID + type: string + lastProbeTimestamp: + description: Indicates when the last check for a new version was + performed + format: date-time + type: string + source: + description: Source of the image (tenant-registry, public-registry, + ...) + type: string + type: + description: Image type + type: string + version: + description: Image version + type: string + type: object + conditions: + description: Conditions includes status about the current state of + the instance + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + dynatraceApi: + description: Observed state of Dynatrace API + properties: + lastTokenScopeRequest: + description: Time of the last token request + format: date-time + type: string + type: object + kspm: + description: Observed state of Kspm + properties: + tokenSecretHash: + description: TokenSecretHash contains the hash of the token that + is passed to both the ActiveGate and... + type: string + type: object + kubeSystemUUID: + description: KubeSystemUUID contains the UUID of the current Kubernetes + cluster + type: string + kubernetesClusterMEID: + description: KubernetesClusterMEID contains the ID of the monitored + entity that points to the Kubernetes cluster + type: string + kubernetesClusterName: + description: KubernetesClusterName contains the display name (also + know as label) of the monitored entity that... + type: string + metadataEnrichment: + description: Observed state of Metadata-Enrichment + properties: + rules: + items: + properties: + enabled: + type: boolean + source: + type: string + target: + type: string + type: + type: string + type: object + type: array + type: object + oneAgent: + description: Observed state of OneAgent + properties: + connectionInfoStatus: + description: Information about OneAgent's connections + properties: + communicationHosts: + description: List of communication hosts + items: + properties: + host: + description: Host domain + type: string + port: + description: Connection port + format: int32 + type: integer + protocol: + description: Connection protocol + type: string + type: object + type: array + endpoints: + description: Available connection endpoints + type: string + lastRequest: + description: Time of the last connection request + format: date-time + type: string + tenantTokenHash: + description: Hash of the tenant token + type: string + tenantUUID: + description: UUID of the tenant, received from the tenant + type: string + type: object + healthcheck: + description: Commands used for OneAgent's readiness probe + type: object + x-kubernetes-preserve-unknown-fields: true + imageID: + description: Image ID + type: string + instances: + additionalProperties: + properties: + ipAddress: + description: IP address of the pod + type: string + podName: + description: Name of the OneAgent pod + type: string + type: object + description: List of deployed OneAgent instances + type: object + lastInstanceStatusUpdate: + description: Time of the last instance status update + format: date-time + type: string + lastProbeTimestamp: + description: Indicates when the last check for a new version was + performed + format: date-time + type: string + source: + description: Source of the image (tenant-registry, public-registry, + ...) + type: string + type: + description: Image type + type: string + version: + description: Image version + type: string + type: object + phase: + description: Defines the current state (Running, Updating, Error, + ...) + type: string + updatedTimestamp: + description: UpdatedTimestamp indicates when the instance was last + updated + format: date-time + type: string + type: object + type: object + served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .spec.apiUrl + name: ApiUrl + type: string + - jsonPath: .status.phase + name: Status + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta4 + schema: + openAPIV3Schema: + description: DynaKube is the Schema for the DynaKube API + properties: + apiVersion: + description: APIVersion defines the versioned schema of this representation + of an object. + type: string + kind: + description: Kind is a string value representing the REST resource this + object represents. + type: string + metadata: + type: object + spec: + description: DynaKubeSpec defines the desired state of DynaKube + properties: + activeGate: + description: General configuration about ActiveGate instances. + properties: + annotations: + additionalProperties: + type: string + description: Adds additional annotations to the ActiveGate pods + type: object + capabilities: + description: Activegate capabilities enabled (routing, kubernetes-monitoring, + metrics-ingest, dynatrace-api) + items: + type: string + type: array + customProperties: + description: |- + Add a custom properties file by providing it as a value or reference it from a secret + If referenced... + properties: + value: + description: Raw value for given property. + nullable: true + type: string + valueFrom: + description: Name of the secret to get the property from. + nullable: true + type: string + type: object + dnsPolicy: + description: Sets DNS Policy for the ActiveGate pods + type: string + env: + description: List of environment variables to set for the ActiveGate + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must be a + C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in... + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: Name of the referent. + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports metadata.name, + metadata.namespace, `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: Name of the referent. + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + group: + description: Set activation group for ActiveGate + type: string + image: + description: The ActiveGate container image. + type: string + labels: + additionalProperties: + type: string + description: Adds additional labels for the ActiveGate pods + type: object + nodeSelector: + additionalProperties: + type: string + description: Node selector to control the selection of nodes + type: object + persistentVolumeClaim: + description: Defines storage device + properties: + accessModes: + description: |- + accessModes contains the desired access modes the volume should have. + More info: https://kubernetes. + items: + type: string + type: array + x-kubernetes-list-type: atomic + dataSource: + description: |- + dataSource field can be used to specify either: + * An existing VolumeSnapshot object (snapshot. + properties: + apiGroup: + description: APIGroup is the group for the resource being + referenced. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + description: dataSourceRef specifies the object from which + to populate the volume with data, if a non-empty... + properties: + apiGroup: + description: APIGroup is the group for the resource being + referenced. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + namespace: + description: |- + Namespace is the namespace of resource being referenced + Note that when a namespace is specified, a... + type: string + required: + - kind + - name + type: object + resources: + description: resources represents the minimum resources the + volume should have. + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount of + compute resources required. + type: object type: object - requests: + selector: + description: selector is a label query over volumes to consider + for binding. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that... + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. + type: string + values: + description: values is an array of string values. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + description: storageClassName is the name of the StorageClass + required by the claim. + type: string + volumeAttributesClassName: + description: volumeAttributesClassName may be used to set + the VolumeAttributesClass used by this claim. + type: string + volumeMode: + description: volumeMode defines what type of volume is required + by the claim. + type: string + volumeName: + description: volumeName is the binding reference to the PersistentVolume + backing this claim. + type: string + type: object + priorityClassName: + description: If specified, indicates the pod's priority. + type: string + replicas: + description: Amount of replicas for your ActiveGates + format: int32 + type: integer + resources: + description: Define resources requests and limits for single ActiveGate + pods + properties: + claims: + description: Claims lists the names of resources, defined + in spec. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: Name must match the name of one entry in + pod.spec. + type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: additionalProperties: anyOf: - type: integer @@ -4607,53 +6778,58 @@ spec: pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. + Limits describes the maximum amount of compute resources allowed. More info: https://kubernetes. type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount of compute + resources required. + type: object type: object + terminationGracePeriodSeconds: + description: Configures the terminationGracePeriodSeconds parameter + of the ActiveGate pod. + format: int64 + type: integer tlsSecretName: - description: |- - The name of a secret containing ActiveGate TLS cert+key and password. If not set, self-signed certificate is used. - server.p12: certificate+key pair in pkcs12 format - password: passphrase to read server.p12 + description: The name of a secret containing ActiveGate TLS cert+key + and password. type: string tolerations: description: Set tolerations for the ActiveGate pods items: description: |- The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . + the triple... properties: effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + description: Effect indicates the taint effect to match. + Empty means match all taint effects. type: string key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. + description: Key is the taint key that the toleration applies + to. Empty means match all taint keys. type: string operator: description: |- Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. + Valid operators are Exists and Equal. type: string tolerationSeconds: description: |- TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). + of effect NoExecute,... format: int64 type: integer value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. + description: Value is the taint value the toleration matches + to. type: string type: object type: array @@ -4665,34 +6841,25 @@ spec: matching pods among the given topology. properties: labelSelector: - description: |- - LabelSelector is used to find matching pods. - Pods that match this label selector are counted to determine the number of pods - in their corresponding topology domain. + description: LabelSelector is used to find matching pods. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. + description: A label selector requirement is a selector + that contains values, a key, and an operator that... properties: key: description: key is the label key that the selector applies to. type: string operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. + description: operator represents a key's relationship + to a set of values. type: string values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. + description: values is an array of string values. items: type: string type: array @@ -4706,62 +6873,45 @@ spec: matchLabels: additionalProperties: type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. + description: matchLabels is a map of {key,value} pairs. type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: description: |- MatchLabelKeys is a set of pod label keys to select the pods over which - spreading will be calculated. + spreading will be... items: type: string type: array x-kubernetes-list-type: atomic maxSkew: - description: |- - MaxSkew describes the degree to which pods may be unevenly distributed. - When `whenUnsatisfiable=DoNotSchedule`, it is the maximum permitted difference - between the number of matching pods in the target topology and the global minimum. + description: MaxSkew describes the degree to which pods + may be unevenly distributed. format: int32 type: integer minDomains: - description: |- - MinDomains indicates a minimum number of eligible domains. - When the number of eligible domains with matching topology keys is less than minDomains, - Pod Topology Spread treats "global minimum" as 0, and then the calculation of Skew is performed. + description: MinDomains indicates a minimum number of eligible + domains. format: int32 type: integer nodeAffinityPolicy: description: |- NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector - when calculating pod topology spread skew. Options are: - - Honor: only nodes matching nodeAffinity/nodeSelector are included in the calculations. - - Ignore: nodeAffinity/nodeSelector are ignored. + when calculating pod... type: string nodeTaintsPolicy: description: |- NodeTaintsPolicy indicates how we will treat node taints when calculating - pod topology spread skew. Options are: - - Honor: nodes without taints, along with tainted nodes for which the incoming pod - has a toleration, are included. - - Ignore: node taints are ignored. All nodes are included. + pod topology spread skew. type: string topologyKey: - description: |- - TopologyKey is the key of node labels. Nodes that have a label with this key - and identical values are considered to be in the same topology. - We consider each as a "bucket", and try to put balanced number - of pods into each bucket. + description: TopologyKey is the key of node labels. type: string whenUnsatisfiable: description: |- WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy the spread constraint. - - DoNotSchedule (default) tells the scheduler not to schedule it. type: string required: - maxSkew @@ -4769,32 +6919,28 @@ spec: - whenUnsatisfiable type: object type: array + useEphemeralVolume: + description: UseEphemeralVolume + type: boolean type: object apiUrl: - description: |- - Dynatrace apiUrl, including the /api path at the end. For SaaS, set YOUR_ENVIRONMENT_ID to your environment ID. For Managed, change the apiUrl address. - For instructions on how to determine the environment ID and how to configure the apiUrl address, see Environment ID (https://www.dynatrace. + description: Dynatrace apiUrl, including the /api path at the end. maxLength: 128 type: string customPullSecret: - description: |- - Defines a custom pull secret in case you use a private registry when pulling images from the Dynatrace environment. - To define a custom pull secret and learn about the expected behavior, see Configure customPullSecret - (https://www.dynatrace. + description: Defines a custom pull secret in case you use a private + registry when pulling images from the... type: string dynatraceApiRequestThreshold: description: Configuration for thresholding Dynatrace API requests. type: integer enableIstio: - description: |- - When enabled, and if Istio is installed on the Kubernetes environment, Dynatrace Operator will create the corresponding - VirtualService and ServiceEntry objects to allow access to the Dynatrace Cluster from the OneAgent or ActiveGate. - Disabled by default. + description: When enabled, and if Istio is installed on the Kubernetes + environment, Dynatrace Operator will... type: boolean extensions: - description: |- - When an (empty) ExtensionsSpec is provided, the extensions related components (extensions controller and extensions collector) - are deployed by the operator. + description: When an (empty) ExtensionsSpec is provided, the extensions + related components (extensions... type: object kspm: description: General configuration about the KSPM feature. @@ -4828,25 +6974,19 @@ spec: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. + description: A label selector requirement is a selector + that contains values, a key, and an operator that... properties: key: description: key is the label key that the selector applies to. type: string operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. + description: operator represents a key's relationship + to a set of values. type: string values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. + description: values is an array of string values. items: type: string type: array @@ -4860,10 +7000,7 @@ spec: matchLabels: additionalProperties: type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. + description: matchLabels is a map of {key,value} pairs. type: object type: object x-kubernetes-map-type: atomic @@ -4872,14 +7009,11 @@ spec: description: Sets a network zone for the OneAgent and ActiveGate pods. type: string oneAgent: - description: |- - General configuration about OneAgent instances. - You can't enable more than one module (classicFullStack, cloudNativeFullStack, hostMonitoring, or applicationMonitoring). + description: General configuration about OneAgent instances. properties: applicationMonitoring: - description: |- - dynatrace-webhook injects into application pods based on labeled namespaces. - Has an optional CSI driver per node via DaemonSet to provide binaries to pods. + description: dynatrace-webhook injects into application pods based + on labeled namespaces. nullable: true properties: codeModulesImage: @@ -4887,33 +7021,22 @@ spec: binaries. type: string initResources: - description: |- - Define resources requests and limits for the initContainer. For details, see Managing resources for containers - (https://kubernetes.io/docs/concepts/configuration/manage-resources-containers). + description: Define resources requests and limits for the + initContainer. properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry + in pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -4931,7 +7054,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -4940,41 +7063,31 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of + compute resources required. type: object type: object namespaceSelector: - description: |- - Applicable only for applicationMonitoring or cloudNativeFullStack configuration types. The namespaces where you want Dynatrace Operator to inject. - For more information, see Configure monitoring for namespaces and pods (https://www.dynatrace. + description: Applicable only for applicationMonitoring or + cloudNativeFullStack configuration types. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. + description: A label selector requirement is a selector + that contains values, a key, and an operator that... properties: key: description: key is the label key that the selector applies to. type: string operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. + description: operator represents a key's relationship + to a set of values. type: string values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. + description: values is an array of string values. items: type: string type: array @@ -4988,22 +7101,16 @@ spec: matchLabels: additionalProperties: type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. + description: matchLabels is a map of {key,value} pairs. type: object type: object x-kubernetes-map-type: atomic version: - description: Use a specific OneAgent CodeModule version. Defaults - to the latest version from the Dynatrace cluster. + description: Use a specific OneAgent CodeModule version. type: string type: object classicFullStack: - description: |- - Has a single OneAgent per node via DaemonSet. - Injection is performed via the same OneAgent DaemonSet. + description: Has a single OneAgent per node via DaemonSet. nullable: true properties: annotations: @@ -5012,21 +7119,18 @@ spec: description: Add custom OneAgent annotations. type: object args: - description: |- - Set additional arguments to the OneAgent installer. - For available options, see Linux custom installation (https://www.dynatrace.com/support/help/setup-and-configuration/dynatrace-oneagent/installation-and-operation/linux/installation/customize-oneagent-installation-on-linux). + description: Set additional arguments to the OneAgent installer. items: type: string type: array x-kubernetes-list-type: set autoUpdate: - description: |- - Disables automatic restarts of OneAgent pods in case a new version is available (https://www.dynatrace.com/support/help/setup-and-configuration/setup-on-container-platforms/kubernetes/get-started-with-kubernetes-monitoring#disable-auto). - Enabled by default. + description: Disables automatic restarts of OneAgent pods + in case a new version is available (https://www. type: boolean dnsPolicy: description: Set the DNS Policy for OneAgent pods. For details, - see Pods DNS Policy (https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-s-dns-policy). + see Pods DNS Policy (https://kubernetes. type: string env: description: Set additional environment variables for the @@ -5042,9 +7146,7 @@ spec: value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. + using the previously defined environment variables in... type: string valueFrom: description: Source for the environment variable's value. @@ -5058,12 +7160,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the ConfigMap or @@ -5074,9 +7171,8 @@ spec: type: object x-kubernetes-map-type: atomic fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.' properties: apiVersion: description: Version of the schema the FieldPath @@ -5093,7 +7189,7 @@ spec: resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + (limits.cpu, limits. properties: containerName: description: 'Container name: required for volumes, @@ -5124,12 +7220,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the Secret or its @@ -5161,33 +7252,21 @@ spec: nodes OneAgent will be deployed. type: object oneAgentResources: - description: |- - Resource settings for OneAgent container. Consumption of the OneAgent heavily depends on the workload to monitor. You can use the default settings in the CR. - Note: resource.requests shows the values needed to run; resource.limits shows the maximum limits for the pod. + description: Resource settings for OneAgent container. properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry + in pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -5205,7 +7284,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -5214,58 +7293,51 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of + compute resources required. type: object type: object priorityClassName: - description: |- - Assign a priority class to the OneAgent pods. By default, no class is set. - For details, see Pod Priority and Preemption (https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/). + description: Assign a priority class to the OneAgent pods. + By default, no class is set. type: string secCompProfile: description: The SecComp Profile that will be configured in order to run in secure computing mode. type: string + storageHostPath: + description: StorageHostPath is the writable directory on + the host filesystem where OneAgent configurations will... + type: string tolerations: description: Tolerations to include with the OneAgent DaemonSet. - For details, see Taints and Tolerations (https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/). items: description: |- The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . + the triple... properties: effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + description: Effect indicates the taint effect to match. + Empty means match all taint effects. type: string key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. + description: Key is the taint key that the toleration + applies to. Empty means match all taint keys. type: string operator: description: |- Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. + Valid operators are Exists and Equal. type: string tolerationSeconds: description: |- TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). + of effect NoExecute,... format: int64 type: integer value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. + description: Value is the taint value the toleration + matches to. type: string type: object type: array @@ -5275,10 +7347,7 @@ spec: type: string type: object cloudNativeFullStack: - description: |- - Has a single OneAgent per node via DaemonSet. - dynatrace-webhook injects into application pods based on labeled namespaces. - Has a CSI driver per node via DaemonSet to provide binaries to pods. + description: Has a single OneAgent per node via DaemonSet. nullable: true properties: annotations: @@ -5287,17 +7356,14 @@ spec: description: Add custom OneAgent annotations. type: object args: - description: |- - Set additional arguments to the OneAgent installer. - For available options, see Linux custom installation (https://www.dynatrace.com/support/help/setup-and-configuration/dynatrace-oneagent/installation-and-operation/linux/installation/customize-oneagent-installation-on-linux). + description: Set additional arguments to the OneAgent installer. items: type: string type: array x-kubernetes-list-type: set autoUpdate: - description: |- - Disables automatic restarts of OneAgent pods in case a new version is available (https://www.dynatrace.com/support/help/setup-and-configuration/setup-on-container-platforms/kubernetes/get-started-with-kubernetes-monitoring#disable-auto). - Enabled by default. + description: Disables automatic restarts of OneAgent pods + in case a new version is available (https://www. type: boolean codeModulesImage: description: Use a custom OneAgent CodeModule image to download @@ -5305,7 +7371,7 @@ spec: type: string dnsPolicy: description: Set the DNS Policy for OneAgent pods. For details, - see Pods DNS Policy (https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-s-dns-policy). + see Pods DNS Policy (https://kubernetes. type: string env: description: Set additional environment variables for the @@ -5321,9 +7387,7 @@ spec: value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. + using the previously defined environment variables in... type: string valueFrom: description: Source for the environment variable's value. @@ -5337,12 +7401,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the ConfigMap or @@ -5353,9 +7412,8 @@ spec: type: object x-kubernetes-map-type: atomic fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.' properties: apiVersion: description: Version of the schema the FieldPath @@ -5372,7 +7430,7 @@ spec: resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + (limits.cpu, limits. properties: containerName: description: 'Container name: required for volumes, @@ -5403,12 +7461,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the Secret or its @@ -5428,33 +7481,22 @@ spec: latest image from the Dynatrace cluster. type: string initResources: - description: |- - Define resources requests and limits for the initContainer. For details, see Managing resources for containers - (https://kubernetes.io/docs/concepts/configuration/manage-resources-containers). + description: Define resources requests and limits for the + initContainer. properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry + in pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -5472,7 +7514,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -5481,11 +7523,8 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of + compute resources required. type: object type: object labels: @@ -5495,33 +7534,26 @@ spec: to structure workloads as desired. type: object namespaceSelector: - description: |- - Applicable only for applicationMonitoring or cloudNativeFullStack configuration types. The namespaces where you want Dynatrace Operator to inject. - For more information, see Configure monitoring for namespaces and pods (https://www.dynatrace. + description: Applicable only for applicationMonitoring or + cloudNativeFullStack configuration types. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. + description: A label selector requirement is a selector + that contains values, a key, and an operator that... properties: key: description: key is the label key that the selector applies to. type: string operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. + description: operator represents a key's relationship + to a set of values. type: string values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. + description: values is an array of string values. items: type: string type: array @@ -5535,10 +7567,7 @@ spec: matchLabels: additionalProperties: type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. + description: matchLabels is a map of {key,value} pairs. type: object type: object x-kubernetes-map-type: atomic @@ -5549,33 +7578,21 @@ spec: nodes OneAgent will be deployed. type: object oneAgentResources: - description: |- - Resource settings for OneAgent container. Consumption of the OneAgent heavily depends on the workload to monitor. You can use the default settings in the CR. - Note: resource.requests shows the values needed to run; resource.limits shows the maximum limits for the pod. + description: Resource settings for OneAgent container. properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry + in pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -5593,7 +7610,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -5602,58 +7619,51 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of + compute resources required. type: object type: object priorityClassName: - description: |- - Assign a priority class to the OneAgent pods. By default, no class is set. - For details, see Pod Priority and Preemption (https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/). + description: Assign a priority class to the OneAgent pods. + By default, no class is set. type: string secCompProfile: description: The SecComp Profile that will be configured in order to run in secure computing mode. type: string + storageHostPath: + description: StorageHostPath is the writable directory on + the host filesystem where OneAgent configurations will... + type: string tolerations: description: Tolerations to include with the OneAgent DaemonSet. - For details, see Taints and Tolerations (https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/). items: description: |- The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . + the triple... properties: effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + description: Effect indicates the taint effect to match. + Empty means match all taint effects. type: string key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. + description: Key is the taint key that the toleration + applies to. Empty means match all taint keys. type: string operator: description: |- Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. + Valid operators are Exists and Equal. type: string tolerationSeconds: description: |- TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). + of effect NoExecute,... format: int64 type: integer value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. + description: Value is the taint value the toleration + matches to. type: string type: object type: array @@ -5677,21 +7687,18 @@ spec: description: Add custom OneAgent annotations. type: object args: - description: |- - Set additional arguments to the OneAgent installer. - For available options, see Linux custom installation (https://www.dynatrace.com/support/help/setup-and-configuration/dynatrace-oneagent/installation-and-operation/linux/installation/customize-oneagent-installation-on-linux). + description: Set additional arguments to the OneAgent installer. items: type: string type: array x-kubernetes-list-type: set autoUpdate: - description: |- - Disables automatic restarts of OneAgent pods in case a new version is available (https://www.dynatrace.com/support/help/setup-and-configuration/setup-on-container-platforms/kubernetes/get-started-with-kubernetes-monitoring#disable-auto). - Enabled by default. + description: Disables automatic restarts of OneAgent pods + in case a new version is available (https://www. type: boolean dnsPolicy: description: Set the DNS Policy for OneAgent pods. For details, - see Pods DNS Policy (https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-s-dns-policy). + see Pods DNS Policy (https://kubernetes. type: string env: description: Set additional environment variables for the @@ -5707,9 +7714,7 @@ spec: value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. + using the previously defined environment variables in... type: string valueFrom: description: Source for the environment variable's value. @@ -5723,12 +7728,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the ConfigMap or @@ -5739,9 +7739,8 @@ spec: type: object x-kubernetes-map-type: atomic fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.' properties: apiVersion: description: Version of the schema the FieldPath @@ -5758,7 +7757,7 @@ spec: resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + (limits.cpu, limits. properties: containerName: description: 'Container name: required for volumes, @@ -5789,12 +7788,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the Secret or its @@ -5826,33 +7820,21 @@ spec: nodes OneAgent will be deployed. type: object oneAgentResources: - description: |- - Resource settings for OneAgent container. Consumption of the OneAgent heavily depends on the workload to monitor. You can use the default settings in the CR. - Note: resource.requests shows the values needed to run; resource.limits shows the maximum limits for the pod. + description: Resource settings for OneAgent container. properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry + in pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -5870,7 +7852,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -5879,58 +7861,51 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of + compute resources required. type: object type: object priorityClassName: - description: |- - Assign a priority class to the OneAgent pods. By default, no class is set. - For details, see Pod Priority and Preemption (https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/). + description: Assign a priority class to the OneAgent pods. + By default, no class is set. type: string secCompProfile: description: The SecComp Profile that will be configured in order to run in secure computing mode. type: string + storageHostPath: + description: StorageHostPath is the writable directory on + the host filesystem where OneAgent configurations will... + type: string tolerations: description: Tolerations to include with the OneAgent DaemonSet. - For details, see Taints and Tolerations (https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/). items: description: |- The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . + the triple... properties: effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + description: Effect indicates the taint effect to match. + Empty means match all taint effects. type: string key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. + description: Key is the taint key that the toleration + applies to. Empty means match all taint keys. type: string operator: description: |- Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. + Valid operators are Exists and Equal. type: string tolerationSeconds: description: |- TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). + of effect NoExecute,... format: int64 type: integer value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. + description: Value is the taint value the toleration + matches to. type: string type: object type: array @@ -5941,9 +7916,8 @@ spec: type: object type: object proxy: - description: |- - Set custom proxy settings either directly or from a secret with the field proxy. - Note: Applies to Dynatrace Operator, ActiveGate, and OneAgents. + description: Set custom proxy settings either directly or from a secret + with the field proxy. properties: value: description: Raw value for given property. @@ -5955,10 +7929,22 @@ spec: type: string type: object skipCertCheck: - description: |- - Disable certificate check for the connection between Dynatrace Operator and the Dynatrace Cluster. - Set to true if you want to skip certification validation checks. + description: Disable certificate check for the connection between + Dynatrace Operator and the Dynatrace Cluster. type: boolean + telemetryIngest: + description: When a TelemetryIngestSpec is provided, the OTEL collector + is deployed by the operator. + properties: + protocols: + items: + type: string + type: array + serviceName: + type: string + tlsRefName: + type: string + type: object templates: properties: extensionExecutionController: @@ -6000,7 +7986,7 @@ spec: accessModes: description: |- accessModes contains the desired access modes the volume should have. - More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1 + More info: https://kubernetes. items: type: string type: array @@ -6008,13 +7994,11 @@ spec: dataSource: description: |- dataSource field can be used to specify either: - * An existing VolumeSnapshot object (snapshot.storage.k8s. + * An existing VolumeSnapshot object (snapshot. properties: apiGroup: - description: |- - APIGroup is the group for the resource being referenced. - If APIGroup is not specified, the specified Kind must be in the core API group. - For any other third-party types, APIGroup is required. + description: APIGroup is the group for the resource + being referenced. type: string kind: description: Kind is the type of resource being referenced @@ -6028,16 +8012,12 @@ spec: type: object x-kubernetes-map-type: atomic dataSourceRef: - description: |- - dataSourceRef specifies the object from which to populate the volume with data, if a non-empty - volume is desired. This may be any object from a non-empty API group (non - core object) or a PersistentVolumeClaim object. + description: dataSourceRef specifies the object from which + to populate the volume with data, if a non-empty... properties: apiGroup: - description: |- - APIGroup is the group for the resource being referenced. - If APIGroup is not specified, the specified Kind must be in the core API group. - For any other third-party types, APIGroup is required. + description: APIGroup is the group for the resource + being referenced. type: string kind: description: Kind is the type of resource being referenced @@ -6048,18 +8028,15 @@ spec: namespace: description: |- Namespace is the namespace of resource being referenced - Note that when a namespace is specified, a gateway.networking.k8s.io/ReferenceGrant object is required in the referent namespace to allow that namespace's owner to accept the reference. See the ReferenceGrant documentation for details. + Note that when a namespace is specified, a... type: string required: - kind - name type: object resources: - description: |- - resources represents the minimum resources the volume should have. - If RecoverVolumeExpansionFailure feature is enabled users are allowed to specify resource requirements - that are lower than previous value but must still be higher than capacity recorded in the - status field of the claim. + description: resources represents the minimum resources + the volume should have. properties: limits: additionalProperties: @@ -6070,7 +8047,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -6079,11 +8056,8 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount + of compute resources required. type: object type: object selector: @@ -6094,25 +8068,19 @@ spec: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. + description: A label selector requirement is a selector + that contains values, a key, and an operator that... properties: key: description: key is the label key that the selector applies to. type: string operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. + description: operator represents a key's relationship + to a set of values. type: string values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. + description: values is an array of string values. items: type: string type: array @@ -6126,28 +8094,21 @@ spec: matchLabels: additionalProperties: type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. + description: matchLabels is a map of {key,value} pairs. type: object type: object x-kubernetes-map-type: atomic storageClassName: - description: |- - storageClassName is the name of the StorageClass required by the claim. - More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1 + description: storageClassName is the name of the StorageClass + required by the claim. type: string volumeAttributesClassName: - description: |- - volumeAttributesClassName may be used to set the VolumeAttributesClass used by this claim. - If specified, the CSI driver will create or update the volume with the attributes defined - in the corresponding VolumeAttributesClass. + description: volumeAttributesClassName may be used to + set the VolumeAttributesClass used by this claim. type: string volumeMode: - description: |- - volumeMode defines what type of volume is required by the claim. - Value of Filesystem is implied when not included in claim spec. + description: volumeMode defines what type of volume is + required by the claim. type: string volumeName: description: volumeName is the binding reference to the @@ -6159,28 +8120,18 @@ spec: ExtensionExecutionController pod properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry + in pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -6198,7 +8149,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -6207,11 +8158,8 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of + compute resources required. type: object type: object tlsRefName: @@ -6222,36 +8170,30 @@ spec: items: description: |- The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . + the triple... properties: effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + description: Effect indicates the taint effect to match. + Empty means match all taint effects. type: string key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. + description: Key is the taint key that the toleration + applies to. Empty means match all taint keys. type: string operator: description: |- Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. + Valid operators are Exists and Equal. type: string tolerationSeconds: description: |- TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). + of effect NoExecute,... format: int64 type: integer value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. + description: Value is the taint value the toleration + matches to. type: string type: object type: array @@ -6263,34 +8205,28 @@ spec: matching pods among the given topology. properties: labelSelector: - description: |- - LabelSelector is used to find matching pods. - Pods that match this label selector are counted to determine the number of pods - in their corresponding topology domain. + description: LabelSelector is used to find matching + pods. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. + description: A label selector requirement is a + selector that contains values, a key, and an + operator that... properties: key: description: key is the label key that the selector applies to. type: string operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. + description: operator represents a key's relationship + to a set of values. type: string values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. + description: values is an array of string + values. items: type: string type: array @@ -6304,62 +8240,46 @@ spec: matchLabels: additionalProperties: type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. + description: matchLabels is a map of {key,value} + pairs. type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: description: |- MatchLabelKeys is a set of pod label keys to select the pods over which - spreading will be calculated. + spreading will be... items: type: string type: array x-kubernetes-list-type: atomic maxSkew: - description: |- - MaxSkew describes the degree to which pods may be unevenly distributed. - When `whenUnsatisfiable=DoNotSchedule`, it is the maximum permitted difference - between the number of matching pods in the target topology and the global minimum. + description: MaxSkew describes the degree to which pods + may be unevenly distributed. format: int32 type: integer minDomains: - description: |- - MinDomains indicates a minimum number of eligible domains. - When the number of eligible domains with matching topology keys is less than minDomains, - Pod Topology Spread treats "global minimum" as 0, and then the calculation of Skew is performed. + description: MinDomains indicates a minimum number of + eligible domains. format: int32 type: integer nodeAffinityPolicy: description: |- NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector - when calculating pod topology spread skew. Options are: - - Honor: only nodes matching nodeAffinity/nodeSelector are included in the calculations. - - Ignore: nodeAffinity/nodeSelector are ignored. + when calculating pod... type: string nodeTaintsPolicy: description: |- NodeTaintsPolicy indicates how we will treat node taints when calculating - pod topology spread skew. Options are: - - Honor: nodes without taints, along with tainted nodes for which the incoming pod - has a toleration, are included. - - Ignore: node taints are ignored. All nodes are included. + pod topology spread skew. type: string topologyKey: - description: |- - TopologyKey is the key of node labels. Nodes that have a label with this key - and identical values are considered to be in the same topology. - We consider each as a "bucket", and try to put balanced number - of pods into each bucket. + description: TopologyKey is the key of node labels. type: string whenUnsatisfiable: description: |- WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy the spread constraint. - - DoNotSchedule (default) tells the scheduler not to schedule it. type: string required: - maxSkew @@ -6399,9 +8319,7 @@ spec: value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. + using the previously defined environment variables in... type: string valueFrom: description: Source for the environment variable's value. @@ -6415,12 +8333,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the ConfigMap or @@ -6431,9 +8344,8 @@ spec: type: object x-kubernetes-map-type: atomic fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.' properties: apiVersion: description: Version of the schema the FieldPath @@ -6450,7 +8362,7 @@ spec: resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + (limits.cpu, limits. properties: containerName: description: 'Container name: required for volumes, @@ -6481,12 +8393,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the Secret or its @@ -6525,13 +8432,11 @@ spec: preferredDuringSchedulingIgnoredDuringExecution: description: |- The scheduler will prefer to schedule pods to nodes that satisfy - the affinity expressions specified by this field, but it may choose - a node that violates one or more of the expressions. The node that is - most preferred is the one with the greatest sum of weights, i.e. + the affinity expressions specified... items: description: |- An empty preferred scheduling term matches all objects with implicit weight 0 - (i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op). + (i.e. it's a no-op). properties: preference: description: A node selector term, associated with @@ -6543,23 +8448,20 @@ spec: items: description: |- A node selector requirement is a selector that contains values, a key, and an operator - that relates the key and values. + that relates... properties: key: description: The label key that the selector applies to. type: string operator: - description: |- - Represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + description: Represents a key's relationship + to a set of values. type: string values: description: |- An array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. If the operator is Gt or Lt, the values - array must have a single element, which will be interpreted as an integer. + the values array must be non-empty. items: type: string type: array @@ -6576,23 +8478,20 @@ spec: items: description: |- A node selector requirement is a selector that contains values, a key, and an operator - that relates the key and values. + that relates... properties: key: description: The label key that the selector applies to. type: string operator: - description: |- - Represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + description: Represents a key's relationship + to a set of values. type: string values: description: |- An array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. If the operator is Gt or Lt, the values - array must have a single element, which will be interpreted as an integer. + the values array must be non-empty. items: type: string type: array @@ -6619,9 +8518,7 @@ spec: requiredDuringSchedulingIgnoredDuringExecution: description: |- If the affinity requirements specified by this field are not met at - scheduling time, the pod will not be scheduled onto the node. - If the affinity requirements specified by this field cease to be met - at some point during pod execution (e.g. + scheduling time, the pod will... properties: nodeSelectorTerms: description: Required. A list of node selector terms. @@ -6630,7 +8527,6 @@ spec: description: |- A null or empty node selector term matches no objects. The requirements of them are ANDed. - The TopologySelectorTerm type implements a subset of the NodeSelectorTerm. properties: matchExpressions: description: A list of node selector requirements @@ -6638,23 +8534,20 @@ spec: items: description: |- A node selector requirement is a selector that contains values, a key, and an operator - that relates the key and values. + that relates... properties: key: description: The label key that the selector applies to. type: string operator: - description: |- - Represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + description: Represents a key's relationship + to a set of values. type: string values: description: |- An array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. If the operator is Gt or Lt, the values - array must have a single element, which will be interpreted as an integer. + the values array must be non-empty. items: type: string type: array @@ -6671,23 +8564,20 @@ spec: items: description: |- A node selector requirement is a selector that contains values, a key, and an operator - that relates the key and values. + that relates... properties: key: description: The label key that the selector applies to. type: string operator: - description: |- - Represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + description: Represents a key's relationship + to a set of values. type: string values: description: |- An array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. If the operator is Gt or Lt, the values - array must have a single element, which will be interpreted as an integer. + the values array must be non-empty. items: type: string type: array @@ -6711,40 +8601,28 @@ spec: additionalProperties: type: string description: Specify the node selector that controls on which - nodes NodeConfigurationCollector pods will be deployed. + nodes NodeConfigurationCollector pods will be... type: object priorityClassName: - description: |- - If specified, indicates the pod's priority. Name must be defined by creating a PriorityClass object with that - name. If not specified the setting will be removed from the DaemonSet. + description: If specified, indicates the pod's priority. type: string resources: description: Define resources' requests and limits for single NodeConfigurationCollector pod properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry + in pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -6762,7 +8640,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -6771,11 +8649,8 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of + compute resources required. type: object type: object tolerations: @@ -6784,36 +8659,30 @@ spec: items: description: |- The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . + the triple... properties: effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + description: Effect indicates the taint effect to match. + Empty means match all taint effects. type: string key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. + description: Key is the taint key that the toleration + applies to. Empty means match all taint keys. type: string operator: description: |- Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. + Valid operators are Exists and Equal. type: string tolerationSeconds: description: |- TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). + of effect NoExecute,... format: int64 type: integer value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. + description: Value is the taint value the toleration + matches to. type: string type: object type: array @@ -6831,9 +8700,7 @@ spec: - type: string description: |- The maximum number of nodes with an existing available DaemonSet pod that - can have an updated DaemonSet pod during during an update. - Value can be an absolute number (ex: 5) or a percentage of desired pods (ex: 10%). - This can not be 0 if MaxUnavailable is 0. + can have an updated... x-kubernetes-int-or-string: true maxUnavailable: anyOf: @@ -6841,9 +8708,7 @@ spec: - type: string description: |- The maximum number of DaemonSet pods that can be unavailable during the - update. Value can be an absolute number (ex: 5) or a percentage of total - number of DaemonSet pods at the start of the update (ex: 10%). Absolute - number is calculated from percentage by rounding up. + update. x-kubernetes-int-or-string: true type: object type: @@ -6902,28 +8767,18 @@ spec: the LogMonitoring pods properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry + in pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -6941,7 +8796,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -6950,58 +8805,48 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of + compute resources required. type: object type: object secCompProfile: description: The SecComp Profile that will be configured in - order to run in secure computing mode for the LogMonitoring - pods + order to run in secure computing mode for the... type: string tolerations: description: Set tolerations for the LogMonitoring pods items: description: |- The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . + the triple... properties: effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + description: Effect indicates the taint effect to match. + Empty means match all taint effects. type: string key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. + description: Key is the taint key that the toleration + applies to. Empty means match all taint keys. type: string operator: description: |- Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. + Valid operators are Exists and Equal. type: string tolerationSeconds: description: |- TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). + of effect NoExecute,... format: int64 type: integer value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. + description: Value is the taint value the toleration + matches to. type: string type: object type: array type: object - openTelemetryCollector: + otelCollector: properties: annotations: additionalProperties: @@ -7035,28 +8880,18 @@ spec: OtelCollector pod properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry + in pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -7074,7 +8909,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -7083,11 +8918,8 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of + compute resources required. type: object type: object tlsRefName: @@ -7097,36 +8929,30 @@ spec: items: description: |- The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . + the triple... properties: effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + description: Effect indicates the taint effect to match. + Empty means match all taint effects. type: string key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. + description: Key is the taint key that the toleration + applies to. Empty means match all taint keys. type: string operator: description: |- Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. + Valid operators are Exists and Equal. type: string tolerationSeconds: description: |- TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). + of effect NoExecute,... format: int64 type: integer value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. + description: Value is the taint value the toleration + matches to. type: string type: object type: array @@ -7138,34 +8964,28 @@ spec: matching pods among the given topology. properties: labelSelector: - description: |- - LabelSelector is used to find matching pods. - Pods that match this label selector are counted to determine the number of pods - in their corresponding topology domain. + description: LabelSelector is used to find matching + pods. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. + description: A label selector requirement is a + selector that contains values, a key, and an + operator that... properties: key: description: key is the label key that the selector applies to. type: string operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. + description: operator represents a key's relationship + to a set of values. type: string values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. + description: values is an array of string + values. items: type: string type: array @@ -7179,62 +8999,46 @@ spec: matchLabels: additionalProperties: type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. + description: matchLabels is a map of {key,value} + pairs. type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: description: |- MatchLabelKeys is a set of pod label keys to select the pods over which - spreading will be calculated. + spreading will be... items: type: string type: array x-kubernetes-list-type: atomic maxSkew: - description: |- - MaxSkew describes the degree to which pods may be unevenly distributed. - When `whenUnsatisfiable=DoNotSchedule`, it is the maximum permitted difference - between the number of matching pods in the target topology and the global minimum. + description: MaxSkew describes the degree to which pods + may be unevenly distributed. format: int32 type: integer minDomains: - description: |- - MinDomains indicates a minimum number of eligible domains. - When the number of eligible domains with matching topology keys is less than minDomains, - Pod Topology Spread treats "global minimum" as 0, and then the calculation of Skew is performed. + description: MinDomains indicates a minimum number of + eligible domains. format: int32 type: integer nodeAffinityPolicy: description: |- NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector - when calculating pod topology spread skew. Options are: - - Honor: only nodes matching nodeAffinity/nodeSelector are included in the calculations. - - Ignore: nodeAffinity/nodeSelector are ignored. + when calculating pod... type: string nodeTaintsPolicy: description: |- NodeTaintsPolicy indicates how we will treat node taints when calculating - pod topology spread skew. Options are: - - Honor: nodes without taints, along with tainted nodes for which the incoming pod - has a toleration, are included. - - Ignore: node taints are ignored. All nodes are included. + pod topology spread skew. type: string topologyKey: - description: |- - TopologyKey is the key of node labels. Nodes that have a label with this key - and identical values are considered to be in the same topology. - We consider each as a "bucket", and try to put balanced number - of pods into each bucket. + description: TopologyKey is the key of node labels. type: string whenUnsatisfiable: description: |- WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy the spread constraint. - - DoNotSchedule (default) tells the scheduler not to schedule it. type: string required: - maxSkew @@ -7249,9 +9053,8 @@ spec: to Dynatrace. type: string trustedCAs: - description: |- - Adds custom RootCAs from a configmap. Put the certificate under certs within your configmap. - Note: Applies to Dynatrace Operator, OneAgent and ActiveGate. + description: Adds custom RootCAs from a configmap. Put the certificate + under certs within your configmap. type: string required: - apiUrl @@ -7272,6 +9075,9 @@ spec: description: Time of the last connection request format: date-time type: string + tenantTokenHash: + description: Hash of the tenant token + type: string tenantUUID: description: UUID of the tenant, received from the tenant type: string @@ -7331,31 +9137,24 @@ spec: state of this API Resource. properties: lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + description: lastTransitionTime is the last time the condition + transitioned from one status to another. format: date-time type: string message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. + description: message is a human readable message indicating + details about the transition. maxLength: 32768 type: string observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. format: int64 minimum: 0 type: integer reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. maxLength: 1024 minLength: 1 pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ @@ -7392,9 +9191,8 @@ spec: description: Observed state of Kspm properties: tokenSecretHash: - description: |- - TokenSecretHash contains the hash of the token that is passed to both the ActiveGate and Node-Configuration-Collector. - Meant to keep the two in sync. + description: TokenSecretHash contains the hash of the token that + is passed to both the ActiveGate and... type: string type: object kubeSystemUUID: @@ -7407,8 +9205,7 @@ spec: type: string kubernetesClusterName: description: KubernetesClusterName contains the display name (also - know as label) of the monitored entity that points to the Kubernetes - cluster + know as label) of the monitored entity that... type: string metadataEnrichment: description: Observed state of Metadata-Enrichment @@ -7456,6 +9253,9 @@ spec: description: Time of the last connection request format: date-time type: string + tenantTokenHash: + description: Hash of the tenant token + type: string tenantUUID: description: UUID of the tenant, received from the tenant type: string diff --git a/config/crd/bases/dynatrace.com_edgeconnects.yaml b/config/crd/bases/dynatrace.com_edgeconnects.yaml index d56e1ccc41..684a2e8286 100644 --- a/config/crd/bases/dynatrace.com_edgeconnects.yaml +++ b/config/crd/bases/dynatrace.com_edgeconnects.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.16.5 + controller-gen.kubebuilder.io/version: v0.17.2 name: edgeconnects.dynatrace.com spec: group: dynatrace.com @@ -35,19 +35,12 @@ spec: description: EdgeConnect is the Schema for the EdgeConnect API properties: apiVersion: - 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 + description: APIVersion defines the versioned schema of this representation + of an object. type: string kind: - 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 + description: Kind is a string value representing the REST resource this + object represents. type: string metadata: type: object @@ -65,12 +58,11 @@ spec: type: string autoUpdate: default: true - description: 'Enables automatic restarts of EdgeConnect pods in case - a new version is available (the default value is: true)' + description: Enables automatic restarts of EdgeConnect pods in case + a new version is available (the default... type: boolean caCertsRef: - description: Adds custom root certificate from a configmap. Put the - certificate under certs within your configmap. + description: Adds custom root certificate from a configmap. type: string customPullSecret: description: Pull secret for your private registry @@ -88,9 +80,7 @@ spec: value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. + using the previously defined environment variables in... type: string valueFrom: description: Source for the environment variable's value. Cannot @@ -104,12 +94,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the ConfigMap or its key @@ -120,9 +105,8 @@ spec: type: object x-kubernetes-map-type: atomic fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + description: 'Selects a field of the pod: supports metadata.name, + metadata.namespace, `metadata.' properties: apiVersion: description: Version of the schema the FieldPath is @@ -139,7 +123,7 @@ spec: resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + (limits.cpu, limits. properties: containerName: description: 'Container name: required for volumes, @@ -169,12 +153,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the Secret or its key must @@ -242,8 +221,7 @@ spec: type: string provisioner: description: Determines if the operator will create the EdgeConnect - and light OAuth client on the cluster using the credentials - provided. Requires more scopes than default behavior. + and light OAuth client on the cluster using... type: boolean resource: description: URN identifying your account. You get the URN when @@ -258,9 +236,8 @@ spec: description: General configurations for proxy settings. properties: authRef: - description: |- - Secret name which contains the username and password used for authentication with the proxy, using the - "Basic" HTTP authentication scheme. + description: Secret name which contains the username and password + used for authentication with the proxy, using... type: string host: description: Server address (hostname or IP address) of the proxy. @@ -268,8 +245,7 @@ spec: noProxy: description: |- NoProxy represents the NO_PROXY or no_proxy environment - variable. It specifies a string that contains comma-separated values - specifying hosts that should be excluded from proxying. + variable. type: string port: description: Port of the proxy. @@ -286,28 +262,16 @@ spec: description: Defines resources requests and limits for single pods properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry in pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request in + the referenced claim. type: string required: - name @@ -325,7 +289,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -334,11 +298,8 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of compute + resources required. type: object type: object serviceAccountName: @@ -351,36 +312,30 @@ spec: items: description: |- The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . + the triple... properties: effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + description: Effect indicates the taint effect to match. Empty + means match all taint effects. type: string key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. + description: Key is the taint key that the toleration applies + to. Empty means match all taint keys. type: string operator: description: |- Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. + Valid operators are Exists and Equal. type: string tolerationSeconds: description: |- TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). + of effect NoExecute,... format: int64 type: integer value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. + description: Value is the taint value the toleration matches + to. type: string type: object type: array @@ -392,34 +347,25 @@ spec: pods among the given topology. properties: labelSelector: - description: |- - LabelSelector is used to find matching pods. - Pods that match this label selector are counted to determine the number of pods - in their corresponding topology domain. + description: LabelSelector is used to find matching pods. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. + description: A label selector requirement is a selector + that contains values, a key, and an operator that... properties: key: description: key is the label key that the selector applies to. type: string operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. + description: operator represents a key's relationship + to a set of values. type: string values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. + description: values is an array of string values. items: type: string type: array @@ -433,62 +379,45 @@ spec: matchLabels: additionalProperties: type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. + description: matchLabels is a map of {key,value} pairs. type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: description: |- MatchLabelKeys is a set of pod label keys to select the pods over which - spreading will be calculated. + spreading will be... items: type: string type: array x-kubernetes-list-type: atomic maxSkew: - description: |- - MaxSkew describes the degree to which pods may be unevenly distributed. - When `whenUnsatisfiable=DoNotSchedule`, it is the maximum permitted difference - between the number of matching pods in the target topology and the global minimum. + description: MaxSkew describes the degree to which pods may + be unevenly distributed. format: int32 type: integer minDomains: - description: |- - MinDomains indicates a minimum number of eligible domains. - When the number of eligible domains with matching topology keys is less than minDomains, - Pod Topology Spread treats "global minimum" as 0, and then the calculation of Skew is performed. + description: MinDomains indicates a minimum number of eligible + domains. format: int32 type: integer nodeAffinityPolicy: description: |- NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector - when calculating pod topology spread skew. Options are: - - Honor: only nodes matching nodeAffinity/nodeSelector are included in the calculations. - - Ignore: nodeAffinity/nodeSelector are ignored. + when calculating pod... type: string nodeTaintsPolicy: description: |- NodeTaintsPolicy indicates how we will treat node taints when calculating - pod topology spread skew. Options are: - - Honor: nodes without taints, along with tainted nodes for which the incoming pod - has a toleration, are included. - - Ignore: node taints are ignored. All nodes are included. + pod topology spread skew. type: string topologyKey: - description: |- - TopologyKey is the key of node labels. Nodes that have a label with this key - and identical values are considered to be in the same topology. - We consider each as a "bucket", and try to put balanced number - of pods into each bucket. + description: TopologyKey is the key of node labels. type: string whenUnsatisfiable: description: |- WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy the spread constraint. - - DoNotSchedule (default) tells the scheduler not to schedule it. type: string required: - maxSkew @@ -511,31 +440,24 @@ spec: state of this API Resource. properties: lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + description: lastTransitionTime is the last time the condition + transitioned from one status to another. format: date-time type: string message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. + description: message is a human readable message indicating + details about the transition. maxLength: 32768 type: string observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. format: int64 minimum: 0 type: integer reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. maxLength: 1024 minLength: 1 pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ @@ -615,19 +537,12 @@ spec: description: EdgeConnect is the Schema for the EdgeConnect API properties: apiVersion: - 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 + description: APIVersion defines the versioned schema of this representation + of an object. type: string kind: - 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 + description: Kind is a string value representing the REST resource this + object represents. type: string metadata: type: object @@ -644,12 +559,11 @@ spec: your specific environment UUID type: string autoUpdate: - description: 'Enables automatic restarts of EdgeConnect pods in case - a new version is available (the default value is: true)' + description: Enables automatic restarts of EdgeConnect pods in case + a new version is available (the default... type: boolean caCertsRef: - description: Adds custom root certificate from a configmap. Put the - certificate under certs within your configmap. + description: Adds custom root certificate from a configmap. type: string customPullSecret: description: Pull secret for your private registry @@ -667,9 +581,7 @@ spec: value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. + using the previously defined environment variables in... type: string valueFrom: description: Source for the environment variable's value. Cannot @@ -683,12 +595,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the ConfigMap or its key @@ -699,9 +606,8 @@ spec: type: object x-kubernetes-map-type: atomic fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + description: 'Selects a field of the pod: supports metadata.name, + metadata.namespace, `metadata.' properties: apiVersion: description: Version of the schema the FieldPath is @@ -718,7 +624,7 @@ spec: resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + (limits.cpu, limits. properties: containerName: description: 'Container name: required for volumes, @@ -748,12 +654,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the Secret or its key must @@ -823,8 +724,7 @@ spec: type: string provisioner: description: Determines if the operator will create the EdgeConnect - and light OAuth client on the cluster using the credentials - provided. Requires more scopes than default behavior. + and light OAuth client on the cluster using... type: boolean resource: description: URN identifying your account. You get the URN when @@ -839,9 +739,8 @@ spec: description: General configurations for proxy settings. properties: authRef: - description: |- - Secret name which contains the username and password used for authentication with the proxy, using the - "Basic" HTTP authentication scheme. + description: Secret name which contains the username and password + used for authentication with the proxy, using... type: string host: description: Server address (hostname or IP address) of the proxy. @@ -849,8 +748,7 @@ spec: noProxy: description: |- NoProxy represents the NO_PROXY or no_proxy environment - variable. It specifies a string that contains comma-separated values - specifying hosts that should be excluded from proxying. + variable. type: string port: description: Port of the proxy. @@ -866,28 +764,16 @@ spec: description: Defines resources requests and limits for single pods properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry in pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request in + the referenced claim. type: string required: - name @@ -905,7 +791,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -914,11 +800,8 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of compute + resources required. type: object type: object serviceAccountName: @@ -930,36 +813,30 @@ spec: items: description: |- The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . + the triple... properties: effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + description: Effect indicates the taint effect to match. Empty + means match all taint effects. type: string key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. + description: Key is the taint key that the toleration applies + to. Empty means match all taint keys. type: string operator: description: |- Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. + Valid operators are Exists and Equal. type: string tolerationSeconds: description: |- TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). + of effect NoExecute,... format: int64 type: integer value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. + description: Value is the taint value the toleration matches + to. type: string type: object type: array @@ -971,34 +848,25 @@ spec: pods among the given topology. properties: labelSelector: - description: |- - LabelSelector is used to find matching pods. - Pods that match this label selector are counted to determine the number of pods - in their corresponding topology domain. + description: LabelSelector is used to find matching pods. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. + description: A label selector requirement is a selector + that contains values, a key, and an operator that... properties: key: description: key is the label key that the selector applies to. type: string operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. + description: operator represents a key's relationship + to a set of values. type: string values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. + description: values is an array of string values. items: type: string type: array @@ -1012,62 +880,45 @@ spec: matchLabels: additionalProperties: type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. + description: matchLabels is a map of {key,value} pairs. type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: description: |- MatchLabelKeys is a set of pod label keys to select the pods over which - spreading will be calculated. + spreading will be... items: type: string type: array x-kubernetes-list-type: atomic maxSkew: - description: |- - MaxSkew describes the degree to which pods may be unevenly distributed. - When `whenUnsatisfiable=DoNotSchedule`, it is the maximum permitted difference - between the number of matching pods in the target topology and the global minimum. + description: MaxSkew describes the degree to which pods may + be unevenly distributed. format: int32 type: integer minDomains: - description: |- - MinDomains indicates a minimum number of eligible domains. - When the number of eligible domains with matching topology keys is less than minDomains, - Pod Topology Spread treats "global minimum" as 0, and then the calculation of Skew is performed. + description: MinDomains indicates a minimum number of eligible + domains. format: int32 type: integer nodeAffinityPolicy: description: |- NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector - when calculating pod topology spread skew. Options are: - - Honor: only nodes matching nodeAffinity/nodeSelector are included in the calculations. - - Ignore: nodeAffinity/nodeSelector are ignored. + when calculating pod... type: string nodeTaintsPolicy: description: |- NodeTaintsPolicy indicates how we will treat node taints when calculating - pod topology spread skew. Options are: - - Honor: nodes without taints, along with tainted nodes for which the incoming pod - has a toleration, are included. - - Ignore: node taints are ignored. All nodes are included. + pod topology spread skew. type: string topologyKey: - description: |- - TopologyKey is the key of node labels. Nodes that have a label with this key - and identical values are considered to be in the same topology. - We consider each as a "bucket", and try to put balanced number - of pods into each bucket. + description: TopologyKey is the key of node labels. type: string whenUnsatisfiable: description: |- WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy the spread constraint. - - DoNotSchedule (default) tells the scheduler not to schedule it. type: string required: - maxSkew @@ -1090,31 +941,24 @@ spec: state of this API Resource. properties: lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + description: lastTransitionTime is the last time the condition + transitioned from one status to another. format: date-time type: string message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. + description: message is a human readable message indicating + details about the transition. maxLength: 32768 type: string observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. format: int64 minimum: 0 type: integer reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. maxLength: 1024 minLength: 1 pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ diff --git a/config/helm/chart/default/templates/Common/crd/dynatrace-operator-crd.yaml b/config/helm/chart/default/templates/Common/crd/dynatrace-operator-crd.yaml index ef09d4063c..bb618d3fd1 100644 --- a/config/helm/chart/default/templates/Common/crd/dynatrace-operator-crd.yaml +++ b/config/helm/chart/default/templates/Common/crd/dynatrace-operator-crd.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.16.5 + controller-gen.kubebuilder.io/version: v0.17.2 name: dynakubes.dynatrace.com spec: conversion: @@ -47,19 +47,12 @@ spec: description: DynaKube is the Schema for the DynaKube API properties: apiVersion: - 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 + description: APIVersion defines the versioned schema of this representation + of an object. type: string kind: - 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 + description: Kind is a string value representing the REST resource this + object represents. type: string metadata: type: object @@ -83,7 +76,7 @@ spec: customProperties: description: |- Add a custom properties file by providing it as a value or reference it from a secret - If referenced from a secret, make sure the key is called 'customProperties' + If referenced... properties: value: description: Custom properties value. @@ -110,9 +103,7 @@ spec: value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. + using the previously defined environment variables in... type: string valueFrom: description: Source for the environment variable's value. @@ -126,12 +117,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the ConfigMap or its @@ -142,9 +128,8 @@ spec: type: object x-kubernetes-map-type: atomic fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + description: 'Selects a field of the pod: supports metadata.name, + metadata.namespace, `metadata.' properties: apiVersion: description: Version of the schema the FieldPath @@ -161,7 +146,7 @@ spec: resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + (limits.cpu, limits. properties: containerName: description: 'Container name: required for volumes, @@ -192,12 +177,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the Secret or its key @@ -216,8 +196,7 @@ spec: description: Set activation group for ActiveGate type: string image: - description: The ActiveGate container image. Defaults to the latest - ActiveGate image provided by the registry on the tenant + description: The ActiveGate container image. type: string labels: additionalProperties: @@ -230,9 +209,7 @@ spec: description: Node selector to control the selection of nodes type: object priorityClassName: - description: |- - If specified, indicates the pod's priority. Name must be defined by creating a PriorityClass object with that - name. If not specified the setting will be removed from the StatefulSet. + description: If specified, indicates the pod's priority. type: string replicas: description: Amount of replicas for your ActiveGates @@ -243,28 +220,18 @@ spec: pods properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry in + pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -282,7 +249,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -291,54 +258,43 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of compute + resources required. type: object type: object tlsSecretName: - description: |- - The name of a secret containing ActiveGate TLS cert+key and password. If not set, self-signed certificate is used. - server.p12: certificate+key pair in pkcs12 format - password: passphrase to read server.p12 + description: The name of a secret containing ActiveGate TLS cert+key + and password. type: string tolerations: description: Set tolerations for the ActiveGate pods items: description: |- The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . + the triple... properties: effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + description: Effect indicates the taint effect to match. + Empty means match all taint effects. type: string key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. + description: Key is the taint key that the toleration applies + to. Empty means match all taint keys. type: string operator: description: |- Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. + Valid operators are Exists and Equal. type: string tolerationSeconds: description: |- TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). + of effect NoExecute,... format: int64 type: integer value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. + description: Value is the taint value the toleration matches + to. type: string type: object type: array @@ -350,34 +306,25 @@ spec: matching pods among the given topology. properties: labelSelector: - description: |- - LabelSelector is used to find matching pods. - Pods that match this label selector are counted to determine the number of pods - in their corresponding topology domain. + description: LabelSelector is used to find matching pods. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. + description: A label selector requirement is a selector + that contains values, a key, and an operator that... properties: key: description: key is the label key that the selector applies to. type: string operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. + description: operator represents a key's relationship + to a set of values. type: string values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. + description: values is an array of string values. items: type: string type: array @@ -391,62 +338,45 @@ spec: matchLabels: additionalProperties: type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. + description: matchLabels is a map of {key,value} pairs. type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: description: |- MatchLabelKeys is a set of pod label keys to select the pods over which - spreading will be calculated. + spreading will be... items: type: string type: array x-kubernetes-list-type: atomic maxSkew: - description: |- - MaxSkew describes the degree to which pods may be unevenly distributed. - When `whenUnsatisfiable=DoNotSchedule`, it is the maximum permitted difference - between the number of matching pods in the target topology and the global minimum. + description: MaxSkew describes the degree to which pods + may be unevenly distributed. format: int32 type: integer minDomains: - description: |- - MinDomains indicates a minimum number of eligible domains. - When the number of eligible domains with matching topology keys is less than minDomains, - Pod Topology Spread treats "global minimum" as 0, and then the calculation of Skew is performed. + description: MinDomains indicates a minimum number of eligible + domains. format: int32 type: integer nodeAffinityPolicy: description: |- NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector - when calculating pod topology spread skew. Options are: - - Honor: only nodes matching nodeAffinity/nodeSelector are included in the calculations. - - Ignore: nodeAffinity/nodeSelector are ignored. + when calculating pod... type: string nodeTaintsPolicy: description: |- NodeTaintsPolicy indicates how we will treat node taints when calculating - pod topology spread skew. Options are: - - Honor: nodes without taints, along with tainted nodes for which the incoming pod - has a toleration, are included. - - Ignore: node taints are ignored. All nodes are included. + pod topology spread skew. type: string topologyKey: - description: |- - TopologyKey is the key of node labels. Nodes that have a label with this key - and identical values are considered to be in the same topology. - We consider each as a "bucket", and try to put balanced number - of pods into each bucket. + description: TopologyKey is the key of node labels. type: string whenUnsatisfiable: description: |- WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy the spread constraint. - - DoNotSchedule (default) tells the scheduler not to schedule it. type: string required: - maxSkew @@ -456,21 +386,15 @@ spec: type: array type: object apiUrl: - description: |- - Dynatrace apiUrl, including the /api path at the end. For SaaS, set YOUR_ENVIRONMENT_ID to your environment ID. For Managed, change the apiUrl address. - For instructions on how to determine the environment ID and how to configure the apiUrl address, see Environment ID (https://www.dynatrace. + description: Dynatrace apiUrl, including the /api path at the end. type: string customPullSecret: - description: |- - Defines a custom pull secret in case you use a private registry when pulling images from the Dynatrace environment. - To define a custom pull secret and learn about the expected behavior, see Configure customPullSecret - (https://www.dynatrace. + description: Defines a custom pull secret in case you use a private + registry when pulling images from the... type: string enableIstio: - description: |- - When enabled, and if Istio is installed on the Kubernetes environment, Dynatrace Operator will create the corresponding - VirtualService and ServiceEntry objects to allow access to the Dynatrace Cluster from the OneAgent or ActiveGate. - Disabled by default. + description: When enabled, and if Istio is installed on the Kubernetes + environment, Dynatrace Operator will... type: boolean kubernetesMonitoring: description: Configuration for Kubernetes Monitoring @@ -478,7 +402,7 @@ spec: customProperties: description: |- Add a custom properties file by providing it as a value or reference it from a secret - If referenced from a secret, make sure the key is called 'customProperties' + If referenced... properties: value: description: Custom properties value. @@ -505,9 +429,7 @@ spec: value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. + using the previously defined environment variables in... type: string valueFrom: description: Source for the environment variable's value. @@ -521,12 +443,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the ConfigMap or its @@ -537,9 +454,8 @@ spec: type: object x-kubernetes-map-type: atomic fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + description: 'Selects a field of the pod: supports metadata.name, + metadata.namespace, `metadata.' properties: apiVersion: description: Version of the schema the FieldPath @@ -556,7 +472,7 @@ spec: resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + (limits.cpu, limits. properties: containerName: description: 'Container name: required for volumes, @@ -587,12 +503,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the Secret or its key @@ -611,8 +522,7 @@ spec: description: Set activation group for ActiveGate type: string image: - description: The ActiveGate container image. Defaults to the latest - ActiveGate image provided by the registry on the tenant + description: The ActiveGate container image. type: string labels: additionalProperties: @@ -633,28 +543,18 @@ spec: pods properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry in + pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -672,7 +572,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -681,11 +581,8 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of compute + resources required. type: object type: object tolerations: @@ -693,36 +590,30 @@ spec: items: description: |- The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . + the triple... properties: effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + description: Effect indicates the taint effect to match. + Empty means match all taint effects. type: string key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. + description: Key is the taint key that the toleration applies + to. Empty means match all taint keys. type: string operator: description: |- Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. + Valid operators are Exists and Equal. type: string tolerationSeconds: description: |- TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). + of effect NoExecute,... format: int64 type: integer value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. + description: Value is the taint value the toleration matches + to. type: string type: object type: array @@ -734,34 +625,25 @@ spec: matching pods among the given topology. properties: labelSelector: - description: |- - LabelSelector is used to find matching pods. - Pods that match this label selector are counted to determine the number of pods - in their corresponding topology domain. + description: LabelSelector is used to find matching pods. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. + description: A label selector requirement is a selector + that contains values, a key, and an operator that... properties: key: description: key is the label key that the selector applies to. type: string operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. + description: operator represents a key's relationship + to a set of values. type: string values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. + description: values is an array of string values. items: type: string type: array @@ -775,62 +657,45 @@ spec: matchLabels: additionalProperties: type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. + description: matchLabels is a map of {key,value} pairs. type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: description: |- MatchLabelKeys is a set of pod label keys to select the pods over which - spreading will be calculated. + spreading will be... items: type: string type: array x-kubernetes-list-type: atomic maxSkew: - description: |- - MaxSkew describes the degree to which pods may be unevenly distributed. - When `whenUnsatisfiable=DoNotSchedule`, it is the maximum permitted difference - between the number of matching pods in the target topology and the global minimum. + description: MaxSkew describes the degree to which pods + may be unevenly distributed. format: int32 type: integer minDomains: - description: |- - MinDomains indicates a minimum number of eligible domains. - When the number of eligible domains with matching topology keys is less than minDomains, - Pod Topology Spread treats "global minimum" as 0, and then the calculation of Skew is performed. + description: MinDomains indicates a minimum number of eligible + domains. format: int32 type: integer nodeAffinityPolicy: description: |- NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector - when calculating pod topology spread skew. Options are: - - Honor: only nodes matching nodeAffinity/nodeSelector are included in the calculations. - - Ignore: nodeAffinity/nodeSelector are ignored. + when calculating pod... type: string nodeTaintsPolicy: description: |- NodeTaintsPolicy indicates how we will treat node taints when calculating - pod topology spread skew. Options are: - - Honor: nodes without taints, along with tainted nodes for which the incoming pod - has a toleration, are included. - - Ignore: node taints are ignored. All nodes are included. + pod topology spread skew. type: string topologyKey: - description: |- - TopologyKey is the key of node labels. Nodes that have a label with this key - and identical values are considered to be in the same topology. - We consider each as a "bucket", and try to put balanced number - of pods into each bucket. + description: TopologyKey is the key of node labels. type: string whenUnsatisfiable: description: |- WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy the spread constraint. - - DoNotSchedule (default) tells the scheduler not to schedule it. type: string required: - maxSkew @@ -840,33 +705,26 @@ spec: type: array type: object namespaceSelector: - description: |- - Applicable only for applicationMonitoring or cloudNativeFullStack configuration types. The namespaces where you want Dynatrace Operator to inject. - For more information, see Configure monitoring for namespaces and pods (https://www.dynatrace. + description: Applicable only for applicationMonitoring or cloudNativeFullStack + configuration types. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. + description: A label selector requirement is a selector that + contains values, a key, and an operator that... properties: key: description: key is the label key that the selector applies to. type: string operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. + description: operator represents a key's relationship to + a set of values. type: string values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. + description: values is an array of string values. items: type: string type: array @@ -880,10 +738,7 @@ spec: matchLabels: additionalProperties: type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. + description: matchLabels is a map of {key,value} pairs. type: object type: object x-kubernetes-map-type: atomic @@ -891,14 +746,11 @@ spec: description: Sets a network zone for the OneAgent and ActiveGate pods. type: string oneAgent: - description: |- - General configuration about OneAgent instances. - You can't enable more than one module (classicFullStack, cloudNativeFullStack, hostMonitoring, or applicationMonitoring). + description: General configuration about OneAgent instances. properties: applicationMonitoring: - description: |- - dynatrace-webhook injects into application pods based on labeled namespaces. - Has an optional CSI driver per node via DaemonSet to provide binaries to pods. + description: dynatrace-webhook injects into application pods based + on labeled namespaces. nullable: true properties: codeModulesImage: @@ -906,33 +758,22 @@ spec: Pods. type: string initResources: - description: |- - Define resources requests and limits for the initContainer. For details, see Managing resources for containers - (https://kubernetes.io/docs/concepts/configuration/manage-resources-containers). + description: Define resources requests and limits for the + initContainer. properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry + in pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -950,7 +791,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -959,26 +800,19 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of + compute resources required. type: object type: object useCSIDriver: - description: Set if you want to use the CSIDriver. Don't enable - it if you do not have access to Kubernetes nodes or if you - lack privileges. + description: Set if you want to use the CSIDriver. type: boolean version: description: The OneAgent version to be used. type: string type: object classicFullStack: - description: |- - Has a single OneAgent per node via DaemonSet. - Injection is performed via the same OneAgent DaemonSet. + description: Has a single OneAgent per node via DaemonSet. nullable: true properties: annotations: @@ -987,21 +821,18 @@ spec: description: Add custom OneAgent annotations. type: object args: - description: |- - Set additional arguments to the OneAgent installer. - For available options, see Linux custom installation (https://www.dynatrace.com/support/help/setup-and-configuration/dynatrace-oneagent/installation-and-operation/linux/installation/customize-oneagent-installation-on-linux). + description: Set additional arguments to the OneAgent installer. items: type: string type: array x-kubernetes-list-type: set autoUpdate: - description: |- - Disables automatic restarts of OneAgent pods in case a new version is available (https://www.dynatrace.com/support/help/setup-and-configuration/setup-on-container-platforms/kubernetes/get-started-with-kubernetes-monitoring#disable-auto). - Enabled by default. + description: Disables automatic restarts of OneAgent pods + in case a new version is available (https://www. type: boolean dnsPolicy: description: Set the DNS Policy for OneAgent pods. For details, - see Pods DNS Policy (https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-s-dns-policy). + see Pods DNS Policy (https://kubernetes. type: string env: description: Set additional environment variables for the @@ -1017,9 +848,7 @@ spec: value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. + using the previously defined environment variables in... type: string valueFrom: description: Source for the environment variable's value. @@ -1033,12 +862,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the ConfigMap or @@ -1049,9 +873,8 @@ spec: type: object x-kubernetes-map-type: atomic fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.' properties: apiVersion: description: Version of the schema the FieldPath @@ -1068,7 +891,7 @@ spec: resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + (limits.cpu, limits. properties: containerName: description: 'Container name: required for volumes, @@ -1099,12 +922,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the Secret or its @@ -1136,33 +954,21 @@ spec: nodes OneAgent will be deployed. type: object oneAgentResources: - description: |- - Resource settings for OneAgent container. Consumption of the OneAgent heavily depends on the workload to monitor. You can use the default settings in the CR. - Note: resource.requests shows the values needed to run; resource.limits shows the maximum limits for the pod. + description: Resource settings for OneAgent container. properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry + in pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -1180,7 +986,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -1189,54 +995,43 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of + compute resources required. type: object type: object priorityClassName: - description: |- - Assign a priority class to the OneAgent pods. By default, no class is set. - For details, see Pod Priority and Preemption (https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/). + description: Assign a priority class to the OneAgent pods. + By default, no class is set. type: string tolerations: description: Tolerations to include with the OneAgent DaemonSet. - For details, see Taints and Tolerations (https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/). items: description: |- The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . + the triple... properties: effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + description: Effect indicates the taint effect to match. + Empty means match all taint effects. type: string key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. + description: Key is the taint key that the toleration + applies to. Empty means match all taint keys. type: string operator: description: |- Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. + Valid operators are Exists and Equal. type: string tolerationSeconds: description: |- TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). + of effect NoExecute,... format: int64 type: integer value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. + description: Value is the taint value the toleration + matches to. type: string type: object type: array @@ -1245,10 +1040,7 @@ spec: type: string type: object cloudNativeFullStack: - description: |- - Has a single OneAgent per node via DaemonSet. - dynatrace-webhook injects into application pods based on labeled namespaces. - Has a CSI driver per node via DaemonSet to provide binaries to pods. + description: Has a single OneAgent per node via DaemonSet. nullable: true properties: annotations: @@ -1257,17 +1049,14 @@ spec: description: Add custom OneAgent annotations. type: object args: - description: |- - Set additional arguments to the OneAgent installer. - For available options, see Linux custom installation (https://www.dynatrace.com/support/help/setup-and-configuration/dynatrace-oneagent/installation-and-operation/linux/installation/customize-oneagent-installation-on-linux). + description: Set additional arguments to the OneAgent installer. items: type: string type: array x-kubernetes-list-type: set autoUpdate: - description: |- - Disables automatic restarts of OneAgent pods in case a new version is available (https://www.dynatrace.com/support/help/setup-and-configuration/setup-on-container-platforms/kubernetes/get-started-with-kubernetes-monitoring#disable-auto). - Enabled by default. + description: Disables automatic restarts of OneAgent pods + in case a new version is available (https://www. type: boolean codeModulesImage: description: The OneAgent image that is used to inject into @@ -1275,7 +1064,7 @@ spec: type: string dnsPolicy: description: Set the DNS Policy for OneAgent pods. For details, - see Pods DNS Policy (https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-s-dns-policy). + see Pods DNS Policy (https://kubernetes. type: string env: description: Set additional environment variables for the @@ -1291,9 +1080,7 @@ spec: value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. + using the previously defined environment variables in... type: string valueFrom: description: Source for the environment variable's value. @@ -1307,12 +1094,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the ConfigMap or @@ -1323,9 +1105,8 @@ spec: type: object x-kubernetes-map-type: atomic fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.' properties: apiVersion: description: Version of the schema the FieldPath @@ -1342,7 +1123,7 @@ spec: resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + (limits.cpu, limits. properties: containerName: description: 'Container name: required for volumes, @@ -1373,12 +1154,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the Secret or its @@ -1398,33 +1174,22 @@ spec: to the image from the Dynatrace cluster. type: string initResources: - description: |- - Define resources requests and limits for the initContainer. For details, see Managing resources for containers - (https://kubernetes.io/docs/concepts/configuration/manage-resources-containers). + description: Define resources requests and limits for the + initContainer. properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry + in pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -1442,7 +1207,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -1451,11 +1216,8 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of + compute resources required. type: object type: object labels: @@ -1471,33 +1233,21 @@ spec: nodes OneAgent will be deployed. type: object oneAgentResources: - description: |- - Resource settings for OneAgent container. Consumption of the OneAgent heavily depends on the workload to monitor. You can use the default settings in the CR. - Note: resource.requests shows the values needed to run; resource.limits shows the maximum limits for the pod. + description: Resource settings for OneAgent container. properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry + in pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -1515,7 +1265,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -1524,54 +1274,43 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of + compute resources required. type: object type: object priorityClassName: - description: |- - Assign a priority class to the OneAgent pods. By default, no class is set. - For details, see Pod Priority and Preemption (https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/). + description: Assign a priority class to the OneAgent pods. + By default, no class is set. type: string tolerations: description: Tolerations to include with the OneAgent DaemonSet. - For details, see Taints and Tolerations (https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/). items: description: |- The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . + the triple... properties: effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + description: Effect indicates the taint effect to match. + Empty means match all taint effects. type: string key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. + description: Key is the taint key that the toleration + applies to. Empty means match all taint keys. type: string operator: description: |- Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. + Valid operators are Exists and Equal. type: string tolerationSeconds: description: |- TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). + of effect NoExecute,... format: int64 type: integer value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. + description: Value is the taint value the toleration + matches to. type: string type: object type: array @@ -1594,21 +1333,18 @@ spec: description: Add custom OneAgent annotations. type: object args: - description: |- - Set additional arguments to the OneAgent installer. - For available options, see Linux custom installation (https://www.dynatrace.com/support/help/setup-and-configuration/dynatrace-oneagent/installation-and-operation/linux/installation/customize-oneagent-installation-on-linux). + description: Set additional arguments to the OneAgent installer. items: type: string type: array x-kubernetes-list-type: set autoUpdate: - description: |- - Disables automatic restarts of OneAgent pods in case a new version is available (https://www.dynatrace.com/support/help/setup-and-configuration/setup-on-container-platforms/kubernetes/get-started-with-kubernetes-monitoring#disable-auto). - Enabled by default. + description: Disables automatic restarts of OneAgent pods + in case a new version is available (https://www. type: boolean dnsPolicy: description: Set the DNS Policy for OneAgent pods. For details, - see Pods DNS Policy (https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-s-dns-policy). + see Pods DNS Policy (https://kubernetes. type: string env: description: Set additional environment variables for the @@ -1624,9 +1360,7 @@ spec: value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. + using the previously defined environment variables in... type: string valueFrom: description: Source for the environment variable's value. @@ -1640,12 +1374,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the ConfigMap or @@ -1656,9 +1385,8 @@ spec: type: object x-kubernetes-map-type: atomic fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.' properties: apiVersion: description: Version of the schema the FieldPath @@ -1675,7 +1403,7 @@ spec: resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + (limits.cpu, limits. properties: containerName: description: 'Container name: required for volumes, @@ -1706,12 +1434,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the Secret or its @@ -1743,33 +1466,21 @@ spec: nodes OneAgent will be deployed. type: object oneAgentResources: - description: |- - Resource settings for OneAgent container. Consumption of the OneAgent heavily depends on the workload to monitor. You can use the default settings in the CR. - Note: resource.requests shows the values needed to run; resource.limits shows the maximum limits for the pod. + description: Resource settings for OneAgent container. properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry + in pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -1787,7 +1498,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -1796,54 +1507,43 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of + compute resources required. type: object type: object priorityClassName: - description: |- - Assign a priority class to the OneAgent pods. By default, no class is set. - For details, see Pod Priority and Preemption (https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/). + description: Assign a priority class to the OneAgent pods. + By default, no class is set. type: string tolerations: description: Tolerations to include with the OneAgent DaemonSet. - For details, see Taints and Tolerations (https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/). items: description: |- The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . + the triple... properties: effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + description: Effect indicates the taint effect to match. + Empty means match all taint effects. type: string key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. + description: Key is the taint key that the toleration + applies to. Empty means match all taint keys. type: string operator: description: |- Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. + Valid operators are Exists and Equal. type: string tolerationSeconds: description: |- TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). + of effect NoExecute,... format: int64 type: integer value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. + description: Value is the taint value the toleration + matches to. type: string type: object type: array @@ -1853,9 +1553,8 @@ spec: type: object type: object proxy: - description: |- - Set custom proxy settings either directly or from a secret with the field proxy. - Note: Applies to Dynatrace Operator, ActiveGate, and OneAgents. + description: Set custom proxy settings either directly or from a secret + with the field proxy. properties: value: description: Proxy URL. It has preference over ValueFrom. @@ -1872,7 +1571,7 @@ spec: customProperties: description: |- Add a custom properties file by providing it as a value or reference it from a secret - If referenced from a secret, make sure the key is called 'customProperties' + If referenced... properties: value: description: Custom properties value. @@ -1899,9 +1598,7 @@ spec: value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. + using the previously defined environment variables in... type: string valueFrom: description: Source for the environment variable's value. @@ -1915,12 +1612,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the ConfigMap or its @@ -1931,9 +1623,8 @@ spec: type: object x-kubernetes-map-type: atomic fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + description: 'Selects a field of the pod: supports metadata.name, + metadata.namespace, `metadata.' properties: apiVersion: description: Version of the schema the FieldPath @@ -1950,7 +1641,7 @@ spec: resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + (limits.cpu, limits. properties: containerName: description: 'Container name: required for volumes, @@ -1981,12 +1672,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the Secret or its key @@ -2005,8 +1691,7 @@ spec: description: Set activation group for ActiveGate type: string image: - description: The ActiveGate container image. Defaults to the latest - ActiveGate image provided by the registry on the tenant + description: The ActiveGate container image. type: string labels: additionalProperties: @@ -2027,28 +1712,18 @@ spec: pods properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry in + pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -2066,7 +1741,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -2075,11 +1750,8 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of compute + resources required. type: object type: object tolerations: @@ -2087,36 +1759,30 @@ spec: items: description: |- The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . + the triple... properties: effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + description: Effect indicates the taint effect to match. + Empty means match all taint effects. type: string key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. + description: Key is the taint key that the toleration applies + to. Empty means match all taint keys. type: string operator: description: |- Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. + Valid operators are Exists and Equal. type: string tolerationSeconds: description: |- TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). + of effect NoExecute,... format: int64 type: integer value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. + description: Value is the taint value the toleration matches + to. type: string type: object type: array @@ -2128,34 +1794,25 @@ spec: matching pods among the given topology. properties: labelSelector: - description: |- - LabelSelector is used to find matching pods. - Pods that match this label selector are counted to determine the number of pods - in their corresponding topology domain. + description: LabelSelector is used to find matching pods. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. + description: A label selector requirement is a selector + that contains values, a key, and an operator that... properties: key: description: key is the label key that the selector applies to. type: string operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. + description: operator represents a key's relationship + to a set of values. type: string values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. + description: values is an array of string values. items: type: string type: array @@ -2169,62 +1826,45 @@ spec: matchLabels: additionalProperties: type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. + description: matchLabels is a map of {key,value} pairs. type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: description: |- MatchLabelKeys is a set of pod label keys to select the pods over which - spreading will be calculated. + spreading will be... items: type: string type: array x-kubernetes-list-type: atomic maxSkew: - description: |- - MaxSkew describes the degree to which pods may be unevenly distributed. - When `whenUnsatisfiable=DoNotSchedule`, it is the maximum permitted difference - between the number of matching pods in the target topology and the global minimum. + description: MaxSkew describes the degree to which pods + may be unevenly distributed. format: int32 type: integer minDomains: - description: |- - MinDomains indicates a minimum number of eligible domains. - When the number of eligible domains with matching topology keys is less than minDomains, - Pod Topology Spread treats "global minimum" as 0, and then the calculation of Skew is performed. + description: MinDomains indicates a minimum number of eligible + domains. format: int32 type: integer nodeAffinityPolicy: description: |- NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector - when calculating pod topology spread skew. Options are: - - Honor: only nodes matching nodeAffinity/nodeSelector are included in the calculations. - - Ignore: nodeAffinity/nodeSelector are ignored. + when calculating pod... type: string nodeTaintsPolicy: description: |- NodeTaintsPolicy indicates how we will treat node taints when calculating - pod topology spread skew. Options are: - - Honor: nodes without taints, along with tainted nodes for which the incoming pod - has a toleration, are included. - - Ignore: node taints are ignored. All nodes are included. + pod topology spread skew. type: string topologyKey: - description: |- - TopologyKey is the key of node labels. Nodes that have a label with this key - and identical values are considered to be in the same topology. - We consider each as a "bucket", and try to put balanced number - of pods into each bucket. + description: TopologyKey is the key of node labels. type: string whenUnsatisfiable: description: |- WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy the spread constraint. - - DoNotSchedule (default) tells the scheduler not to schedule it. type: string required: - maxSkew @@ -2234,18 +1874,16 @@ spec: type: array type: object skipCertCheck: - description: |- - Disable certificate check for the connection between Dynatrace Operator and the Dynatrace Cluster. - Set to true if you want to skip certification validation checks. + description: Disable certificate check for the connection between + Dynatrace Operator and the Dynatrace Cluster. type: boolean tokens: description: Name of the secret holding the tokens used for connecting to Dynatrace. type: string trustedCAs: - description: |- - Adds custom RootCAs from a configmap. Put the certificate under certs within your configmap. - Note: Applies to Dynatrace Operator, OneAgent and ActiveGate. + description: Adds custom RootCAs from a configmap. Put the certificate + under certs within your configmap. type: string required: - apiUrl @@ -2266,6 +1904,9 @@ spec: description: Time of the last connection request format: date-time type: string + tenantTokenHash: + description: Hash of the tenant token + type: string tenantUUID: description: UUID of the tenant, received from the tenant type: string @@ -2325,31 +1966,24 @@ spec: state of this API Resource. properties: lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + description: lastTransitionTime is the last time the condition + transitioned from one status to another. format: date-time type: string message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. + description: message is a human readable message indicating + details about the transition. maxLength: 32768 type: string observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. format: int64 minimum: 0 type: integer reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. maxLength: 1024 minLength: 1 pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ @@ -2420,6 +2054,9 @@ spec: description: Time of the last connection request format: date-time type: string + tenantTokenHash: + description: Hash of the tenant token + type: string tenantUUID: description: UUID of the tenant, received from the tenant type: string @@ -2494,19 +2131,12 @@ spec: description: DynaKube is the Schema for the DynaKube API properties: apiVersion: - 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 + description: APIVersion defines the versioned schema of this representation + of an object. type: string kind: - 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 + description: Kind is a string value representing the REST resource this + object represents. type: string metadata: type: object @@ -2523,17 +2153,14 @@ spec: type: object capabilities: description: "Defines the ActiveGate pod capabilities\nPossible - values:\n\t- `routing` enables OneAgent routing.\n\t- `kubernetes-monitoring` - enables Kubernetes API monitoring.\n\t- `metrics-ingest` opens - the metrics ingest endpoint on the DynaKube ActiveGate and redirects - all pods to it." + values:\n\t- `routing` enables OneAgent routing." items: type: string type: array customProperties: description: |- Add a custom properties file by providing it as a value or reference it from a secret - If referenced from a secret, make sure the key is called `customProperties` + If referenced... properties: value: description: Custom properties value. @@ -2560,9 +2187,7 @@ spec: value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. + using the previously defined environment variables in... type: string valueFrom: description: Source for the environment variable's value. @@ -2576,12 +2201,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the ConfigMap or its @@ -2592,9 +2212,8 @@ spec: type: object x-kubernetes-map-type: atomic fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + description: 'Selects a field of the pod: supports metadata.name, + metadata.namespace, `metadata.' properties: apiVersion: description: Version of the schema the FieldPath @@ -2611,7 +2230,7 @@ spec: resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + (limits.cpu, limits. properties: containerName: description: 'Container name: required for volumes, @@ -2642,12 +2261,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the Secret or its key @@ -2666,8 +2280,7 @@ spec: description: Set activation group for ActiveGate type: string image: - description: Use a custom ActiveGate image. Defaults to the latest - ActiveGate image provided by the registry on the tenant + description: Use a custom ActiveGate image. type: string labels: additionalProperties: @@ -2682,9 +2295,8 @@ spec: nodes ActiveGate will be deployed. type: object priorityClassName: - description: |- - Assign a priority class to the ActiveGate pods. By default, no class is set. - For details, see Pod Priority and Preemption. (https://dt-url.net/n8437bl) + description: Assign a priority class to the ActiveGate pods. By + default, no class is set. type: string replicas: default: 1 @@ -2692,33 +2304,21 @@ spec: format: int32 type: integer resources: - description: |- - Resource settings for ActiveGate container. - Consumption of the ActiveGate heavily depends on the workload to monitor. Adjust values accordingly. + description: Resource settings for ActiveGate container. properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry in + pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -2736,7 +2336,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -2745,54 +2345,43 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of compute + resources required. type: object type: object tlsSecretName: - description: |- - The name of a secret containing ActiveGate TLS cert+key and password. If not set, self-signed certificate is used. - `server.p12`: certificate+key pair in pkcs12 format - `password`: passphrase to read server.p12 + description: The name of a secret containing ActiveGate TLS cert+key + and password. type: string tolerations: description: Set tolerations for the ActiveGate pods items: description: |- The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . + the triple... properties: effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + description: Effect indicates the taint effect to match. + Empty means match all taint effects. type: string key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. + description: Key is the taint key that the toleration applies + to. Empty means match all taint keys. type: string operator: description: |- Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. + Valid operators are Exists and Equal. type: string tolerationSeconds: description: |- TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). + of effect NoExecute,... format: int64 type: integer value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. + description: Value is the taint value the toleration matches + to. type: string type: object type: array @@ -2804,34 +2393,25 @@ spec: matching pods among the given topology. properties: labelSelector: - description: |- - LabelSelector is used to find matching pods. - Pods that match this label selector are counted to determine the number of pods - in their corresponding topology domain. + description: LabelSelector is used to find matching pods. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. + description: A label selector requirement is a selector + that contains values, a key, and an operator that... properties: key: description: key is the label key that the selector applies to. type: string operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. + description: operator represents a key's relationship + to a set of values. type: string values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. + description: values is an array of string values. items: type: string type: array @@ -2845,62 +2425,45 @@ spec: matchLabels: additionalProperties: type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. + description: matchLabels is a map of {key,value} pairs. type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: description: |- MatchLabelKeys is a set of pod label keys to select the pods over which - spreading will be calculated. + spreading will be... items: type: string type: array x-kubernetes-list-type: atomic maxSkew: - description: |- - MaxSkew describes the degree to which pods may be unevenly distributed. - When `whenUnsatisfiable=DoNotSchedule`, it is the maximum permitted difference - between the number of matching pods in the target topology and the global minimum. + description: MaxSkew describes the degree to which pods + may be unevenly distributed. format: int32 type: integer minDomains: - description: |- - MinDomains indicates a minimum number of eligible domains. - When the number of eligible domains with matching topology keys is less than minDomains, - Pod Topology Spread treats "global minimum" as 0, and then the calculation of Skew is performed. + description: MinDomains indicates a minimum number of eligible + domains. format: int32 type: integer nodeAffinityPolicy: description: |- NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector - when calculating pod topology spread skew. Options are: - - Honor: only nodes matching nodeAffinity/nodeSelector are included in the calculations. - - Ignore: nodeAffinity/nodeSelector are ignored. + when calculating pod... type: string nodeTaintsPolicy: description: |- NodeTaintsPolicy indicates how we will treat node taints when calculating - pod topology spread skew. Options are: - - Honor: nodes without taints, along with tainted nodes for which the incoming pod - has a toleration, are included. - - Ignore: node taints are ignored. All nodes are included. + pod topology spread skew. type: string topologyKey: - description: |- - TopologyKey is the key of node labels. Nodes that have a label with this key - and identical values are considered to be in the same topology. - We consider each as a "bucket", and try to put balanced number - of pods into each bucket. + description: TopologyKey is the key of node labels. type: string whenUnsatisfiable: description: |- WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy the spread constraint. - - DoNotSchedule (default) tells the scheduler not to schedule it. type: string required: - maxSkew @@ -2910,26 +2473,20 @@ spec: type: array type: object apiUrl: - description: |- - Dynatrace `apiUrl`, including the `/api` path at the end. - - For SaaS, set `YOUR_ENVIRONMENT_ID` to your environment ID. - - For Managed, change the `apiUrl` address. - For instructions on how to determine the environment ID and how to configure the apiUrl address, see Environment ID (https://www. + description: Dynatrace `apiUrl`, including the `/api` path at the + end. type: string customPullSecret: - description: |- - Defines a custom pull secret in case you use a private registry when pulling images from the Dynatrace environment. - To define a custom pull secret and learn about the expected behavior, see Configure customPullSecret - (https://www.dynatrace. + description: Defines a custom pull secret in case you use a private + registry when pulling images from the... type: string dynatraceApiRequestThreshold: default: 15 description: Minimum minutes between Dynatrace API requests. type: integer enableIstio: - description: |- - When enabled, and if Istio is installed in the Kubernetes environment, Dynatrace Operator will create the corresponding VirtualService and ServiceEntry objects to allow access to the Dynatrace Cluster from the OneAgent or ActiveGate. - Disabled by default. + description: When enabled, and if Istio is installed in the Kubernetes + environment, Dynatrace Operator will... type: boolean metadataEnrichment: description: Configuration for Metadata Enrichment. @@ -2946,25 +2503,19 @@ spec: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. + description: A label selector requirement is a selector + that contains values, a key, and an operator that... properties: key: description: key is the label key that the selector applies to. type: string operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. + description: operator represents a key's relationship + to a set of values. type: string values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. + description: values is an array of string values. items: type: string type: array @@ -2978,10 +2529,7 @@ spec: matchLabels: additionalProperties: type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. + description: matchLabels is a map of {key,value} pairs. type: object type: object x-kubernetes-map-type: atomic @@ -2992,14 +2540,11 @@ spec: description: Sets a network zone for the OneAgent and ActiveGate pods. type: string oneAgent: - description: |- - General configuration about OneAgent instances. - You can't enable more than one module (classicFullStack, cloudNativeFullStack, hostMonitoring, or applicationMonitoring). + description: General configuration about OneAgent instances. properties: applicationMonitoring: - description: |- - dynatrace-webhook injects into application pods based on labeled namespaces. - Has an optional CSI driver per node via DaemonSet to provide binaries to pods. + description: dynatrace-webhook injects into application pods based + on labeled namespaces. nullable: true properties: codeModulesImage: @@ -3007,33 +2552,22 @@ spec: Pods. type: string initResources: - description: |- - Define resources requests and limits for the initContainer. For details, see Managing resources for containers - (https://kubernetes.io/docs/concepts/configuration/manage-resources-containers). + description: Define resources requests and limits for the + initContainer. properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry + in pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -3051,7 +2585,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -3060,41 +2594,31 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of + compute resources required. type: object type: object namespaceSelector: - description: |- - Applicable only for applicationMonitoring or cloudNativeFullStack configuration types. The namespaces where you want Dynatrace Operator to inject. - For more information, see Configure monitoring for namespaces and pods (https://www.dynatrace. + description: Applicable only for applicationMonitoring or + cloudNativeFullStack configuration types. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. + description: A label selector requirement is a selector + that contains values, a key, and an operator that... properties: key: description: key is the label key that the selector applies to. type: string operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. + description: operator represents a key's relationship + to a set of values. type: string values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. + description: values is an array of string values. items: type: string type: array @@ -3108,27 +2632,20 @@ spec: matchLabels: additionalProperties: type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. + description: matchLabels is a map of {key,value} pairs. type: object type: object x-kubernetes-map-type: atomic useCSIDriver: default: false - description: Set if you want to use the CSIDriver. Don't enable - it if you do not have access to Kubernetes nodes or if you - lack privileges. + description: Set if you want to use the CSIDriver. type: boolean version: description: The OneAgent version to be used. type: string type: object classicFullStack: - description: |- - Has a single OneAgent per node via DaemonSet. - Injection is performed via the same OneAgent DaemonSet. + description: Has a single OneAgent per node via DaemonSet. nullable: true properties: annotations: @@ -3137,22 +2654,19 @@ spec: description: Add custom OneAgent annotations. type: object args: - description: |- - Set additional arguments to the OneAgent installer. - For available options, see Linux custom installation (https://www.dynatrace.com/support/help/setup-and-configuration/dynatrace-oneagent/installation-and-operation/linux/installation/customize-oneagent-installation-on-linux). + description: Set additional arguments to the OneAgent installer. items: type: string type: array x-kubernetes-list-type: set autoUpdate: default: true - description: |- - Disables automatic restarts of OneAgent pods in case a new version is available (https://www.dynatrace.com/support/help/setup-and-configuration/setup-on-container-platforms/kubernetes/get-started-with-kubernetes-monitoring#disable-auto). - Enabled by default. + description: Disables automatic restarts of OneAgent pods + in case a new version is available (https://www. type: boolean dnsPolicy: description: Set the DNS Policy for OneAgent pods. For details, - see Pods DNS Policy (https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-s-dns-policy). + see Pods DNS Policy (https://kubernetes. type: string env: description: Set additional environment variables for the @@ -3168,9 +2682,7 @@ spec: value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. + using the previously defined environment variables in... type: string valueFrom: description: Source for the environment variable's value. @@ -3184,12 +2696,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the ConfigMap or @@ -3200,9 +2707,8 @@ spec: type: object x-kubernetes-map-type: atomic fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.' properties: apiVersion: description: Version of the schema the FieldPath @@ -3219,7 +2725,7 @@ spec: resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + (limits.cpu, limits. properties: containerName: description: 'Container name: required for volumes, @@ -3250,12 +2756,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the Secret or its @@ -3287,34 +2788,21 @@ spec: nodes OneAgent will be deployed. type: object oneAgentResources: - description: |- - Resource settings for OneAgent container. Consumption of the OneAgent heavily depends on the workload to monitor. You can use the default settings in the CR. - - `resource.requests` shows the values needed to run - - `resource.limits` shows the maximum limits for the pod + description: Resource settings for OneAgent container. properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry + in pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -3332,7 +2820,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -3341,17 +2829,13 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of + compute resources required. type: object type: object priorityClassName: - description: |- - Assign a priority class to the OneAgent pods. By default, no class is set. - For details, see Pod Priority and Preemption (https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/). + description: Assign a priority class to the OneAgent pods. + By default, no class is set. type: string secCompProfile: description: The SecComp Profile that will be configured in @@ -3359,54 +2843,43 @@ spec: type: string tolerations: description: Tolerations to include with the OneAgent DaemonSet. - For details, see Taints and Tolerations (https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/). items: description: |- The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . + the triple... properties: effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + description: Effect indicates the taint effect to match. + Empty means match all taint effects. type: string key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. + description: Key is the taint key that the toleration + applies to. Empty means match all taint keys. type: string operator: description: |- Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. + Valid operators are Exists and Equal. type: string tolerationSeconds: description: |- TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). + of effect NoExecute,... format: int64 type: integer value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. + description: Value is the taint value the toleration + matches to. type: string type: object type: array version: description: The OneAgent version to be used for OneAgents - running in the dedicated pod. This setting doesn't affect - the OneAgent version used for application monitoring. + running in the dedicated pod. type: string type: object cloudNativeFullStack: - description: |- - Has a single OneAgent per node via DaemonSet. - dynatrace-webhook injects into application pods based on labeled namespaces. - Has a CSI driver per node via DaemonSet to provide binaries to pods. + description: Has a single OneAgent per node via DaemonSet. nullable: true properties: annotations: @@ -3415,18 +2888,15 @@ spec: description: Add custom OneAgent annotations. type: object args: - description: |- - Set additional arguments to the OneAgent installer. - For available options, see Linux custom installation (https://www.dynatrace.com/support/help/setup-and-configuration/dynatrace-oneagent/installation-and-operation/linux/installation/customize-oneagent-installation-on-linux). + description: Set additional arguments to the OneAgent installer. items: type: string type: array x-kubernetes-list-type: set autoUpdate: default: true - description: |- - Disables automatic restarts of OneAgent pods in case a new version is available (https://www.dynatrace.com/support/help/setup-and-configuration/setup-on-container-platforms/kubernetes/get-started-with-kubernetes-monitoring#disable-auto). - Enabled by default. + description: Disables automatic restarts of OneAgent pods + in case a new version is available (https://www. type: boolean codeModulesImage: description: The OneAgent image that is used to inject into @@ -3434,7 +2904,7 @@ spec: type: string dnsPolicy: description: Set the DNS Policy for OneAgent pods. For details, - see Pods DNS Policy (https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-s-dns-policy). + see Pods DNS Policy (https://kubernetes. type: string env: description: Set additional environment variables for the @@ -3450,9 +2920,7 @@ spec: value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. + using the previously defined environment variables in... type: string valueFrom: description: Source for the environment variable's value. @@ -3466,12 +2934,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the ConfigMap or @@ -3482,9 +2945,8 @@ spec: type: object x-kubernetes-map-type: atomic fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.' properties: apiVersion: description: Version of the schema the FieldPath @@ -3501,7 +2963,7 @@ spec: resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + (limits.cpu, limits. properties: containerName: description: 'Container name: required for volumes, @@ -3532,12 +2994,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the Secret or its @@ -3557,33 +3014,22 @@ spec: to the image from the Dynatrace cluster. type: string initResources: - description: |- - Define resources requests and limits for the initContainer. For details, see Managing resources for containers - (https://kubernetes.io/docs/concepts/configuration/manage-resources-containers). + description: Define resources requests and limits for the + initContainer. properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry + in pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -3601,7 +3047,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -3610,11 +3056,8 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of + compute resources required. type: object type: object labels: @@ -3624,33 +3067,26 @@ spec: to structure workloads as desired. type: object namespaceSelector: - description: |- - Applicable only for applicationMonitoring or cloudNativeFullStack configuration types. The namespaces where you want Dynatrace Operator to inject. - For more information, see Configure monitoring for namespaces and pods (https://www.dynatrace. + description: Applicable only for applicationMonitoring or + cloudNativeFullStack configuration types. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. + description: A label selector requirement is a selector + that contains values, a key, and an operator that... properties: key: description: key is the label key that the selector applies to. type: string operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. + description: operator represents a key's relationship + to a set of values. type: string values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. + description: values is an array of string values. items: type: string type: array @@ -3664,10 +3100,7 @@ spec: matchLabels: additionalProperties: type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. + description: matchLabels is a map of {key,value} pairs. type: object type: object x-kubernetes-map-type: atomic @@ -3678,34 +3111,21 @@ spec: nodes OneAgent will be deployed. type: object oneAgentResources: - description: |- - Resource settings for OneAgent container. Consumption of the OneAgent heavily depends on the workload to monitor. You can use the default settings in the CR. - - `resource.requests` shows the values needed to run - - `resource.limits` shows the maximum limits for the pod + description: Resource settings for OneAgent container. properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry + in pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -3723,7 +3143,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -3732,17 +3152,13 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of + compute resources required. type: object type: object priorityClassName: - description: |- - Assign a priority class to the OneAgent pods. By default, no class is set. - For details, see Pod Priority and Preemption (https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/). + description: Assign a priority class to the OneAgent pods. + By default, no class is set. type: string secCompProfile: description: The SecComp Profile that will be configured in @@ -3750,54 +3166,44 @@ spec: type: string tolerations: description: Tolerations to include with the OneAgent DaemonSet. - For details, see Taints and Tolerations (https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/). items: description: |- The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . + the triple... properties: effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + description: Effect indicates the taint effect to match. + Empty means match all taint effects. type: string key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. + description: Key is the taint key that the toleration + applies to. Empty means match all taint keys. type: string operator: description: |- Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. + Valid operators are Exists and Equal. type: string tolerationSeconds: description: |- TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). + of effect NoExecute,... format: int64 type: integer value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. + description: Value is the taint value the toleration + matches to. type: string type: object type: array version: description: The OneAgent version to be used for OneAgents - running in the dedicated pod. This setting doesn't affect - the OneAgent version used for application monitoring. + running in the dedicated pod. type: string type: object hostGroup: - description: |- - Specify the name of the group to which you want to assign the host. - This method is preferred over the now obsolete `--set-host-group` argument. - If both settings are used, this field takes precedence over the `--set-host-group` argument. + description: Specify the name of the group to which you want to + assign the host. type: string hostMonitoring: description: |- @@ -3811,22 +3217,19 @@ spec: description: Add custom OneAgent annotations. type: object args: - description: |- - Set additional arguments to the OneAgent installer. - For available options, see Linux custom installation (https://www.dynatrace.com/support/help/setup-and-configuration/dynatrace-oneagent/installation-and-operation/linux/installation/customize-oneagent-installation-on-linux). + description: Set additional arguments to the OneAgent installer. items: type: string type: array x-kubernetes-list-type: set autoUpdate: default: true - description: |- - Disables automatic restarts of OneAgent pods in case a new version is available (https://www.dynatrace.com/support/help/setup-and-configuration/setup-on-container-platforms/kubernetes/get-started-with-kubernetes-monitoring#disable-auto). - Enabled by default. + description: Disables automatic restarts of OneAgent pods + in case a new version is available (https://www. type: boolean dnsPolicy: description: Set the DNS Policy for OneAgent pods. For details, - see Pods DNS Policy (https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-s-dns-policy). + see Pods DNS Policy (https://kubernetes. type: string env: description: Set additional environment variables for the @@ -3842,9 +3245,7 @@ spec: value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. + using the previously defined environment variables in... type: string valueFrom: description: Source for the environment variable's value. @@ -3858,12 +3259,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the ConfigMap or @@ -3874,9 +3270,8 @@ spec: type: object x-kubernetes-map-type: atomic fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.' properties: apiVersion: description: Version of the schema the FieldPath @@ -3893,7 +3288,7 @@ spec: resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + (limits.cpu, limits. properties: containerName: description: 'Container name: required for volumes, @@ -3924,12 +3319,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the Secret or its @@ -3961,34 +3351,21 @@ spec: nodes OneAgent will be deployed. type: object oneAgentResources: - description: |- - Resource settings for OneAgent container. Consumption of the OneAgent heavily depends on the workload to monitor. You can use the default settings in the CR. - - `resource.requests` shows the values needed to run - - `resource.limits` shows the maximum limits for the pod + description: Resource settings for OneAgent container. properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry + in pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -4006,7 +3383,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -4015,17 +3392,13 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of + compute resources required. type: object type: object priorityClassName: - description: |- - Assign a priority class to the OneAgent pods. By default, no class is set. - For details, see Pod Priority and Preemption (https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/). + description: Assign a priority class to the OneAgent pods. + By default, no class is set. type: string secCompProfile: description: The SecComp Profile that will be configured in @@ -4033,54 +3406,45 @@ spec: type: string tolerations: description: Tolerations to include with the OneAgent DaemonSet. - For details, see Taints and Tolerations (https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/). items: description: |- The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . + the triple... properties: effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + description: Effect indicates the taint effect to match. + Empty means match all taint effects. type: string key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. + description: Key is the taint key that the toleration + applies to. Empty means match all taint keys. type: string operator: description: |- Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. + Valid operators are Exists and Equal. type: string tolerationSeconds: description: |- TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). + of effect NoExecute,... format: int64 type: integer value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. + description: Value is the taint value the toleration + matches to. type: string type: object type: array version: description: The OneAgent version to be used for OneAgents - running in the dedicated pod. This setting doesn't affect - the OneAgent version used for application monitoring. + running in the dedicated pod. type: string type: object type: object proxy: - description: |- - Set custom proxy settings either directly or from a secret with the field `proxy`. - Applies to Dynatrace Operator, ActiveGate, and OneAgents. + description: Set custom proxy settings either directly or from a secret + with the field `proxy`. properties: value: description: Proxy URL. It has preference over ValueFrom. @@ -4092,9 +3456,8 @@ spec: type: string type: object skipCertCheck: - description: |- - Disable certificate check for the connection between Dynatrace Operator and the Dynatrace Cluster. - Set to `true` if you want to skip certification validation checks. + description: Disable certificate check for the connection between + Dynatrace Operator and the Dynatrace Cluster. type: boolean tokens: description: Name of the secret holding the tokens used for connecting @@ -4104,7 +3467,6 @@ spec: description: |- Adds custom RootCAs from a configmap. The key to the data must be `certs`. - This applies to both the Dynatrace Operator and the OneAgent. Doesn't apply to ActiveGate. type: string required: - apiUrl @@ -4125,6 +3487,9 @@ spec: description: Time of the last connection request format: date-time type: string + tenantTokenHash: + description: Hash of the tenant token + type: string tenantUUID: description: UUID of the tenant, received from the tenant type: string @@ -4184,31 +3549,24 @@ spec: state of this API Resource. properties: lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + description: lastTransitionTime is the last time the condition + transitioned from one status to another. format: date-time type: string message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. + description: message is a human readable message indicating + details about the transition. maxLength: 32768 type: string observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. format: int64 minimum: 0 type: integer reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. maxLength: 1024 minLength: 1 pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ @@ -4251,8 +3609,7 @@ spec: type: string kubernetesClusterName: description: KubernetesClusterName contains the display name (also - know as label) of the monitored entity that points to the Kubernetes - cluster + know as label) of the monitored entity that... type: string metadataEnrichment: description: Observed state of Metadata-Enrichment @@ -4300,6 +3657,9 @@ spec: description: Time of the last connection request format: date-time type: string + tenantTokenHash: + description: Hash of the tenant token + type: string tenantUUID: description: UUID of the tenant, received from the tenant type: string @@ -4374,19 +3734,12 @@ spec: description: DynaKube is the Schema for the DynaKube API properties: apiVersion: - 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 + description: APIVersion defines the versioned schema of this representation + of an object. type: string kind: - 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 + description: Kind is a string value representing the REST resource this + object represents. type: string metadata: type: object @@ -4410,7 +3763,7 @@ spec: customProperties: description: |- Add a custom properties file by providing it as a value or reference it from a secret - If referenced from a secret, make sure the key is called 'customProperties' + If referenced... properties: value: description: Raw value for given property. @@ -4437,9 +3790,7 @@ spec: value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. + using the previously defined environment variables in... type: string valueFrom: description: Source for the environment variable's value. @@ -4453,12 +3804,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the ConfigMap or its @@ -4469,9 +3815,8 @@ spec: type: object x-kubernetes-map-type: atomic fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + description: 'Selects a field of the pod: supports metadata.name, + metadata.namespace, `metadata.' properties: apiVersion: description: Version of the schema the FieldPath @@ -4488,7 +3833,7 @@ spec: resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + (limits.cpu, limits. properties: containerName: description: 'Container name: required for volumes, @@ -4519,12 +3864,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the Secret or its key @@ -4543,8 +3883,7 @@ spec: description: Set activation group for ActiveGate type: string image: - description: The ActiveGate container image. Defaults to the latest - ActiveGate image provided by the registry on the tenant + description: The ActiveGate container image. type: string labels: additionalProperties: @@ -4557,9 +3896,7 @@ spec: description: Node selector to control the selection of nodes type: object priorityClassName: - description: |- - If specified, indicates the pod's priority. Name must be defined by creating a PriorityClass object with that - name. If not specified the setting will be removed from the StatefulSet. + description: If specified, indicates the pod's priority. type: string replicas: description: Amount of replicas for your ActiveGates @@ -4570,28 +3907,18 @@ spec: pods properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry in + pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -4609,9 +3936,2853 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount of compute + resources required. + type: object + type: object + tlsSecretName: + description: The name of a secret containing ActiveGate TLS cert+key + and password. + type: string + tolerations: + description: Set tolerations for the ActiveGate pods + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple... + properties: + effect: + description: Effect indicates the taint effect to match. + Empty means match all taint effects. + type: string + key: + description: Key is the taint key that the toleration applies + to. Empty means match all taint keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute,... + format: int64 + type: integer + value: + description: Value is the taint value the toleration matches + to. + type: string + type: object + type: array + topologySpreadConstraints: + description: Adds TopologySpreadConstraints for the ActiveGate + pods + items: + description: TopologySpreadConstraint specifies how to spread + matching pods among the given topology. + properties: + labelSelector: + description: LabelSelector is used to find matching pods. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that... + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. + type: string + values: + description: values is an array of string values. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select the pods over which + spreading will be... + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + description: MaxSkew describes the degree to which pods + may be unevenly distributed. + format: int32 + type: integer + minDomains: + description: MinDomains indicates a minimum number of eligible + domains. + format: int32 + type: integer + nodeAffinityPolicy: + description: |- + NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector + when calculating pod... + type: string + nodeTaintsPolicy: + description: |- + NodeTaintsPolicy indicates how we will treat node taints when calculating + pod topology spread skew. + type: string + topologyKey: + description: TopologyKey is the key of node labels. + type: string + whenUnsatisfiable: + description: |- + WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy + the spread constraint. + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + type: object + apiUrl: + description: Dynatrace apiUrl, including the /api path at the end. + maxLength: 128 + type: string + customPullSecret: + description: Defines a custom pull secret in case you use a private + registry when pulling images from the... + type: string + dynatraceApiRequestThreshold: + description: Configuration for thresholding Dynatrace API requests. + type: integer + enableIstio: + description: When enabled, and if Istio is installed on the Kubernetes + environment, Dynatrace Operator will... + type: boolean + extensions: + description: When an (empty) ExtensionsSpec is provided, the extensions + related components (extensions... + type: object + kspm: + description: General configuration about the KSPM feature. + type: object + logMonitoring: + description: General configuration about the LogMonitoring feature. + properties: + ingestRuleMatchers: + items: + properties: + attribute: + type: string + values: + items: + type: string + type: array + type: object + type: array + type: object + metadataEnrichment: + description: Configuration for Metadata Enrichment. + properties: + enabled: + description: Enables MetadataEnrichment, `false` by default. + type: boolean + namespaceSelector: + description: The namespaces where you want Dynatrace Operator + to inject enrichment. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that... + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. + type: string + values: + description: values is an array of string values. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + type: object + networkZone: + description: Sets a network zone for the OneAgent and ActiveGate pods. + type: string + oneAgent: + description: General configuration about OneAgent instances. + properties: + applicationMonitoring: + description: dynatrace-webhook injects into application pods based + on labeled namespaces. + nullable: true + properties: + codeModulesImage: + description: Use a custom OneAgent CodeModule image to download + binaries. + type: string + initResources: + description: Define resources requests and limits for the + initContainer. + properties: + claims: + description: Claims lists the names of resources, defined + in spec. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: Name must match the name of one entry + in pod.spec. + type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount of + compute resources required. + type: object + type: object + namespaceSelector: + description: Applicable only for applicationMonitoring or + cloudNativeFullStack configuration types. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that... + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. + type: string + values: + description: values is an array of string values. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + version: + description: Use a specific OneAgent CodeModule version. + type: string + type: object + classicFullStack: + description: Has a single OneAgent per node via DaemonSet. + nullable: true + properties: + annotations: + additionalProperties: + type: string + description: Add custom OneAgent annotations. + type: object + args: + description: Set additional arguments to the OneAgent installer. + items: + type: string + type: array + x-kubernetes-list-type: set + autoUpdate: + description: Disables automatic restarts of OneAgent pods + in case a new version is available (https://www. + type: boolean + dnsPolicy: + description: Set the DNS Policy for OneAgent pods. For details, + see Pods DNS Policy (https://kubernetes. + type: string + env: + description: Set additional environment variables for the + OneAgent pods. + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must + be a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in... + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: Name of the referent. + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in + the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of + the exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: Name of the referent. + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + description: Use a custom OneAgent image. Defaults to the + latest image from the Dynatrace cluster. + type: string + labels: + additionalProperties: + type: string + description: Your defined labels for OneAgent pods in order + to structure workloads as desired. + type: object + nodeSelector: + additionalProperties: + type: string + description: Specify the node selector that controls on which + nodes OneAgent will be deployed. + type: object + oneAgentResources: + description: Resource settings for OneAgent container. + properties: + claims: + description: Claims lists the names of resources, defined + in spec. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: Name must match the name of one entry + in pod.spec. + type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount of + compute resources required. + type: object + type: object + priorityClassName: + description: Assign a priority class to the OneAgent pods. + By default, no class is set. + type: string + secCompProfile: + description: The SecComp Profile that will be configured in + order to run in secure computing mode. + type: string + tolerations: + description: Tolerations to include with the OneAgent DaemonSet. + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple... + properties: + effect: + description: Effect indicates the taint effect to match. + Empty means match all taint effects. + type: string + key: + description: Key is the taint key that the toleration + applies to. Empty means match all taint keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute,... + format: int64 + type: integer + value: + description: Value is the taint value the toleration + matches to. + type: string + type: object + type: array + version: + description: Use a specific OneAgent version. Defaults to + the latest version from the Dynatrace cluster. + type: string + type: object + cloudNativeFullStack: + description: Has a single OneAgent per node via DaemonSet. + nullable: true + properties: + annotations: + additionalProperties: + type: string + description: Add custom OneAgent annotations. + type: object + args: + description: Set additional arguments to the OneAgent installer. + items: + type: string + type: array + x-kubernetes-list-type: set + autoUpdate: + description: Disables automatic restarts of OneAgent pods + in case a new version is available (https://www. + type: boolean + codeModulesImage: + description: Use a custom OneAgent CodeModule image to download + binaries. + type: string + dnsPolicy: + description: Set the DNS Policy for OneAgent pods. For details, + see Pods DNS Policy (https://kubernetes. + type: string + env: + description: Set additional environment variables for the + OneAgent pods. + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must + be a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in... + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: Name of the referent. + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in + the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of + the exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: Name of the referent. + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + description: Use a custom OneAgent image. Defaults to the + latest image from the Dynatrace cluster. + type: string + initResources: + description: Define resources requests and limits for the + initContainer. + properties: + claims: + description: Claims lists the names of resources, defined + in spec. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: Name must match the name of one entry + in pod.spec. + type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount of + compute resources required. + type: object + type: object + labels: + additionalProperties: + type: string + description: Your defined labels for OneAgent pods in order + to structure workloads as desired. + type: object + namespaceSelector: + description: Applicable only for applicationMonitoring or + cloudNativeFullStack configuration types. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that... + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. + type: string + values: + description: values is an array of string values. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + nodeSelector: + additionalProperties: + type: string + description: Specify the node selector that controls on which + nodes OneAgent will be deployed. + type: object + oneAgentResources: + description: Resource settings for OneAgent container. + properties: + claims: + description: Claims lists the names of resources, defined + in spec. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: Name must match the name of one entry + in pod.spec. + type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount of + compute resources required. + type: object + type: object + priorityClassName: + description: Assign a priority class to the OneAgent pods. + By default, no class is set. + type: string + secCompProfile: + description: The SecComp Profile that will be configured in + order to run in secure computing mode. + type: string + tolerations: + description: Tolerations to include with the OneAgent DaemonSet. + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple... + properties: + effect: + description: Effect indicates the taint effect to match. + Empty means match all taint effects. + type: string + key: + description: Key is the taint key that the toleration + applies to. Empty means match all taint keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute,... + format: int64 + type: integer + value: + description: Value is the taint value the toleration + matches to. + type: string + type: object + type: array + version: + description: Use a specific OneAgent version. Defaults to + the latest version from the Dynatrace cluster. + type: string + type: object + hostGroup: + description: Sets a host group for OneAgent. + type: string + hostMonitoring: + description: |- + Has a single OneAgent per node via DaemonSet. + Doesn't inject into application pods. + nullable: true + properties: + annotations: + additionalProperties: + type: string + description: Add custom OneAgent annotations. + type: object + args: + description: Set additional arguments to the OneAgent installer. + items: + type: string + type: array + x-kubernetes-list-type: set + autoUpdate: + description: Disables automatic restarts of OneAgent pods + in case a new version is available (https://www. + type: boolean + dnsPolicy: + description: Set the DNS Policy for OneAgent pods. For details, + see Pods DNS Policy (https://kubernetes. + type: string + env: + description: Set additional environment variables for the + OneAgent pods. + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must + be a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in... + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: Name of the referent. + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in + the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of + the exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: Name of the referent. + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + description: Use a custom OneAgent image. Defaults to the + latest image from the Dynatrace cluster. + type: string + labels: + additionalProperties: + type: string + description: Your defined labels for OneAgent pods in order + to structure workloads as desired. + type: object + nodeSelector: + additionalProperties: + type: string + description: Specify the node selector that controls on which + nodes OneAgent will be deployed. + type: object + oneAgentResources: + description: Resource settings for OneAgent container. + properties: + claims: + description: Claims lists the names of resources, defined + in spec. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: Name must match the name of one entry + in pod.spec. + type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount of + compute resources required. + type: object + type: object + priorityClassName: + description: Assign a priority class to the OneAgent pods. + By default, no class is set. + type: string + secCompProfile: + description: The SecComp Profile that will be configured in + order to run in secure computing mode. + type: string + tolerations: + description: Tolerations to include with the OneAgent DaemonSet. + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple... + properties: + effect: + description: Effect indicates the taint effect to match. + Empty means match all taint effects. + type: string + key: + description: Key is the taint key that the toleration + applies to. Empty means match all taint keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute,... + format: int64 + type: integer + value: + description: Value is the taint value the toleration + matches to. + type: string + type: object + type: array + version: + description: Use a specific OneAgent version. Defaults to + the latest version from the Dynatrace cluster. + type: string + type: object + type: object + proxy: + description: Set custom proxy settings either directly or from a secret + with the field proxy. + properties: + value: + description: Raw value for given property. + nullable: true + type: string + valueFrom: + description: Name of the secret to get the property from. + nullable: true + type: string + type: object + skipCertCheck: + description: Disable certificate check for the connection between + Dynatrace Operator and the Dynatrace Cluster. + type: boolean + templates: + properties: + extensionExecutionController: + properties: + annotations: + additionalProperties: + type: string + description: Adds additional annotations to the ExtensionExecutionController + pods + type: object + customConfig: + description: Defines name of ConfigMap containing custom configuration + file + type: string + customExtensionCertificates: + description: Defines name of Secret containing certificates + for custom extensions signature validation + type: string + imageRef: + description: Overrides the default image + properties: + repository: + description: Custom image repository + example: docker.io/dynatrace/image-name + type: string + tag: + description: Indicates a tag of the image to use + type: string + type: object + labels: + additionalProperties: + type: string + description: Adds additional labels for the ExtensionExecutionController + pods + type: object + persistentVolumeClaim: + description: Defines storage device + properties: + accessModes: + description: |- + accessModes contains the desired access modes the volume should have. + More info: https://kubernetes. + items: + type: string + type: array + x-kubernetes-list-type: atomic + dataSource: + description: |- + dataSource field can be used to specify either: + * An existing VolumeSnapshot object (snapshot. + properties: + apiGroup: + description: APIGroup is the group for the resource + being referenced. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + description: dataSourceRef specifies the object from which + to populate the volume with data, if a non-empty... + properties: + apiGroup: + description: APIGroup is the group for the resource + being referenced. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + namespace: + description: |- + Namespace is the namespace of resource being referenced + Note that when a namespace is specified, a... + type: string + required: + - kind + - name + type: object + resources: + description: resources represents the minimum resources + the volume should have. + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount + of compute resources required. + type: object + type: object + selector: + description: selector is a label query over volumes to + consider for binding. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that... + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. + type: string + values: + description: values is an array of string values. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + description: storageClassName is the name of the StorageClass + required by the claim. + type: string + volumeAttributesClassName: + description: volumeAttributesClassName may be used to + set the VolumeAttributesClass used by this claim. + type: string + volumeMode: + description: volumeMode defines what type of volume is + required by the claim. + type: string + volumeName: + description: volumeName is the binding reference to the + PersistentVolume backing this claim. + type: string + type: object + resources: + description: Define resources' requests and limits for single + ExtensionExecutionController pod + properties: + claims: + description: Claims lists the names of resources, defined + in spec. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: Name must match the name of one entry + in pod.spec. + type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount of + compute resources required. + type: object + type: object + tlsRefName: + type: string + tolerations: + description: Set tolerations for the ExtensionExecutionController + pods + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple... + properties: + effect: + description: Effect indicates the taint effect to match. + Empty means match all taint effects. + type: string + key: + description: Key is the taint key that the toleration + applies to. Empty means match all taint keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute,... + format: int64 + type: integer + value: + description: Value is the taint value the toleration + matches to. + type: string + type: object + type: array + topologySpreadConstraints: + description: Adds TopologySpreadConstraints for the ExtensionExecutionController + pods + items: + description: TopologySpreadConstraint specifies how to spread + matching pods among the given topology. + properties: + labelSelector: + description: LabelSelector is used to find matching + pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: A label selector requirement is a + selector that contains values, a key, and an + operator that... + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. + type: string + values: + description: values is an array of string + values. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select the pods over which + spreading will be... + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + description: MaxSkew describes the degree to which pods + may be unevenly distributed. + format: int32 + type: integer + minDomains: + description: MinDomains indicates a minimum number of + eligible domains. + format: int32 + type: integer + nodeAffinityPolicy: + description: |- + NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector + when calculating pod... + type: string + nodeTaintsPolicy: + description: |- + NodeTaintsPolicy indicates how we will treat node taints when calculating + pod topology spread skew. + type: string + topologyKey: + description: TopologyKey is the key of node labels. + type: string + whenUnsatisfiable: + description: |- + WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy + the spread constraint. + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + useEphemeralVolume: + description: Selects EmptyDir volume to be storage device + type: boolean + type: object + kspmNodeConfigurationCollector: + properties: + annotations: + additionalProperties: + type: string + description: Adds additional annotations for the NodeConfigurationCollector + pods + type: object + args: + description: Set additional arguments to the NodeConfigurationCollector + pods + items: + type: string + type: array + env: + description: Set additional environment variables for the + NodeConfigurationCollector pods + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must + be a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in... + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: Name of the referent. + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in + the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of + the exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: Name of the referent. + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + imageRef: + description: Overrides the default image + properties: + repository: + description: Custom image repository + example: docker.io/dynatrace/image-name + type: string + tag: + description: Indicates a tag of the image to use + type: string + type: object + labels: + additionalProperties: + type: string + description: Adds additional labels for the NodeConfigurationCollector + pods + type: object + nodeAffinity: + description: Define the nodeAffinity for the DaemonSet of + the NodeConfigurationCollector + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified... + items: + description: |- + An empty preferred scheduling term matches all objects with implicit weight 0 + (i.e. it's a no-op). + properties: + preference: + description: A node selector term, associated with + the corresponding weight. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates... + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: Represents a key's relationship + to a set of values. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates... + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: Represents a key's relationship + to a set of values. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + description: Weight associated with matching the + corresponding nodeSelectorTerm, in the range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will... + properties: + nodeSelectorTerms: + description: Required. A list of node selector terms. + The terms are ORed. + items: + description: |- + A null or empty node selector term matches no objects. The requirements of + them are ANDed. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates... + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: Represents a key's relationship + to a set of values. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates... + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: Represents a key's relationship + to a set of values. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + nodeSelector: + additionalProperties: + type: string + description: Specify the node selector that controls on which + nodes NodeConfigurationCollector pods will be... + type: object + priorityClassName: + description: If specified, indicates the pod's priority. + type: string + resources: + description: Define resources' requests and limits for single + NodeConfigurationCollector pod + properties: + claims: + description: Claims lists the names of resources, defined + in spec. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: Name must match the name of one entry + in pod.spec. + type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount of + compute resources required. + type: object + type: object + tolerations: + description: Set tolerations for the NodeConfigurationCollector + pods + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple... + properties: + effect: + description: Effect indicates the taint effect to match. + Empty means match all taint effects. + type: string + key: + description: Key is the taint key that the toleration + applies to. Empty means match all taint keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute,... + format: int64 + type: integer + value: + description: Value is the taint value the toleration + matches to. + type: string + type: object + type: array + updateStrategy: + description: Define the NodeConfigurationCollector daemonSet + updateStrategy + properties: + rollingUpdate: + description: Rolling update config params. Present only + if type = "RollingUpdate". + properties: + maxSurge: + anyOf: + - type: integer + - type: string + description: |- + The maximum number of nodes with an existing available DaemonSet pod that + can have an updated... + x-kubernetes-int-or-string: true + maxUnavailable: + anyOf: + - type: integer + - type: string + description: |- + The maximum number of DaemonSet pods that can be unavailable during the + update. + x-kubernetes-int-or-string: true + type: object + type: + description: Type of daemon set update. Can be "RollingUpdate" + or "OnDelete". Default is RollingUpdate. + type: string + type: object + type: object + logMonitoring: + description: Low-level configuration options for the LogMonitoring + feature. + properties: + annotations: + additionalProperties: + type: string + description: Add custom annotations to the LogMonitoring pods + type: object + args: + description: Set additional arguments to the LogMonitoring + main container + items: + type: string + type: array + dnsPolicy: + description: Sets DNS Policy for the LogMonitoring pods + type: string + imageRef: + description: Overrides the default image for the LogMonitoring + pods + properties: + repository: + description: Custom image repository + example: docker.io/dynatrace/image-name + type: string + tag: + description: Indicates a tag of the image to use + type: string + type: object + labels: + additionalProperties: + type: string + description: Add custom labels to the LogMonitoring pods + type: object + nodeSelector: + additionalProperties: + type: string + description: Node selector to control the selection of nodes + for the LogMonitoring pods + type: object + priorityClassName: + description: Assign a priority class to the LogMonitoring + pods. By default, no class is set + type: string + resources: + description: Define resources' requests and limits for all + the LogMonitoring pods + properties: + claims: + description: Claims lists the names of resources, defined + in spec. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: Name must match the name of one entry + in pod.spec. + type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount of + compute resources required. + type: object + type: object + secCompProfile: + description: The SecComp Profile that will be configured in + order to run in secure computing mode for the... + type: string + tolerations: + description: Set tolerations for the LogMonitoring pods + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple... + properties: + effect: + description: Effect indicates the taint effect to match. + Empty means match all taint effects. + type: string + key: + description: Key is the taint key that the toleration + applies to. Empty means match all taint keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute,... + format: int64 + type: integer + value: + description: Value is the taint value the toleration + matches to. + type: string + type: object + type: array + type: object + otelCollector: + properties: + annotations: + additionalProperties: + type: string + description: Adds additional annotations to the OtelCollector + pods + type: object + imageRef: + description: Overrides the default image + properties: + repository: + description: Custom image repository + example: docker.io/dynatrace/image-name + type: string + tag: + description: Indicates a tag of the image to use + type: string + type: object + labels: + additionalProperties: + type: string + description: Adds additional labels for the OtelCollector + pods + type: object + replicas: + description: Number of replicas for your OtelCollector + format: int32 + type: integer + resources: + description: Define resources' requests and limits for single + OtelCollector pod + properties: + claims: + description: Claims lists the names of resources, defined + in spec. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: Name must match the name of one entry + in pod.spec. + type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount of + compute resources required. + type: object + type: object + tlsRefName: + type: string + tolerations: + description: Set tolerations for the OtelCollector pods + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple... + properties: + effect: + description: Effect indicates the taint effect to match. + Empty means match all taint effects. + type: string + key: + description: Key is the taint key that the toleration + applies to. Empty means match all taint keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute,... + format: int64 + type: integer + value: + description: Value is the taint value the toleration + matches to. + type: string + type: object + type: array + topologySpreadConstraints: + description: Adds TopologySpreadConstraints for the OtelCollector + pods + items: + description: TopologySpreadConstraint specifies how to spread + matching pods among the given topology. + properties: + labelSelector: + description: LabelSelector is used to find matching + pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: A label selector requirement is a + selector that contains values, a key, and an + operator that... + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. + type: string + values: + description: values is an array of string + values. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select the pods over which + spreading will be... + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + description: MaxSkew describes the degree to which pods + may be unevenly distributed. + format: int32 + type: integer + minDomains: + description: MinDomains indicates a minimum number of + eligible domains. + format: int32 + type: integer + nodeAffinityPolicy: + description: |- + NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector + when calculating pod... + type: string + nodeTaintsPolicy: + description: |- + NodeTaintsPolicy indicates how we will treat node taints when calculating + pod topology spread skew. + type: string + topologyKey: + description: TopologyKey is the key of node labels. + type: string + whenUnsatisfiable: + description: |- + WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy + the spread constraint. + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + type: object + type: object + tokens: + description: Name of the secret holding the tokens used for connecting + to Dynatrace. + type: string + trustedCAs: + description: Adds custom RootCAs from a configmap. Put the certificate + under certs within your configmap. + type: string + required: + - apiUrl + type: object + status: + description: DynaKubeStatus defines the observed state of DynaKube + properties: + activeGate: + description: Observed state of ActiveGate + properties: + connectionInfoStatus: + description: Information about Active Gate's connections + properties: + endpoints: + description: Available connection endpoints + type: string + lastRequest: + description: Time of the last connection request + format: date-time + type: string + tenantTokenHash: + description: Hash of the tenant token + type: string + tenantUUID: + description: UUID of the tenant, received from the tenant + type: string + type: object + imageID: + description: Image ID + type: string + lastProbeTimestamp: + description: Indicates when the last check for a new version was + performed + format: date-time + type: string + serviceIPs: + description: The ClusterIPs set by Kubernetes on the ActiveGate + Service created by the Operator + items: + type: string + type: array + source: + description: Source of the image (tenant-registry, public-registry, + ...) + type: string + type: + description: Image type + type: string + version: + description: Image version + type: string + type: object + codeModules: + description: Observed state of Code Modules + properties: + imageID: + description: Image ID + type: string + lastProbeTimestamp: + description: Indicates when the last check for a new version was + performed + format: date-time + type: string + source: + description: Source of the image (tenant-registry, public-registry, + ...) + type: string + type: + description: Image type + type: string + version: + description: Image version + type: string + type: object + conditions: + description: Conditions includes status about the current state of + the instance + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + dynatraceApi: + description: Observed state of Dynatrace API + properties: + lastTokenScopeRequest: + description: Time of the last token request + format: date-time + type: string + type: object + kspm: + description: Observed state of Kspm + properties: + tokenSecretHash: + description: TokenSecretHash contains the hash of the token that + is passed to both the ActiveGate and... + type: string + type: object + kubeSystemUUID: + description: KubeSystemUUID contains the UUID of the current Kubernetes + cluster + type: string + kubernetesClusterMEID: + description: KubernetesClusterMEID contains the ID of the monitored + entity that points to the Kubernetes cluster + type: string + kubernetesClusterName: + description: KubernetesClusterName contains the display name (also + know as label) of the monitored entity that... + type: string + metadataEnrichment: + description: Observed state of Metadata-Enrichment + properties: + rules: + items: + properties: + enabled: + type: boolean + source: + type: string + target: + type: string + type: + type: string + type: object + type: array + type: object + oneAgent: + description: Observed state of OneAgent + properties: + connectionInfoStatus: + description: Information about OneAgent's connections + properties: + communicationHosts: + description: List of communication hosts + items: + properties: + host: + description: Host domain + type: string + port: + description: Connection port + format: int32 + type: integer + protocol: + description: Connection protocol + type: string + type: object + type: array + endpoints: + description: Available connection endpoints + type: string + lastRequest: + description: Time of the last connection request + format: date-time + type: string + tenantTokenHash: + description: Hash of the tenant token + type: string + tenantUUID: + description: UUID of the tenant, received from the tenant + type: string + type: object + healthcheck: + description: Commands used for OneAgent's readiness probe + type: object + x-kubernetes-preserve-unknown-fields: true + imageID: + description: Image ID + type: string + instances: + additionalProperties: + properties: + ipAddress: + description: IP address of the pod + type: string + podName: + description: Name of the OneAgent pod + type: string + type: object + description: List of deployed OneAgent instances + type: object + lastInstanceStatusUpdate: + description: Time of the last instance status update + format: date-time + type: string + lastProbeTimestamp: + description: Indicates when the last check for a new version was + performed + format: date-time + type: string + source: + description: Source of the image (tenant-registry, public-registry, + ...) + type: string + type: + description: Image type + type: string + version: + description: Image version + type: string + type: object + phase: + description: Defines the current state (Running, Updating, Error, + ...) + type: string + updatedTimestamp: + description: UpdatedTimestamp indicates when the instance was last + updated + format: date-time + type: string + type: object + type: object + served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .spec.apiUrl + name: ApiUrl + type: string + - jsonPath: .status.phase + name: Status + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta4 + schema: + openAPIV3Schema: + description: DynaKube is the Schema for the DynaKube API + properties: + apiVersion: + description: APIVersion defines the versioned schema of this representation + of an object. + type: string + kind: + description: Kind is a string value representing the REST resource this + object represents. + type: string + metadata: + type: object + spec: + description: DynaKubeSpec defines the desired state of DynaKube + properties: + activeGate: + description: General configuration about ActiveGate instances. + properties: + annotations: + additionalProperties: + type: string + description: Adds additional annotations to the ActiveGate pods + type: object + capabilities: + description: Activegate capabilities enabled (routing, kubernetes-monitoring, + metrics-ingest, dynatrace-api) + items: + type: string + type: array + customProperties: + description: |- + Add a custom properties file by providing it as a value or reference it from a secret + If referenced... + properties: + value: + description: Raw value for given property. + nullable: true + type: string + valueFrom: + description: Name of the secret to get the property from. + nullable: true + type: string + type: object + dnsPolicy: + description: Sets DNS Policy for the ActiveGate pods + type: string + env: + description: List of environment variables to set for the ActiveGate + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must be a + C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in... + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: Name of the referent. + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports metadata.name, + metadata.namespace, `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: Name of the referent. + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + group: + description: Set activation group for ActiveGate + type: string + image: + description: The ActiveGate container image. + type: string + labels: + additionalProperties: + type: string + description: Adds additional labels for the ActiveGate pods + type: object + nodeSelector: + additionalProperties: + type: string + description: Node selector to control the selection of nodes + type: object + persistentVolumeClaim: + description: Defines storage device + properties: + accessModes: + description: |- + accessModes contains the desired access modes the volume should have. + More info: https://kubernetes. + items: + type: string + type: array + x-kubernetes-list-type: atomic + dataSource: + description: |- + dataSource field can be used to specify either: + * An existing VolumeSnapshot object (snapshot. + properties: + apiGroup: + description: APIGroup is the group for the resource being + referenced. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + description: dataSourceRef specifies the object from which + to populate the volume with data, if a non-empty... + properties: + apiGroup: + description: APIGroup is the group for the resource being + referenced. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + namespace: + description: |- + Namespace is the namespace of resource being referenced + Note that when a namespace is specified, a... + type: string + required: + - kind + - name + type: object + resources: + description: resources represents the minimum resources the + volume should have. + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount of + compute resources required. + type: object + type: object + selector: + description: selector is a label query over volumes to consider + for binding. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that... + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. + type: string + values: + description: values is an array of string values. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + description: storageClassName is the name of the StorageClass + required by the claim. + type: string + volumeAttributesClassName: + description: volumeAttributesClassName may be used to set + the VolumeAttributesClass used by this claim. + type: string + volumeMode: + description: volumeMode defines what type of volume is required + by the claim. + type: string + volumeName: + description: volumeName is the binding reference to the PersistentVolume + backing this claim. + type: string + type: object + priorityClassName: + description: If specified, indicates the pod's priority. + type: string + replicas: + description: Amount of replicas for your ActiveGates + format: int32 + type: integer + resources: + description: Define resources requests and limits for single ActiveGate + pods + properties: + claims: + description: Claims lists the names of resources, defined + in spec. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: Name must match the name of one entry in + pod.spec. + type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: additionalProperties: anyOf: - type: integer @@ -4619,53 +6790,58 @@ spec: pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. + Limits describes the maximum amount of compute resources allowed. More info: https://kubernetes. type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount of compute + resources required. + type: object type: object + terminationGracePeriodSeconds: + description: Configures the terminationGracePeriodSeconds parameter + of the ActiveGate pod. + format: int64 + type: integer tlsSecretName: - description: |- - The name of a secret containing ActiveGate TLS cert+key and password. If not set, self-signed certificate is used. - server.p12: certificate+key pair in pkcs12 format - password: passphrase to read server.p12 + description: The name of a secret containing ActiveGate TLS cert+key + and password. type: string tolerations: description: Set tolerations for the ActiveGate pods items: description: |- The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . + the triple... properties: effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + description: Effect indicates the taint effect to match. + Empty means match all taint effects. type: string key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. + description: Key is the taint key that the toleration applies + to. Empty means match all taint keys. type: string operator: description: |- Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. + Valid operators are Exists and Equal. type: string tolerationSeconds: description: |- TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). + of effect NoExecute,... format: int64 type: integer value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. + description: Value is the taint value the toleration matches + to. type: string type: object type: array @@ -4677,34 +6853,25 @@ spec: matching pods among the given topology. properties: labelSelector: - description: |- - LabelSelector is used to find matching pods. - Pods that match this label selector are counted to determine the number of pods - in their corresponding topology domain. + description: LabelSelector is used to find matching pods. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. + description: A label selector requirement is a selector + that contains values, a key, and an operator that... properties: key: description: key is the label key that the selector applies to. type: string operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. + description: operator represents a key's relationship + to a set of values. type: string values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. + description: values is an array of string values. items: type: string type: array @@ -4718,62 +6885,45 @@ spec: matchLabels: additionalProperties: type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. + description: matchLabels is a map of {key,value} pairs. type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: description: |- MatchLabelKeys is a set of pod label keys to select the pods over which - spreading will be calculated. + spreading will be... items: type: string type: array x-kubernetes-list-type: atomic maxSkew: - description: |- - MaxSkew describes the degree to which pods may be unevenly distributed. - When `whenUnsatisfiable=DoNotSchedule`, it is the maximum permitted difference - between the number of matching pods in the target topology and the global minimum. + description: MaxSkew describes the degree to which pods + may be unevenly distributed. format: int32 type: integer minDomains: - description: |- - MinDomains indicates a minimum number of eligible domains. - When the number of eligible domains with matching topology keys is less than minDomains, - Pod Topology Spread treats "global minimum" as 0, and then the calculation of Skew is performed. + description: MinDomains indicates a minimum number of eligible + domains. format: int32 type: integer nodeAffinityPolicy: description: |- NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector - when calculating pod topology spread skew. Options are: - - Honor: only nodes matching nodeAffinity/nodeSelector are included in the calculations. - - Ignore: nodeAffinity/nodeSelector are ignored. + when calculating pod... type: string nodeTaintsPolicy: description: |- NodeTaintsPolicy indicates how we will treat node taints when calculating - pod topology spread skew. Options are: - - Honor: nodes without taints, along with tainted nodes for which the incoming pod - has a toleration, are included. - - Ignore: node taints are ignored. All nodes are included. + pod topology spread skew. type: string topologyKey: - description: |- - TopologyKey is the key of node labels. Nodes that have a label with this key - and identical values are considered to be in the same topology. - We consider each as a "bucket", and try to put balanced number - of pods into each bucket. + description: TopologyKey is the key of node labels. type: string whenUnsatisfiable: description: |- WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy the spread constraint. - - DoNotSchedule (default) tells the scheduler not to schedule it. type: string required: - maxSkew @@ -4781,32 +6931,28 @@ spec: - whenUnsatisfiable type: object type: array + useEphemeralVolume: + description: UseEphemeralVolume + type: boolean type: object apiUrl: - description: |- - Dynatrace apiUrl, including the /api path at the end. For SaaS, set YOUR_ENVIRONMENT_ID to your environment ID. For Managed, change the apiUrl address. - For instructions on how to determine the environment ID and how to configure the apiUrl address, see Environment ID (https://www.dynatrace. + description: Dynatrace apiUrl, including the /api path at the end. maxLength: 128 type: string customPullSecret: - description: |- - Defines a custom pull secret in case you use a private registry when pulling images from the Dynatrace environment. - To define a custom pull secret and learn about the expected behavior, see Configure customPullSecret - (https://www.dynatrace. + description: Defines a custom pull secret in case you use a private + registry when pulling images from the... type: string dynatraceApiRequestThreshold: description: Configuration for thresholding Dynatrace API requests. type: integer enableIstio: - description: |- - When enabled, and if Istio is installed on the Kubernetes environment, Dynatrace Operator will create the corresponding - VirtualService and ServiceEntry objects to allow access to the Dynatrace Cluster from the OneAgent or ActiveGate. - Disabled by default. + description: When enabled, and if Istio is installed on the Kubernetes + environment, Dynatrace Operator will... type: boolean extensions: - description: |- - When an (empty) ExtensionsSpec is provided, the extensions related components (extensions controller and extensions collector) - are deployed by the operator. + description: When an (empty) ExtensionsSpec is provided, the extensions + related components (extensions... type: object kspm: description: General configuration about the KSPM feature. @@ -4840,25 +6986,19 @@ spec: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. + description: A label selector requirement is a selector + that contains values, a key, and an operator that... properties: key: description: key is the label key that the selector applies to. type: string operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. + description: operator represents a key's relationship + to a set of values. type: string values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. + description: values is an array of string values. items: type: string type: array @@ -4872,10 +7012,7 @@ spec: matchLabels: additionalProperties: type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. + description: matchLabels is a map of {key,value} pairs. type: object type: object x-kubernetes-map-type: atomic @@ -4884,14 +7021,11 @@ spec: description: Sets a network zone for the OneAgent and ActiveGate pods. type: string oneAgent: - description: |- - General configuration about OneAgent instances. - You can't enable more than one module (classicFullStack, cloudNativeFullStack, hostMonitoring, or applicationMonitoring). + description: General configuration about OneAgent instances. properties: applicationMonitoring: - description: |- - dynatrace-webhook injects into application pods based on labeled namespaces. - Has an optional CSI driver per node via DaemonSet to provide binaries to pods. + description: dynatrace-webhook injects into application pods based + on labeled namespaces. nullable: true properties: codeModulesImage: @@ -4899,33 +7033,22 @@ spec: binaries. type: string initResources: - description: |- - Define resources requests and limits for the initContainer. For details, see Managing resources for containers - (https://kubernetes.io/docs/concepts/configuration/manage-resources-containers). + description: Define resources requests and limits for the + initContainer. properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry + in pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -4943,7 +7066,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -4952,41 +7075,31 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of + compute resources required. type: object type: object namespaceSelector: - description: |- - Applicable only for applicationMonitoring or cloudNativeFullStack configuration types. The namespaces where you want Dynatrace Operator to inject. - For more information, see Configure monitoring for namespaces and pods (https://www.dynatrace. + description: Applicable only for applicationMonitoring or + cloudNativeFullStack configuration types. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. + description: A label selector requirement is a selector + that contains values, a key, and an operator that... properties: key: description: key is the label key that the selector applies to. type: string operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. + description: operator represents a key's relationship + to a set of values. type: string values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. + description: values is an array of string values. items: type: string type: array @@ -5000,22 +7113,16 @@ spec: matchLabels: additionalProperties: type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. + description: matchLabels is a map of {key,value} pairs. type: object type: object x-kubernetes-map-type: atomic version: - description: Use a specific OneAgent CodeModule version. Defaults - to the latest version from the Dynatrace cluster. + description: Use a specific OneAgent CodeModule version. type: string type: object classicFullStack: - description: |- - Has a single OneAgent per node via DaemonSet. - Injection is performed via the same OneAgent DaemonSet. + description: Has a single OneAgent per node via DaemonSet. nullable: true properties: annotations: @@ -5024,21 +7131,18 @@ spec: description: Add custom OneAgent annotations. type: object args: - description: |- - Set additional arguments to the OneAgent installer. - For available options, see Linux custom installation (https://www.dynatrace.com/support/help/setup-and-configuration/dynatrace-oneagent/installation-and-operation/linux/installation/customize-oneagent-installation-on-linux). + description: Set additional arguments to the OneAgent installer. items: type: string type: array x-kubernetes-list-type: set autoUpdate: - description: |- - Disables automatic restarts of OneAgent pods in case a new version is available (https://www.dynatrace.com/support/help/setup-and-configuration/setup-on-container-platforms/kubernetes/get-started-with-kubernetes-monitoring#disable-auto). - Enabled by default. + description: Disables automatic restarts of OneAgent pods + in case a new version is available (https://www. type: boolean dnsPolicy: description: Set the DNS Policy for OneAgent pods. For details, - see Pods DNS Policy (https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-s-dns-policy). + see Pods DNS Policy (https://kubernetes. type: string env: description: Set additional environment variables for the @@ -5054,9 +7158,7 @@ spec: value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. + using the previously defined environment variables in... type: string valueFrom: description: Source for the environment variable's value. @@ -5070,12 +7172,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the ConfigMap or @@ -5086,9 +7183,8 @@ spec: type: object x-kubernetes-map-type: atomic fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.' properties: apiVersion: description: Version of the schema the FieldPath @@ -5105,7 +7201,7 @@ spec: resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + (limits.cpu, limits. properties: containerName: description: 'Container name: required for volumes, @@ -5136,12 +7232,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the Secret or its @@ -5173,33 +7264,21 @@ spec: nodes OneAgent will be deployed. type: object oneAgentResources: - description: |- - Resource settings for OneAgent container. Consumption of the OneAgent heavily depends on the workload to monitor. You can use the default settings in the CR. - Note: resource.requests shows the values needed to run; resource.limits shows the maximum limits for the pod. + description: Resource settings for OneAgent container. properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry + in pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -5217,7 +7296,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -5226,58 +7305,51 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of + compute resources required. type: object type: object priorityClassName: - description: |- - Assign a priority class to the OneAgent pods. By default, no class is set. - For details, see Pod Priority and Preemption (https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/). + description: Assign a priority class to the OneAgent pods. + By default, no class is set. type: string secCompProfile: description: The SecComp Profile that will be configured in order to run in secure computing mode. type: string + storageHostPath: + description: StorageHostPath is the writable directory on + the host filesystem where OneAgent configurations will... + type: string tolerations: description: Tolerations to include with the OneAgent DaemonSet. - For details, see Taints and Tolerations (https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/). items: description: |- The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . + the triple... properties: effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + description: Effect indicates the taint effect to match. + Empty means match all taint effects. type: string key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. + description: Key is the taint key that the toleration + applies to. Empty means match all taint keys. type: string operator: description: |- Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. + Valid operators are Exists and Equal. type: string tolerationSeconds: description: |- TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). + of effect NoExecute,... format: int64 type: integer value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. + description: Value is the taint value the toleration + matches to. type: string type: object type: array @@ -5287,10 +7359,7 @@ spec: type: string type: object cloudNativeFullStack: - description: |- - Has a single OneAgent per node via DaemonSet. - dynatrace-webhook injects into application pods based on labeled namespaces. - Has a CSI driver per node via DaemonSet to provide binaries to pods. + description: Has a single OneAgent per node via DaemonSet. nullable: true properties: annotations: @@ -5299,17 +7368,14 @@ spec: description: Add custom OneAgent annotations. type: object args: - description: |- - Set additional arguments to the OneAgent installer. - For available options, see Linux custom installation (https://www.dynatrace.com/support/help/setup-and-configuration/dynatrace-oneagent/installation-and-operation/linux/installation/customize-oneagent-installation-on-linux). + description: Set additional arguments to the OneAgent installer. items: type: string type: array x-kubernetes-list-type: set autoUpdate: - description: |- - Disables automatic restarts of OneAgent pods in case a new version is available (https://www.dynatrace.com/support/help/setup-and-configuration/setup-on-container-platforms/kubernetes/get-started-with-kubernetes-monitoring#disable-auto). - Enabled by default. + description: Disables automatic restarts of OneAgent pods + in case a new version is available (https://www. type: boolean codeModulesImage: description: Use a custom OneAgent CodeModule image to download @@ -5317,7 +7383,7 @@ spec: type: string dnsPolicy: description: Set the DNS Policy for OneAgent pods. For details, - see Pods DNS Policy (https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-s-dns-policy). + see Pods DNS Policy (https://kubernetes. type: string env: description: Set additional environment variables for the @@ -5333,9 +7399,7 @@ spec: value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. + using the previously defined environment variables in... type: string valueFrom: description: Source for the environment variable's value. @@ -5349,12 +7413,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the ConfigMap or @@ -5365,9 +7424,8 @@ spec: type: object x-kubernetes-map-type: atomic fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.' properties: apiVersion: description: Version of the schema the FieldPath @@ -5384,7 +7442,7 @@ spec: resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + (limits.cpu, limits. properties: containerName: description: 'Container name: required for volumes, @@ -5415,12 +7473,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the Secret or its @@ -5440,33 +7493,22 @@ spec: latest image from the Dynatrace cluster. type: string initResources: - description: |- - Define resources requests and limits for the initContainer. For details, see Managing resources for containers - (https://kubernetes.io/docs/concepts/configuration/manage-resources-containers). + description: Define resources requests and limits for the + initContainer. properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry + in pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -5484,7 +7526,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -5493,11 +7535,8 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of + compute resources required. type: object type: object labels: @@ -5507,33 +7546,26 @@ spec: to structure workloads as desired. type: object namespaceSelector: - description: |- - Applicable only for applicationMonitoring or cloudNativeFullStack configuration types. The namespaces where you want Dynatrace Operator to inject. - For more information, see Configure monitoring for namespaces and pods (https://www.dynatrace. + description: Applicable only for applicationMonitoring or + cloudNativeFullStack configuration types. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. + description: A label selector requirement is a selector + that contains values, a key, and an operator that... properties: key: description: key is the label key that the selector applies to. type: string operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. + description: operator represents a key's relationship + to a set of values. type: string values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. + description: values is an array of string values. items: type: string type: array @@ -5547,10 +7579,7 @@ spec: matchLabels: additionalProperties: type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. + description: matchLabels is a map of {key,value} pairs. type: object type: object x-kubernetes-map-type: atomic @@ -5561,33 +7590,21 @@ spec: nodes OneAgent will be deployed. type: object oneAgentResources: - description: |- - Resource settings for OneAgent container. Consumption of the OneAgent heavily depends on the workload to monitor. You can use the default settings in the CR. - Note: resource.requests shows the values needed to run; resource.limits shows the maximum limits for the pod. + description: Resource settings for OneAgent container. properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry + in pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -5605,7 +7622,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -5614,58 +7631,51 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of + compute resources required. type: object type: object priorityClassName: - description: |- - Assign a priority class to the OneAgent pods. By default, no class is set. - For details, see Pod Priority and Preemption (https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/). + description: Assign a priority class to the OneAgent pods. + By default, no class is set. type: string secCompProfile: description: The SecComp Profile that will be configured in order to run in secure computing mode. type: string + storageHostPath: + description: StorageHostPath is the writable directory on + the host filesystem where OneAgent configurations will... + type: string tolerations: description: Tolerations to include with the OneAgent DaemonSet. - For details, see Taints and Tolerations (https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/). items: description: |- The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . + the triple... properties: effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + description: Effect indicates the taint effect to match. + Empty means match all taint effects. type: string key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. + description: Key is the taint key that the toleration + applies to. Empty means match all taint keys. type: string operator: description: |- Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. + Valid operators are Exists and Equal. type: string tolerationSeconds: description: |- TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). + of effect NoExecute,... format: int64 type: integer value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. + description: Value is the taint value the toleration + matches to. type: string type: object type: array @@ -5689,21 +7699,18 @@ spec: description: Add custom OneAgent annotations. type: object args: - description: |- - Set additional arguments to the OneAgent installer. - For available options, see Linux custom installation (https://www.dynatrace.com/support/help/setup-and-configuration/dynatrace-oneagent/installation-and-operation/linux/installation/customize-oneagent-installation-on-linux). + description: Set additional arguments to the OneAgent installer. items: type: string type: array x-kubernetes-list-type: set autoUpdate: - description: |- - Disables automatic restarts of OneAgent pods in case a new version is available (https://www.dynatrace.com/support/help/setup-and-configuration/setup-on-container-platforms/kubernetes/get-started-with-kubernetes-monitoring#disable-auto). - Enabled by default. + description: Disables automatic restarts of OneAgent pods + in case a new version is available (https://www. type: boolean dnsPolicy: description: Set the DNS Policy for OneAgent pods. For details, - see Pods DNS Policy (https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-s-dns-policy). + see Pods DNS Policy (https://kubernetes. type: string env: description: Set additional environment variables for the @@ -5719,9 +7726,7 @@ spec: value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. + using the previously defined environment variables in... type: string valueFrom: description: Source for the environment variable's value. @@ -5735,12 +7740,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the ConfigMap or @@ -5751,9 +7751,8 @@ spec: type: object x-kubernetes-map-type: atomic fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.' properties: apiVersion: description: Version of the schema the FieldPath @@ -5770,7 +7769,7 @@ spec: resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + (limits.cpu, limits. properties: containerName: description: 'Container name: required for volumes, @@ -5801,12 +7800,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the Secret or its @@ -5838,33 +7832,21 @@ spec: nodes OneAgent will be deployed. type: object oneAgentResources: - description: |- - Resource settings for OneAgent container. Consumption of the OneAgent heavily depends on the workload to monitor. You can use the default settings in the CR. - Note: resource.requests shows the values needed to run; resource.limits shows the maximum limits for the pod. + description: Resource settings for OneAgent container. properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry + in pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -5882,7 +7864,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -5891,58 +7873,51 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of + compute resources required. type: object type: object priorityClassName: - description: |- - Assign a priority class to the OneAgent pods. By default, no class is set. - For details, see Pod Priority and Preemption (https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/). + description: Assign a priority class to the OneAgent pods. + By default, no class is set. type: string secCompProfile: description: The SecComp Profile that will be configured in order to run in secure computing mode. type: string + storageHostPath: + description: StorageHostPath is the writable directory on + the host filesystem where OneAgent configurations will... + type: string tolerations: description: Tolerations to include with the OneAgent DaemonSet. - For details, see Taints and Tolerations (https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/). items: description: |- The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . + the triple... properties: effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + description: Effect indicates the taint effect to match. + Empty means match all taint effects. type: string key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. + description: Key is the taint key that the toleration + applies to. Empty means match all taint keys. type: string operator: description: |- Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. + Valid operators are Exists and Equal. type: string tolerationSeconds: description: |- TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). + of effect NoExecute,... format: int64 type: integer value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. + description: Value is the taint value the toleration + matches to. type: string type: object type: array @@ -5953,9 +7928,8 @@ spec: type: object type: object proxy: - description: |- - Set custom proxy settings either directly or from a secret with the field proxy. - Note: Applies to Dynatrace Operator, ActiveGate, and OneAgents. + description: Set custom proxy settings either directly or from a secret + with the field proxy. properties: value: description: Raw value for given property. @@ -5967,10 +7941,22 @@ spec: type: string type: object skipCertCheck: - description: |- - Disable certificate check for the connection between Dynatrace Operator and the Dynatrace Cluster. - Set to true if you want to skip certification validation checks. + description: Disable certificate check for the connection between + Dynatrace Operator and the Dynatrace Cluster. type: boolean + telemetryIngest: + description: When a TelemetryIngestSpec is provided, the OTEL collector + is deployed by the operator. + properties: + protocols: + items: + type: string + type: array + serviceName: + type: string + tlsRefName: + type: string + type: object templates: properties: extensionExecutionController: @@ -6012,7 +7998,7 @@ spec: accessModes: description: |- accessModes contains the desired access modes the volume should have. - More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1 + More info: https://kubernetes. items: type: string type: array @@ -6020,13 +8006,11 @@ spec: dataSource: description: |- dataSource field can be used to specify either: - * An existing VolumeSnapshot object (snapshot.storage.k8s. + * An existing VolumeSnapshot object (snapshot. properties: apiGroup: - description: |- - APIGroup is the group for the resource being referenced. - If APIGroup is not specified, the specified Kind must be in the core API group. - For any other third-party types, APIGroup is required. + description: APIGroup is the group for the resource + being referenced. type: string kind: description: Kind is the type of resource being referenced @@ -6040,16 +8024,12 @@ spec: type: object x-kubernetes-map-type: atomic dataSourceRef: - description: |- - dataSourceRef specifies the object from which to populate the volume with data, if a non-empty - volume is desired. This may be any object from a non-empty API group (non - core object) or a PersistentVolumeClaim object. + description: dataSourceRef specifies the object from which + to populate the volume with data, if a non-empty... properties: apiGroup: - description: |- - APIGroup is the group for the resource being referenced. - If APIGroup is not specified, the specified Kind must be in the core API group. - For any other third-party types, APIGroup is required. + description: APIGroup is the group for the resource + being referenced. type: string kind: description: Kind is the type of resource being referenced @@ -6060,18 +8040,15 @@ spec: namespace: description: |- Namespace is the namespace of resource being referenced - Note that when a namespace is specified, a gateway.networking.k8s.io/ReferenceGrant object is required in the referent namespace to allow that namespace's owner to accept the reference. See the ReferenceGrant documentation for details. + Note that when a namespace is specified, a... type: string required: - kind - name type: object resources: - description: |- - resources represents the minimum resources the volume should have. - If RecoverVolumeExpansionFailure feature is enabled users are allowed to specify resource requirements - that are lower than previous value but must still be higher than capacity recorded in the - status field of the claim. + description: resources represents the minimum resources + the volume should have. properties: limits: additionalProperties: @@ -6082,7 +8059,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -6091,11 +8068,8 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount + of compute resources required. type: object type: object selector: @@ -6106,25 +8080,19 @@ spec: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. + description: A label selector requirement is a selector + that contains values, a key, and an operator that... properties: key: description: key is the label key that the selector applies to. type: string operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. + description: operator represents a key's relationship + to a set of values. type: string values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. + description: values is an array of string values. items: type: string type: array @@ -6138,28 +8106,21 @@ spec: matchLabels: additionalProperties: type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. + description: matchLabels is a map of {key,value} pairs. type: object type: object x-kubernetes-map-type: atomic storageClassName: - description: |- - storageClassName is the name of the StorageClass required by the claim. - More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1 + description: storageClassName is the name of the StorageClass + required by the claim. type: string volumeAttributesClassName: - description: |- - volumeAttributesClassName may be used to set the VolumeAttributesClass used by this claim. - If specified, the CSI driver will create or update the volume with the attributes defined - in the corresponding VolumeAttributesClass. + description: volumeAttributesClassName may be used to + set the VolumeAttributesClass used by this claim. type: string volumeMode: - description: |- - volumeMode defines what type of volume is required by the claim. - Value of Filesystem is implied when not included in claim spec. + description: volumeMode defines what type of volume is + required by the claim. type: string volumeName: description: volumeName is the binding reference to the @@ -6171,28 +8132,18 @@ spec: ExtensionExecutionController pod properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry + in pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -6210,7 +8161,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -6219,11 +8170,8 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of + compute resources required. type: object type: object tlsRefName: @@ -6234,36 +8182,30 @@ spec: items: description: |- The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . + the triple... properties: effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + description: Effect indicates the taint effect to match. + Empty means match all taint effects. type: string key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. + description: Key is the taint key that the toleration + applies to. Empty means match all taint keys. type: string operator: description: |- Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. + Valid operators are Exists and Equal. type: string tolerationSeconds: description: |- TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). + of effect NoExecute,... format: int64 type: integer value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. + description: Value is the taint value the toleration + matches to. type: string type: object type: array @@ -6275,34 +8217,28 @@ spec: matching pods among the given topology. properties: labelSelector: - description: |- - LabelSelector is used to find matching pods. - Pods that match this label selector are counted to determine the number of pods - in their corresponding topology domain. + description: LabelSelector is used to find matching + pods. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. + description: A label selector requirement is a + selector that contains values, a key, and an + operator that... properties: key: description: key is the label key that the selector applies to. type: string operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. + description: operator represents a key's relationship + to a set of values. type: string values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. + description: values is an array of string + values. items: type: string type: array @@ -6316,62 +8252,46 @@ spec: matchLabels: additionalProperties: type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. + description: matchLabels is a map of {key,value} + pairs. type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: description: |- MatchLabelKeys is a set of pod label keys to select the pods over which - spreading will be calculated. + spreading will be... items: type: string type: array x-kubernetes-list-type: atomic maxSkew: - description: |- - MaxSkew describes the degree to which pods may be unevenly distributed. - When `whenUnsatisfiable=DoNotSchedule`, it is the maximum permitted difference - between the number of matching pods in the target topology and the global minimum. + description: MaxSkew describes the degree to which pods + may be unevenly distributed. format: int32 type: integer minDomains: - description: |- - MinDomains indicates a minimum number of eligible domains. - When the number of eligible domains with matching topology keys is less than minDomains, - Pod Topology Spread treats "global minimum" as 0, and then the calculation of Skew is performed. + description: MinDomains indicates a minimum number of + eligible domains. format: int32 type: integer nodeAffinityPolicy: description: |- NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector - when calculating pod topology spread skew. Options are: - - Honor: only nodes matching nodeAffinity/nodeSelector are included in the calculations. - - Ignore: nodeAffinity/nodeSelector are ignored. + when calculating pod... type: string nodeTaintsPolicy: description: |- NodeTaintsPolicy indicates how we will treat node taints when calculating - pod topology spread skew. Options are: - - Honor: nodes without taints, along with tainted nodes for which the incoming pod - has a toleration, are included. - - Ignore: node taints are ignored. All nodes are included. + pod topology spread skew. type: string topologyKey: - description: |- - TopologyKey is the key of node labels. Nodes that have a label with this key - and identical values are considered to be in the same topology. - We consider each as a "bucket", and try to put balanced number - of pods into each bucket. + description: TopologyKey is the key of node labels. type: string whenUnsatisfiable: description: |- WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy the spread constraint. - - DoNotSchedule (default) tells the scheduler not to schedule it. type: string required: - maxSkew @@ -6411,9 +8331,7 @@ spec: value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. + using the previously defined environment variables in... type: string valueFrom: description: Source for the environment variable's value. @@ -6427,12 +8345,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the ConfigMap or @@ -6443,9 +8356,8 @@ spec: type: object x-kubernetes-map-type: atomic fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.' properties: apiVersion: description: Version of the schema the FieldPath @@ -6462,7 +8374,7 @@ spec: resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + (limits.cpu, limits. properties: containerName: description: 'Container name: required for volumes, @@ -6493,12 +8405,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the Secret or its @@ -6537,13 +8444,11 @@ spec: preferredDuringSchedulingIgnoredDuringExecution: description: |- The scheduler will prefer to schedule pods to nodes that satisfy - the affinity expressions specified by this field, but it may choose - a node that violates one or more of the expressions. The node that is - most preferred is the one with the greatest sum of weights, i.e. + the affinity expressions specified... items: description: |- An empty preferred scheduling term matches all objects with implicit weight 0 - (i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op). + (i.e. it's a no-op). properties: preference: description: A node selector term, associated with @@ -6555,23 +8460,20 @@ spec: items: description: |- A node selector requirement is a selector that contains values, a key, and an operator - that relates the key and values. + that relates... properties: key: description: The label key that the selector applies to. type: string operator: - description: |- - Represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + description: Represents a key's relationship + to a set of values. type: string values: description: |- An array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. If the operator is Gt or Lt, the values - array must have a single element, which will be interpreted as an integer. + the values array must be non-empty. items: type: string type: array @@ -6588,23 +8490,20 @@ spec: items: description: |- A node selector requirement is a selector that contains values, a key, and an operator - that relates the key and values. + that relates... properties: key: description: The label key that the selector applies to. type: string operator: - description: |- - Represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + description: Represents a key's relationship + to a set of values. type: string values: description: |- An array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. If the operator is Gt or Lt, the values - array must have a single element, which will be interpreted as an integer. + the values array must be non-empty. items: type: string type: array @@ -6631,9 +8530,7 @@ spec: requiredDuringSchedulingIgnoredDuringExecution: description: |- If the affinity requirements specified by this field are not met at - scheduling time, the pod will not be scheduled onto the node. - If the affinity requirements specified by this field cease to be met - at some point during pod execution (e.g. + scheduling time, the pod will... properties: nodeSelectorTerms: description: Required. A list of node selector terms. @@ -6642,7 +8539,6 @@ spec: description: |- A null or empty node selector term matches no objects. The requirements of them are ANDed. - The TopologySelectorTerm type implements a subset of the NodeSelectorTerm. properties: matchExpressions: description: A list of node selector requirements @@ -6650,23 +8546,20 @@ spec: items: description: |- A node selector requirement is a selector that contains values, a key, and an operator - that relates the key and values. + that relates... properties: key: description: The label key that the selector applies to. type: string operator: - description: |- - Represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + description: Represents a key's relationship + to a set of values. type: string values: description: |- An array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. If the operator is Gt or Lt, the values - array must have a single element, which will be interpreted as an integer. + the values array must be non-empty. items: type: string type: array @@ -6683,23 +8576,20 @@ spec: items: description: |- A node selector requirement is a selector that contains values, a key, and an operator - that relates the key and values. + that relates... properties: key: description: The label key that the selector applies to. type: string operator: - description: |- - Represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + description: Represents a key's relationship + to a set of values. type: string values: description: |- An array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. If the operator is Gt or Lt, the values - array must have a single element, which will be interpreted as an integer. + the values array must be non-empty. items: type: string type: array @@ -6723,40 +8613,28 @@ spec: additionalProperties: type: string description: Specify the node selector that controls on which - nodes NodeConfigurationCollector pods will be deployed. + nodes NodeConfigurationCollector pods will be... type: object priorityClassName: - description: |- - If specified, indicates the pod's priority. Name must be defined by creating a PriorityClass object with that - name. If not specified the setting will be removed from the DaemonSet. + description: If specified, indicates the pod's priority. type: string resources: description: Define resources' requests and limits for single NodeConfigurationCollector pod properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry + in pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -6774,7 +8652,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -6783,11 +8661,8 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of + compute resources required. type: object type: object tolerations: @@ -6796,36 +8671,30 @@ spec: items: description: |- The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . + the triple... properties: effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + description: Effect indicates the taint effect to match. + Empty means match all taint effects. type: string key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. + description: Key is the taint key that the toleration + applies to. Empty means match all taint keys. type: string operator: description: |- Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. + Valid operators are Exists and Equal. type: string tolerationSeconds: description: |- TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). + of effect NoExecute,... format: int64 type: integer value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. + description: Value is the taint value the toleration + matches to. type: string type: object type: array @@ -6843,9 +8712,7 @@ spec: - type: string description: |- The maximum number of nodes with an existing available DaemonSet pod that - can have an updated DaemonSet pod during during an update. - Value can be an absolute number (ex: 5) or a percentage of desired pods (ex: 10%). - This can not be 0 if MaxUnavailable is 0. + can have an updated... x-kubernetes-int-or-string: true maxUnavailable: anyOf: @@ -6853,9 +8720,7 @@ spec: - type: string description: |- The maximum number of DaemonSet pods that can be unavailable during the - update. Value can be an absolute number (ex: 5) or a percentage of total - number of DaemonSet pods at the start of the update (ex: 10%). Absolute - number is calculated from percentage by rounding up. + update. x-kubernetes-int-or-string: true type: object type: @@ -6914,28 +8779,18 @@ spec: the LogMonitoring pods properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry + in pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -6953,7 +8808,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -6962,58 +8817,48 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of + compute resources required. type: object type: object secCompProfile: description: The SecComp Profile that will be configured in - order to run in secure computing mode for the LogMonitoring - pods + order to run in secure computing mode for the... type: string tolerations: description: Set tolerations for the LogMonitoring pods items: description: |- The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . + the triple... properties: effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + description: Effect indicates the taint effect to match. + Empty means match all taint effects. type: string key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. + description: Key is the taint key that the toleration + applies to. Empty means match all taint keys. type: string operator: description: |- Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. + Valid operators are Exists and Equal. type: string tolerationSeconds: description: |- TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). + of effect NoExecute,... format: int64 type: integer value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. + description: Value is the taint value the toleration + matches to. type: string type: object type: array type: object - openTelemetryCollector: + otelCollector: properties: annotations: additionalProperties: @@ -7047,28 +8892,18 @@ spec: OtelCollector pod properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined + in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry + in pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request + in the referenced claim. type: string required: - name @@ -7086,7 +8921,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -7095,11 +8930,8 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of + compute resources required. type: object type: object tlsRefName: @@ -7109,36 +8941,30 @@ spec: items: description: |- The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . + the triple... properties: effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + description: Effect indicates the taint effect to match. + Empty means match all taint effects. type: string key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. + description: Key is the taint key that the toleration + applies to. Empty means match all taint keys. type: string operator: description: |- Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. + Valid operators are Exists and Equal. type: string tolerationSeconds: description: |- TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). + of effect NoExecute,... format: int64 type: integer value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. + description: Value is the taint value the toleration + matches to. type: string type: object type: array @@ -7150,34 +8976,28 @@ spec: matching pods among the given topology. properties: labelSelector: - description: |- - LabelSelector is used to find matching pods. - Pods that match this label selector are counted to determine the number of pods - in their corresponding topology domain. + description: LabelSelector is used to find matching + pods. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. + description: A label selector requirement is a + selector that contains values, a key, and an + operator that... properties: key: description: key is the label key that the selector applies to. type: string operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. + description: operator represents a key's relationship + to a set of values. type: string values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. + description: values is an array of string + values. items: type: string type: array @@ -7191,62 +9011,46 @@ spec: matchLabels: additionalProperties: type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. + description: matchLabels is a map of {key,value} + pairs. type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: description: |- MatchLabelKeys is a set of pod label keys to select the pods over which - spreading will be calculated. + spreading will be... items: type: string type: array x-kubernetes-list-type: atomic maxSkew: - description: |- - MaxSkew describes the degree to which pods may be unevenly distributed. - When `whenUnsatisfiable=DoNotSchedule`, it is the maximum permitted difference - between the number of matching pods in the target topology and the global minimum. + description: MaxSkew describes the degree to which pods + may be unevenly distributed. format: int32 type: integer minDomains: - description: |- - MinDomains indicates a minimum number of eligible domains. - When the number of eligible domains with matching topology keys is less than minDomains, - Pod Topology Spread treats "global minimum" as 0, and then the calculation of Skew is performed. + description: MinDomains indicates a minimum number of + eligible domains. format: int32 type: integer nodeAffinityPolicy: description: |- NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector - when calculating pod topology spread skew. Options are: - - Honor: only nodes matching nodeAffinity/nodeSelector are included in the calculations. - - Ignore: nodeAffinity/nodeSelector are ignored. + when calculating pod... type: string nodeTaintsPolicy: description: |- NodeTaintsPolicy indicates how we will treat node taints when calculating - pod topology spread skew. Options are: - - Honor: nodes without taints, along with tainted nodes for which the incoming pod - has a toleration, are included. - - Ignore: node taints are ignored. All nodes are included. + pod topology spread skew. type: string topologyKey: - description: |- - TopologyKey is the key of node labels. Nodes that have a label with this key - and identical values are considered to be in the same topology. - We consider each as a "bucket", and try to put balanced number - of pods into each bucket. + description: TopologyKey is the key of node labels. type: string whenUnsatisfiable: description: |- WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy the spread constraint. - - DoNotSchedule (default) tells the scheduler not to schedule it. type: string required: - maxSkew @@ -7261,9 +9065,8 @@ spec: to Dynatrace. type: string trustedCAs: - description: |- - Adds custom RootCAs from a configmap. Put the certificate under certs within your configmap. - Note: Applies to Dynatrace Operator, OneAgent and ActiveGate. + description: Adds custom RootCAs from a configmap. Put the certificate + under certs within your configmap. type: string required: - apiUrl @@ -7284,6 +9087,9 @@ spec: description: Time of the last connection request format: date-time type: string + tenantTokenHash: + description: Hash of the tenant token + type: string tenantUUID: description: UUID of the tenant, received from the tenant type: string @@ -7343,31 +9149,24 @@ spec: state of this API Resource. properties: lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + description: lastTransitionTime is the last time the condition + transitioned from one status to another. format: date-time type: string message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. + description: message is a human readable message indicating + details about the transition. maxLength: 32768 type: string observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. format: int64 minimum: 0 type: integer reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. maxLength: 1024 minLength: 1 pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ @@ -7404,9 +9203,8 @@ spec: description: Observed state of Kspm properties: tokenSecretHash: - description: |- - TokenSecretHash contains the hash of the token that is passed to both the ActiveGate and Node-Configuration-Collector. - Meant to keep the two in sync. + description: TokenSecretHash contains the hash of the token that + is passed to both the ActiveGate and... type: string type: object kubeSystemUUID: @@ -7419,8 +9217,7 @@ spec: type: string kubernetesClusterName: description: KubernetesClusterName contains the display name (also - know as label) of the monitored entity that points to the Kubernetes - cluster + know as label) of the monitored entity that... type: string metadataEnrichment: description: Observed state of Metadata-Enrichment @@ -7468,6 +9265,9 @@ spec: description: Time of the last connection request format: date-time type: string + tenantTokenHash: + description: Hash of the tenant token + type: string tenantUUID: description: UUID of the tenant, received from the tenant type: string @@ -7531,7 +9331,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.16.5 + controller-gen.kubebuilder.io/version: v0.17.2 name: edgeconnects.dynatrace.com spec: conversion: @@ -7575,19 +9375,12 @@ spec: description: EdgeConnect is the Schema for the EdgeConnect API properties: apiVersion: - 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 + description: APIVersion defines the versioned schema of this representation + of an object. type: string kind: - 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 + description: Kind is a string value representing the REST resource this + object represents. type: string metadata: type: object @@ -7605,12 +9398,11 @@ spec: type: string autoUpdate: default: true - description: 'Enables automatic restarts of EdgeConnect pods in case - a new version is available (the default value is: true)' + description: Enables automatic restarts of EdgeConnect pods in case + a new version is available (the default... type: boolean caCertsRef: - description: Adds custom root certificate from a configmap. Put the - certificate under certs within your configmap. + description: Adds custom root certificate from a configmap. type: string customPullSecret: description: Pull secret for your private registry @@ -7628,9 +9420,7 @@ spec: value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. + using the previously defined environment variables in... type: string valueFrom: description: Source for the environment variable's value. Cannot @@ -7644,12 +9434,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the ConfigMap or its key @@ -7660,9 +9445,8 @@ spec: type: object x-kubernetes-map-type: atomic fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + description: 'Selects a field of the pod: supports metadata.name, + metadata.namespace, `metadata.' properties: apiVersion: description: Version of the schema the FieldPath is @@ -7679,7 +9463,7 @@ spec: resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + (limits.cpu, limits. properties: containerName: description: 'Container name: required for volumes, @@ -7709,12 +9493,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the Secret or its key must @@ -7782,8 +9561,7 @@ spec: type: string provisioner: description: Determines if the operator will create the EdgeConnect - and light OAuth client on the cluster using the credentials - provided. Requires more scopes than default behavior. + and light OAuth client on the cluster using... type: boolean resource: description: URN identifying your account. You get the URN when @@ -7798,9 +9576,8 @@ spec: description: General configurations for proxy settings. properties: authRef: - description: |- - Secret name which contains the username and password used for authentication with the proxy, using the - "Basic" HTTP authentication scheme. + description: Secret name which contains the username and password + used for authentication with the proxy, using... type: string host: description: Server address (hostname or IP address) of the proxy. @@ -7808,8 +9585,7 @@ spec: noProxy: description: |- NoProxy represents the NO_PROXY or no_proxy environment - variable. It specifies a string that contains comma-separated values - specifying hosts that should be excluded from proxying. + variable. type: string port: description: Port of the proxy. @@ -7826,28 +9602,16 @@ spec: description: Defines resources requests and limits for single pods properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry in pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request in + the referenced claim. type: string required: - name @@ -7865,7 +9629,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -7874,11 +9638,8 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of compute + resources required. type: object type: object serviceAccountName: @@ -7891,36 +9652,30 @@ spec: items: description: |- The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . + the triple... properties: effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + description: Effect indicates the taint effect to match. Empty + means match all taint effects. type: string key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. + description: Key is the taint key that the toleration applies + to. Empty means match all taint keys. type: string operator: description: |- Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. + Valid operators are Exists and Equal. type: string tolerationSeconds: description: |- TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). + of effect NoExecute,... format: int64 type: integer value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. + description: Value is the taint value the toleration matches + to. type: string type: object type: array @@ -7932,34 +9687,25 @@ spec: pods among the given topology. properties: labelSelector: - description: |- - LabelSelector is used to find matching pods. - Pods that match this label selector are counted to determine the number of pods - in their corresponding topology domain. + description: LabelSelector is used to find matching pods. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. + description: A label selector requirement is a selector + that contains values, a key, and an operator that... properties: key: description: key is the label key that the selector applies to. type: string operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. + description: operator represents a key's relationship + to a set of values. type: string values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. + description: values is an array of string values. items: type: string type: array @@ -7973,62 +9719,45 @@ spec: matchLabels: additionalProperties: type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. + description: matchLabels is a map of {key,value} pairs. type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: description: |- MatchLabelKeys is a set of pod label keys to select the pods over which - spreading will be calculated. + spreading will be... items: type: string type: array x-kubernetes-list-type: atomic maxSkew: - description: |- - MaxSkew describes the degree to which pods may be unevenly distributed. - When `whenUnsatisfiable=DoNotSchedule`, it is the maximum permitted difference - between the number of matching pods in the target topology and the global minimum. + description: MaxSkew describes the degree to which pods may + be unevenly distributed. format: int32 type: integer minDomains: - description: |- - MinDomains indicates a minimum number of eligible domains. - When the number of eligible domains with matching topology keys is less than minDomains, - Pod Topology Spread treats "global minimum" as 0, and then the calculation of Skew is performed. + description: MinDomains indicates a minimum number of eligible + domains. format: int32 type: integer nodeAffinityPolicy: description: |- NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector - when calculating pod topology spread skew. Options are: - - Honor: only nodes matching nodeAffinity/nodeSelector are included in the calculations. - - Ignore: nodeAffinity/nodeSelector are ignored. + when calculating pod... type: string nodeTaintsPolicy: description: |- NodeTaintsPolicy indicates how we will treat node taints when calculating - pod topology spread skew. Options are: - - Honor: nodes without taints, along with tainted nodes for which the incoming pod - has a toleration, are included. - - Ignore: node taints are ignored. All nodes are included. + pod topology spread skew. type: string topologyKey: - description: |- - TopologyKey is the key of node labels. Nodes that have a label with this key - and identical values are considered to be in the same topology. - We consider each as a "bucket", and try to put balanced number - of pods into each bucket. + description: TopologyKey is the key of node labels. type: string whenUnsatisfiable: description: |- WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy the spread constraint. - - DoNotSchedule (default) tells the scheduler not to schedule it. type: string required: - maxSkew @@ -8051,31 +9780,24 @@ spec: state of this API Resource. properties: lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + description: lastTransitionTime is the last time the condition + transitioned from one status to another. format: date-time type: string message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. + description: message is a human readable message indicating + details about the transition. maxLength: 32768 type: string observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. format: int64 minimum: 0 type: integer reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. maxLength: 1024 minLength: 1 pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ @@ -8155,19 +9877,12 @@ spec: description: EdgeConnect is the Schema for the EdgeConnect API properties: apiVersion: - 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 + description: APIVersion defines the versioned schema of this representation + of an object. type: string kind: - 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 + description: Kind is a string value representing the REST resource this + object represents. type: string metadata: type: object @@ -8184,12 +9899,11 @@ spec: your specific environment UUID type: string autoUpdate: - description: 'Enables automatic restarts of EdgeConnect pods in case - a new version is available (the default value is: true)' + description: Enables automatic restarts of EdgeConnect pods in case + a new version is available (the default... type: boolean caCertsRef: - description: Adds custom root certificate from a configmap. Put the - certificate under certs within your configmap. + description: Adds custom root certificate from a configmap. type: string customPullSecret: description: Pull secret for your private registry @@ -8207,9 +9921,7 @@ spec: value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. + using the previously defined environment variables in... type: string valueFrom: description: Source for the environment variable's value. Cannot @@ -8223,12 +9935,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the ConfigMap or its key @@ -8239,9 +9946,8 @@ spec: type: object x-kubernetes-map-type: atomic fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + description: 'Selects a field of the pod: supports metadata.name, + metadata.namespace, `metadata.' properties: apiVersion: description: Version of the schema the FieldPath is @@ -8258,7 +9964,7 @@ spec: resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + (limits.cpu, limits. properties: containerName: description: 'Container name: required for volumes, @@ -8288,12 +9994,7 @@ spec: type: string name: default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + description: Name of the referent. type: string optional: description: Specify whether the Secret or its key must @@ -8363,8 +10064,7 @@ spec: type: string provisioner: description: Determines if the operator will create the EdgeConnect - and light OAuth client on the cluster using the credentials - provided. Requires more scopes than default behavior. + and light OAuth client on the cluster using... type: boolean resource: description: URN identifying your account. You get the URN when @@ -8379,9 +10079,8 @@ spec: description: General configurations for proxy settings. properties: authRef: - description: |- - Secret name which contains the username and password used for authentication with the proxy, using the - "Basic" HTTP authentication scheme. + description: Secret name which contains the username and password + used for authentication with the proxy, using... type: string host: description: Server address (hostname or IP address) of the proxy. @@ -8389,8 +10088,7 @@ spec: noProxy: description: |- NoProxy represents the NO_PROXY or no_proxy environment - variable. It specifies a string that contains comma-separated values - specifying hosts that should be excluded from proxying. + variable. type: string port: description: Port of the proxy. @@ -8406,28 +10104,16 @@ spec: description: Defines resources requests and limits for single pods properties: claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. + description: Claims lists the names of resources, defined in spec. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. + description: Name must match the name of one entry in pod.spec. type: string request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. + description: Request is the name chosen for a request in + the referenced claim. type: string required: - name @@ -8445,7 +10131,7 @@ spec: x-kubernetes-int-or-string: true description: |- Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + More info: https://kubernetes. type: object requests: additionalProperties: @@ -8454,11 +10140,8 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes. + description: Requests describes the minimum amount of compute + resources required. type: object type: object serviceAccountName: @@ -8470,36 +10153,30 @@ spec: items: description: |- The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . + the triple... properties: effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + description: Effect indicates the taint effect to match. Empty + means match all taint effects. type: string key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. + description: Key is the taint key that the toleration applies + to. Empty means match all taint keys. type: string operator: description: |- Operator represents a key's relationship to the value. - Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. + Valid operators are Exists and Equal. type: string tolerationSeconds: description: |- TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). + of effect NoExecute,... format: int64 type: integer value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. + description: Value is the taint value the toleration matches + to. type: string type: object type: array @@ -8511,34 +10188,25 @@ spec: pods among the given topology. properties: labelSelector: - description: |- - LabelSelector is used to find matching pods. - Pods that match this label selector are counted to determine the number of pods - in their corresponding topology domain. + description: LabelSelector is used to find matching pods. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. + description: A label selector requirement is a selector + that contains values, a key, and an operator that... properties: key: description: key is the label key that the selector applies to. type: string operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. + description: operator represents a key's relationship + to a set of values. type: string values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. + description: values is an array of string values. items: type: string type: array @@ -8552,62 +10220,45 @@ spec: matchLabels: additionalProperties: type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. + description: matchLabels is a map of {key,value} pairs. type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: description: |- MatchLabelKeys is a set of pod label keys to select the pods over which - spreading will be calculated. + spreading will be... items: type: string type: array x-kubernetes-list-type: atomic maxSkew: - description: |- - MaxSkew describes the degree to which pods may be unevenly distributed. - When `whenUnsatisfiable=DoNotSchedule`, it is the maximum permitted difference - between the number of matching pods in the target topology and the global minimum. + description: MaxSkew describes the degree to which pods may + be unevenly distributed. format: int32 type: integer minDomains: - description: |- - MinDomains indicates a minimum number of eligible domains. - When the number of eligible domains with matching topology keys is less than minDomains, - Pod Topology Spread treats "global minimum" as 0, and then the calculation of Skew is performed. + description: MinDomains indicates a minimum number of eligible + domains. format: int32 type: integer nodeAffinityPolicy: description: |- NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector - when calculating pod topology spread skew. Options are: - - Honor: only nodes matching nodeAffinity/nodeSelector are included in the calculations. - - Ignore: nodeAffinity/nodeSelector are ignored. + when calculating pod... type: string nodeTaintsPolicy: description: |- NodeTaintsPolicy indicates how we will treat node taints when calculating - pod topology spread skew. Options are: - - Honor: nodes without taints, along with tainted nodes for which the incoming pod - has a toleration, are included. - - Ignore: node taints are ignored. All nodes are included. + pod topology spread skew. type: string topologyKey: - description: |- - TopologyKey is the key of node labels. Nodes that have a label with this key - and identical values are considered to be in the same topology. - We consider each as a "bucket", and try to put balanced number - of pods into each bucket. + description: TopologyKey is the key of node labels. type: string whenUnsatisfiable: description: |- WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy the spread constraint. - - DoNotSchedule (default) tells the scheduler not to schedule it. type: string required: - maxSkew @@ -8630,31 +10281,24 @@ spec: state of this API Resource. properties: lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + description: lastTransitionTime is the last time the condition + transitioned from one status to another. format: date-time type: string message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. + description: message is a human readable message indicating + details about the transition. maxLength: 32768 type: string observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. format: int64 minimum: 0 type: integer reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. maxLength: 1024 minLength: 1 pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ diff --git a/config/helm/chart/default/templates/Common/csi/daemonset.yaml b/config/helm/chart/default/templates/Common/csi/daemonset.yaml index 4017177d9a..858f575a94 100644 --- a/config/helm/chart/default/templates/Common/csi/daemonset.yaml +++ b/config/helm/chart/default/templates/Common/csi/daemonset.yaml @@ -112,16 +112,14 @@ spec: failureThreshold: 5 httpGet: path: /healthz - port: healthz + port: 9808 scheme: HTTP - initialDelaySeconds: 5 - periodSeconds: 5 + initialDelaySeconds: 15 + periodSeconds: 15 successThreshold: 1 - timeoutSeconds: 4 + timeoutSeconds: 10 {{- end }} ports: - - containerPort: 9808 - name: healthz - containerPort: 8080 name: metrics resources: @@ -166,13 +164,20 @@ spec: fieldRef: apiVersion: v1 fieldPath: metadata.namespace - {{- if .Values.csidriver.maxUnmountedVolumeAge }} - - name: MAX_UNMOUNTED_VOLUME_AGE - value: "{{ .Values.csidriver.maxUnmountedVolumeAge}}" + - name: KUBE_NODE_NAME + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: spec.nodeName + - name: TOLERATIONS + value: '{{ .Values.csidriver.tolerations | toJson }}' + - name: CSI_DATA_DIR + value: {{ include "dynatrace-operator.CSIDataDir" . }} + {{- if .Values.csidriver.cleanupPeriod }} + - name: CLEANUP_PERIOD + value: "{{ .Values.csidriver.cleanupPeriod}}" {{- end }} - envFrom: - - configMapRef: - name: install-config + {{ include "dynatrace-operator.modules-json-env" . | nindent 10 }} {{- include "dynatrace-operator.startupProbe" . | nindent 8 }} {{- if not .Values.debug }} livenessProbe: @@ -203,6 +208,9 @@ spec: mountPropagation: Bidirectional - mountPath: /tmp name: tmp-dir + - mountPath: {{ include "dynatrace-operator.CSIMountPointDir" . }} + name: mountpoint-dir # needed for garbage-collection + readOnly: true # Used to make a gRPC request (GetPluginInfo()) to the driver to get driver name and driver contain # - Needs access to the csi socket, needs to read/write to it, needs root permissions to do so. @@ -242,7 +250,7 @@ spec: args: - --csi-address=/csi/csi.sock - --health-port=9808 - - --probe-timeout=4s + - --probe-timeout=9s command: - livenessprobe resources: diff --git a/config/helm/chart/default/templates/Common/csi/role-csi.yaml b/config/helm/chart/default/templates/Common/csi/role-csi.yaml index 654ab04e13..1d6227f0c2 100644 --- a/config/helm/chart/default/templates/Common/csi/role-csi.yaml +++ b/config/helm/chart/default/templates/Common/csi/role-csi.yaml @@ -32,17 +32,26 @@ rules: - "" resources: - secrets + - configmaps verbs: - get - list - watch - apiGroups: - - "" + - dynatrace.com resources: - - configmaps + - dynakubes/finalizers + verbs: + - update + - apiGroups: + - batch + resources: + - jobs verbs: - get - list + - create + - delete - watch - apiGroups: - "" diff --git a/config/helm/chart/default/templates/Common/extensions-opentelemetry-collector/clusterrole-extensions-opentelemetry-collector.yaml b/config/helm/chart/default/templates/Common/extensions/clusterole-extensions-prometheus.yaml similarity index 77% rename from config/helm/chart/default/templates/Common/extensions-opentelemetry-collector/clusterrole-extensions-opentelemetry-collector.yaml rename to config/helm/chart/default/templates/Common/extensions/clusterole-extensions-prometheus.yaml index 5924728922..01e80e3849 100644 --- a/config/helm/chart/default/templates/Common/extensions-opentelemetry-collector/clusterrole-extensions-opentelemetry-collector.yaml +++ b/config/helm/chart/default/templates/Common/extensions/clusterole-extensions-prometheus.yaml @@ -18,7 +18,7 @@ metadata: name: dynatrace-extensions-prometheus namespace: {{ .Release.Namespace }} labels: - {{- include "dynatrace-operator.extensionsOpenTelemetryCollectorLabels" . | nindent 4 }} + {{- include "dynatrace-operator.openTelemetryCollectorLabels" . | nindent 4 }} rules: - apiGroups: - "" @@ -44,6 +44,16 @@ rules: - get - list - watch + {{- if (eq (include "dynatrace-operator.openshiftOrOlm" .) "true") }} + - apiGroups: + - security.openshift.io + resourceNames: + - privileged + resources: + - securitycontextconstraints + verbs: + - use + {{ end }} - nonResourceURLs: - /metrics - /metrics/cadvisor @@ -56,13 +66,13 @@ metadata: name: dynatrace-extensions-prometheus namespace: {{ .Release.Namespace }} labels: - {{- include "dynatrace-operator.extensionsOpenTelemetryCollectorLabels" . | nindent 4 }} + {{- include "dynatrace-operator.openTelemetryCollectorLabels" . | nindent 4 }} roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: dynatrace-extensions-prometheus subjects: - kind: ServiceAccount - name: dynatrace-extensions-collector + name: dynatrace-opentelemetry-collector namespace: {{ .Release.Namespace }} {{ end }} diff --git a/config/helm/chart/default/templates/Common/extensions-controller/role-extensions-controller.yaml b/config/helm/chart/default/templates/Common/extensions/role-extensions-controller.yaml similarity index 100% rename from config/helm/chart/default/templates/Common/extensions-controller/role-extensions-controller.yaml rename to config/helm/chart/default/templates/Common/extensions/role-extensions-controller.yaml diff --git a/config/helm/chart/default/templates/Common/extensions-controller/serviceaccount-extensions-controller.yaml b/config/helm/chart/default/templates/Common/extensions/serviceaccount-extensions-controller.yaml similarity index 100% rename from config/helm/chart/default/templates/Common/extensions-controller/serviceaccount-extensions-controller.yaml rename to config/helm/chart/default/templates/Common/extensions/serviceaccount-extensions-controller.yaml diff --git a/config/helm/chart/default/templates/Common/kubernetes-monitoring/clusterrole-kubernetes-monitoring-full.yaml b/config/helm/chart/default/templates/Common/kubernetes-monitoring/clusterrole-kubernetes-monitoring-full.yaml new file mode 100644 index 0000000000..06c095665b --- /dev/null +++ b/config/helm/chart/default/templates/Common/kubernetes-monitoring/clusterrole-kubernetes-monitoring-full.yaml @@ -0,0 +1,61 @@ +{{- if and ((.Values.preview).fullObjectCoverage).enabled .Values.rbac.activeGate.create }} +# Copyright 2021 Dynatrace LLC + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: dynatrace-kubernetes-monitoring-full + labels: + {{- include "dynatrace-operator.activegateLabels" . | nindent 4 }} +rules: + - apiGroups: [ "*" ] + resources: [ "*" ] + verbs: + - list + - watch + - get + - nonResourceURLs: + - /metrics + - /version + - /readyz + - /livez + verbs: + - get + {{- if (eq (include "dynatrace-operator.openshiftOrOlm" .) "true") }} + - apiGroups: + - security.openshift.io + resourceNames: + - privileged + - nonroot-v2 + resources: + - securitycontextconstraints + verbs: + - use + {{ end }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: dynatrace-kubernetes-monitoring-full + labels: + {{- include "dynatrace-operator.activegateLabels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: dynatrace-kubernetes-monitoring-full +subjects: + - kind: ServiceAccount + name: dynatrace-kubernetes-monitoring + namespace: {{ .Release.Namespace }} +{{ end }} diff --git a/config/helm/chart/default/templates/Common/kubernetes-monitoring/clusterrole-kubernetes-monitoring.yaml b/config/helm/chart/default/templates/Common/kubernetes-monitoring/clusterrole-kubernetes-monitoring.yaml index a4192ca841..3a46b5a891 100644 --- a/config/helm/chart/default/templates/Common/kubernetes-monitoring/clusterrole-kubernetes-monitoring.yaml +++ b/config/helm/chart/default/templates/Common/kubernetes-monitoring/clusterrole-kubernetes-monitoring.yaml @@ -1,4 +1,4 @@ -{{- if .Values.rbac.activeGate.create }} +{{- if or (eq .Values.rbac.activeGate.create true) (eq .Values.rbac.kspm.create true) }} # Copyright 2021 Dynatrace LLC # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/config/helm/chart/default/templates/Common/kubernetes-monitoring/serviceaccount-kubernetes-monitoring.yaml b/config/helm/chart/default/templates/Common/kubernetes-monitoring/serviceaccount-kubernetes-monitoring.yaml index 6116bb5924..49c4f06843 100644 --- a/config/helm/chart/default/templates/Common/kubernetes-monitoring/serviceaccount-kubernetes-monitoring.yaml +++ b/config/helm/chart/default/templates/Common/kubernetes-monitoring/serviceaccount-kubernetes-monitoring.yaml @@ -1,4 +1,4 @@ -{{- if .Values.rbac.activeGate.create }} +{{- if or (eq .Values.rbac.activeGate.create true) (eq .Values.rbac.kspm.create true) }} # Copyright 2021 Dynatrace LLC # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/config/helm/chart/default/templates/Common/logmonitoring/clusterrole-logmonitoring.yaml b/config/helm/chart/default/templates/Common/logmonitoring/clusterrole-logmonitoring.yaml index 61717eff40..8cb1ffac73 100644 --- a/config/helm/chart/default/templates/Common/logmonitoring/clusterrole-logmonitoring.yaml +++ b/config/helm/chart/default/templates/Common/logmonitoring/clusterrole-logmonitoring.yaml @@ -1,4 +1,4 @@ -{{- if and .Values.rbac.logMonitoring.create (eq (include "dynatrace-operator.openshiftOrOlm" .) "true") }} +{{- if .Values.rbac.logMonitoring.create }} # Copyright 2021 Dynatrace LLC # Licensed under the Apache License, Version 2.0 (the "License"); @@ -48,4 +48,21 @@ subjects: - kind: ServiceAccount name: dynatrace-logmonitoring namespace: {{ .Release.Namespace }} +{{ if .Values.rbac.oneAgent.create }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: dynatrace-logmonitoring-fullstack + labels: + {{- include "dynatrace-operator.logMonitoringLabels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: dynatrace-logmonitoring +subjects: +- kind: ServiceAccount + name: dynatrace-dynakube-oneagent + namespace: {{ .Release.Namespace }} +{{ end }} {{ end }} diff --git a/config/helm/chart/default/templates/Common/oneagent/serviceaccount-oneagent.yaml b/config/helm/chart/default/templates/Common/oneagent/serviceaccount-oneagent.yaml index bad574c2bb..bae5afe682 100644 --- a/config/helm/chart/default/templates/Common/oneagent/serviceaccount-oneagent.yaml +++ b/config/helm/chart/default/templates/Common/oneagent/serviceaccount-oneagent.yaml @@ -23,5 +23,5 @@ metadata: {{- end }} labels: {{- include "dynatrace-operator.oneagentLabels" . | nindent 4 }} -automountServiceAccountToken: false +automountServiceAccountToken: {{.Values.rbac.logMonitoring.create}} {{ end }} diff --git a/config/helm/chart/default/templates/Common/opentelemetry-collector/clusterole-telemetry-endpoints.yaml b/config/helm/chart/default/templates/Common/opentelemetry-collector/clusterole-telemetry-endpoints.yaml new file mode 100644 index 0000000000..21af3c1c81 --- /dev/null +++ b/config/helm/chart/default/templates/Common/opentelemetry-collector/clusterole-telemetry-endpoints.yaml @@ -0,0 +1,67 @@ +{{- if .Values.rbac.telemetryIngest.create }} +# Copyright 2021 Dynatrace LLC + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: dynatrace-telemetry-ingest + namespace: {{ .Release.Namespace }} + labels: + {{- include "dynatrace-operator.openTelemetryCollectorLabels" . | nindent 4 }} +rules: + - apiGroups: + - "" + resources: + - pods + - namespaces + - nodes + verbs: + - get + - watch + - list + - apiGroups: + - apps + resources: + - replicasets + verbs: + - get + - list + - watch + {{- if (eq (include "dynatrace-operator.openshiftOrOlm" .) "true") }} + - apiGroups: + - security.openshift.io + resourceNames: + - privileged + resources: + - securitycontextconstraints + verbs: + - use + {{ end }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: dynatrace-telemetry-ingest + namespace: {{ .Release.Namespace }} + labels: + {{- include "dynatrace-operator.openTelemetryCollectorLabels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: dynatrace-telemetry-ingest +subjects: + - kind: ServiceAccount + name: dynatrace-opentelemetry-collector + namespace: {{ .Release.Namespace }} +{{ end }} diff --git a/config/helm/chart/default/templates/Common/extensions-opentelemetry-collector/serviceaccount-extensions-opentelemetry-collector.yaml b/config/helm/chart/default/templates/Common/opentelemetry-collector/serviceaccount-opentelemetry-collector.yaml similarity index 79% rename from config/helm/chart/default/templates/Common/extensions-opentelemetry-collector/serviceaccount-extensions-opentelemetry-collector.yaml rename to config/helm/chart/default/templates/Common/opentelemetry-collector/serviceaccount-opentelemetry-collector.yaml index e12f0c6af9..255d229edc 100644 --- a/config/helm/chart/default/templates/Common/extensions-opentelemetry-collector/serviceaccount-extensions-opentelemetry-collector.yaml +++ b/config/helm/chart/default/templates/Common/opentelemetry-collector/serviceaccount-opentelemetry-collector.yaml @@ -1,4 +1,4 @@ -{{- if .Values.rbac.extensions.create }} +{{- if or (.Values.rbac.extensions.create) (.Values.rbac.telemetryIngest.create) }} # Copyright 2021 Dynatrace LLC # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,12 +15,12 @@ apiVersion: v1 kind: ServiceAccount metadata: - name: dynatrace-extensions-collector + name: dynatrace-opentelemetry-collector namespace: {{ .Release.Namespace }} {{- if .Values.rbac.extensions.annotations }} annotations: {{- toYaml .Values.rbac.extensions.annotations | nindent 4 }} {{- end }} labels: - {{- include "dynatrace-operator.extensionsOpenTelemetryCollectorLabels" . | nindent 4 }} + {{- include "dynatrace-operator.openTelemetryCollectorLabels" . | nindent 4 }} {{ end }} diff --git a/config/helm/chart/default/templates/Common/operator/allowlistsynchronizer.yaml b/config/helm/chart/default/templates/Common/operator/allowlistsynchronizer.yaml new file mode 100644 index 0000000000..4c8e0cffe7 --- /dev/null +++ b/config/helm/chart/default/templates/Common/operator/allowlistsynchronizer.yaml @@ -0,0 +1,26 @@ +{{ if eq (include "dynatrace-operator.needAutopilotAllowlisting" .) "true" }} +# Copyright 2021 Dynatrace LLC + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +apiVersion: auto.gke.io/v1 +kind: AllowlistSynchronizer +metadata: + name: dynatrace-operator + annotations: + "helm.sh/hook": pre-install, post-delete, pre-upgrade, pre-rollback +spec: + allowlistPaths: + - Dynatrace/csidriver/{{ .Chart.AppVersion }}/* + - Dynatrace/logmonitoring/{{ .Chart.AppVersion }}/* + - Dynatrace/csijob/{{ .Chart.AppVersion }}/* +{{- end -}} diff --git a/config/helm/chart/default/templates/Common/operator/clusterrole-operator.yaml b/config/helm/chart/default/templates/Common/operator/clusterrole-operator.yaml index a9098dd3df..1641438406 100644 --- a/config/helm/chart/default/templates/Common/operator/clusterrole-operator.yaml +++ b/config/helm/chart/default/templates/Common/operator/clusterrole-operator.yaml @@ -11,6 +11,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + +# For more information why the individual permissions are required see +# https://github.com/Dynatrace/dynatrace-operator/blob/main/doc/roles/operator-roles.md apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: @@ -46,6 +49,7 @@ rules: resources: - secrets resourceNames: + - dynatrace-bootstrapper-config - dynatrace-dynakube-config - dynatrace-metadata-enrichment-endpoint verbs: diff --git a/config/helm/chart/default/templates/Common/operator/deployment-operator.yaml b/config/helm/chart/default/templates/Common/operator/deployment-operator.yaml index 12d75d7a3d..06c2e59c4a 100644 --- a/config/helm/chart/default/templates/Common/operator/deployment-operator.yaml +++ b/config/helm/chart/default/templates/Common/operator/deployment-operator.yaml @@ -67,9 +67,7 @@ spec: valueFrom: fieldRef: fieldPath: metadata.name - envFrom: - - configMapRef: - name: install-config + {{ include "dynatrace-operator.modules-json-env" . | nindent 12}} ports: - containerPort: 10080 name: livez diff --git a/config/helm/chart/default/templates/Common/operator/install-config.yaml b/config/helm/chart/default/templates/Common/operator/install-config.yaml deleted file mode 100644 index d6aee6d8db..0000000000 --- a/config/helm/chart/default/templates/Common/operator/install-config.yaml +++ /dev/null @@ -1,20 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: install-config - namespace: {{ .Release.Namespace }} - labels: - {{- include "dynatrace-operator.operatorLabels" . | nindent 4 }} -data: - modules.json: | - { - "csiDriver": {{ .Values.csidriver.enabled }}, - "activeGate": {{ .Values.rbac.activeGate.create }}, - "oneAgent": {{ .Values.rbac.oneAgent.create }}, - "extensions": {{ .Values.rbac.extensions.create }}, - "logMonitoring": {{ .Values.rbac.logMonitoring.create }}, - "edgeConnect": {{ .Values.rbac.edgeConnect.create }}, - "supportability": {{ .Values.rbac.supportability }}, - "kspm": {{ .Values.rbac.kspm.create }} - } - diff --git a/config/helm/chart/default/templates/Common/operator/role-operator.yaml b/config/helm/chart/default/templates/Common/operator/role-operator.yaml index 5f0b851443..58bb8e3f5e 100644 --- a/config/helm/chart/default/templates/Common/operator/role-operator.yaml +++ b/config/helm/chart/default/templates/Common/operator/role-operator.yaml @@ -11,6 +11,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + +# For more information why the individual permissions are required see +# https://github.com/Dynatrace/dynatrace-operator/blob/main/doc/roles/operator-roles.md apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: @@ -42,27 +45,7 @@ rules: - apps resources: - statefulsets - verbs: - - get - - list - - watch - - create - - update - - delete - - apiGroups: - - apps - resources: - daemonsets - verbs: - - get - - list - - watch - - create - - update - - delete - - apiGroups: - - apps - resources: - replicasets - deployments verbs: @@ -82,6 +65,8 @@ rules: - "" resources: - configmaps + - secrets + - services verbs: - get - list @@ -97,17 +82,6 @@ rules: - get - list - watch - - apiGroups: - - "" - resources: - - secrets - verbs: - - get - - list - - watch - - create - - update - - delete - apiGroups: - "" resources: @@ -116,17 +90,6 @@ rules: - create - get - list - - apiGroups: - - "" - resources: - - services - verbs: - - create - - update - - delete - - get - - list - - watch - apiGroups: - networking.istio.io resources: diff --git a/config/helm/chart/default/templates/Common/webhook/clusterrole-webhook.yaml b/config/helm/chart/default/templates/Common/webhook/clusterrole-webhook.yaml index 953802aeb3..ab1e052b2a 100644 --- a/config/helm/chart/default/templates/Common/webhook/clusterrole-webhook.yaml +++ b/config/helm/chart/default/templates/Common/webhook/clusterrole-webhook.yaml @@ -39,6 +39,7 @@ rules: - secrets resourceNames: - dynatrace-dynakube-config + - dynatrace-bootstrapper-config - dynatrace-metadata-enrichment-endpoint verbs: - get diff --git a/config/helm/chart/default/templates/Common/webhook/deployment-webhook.yaml b/config/helm/chart/default/templates/Common/webhook/deployment-webhook.yaml index be0f6718eb..d118cf1bfc 100644 --- a/config/helm/chart/default/templates/Common/webhook/deployment-webhook.yaml +++ b/config/helm/chart/default/templates/Common/webhook/deployment-webhook.yaml @@ -87,9 +87,13 @@ spec: valueFrom: fieldRef: fieldPath: metadata.name - envFrom: - - configMapRef: - name: install-config + - name: WEBHOOK_PORT + value: "{{ .Values.webhook.ports.server | default "8443" }}" + - name: HEALTH_PROBE_BIND_ADDRESS + value: ":{{ .Values.webhook.ports.healthProbe | default "10080" }}" + - name: METRICS_BIND_ADDRESS + value: ":{{ .Values.webhook.ports.metrics | default "8383" }}" + {{ include "dynatrace-operator.modules-json-env" . | nindent 12 }} readinessProbe: httpGet: path: /readyz @@ -106,11 +110,11 @@ spec: periodSeconds: 10 ports: - name: server-port - containerPort: 8443 + containerPort: {{ .Values.webhook.ports.server | default 8443 }} - name: livez - containerPort: 10080 + containerPort: {{ .Values.webhook.ports.healthProbe | default 10080 }} - name: metrics - containerPort: 8080 + containerPort: {{ .Values.webhook.ports.metrics | default 8383 }} resources: requests: {{- toYaml (.Values.webhook).requests | nindent 14 }} diff --git a/config/helm/chart/default/templates/Common/webhook/validatingwebhookconfiguration.yaml b/config/helm/chart/default/templates/Common/webhook/validatingwebhookconfiguration.yaml index 6ecfd954c6..d0a66228fd 100644 --- a/config/helm/chart/default/templates/Common/webhook/validatingwebhookconfiguration.yaml +++ b/config/helm/chart/default/templates/Common/webhook/validatingwebhookconfiguration.yaml @@ -81,6 +81,27 @@ webhooks: timeoutSeconds: {{.Values.webhook.validatingWebhook.timeoutSeconds}} sideEffects: None matchPolicy: Exact + - admissionReviewVersions: + - v1 + clientConfig: + service: + name: dynatrace-webhook + namespace: {{ .Release.Namespace }} + path: /validate-dynatrace-com-v1beta4-dynakube + rules: + - operations: + - CREATE + - UPDATE + apiGroups: + - dynatrace.com + apiVersions: + - v1beta4 + resources: + - dynakubes + name: v1beta4.dynakube.webhook.dynatrace.com + timeoutSeconds: {{.Values.webhook.validatingWebhook.timeoutSeconds}} + sideEffects: None + matchPolicy: Exact - admissionReviewVersions: - v1 clientConfig: diff --git a/config/helm/chart/default/templates/_helpers.tpl b/config/helm/chart/default/templates/_helpers.tpl index 6b7a17949a..e27c8d9f6b 100644 --- a/config/helm/chart/default/templates/_helpers.tpl +++ b/config/helm/chart/default/templates/_helpers.tpl @@ -42,19 +42,19 @@ Check if default image or imageref is used {{- end -}} {{- define "webhook.securityContext" -}} - {{- if ne .Values.debug true -}} + {{- if not .Values.debug -}} {{- toYaml .Values.webhook.securityContext -}} {{- end -}} {{- end -}} {{- define "csidriver.provisioner.resources" -}} - {{- if ne .Values.debug true -}} + {{- if not .Values.debug -}} {{- toYaml .Values.csidriver.provisioner.resources -}} {{- end -}} {{- end -}} {{- define "csidriver.server.resources" -}} - {{- if ne .Values.debug true -}} + {{- if not .Values.debug -}} {{- toYaml .Values.csidriver.server.resources -}} {{- end -}} {{- end -}} @@ -69,3 +69,18 @@ startupProbe: timeoutSeconds: 5 failureThreshold: 1 {{- end -}} + +{{- define "dynatrace-operator.modules-json-env" -}} +- name: modules.json + value: | + { + "csiDriver": {{ .Values.csidriver.enabled }}, + "activeGate": {{ .Values.rbac.activeGate.create }}, + "oneAgent": {{ .Values.rbac.oneAgent.create }}, + "extensions": {{ .Values.rbac.extensions.create }}, + "logMonitoring": {{ .Values.rbac.logMonitoring.create }}, + "edgeConnect": {{ .Values.rbac.edgeConnect.create }}, + "supportability": {{ .Values.rbac.supportability }}, + "kspm": {{ .Values.rbac.kspm.create }} + } +{{- end -}} diff --git a/config/helm/chart/default/templates/_labels.tpl b/config/helm/chart/default/templates/_labels.tpl index 87870e9d8d..fab11a00bd 100644 --- a/config/helm/chart/default/templates/_labels.tpl +++ b/config/helm/chart/default/templates/_labels.tpl @@ -110,11 +110,11 @@ app.kubernetes.io/component: dynatrace-extensions-controller {{- end -}} {{/* -Extensions OpenTelemetry Collector (OTelC) labels +OpenTelemetry Collector (OTelC) labels */}} -{{- define "dynatrace-operator.extensionsOpenTelemetryCollectorLabels" -}} +{{- define "dynatrace-operator.openTelemetryCollectorLabels" -}} {{ include "dynatrace-operator.commonLabels" . }} -app.kubernetes.io/component: dynatrace-extensions-collector +app.kubernetes.io/component: dynatrace-opentelemetry-collector {{- end -}} {{/* diff --git a/config/helm/chart/default/templates/_platform.tpl b/config/helm/chart/default/templates/_platform.tpl index 2080d801e0..ef1130f92b 100644 --- a/config/helm/chart/default/templates/_platform.tpl +++ b/config/helm/chart/default/templates/_platform.tpl @@ -25,6 +25,15 @@ Auto-detect the platform (if not set), according to the available APIVersions {{- end -}} {{- end }} +{{/* +Auto-detect whether or not we need allowlisting for logagent and csi-driver +*/}} +{{- define "dynatrace-operator.needAutopilotAllowlisting" -}} + {{- if .Capabilities.APIVersions.Has "auto.gke.io/v1/AllowlistSynchronizer" }} + {{- printf "true" -}} + {{- end -}} +{{- end }} + {{/* Set install source how the Operator was installed */}} diff --git a/config/helm/chart/default/tests/Common/activegate/serviceaccount-activegate_test.yaml b/config/helm/chart/default/tests/Common/activegate/serviceaccount-activegate_test.yaml index 0d1b0d9dde..d926a9996b 100644 --- a/config/helm/chart/default/tests/Common/activegate/serviceaccount-activegate_test.yaml +++ b/config/helm/chart/default/tests/Common/activegate/serviceaccount-activegate_test.yaml @@ -18,4 +18,4 @@ tests: rbac.activeGate.create: false asserts: - hasDocuments: - count: 0 + count: 0 diff --git a/config/helm/chart/default/tests/Common/csi/daemonset_test.yaml b/config/helm/chart/default/tests/Common/csi/daemonset_test.yaml index 8520864c9e..98bf79d70f 100644 --- a/config/helm/chart/default/tests/Common/csi/daemonset_test.yaml +++ b/config/helm/chart/default/tests/Common/csi/daemonset_test.yaml @@ -42,17 +42,17 @@ tests: key: ToBeDeletedByClusterAutoscaler operator: Exists - - it: should set the env maxUnmountedVolumeAge + - it: should set the env cleanupPeriod set: platform: kubernetes csidriver.enabled: true - csidriver.maxUnmountedVolumeAge: "6" + csidriver.cleanupPeriod: "5m" asserts: - equal: - path: spec.template.spec.containers[1].env[1] #provisioner + path: spec.template.spec.containers[1].env[4] #provisioner value: - name: MAX_UNMOUNTED_VOLUME_AGE - value: "6" + name: CLEANUP_PERIOD + value: "5m" - it: should have nodeSelectors if set set: @@ -185,16 +185,14 @@ tests: failureThreshold: 5 httpGet: path: "/healthz" - port: healthz + port: 9808 scheme: HTTP - initialDelaySeconds: 5 - periodSeconds: 5 + initialDelaySeconds: 15 + periodSeconds: 15 successThreshold: 1 - timeoutSeconds: 4 + timeoutSeconds: 10 name: server ports: - - containerPort: 9808 - name: healthz - containerPort: 8080 name: metrics resources: @@ -236,9 +234,27 @@ tests: fieldRef: apiVersion: v1 fieldPath: metadata.namespace - envFrom: - - configMapRef: - name: install-config + - name: KUBE_NODE_NAME + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: spec.nodeName + - name: TOLERATIONS + value: '[{"effect":"NoSchedule","key":"node-role.kubernetes.io/master","operator":"Exists"},{"effect":"NoSchedule","key":"node-role.kubernetes.io/control-plane","operator":"Exists"}]' + - name: CSI_DATA_DIR + value: /var/lib/kubelet/plugins/csi.oneagent.dynatrace.com/data + - name: modules.json + value: | + { + "csiDriver": true, + "activeGate": true, + "oneAgent": true, + "extensions": true, + "logMonitoring": true, + "edgeConnect": true, + "supportability": true, + "kspm": true + } image: image-name imagePullPolicy: Always startupProbe: @@ -287,6 +303,9 @@ tests: name: data-dir - mountPath: "/tmp" name: tmp-dir + - mountPath: "/var/lib/kubelet/pods/" + name: mountpoint-dir + readOnly: true - args: - "--csi-address=/csi/csi.sock" - "--kubelet-registration-path=$(DRIVER_REG_SOCK_PATH)" @@ -324,7 +343,7 @@ tests: - args: - "--csi-address=/csi/csi.sock" - "--health-port=9808" - - "--probe-timeout=4s" + - "--probe-timeout=9s" command: - livenessprobe image: image-name diff --git a/config/helm/chart/default/tests/Common/csi/role-csi_test.yaml b/config/helm/chart/default/tests/Common/csi/role-csi_test.yaml index a245d4905d..55f67bd121 100644 --- a/config/helm/chart/default/tests/Common/csi/role-csi_test.yaml +++ b/config/helm/chart/default/tests/Common/csi/role-csi_test.yaml @@ -47,17 +47,26 @@ tests: - "" resources: - secrets + - configmaps verbs: - get - list - watch - apiGroups: - - "" + - dynatrace.com resources: - - configmaps + - dynakubes/finalizers + verbs: + - update + - apiGroups: + - batch + resources: + - jobs verbs: - get - list + - create + - delete - watch - apiGroups: - "" diff --git a/config/helm/chart/default/tests/Common/edgeconnect/serviceaccount-edgeconnect_test.yaml b/config/helm/chart/default/tests/Common/edgeconnect/serviceaccount-edgeconnect_test.yaml index 8ec19702f6..becef5cba7 100644 --- a/config/helm/chart/default/tests/Common/edgeconnect/serviceaccount-edgeconnect_test.yaml +++ b/config/helm/chart/default/tests/Common/edgeconnect/serviceaccount-edgeconnect_test.yaml @@ -18,4 +18,4 @@ tests: rbac.edgeConnect.create: false asserts: - hasDocuments: - count: 0 + count: 0 diff --git a/config/helm/chart/default/tests/Common/extensions-opentelemetry-collector/clusterrole-extensions-opentelemetry-collector_test.yaml b/config/helm/chart/default/tests/Common/extensions/clusterrole-extensions-prometheus_test.yaml similarity index 69% rename from config/helm/chart/default/tests/Common/extensions-opentelemetry-collector/clusterrole-extensions-opentelemetry-collector_test.yaml rename to config/helm/chart/default/tests/Common/extensions/clusterrole-extensions-prometheus_test.yaml index a84818b1dc..988393fbe4 100644 --- a/config/helm/chart/default/tests/Common/extensions-opentelemetry-collector/clusterrole-extensions-opentelemetry-collector_test.yaml +++ b/config/helm/chart/default/tests/Common/extensions/clusterrole-extensions-prometheus_test.yaml @@ -1,6 +1,6 @@ -suite: test clusterrole for the extensions OpenTelemetry collector +suite: test clusterrole for the dynatrace OpenTelemetry collector templates: - - Common/extensions-opentelemetry-collector/clusterrole-extensions-opentelemetry-collector.yaml + - Common/extensions/clusterole-extensions-prometheus.yaml tests: - it: ClusterRole and ClusterRoleBinding exists asserts: @@ -57,7 +57,29 @@ tests: - /metrics/cadvisor verbs: - get - + - it: ClusterRole should exist with extra permissions for openshift + documentIndex: 0 + set: + platform: openshift + asserts: + - isKind: + of: ClusterRole + - equal: + path: metadata.name + value: dynatrace-extensions-prometheus + - isNotEmpty: + path: metadata.labels + - contains: + path: rules + content: + apiGroups: + - security.openshift.io + resourceNames: + - privileged + resources: + - securitycontextconstraints + verbs: + - use - it: ClusterRoleBinding exists documentIndex: 1 asserts: @@ -74,4 +96,4 @@ tests: rbac.extensions.create: false asserts: - hasDocuments: - count: 0 + count: 0 diff --git a/config/helm/chart/default/tests/Common/extensions-controller/role-extensions-controller_test.yaml b/config/helm/chart/default/tests/Common/extensions/role-extensions-controller_test.yaml similarity index 93% rename from config/helm/chart/default/tests/Common/extensions-controller/role-extensions-controller_test.yaml rename to config/helm/chart/default/tests/Common/extensions/role-extensions-controller_test.yaml index 6e347718df..c999ea4e57 100644 --- a/config/helm/chart/default/tests/Common/extensions-controller/role-extensions-controller_test.yaml +++ b/config/helm/chart/default/tests/Common/extensions/role-extensions-controller_test.yaml @@ -1,6 +1,6 @@ suite: test role for the extensions controller templates: - - Common/extensions-controller/role-extensions-controller.yaml + - Common/extensions/role-extensions-controller.yaml tests: - it: should not exist by default asserts: @@ -48,4 +48,4 @@ tests: rbac.extensions.create: false asserts: - hasDocuments: - count: 0 + count: 0 diff --git a/config/helm/chart/default/tests/Common/extensions-controller/serviceaccount-extensions-controller_test.yaml b/config/helm/chart/default/tests/Common/extensions/serviceaccount-extensions-controller_test.yaml similarity index 89% rename from config/helm/chart/default/tests/Common/extensions-controller/serviceaccount-extensions-controller_test.yaml rename to config/helm/chart/default/tests/Common/extensions/serviceaccount-extensions-controller_test.yaml index 275f95ac7f..4eacee4b39 100644 --- a/config/helm/chart/default/tests/Common/extensions-controller/serviceaccount-extensions-controller_test.yaml +++ b/config/helm/chart/default/tests/Common/extensions/serviceaccount-extensions-controller_test.yaml @@ -1,6 +1,6 @@ suite: test service account for extensions controller templates: - - Common/extensions-controller/serviceaccount-extensions-controller.yaml + - Common/extensions/serviceaccount-extensions-controller.yaml tests: - it: should exist set: @@ -33,4 +33,4 @@ tests: rbac.extensions.create: false asserts: - hasDocuments: - count: 0 + count: 0 diff --git a/config/helm/chart/default/tests/Common/kspm/clusterrole-kubernetes-monitoring_test.yaml b/config/helm/chart/default/tests/Common/kspm/clusterrole-kubernetes-monitoring_test.yaml index e345bb8268..75264e7ea0 100644 --- a/config/helm/chart/default/tests/Common/kspm/clusterrole-kubernetes-monitoring_test.yaml +++ b/config/helm/chart/default/tests/Common/kspm/clusterrole-kubernetes-monitoring_test.yaml @@ -41,4 +41,4 @@ tests: rbac.kspm.create: false asserts: - hasDocuments: - count: 0 + count: 0 diff --git a/config/helm/chart/default/tests/Common/kspm/serviceaccount-node-config-collector_test.yaml b/config/helm/chart/default/tests/Common/kspm/serviceaccount-node-config-collector_test.yaml index 460d0aba33..4c79e0728f 100644 --- a/config/helm/chart/default/tests/Common/kspm/serviceaccount-node-config-collector_test.yaml +++ b/config/helm/chart/default/tests/Common/kspm/serviceaccount-node-config-collector_test.yaml @@ -43,4 +43,4 @@ tests: rbac.kspm.create: false asserts: - hasDocuments: - count: 0 + count: 0 diff --git a/config/helm/chart/default/tests/Common/kubernetes-monitoring/clusterrole-kubernetes-monitoring-full_test.yaml b/config/helm/chart/default/tests/Common/kubernetes-monitoring/clusterrole-kubernetes-monitoring-full_test.yaml new file mode 100644 index 0000000000..d813fbcec6 --- /dev/null +++ b/config/helm/chart/default/tests/Common/kubernetes-monitoring/clusterrole-kubernetes-monitoring-full_test.yaml @@ -0,0 +1,79 @@ +suite: test clusterrole for kubernetes monitoring full +templates: + - Common/kubernetes-monitoring/clusterrole-kubernetes-monitoring-full.yaml +tests: + - it: ClusterRole should exist if enabled + documentIndex: 0 + set: + rbac.activeGate.create: true + preview.fullObjectCoverage.enabled.create: true + + asserts: + - isKind: + of: ClusterRole + - equal: + path: metadata.name + value: dynatrace-kubernetes-monitoring-full + - isNotEmpty: + path: metadata.labels + - isNotEmpty: + path: rules + - contains: + path: rules + content: + apiGroups: + - "*" + resources: + - "*" + verbs: + - list + - watch + - get + - contains: + path: rules + content: + nonResourceURLs: + - /metrics + - /version + - /readyz + - /livez + verbs: + - get + - it: ClusterRole should exist if enabled and have extra permissions if on openshift + documentIndex: 0 + set: + platform: openshift + rbac.activeGate.create: true + preview.fullObjectCoverage.enabled.create: true + asserts: + - isKind: + of: ClusterRole + - equal: + path: metadata.name + value: dynatrace-kubernetes-monitoring-full + - isNotEmpty: + path: metadata.labels + - contains: + path: rules + content: + apiGroups: + - security.openshift.io + resourceNames: + - privileged + - nonroot-v2 + resources: + - securitycontextconstraints + verbs: + - use + - it: shouldn't exist if activeGate is turned off + set: + preview.fullObjectCoverage.enabled: true + rbac.activeGate.create: false + asserts: + - hasDocuments: + count: 0 + - it: shouldn't exist by default + set: + asserts: + - hasDocuments: + count: 0 diff --git a/config/helm/chart/default/tests/Common/kubernetes-monitoring/clusterrole-kubernetes-monitoring_test.yaml b/config/helm/chart/default/tests/Common/kubernetes-monitoring/clusterrole-kubernetes-monitoring_test.yaml index 2950220666..54f0431ece 100644 --- a/config/helm/chart/default/tests/Common/kubernetes-monitoring/clusterrole-kubernetes-monitoring_test.yaml +++ b/config/helm/chart/default/tests/Common/kubernetes-monitoring/clusterrole-kubernetes-monitoring_test.yaml @@ -149,9 +149,17 @@ tests: kind: ServiceAccount name: dynatrace-kubernetes-monitoring namespace: NAMESPACE + - it: should exist if only kspm is turned on + set: + rbac.activeGate.create: false + rbac.kspm.create: true + asserts: + - hasDocuments: + count: 2 - it: shouldn't exist if turned off set: rbac.activeGate.create: false + rbac.kspm.create: false asserts: - hasDocuments: - count: 0 + count: 0 diff --git a/config/helm/chart/default/tests/Common/kubernetes-monitoring/serviceaccount-kubernetes-monitoring_test.yaml b/config/helm/chart/default/tests/Common/kubernetes-monitoring/serviceaccount-kubernetes-monitoring_test.yaml index 064948045a..8a08f1fcad 100644 --- a/config/helm/chart/default/tests/Common/kubernetes-monitoring/serviceaccount-kubernetes-monitoring_test.yaml +++ b/config/helm/chart/default/tests/Common/kubernetes-monitoring/serviceaccount-kubernetes-monitoring_test.yaml @@ -28,9 +28,17 @@ tests: path: metadata.annotations value: test: test + - it: should exist if only kspm is turned on + set: + rbac.activeGate.create: false + rbac.kspm.create: true + asserts: + - isKind: + of: ServiceAccount - it: shouldn't exist if turned off set: rbac.activeGate.create: false + rbac.kspm.create: false asserts: - hasDocuments: - count: 0 + count: 0 diff --git a/config/helm/chart/default/tests/Common/logmonitoring/clusterrole-logmonitoring_test.yaml b/config/helm/chart/default/tests/Common/logmonitoring/clusterrole-logmonitoring_test.yaml index 83dd7d3dcf..0260050854 100644 --- a/config/helm/chart/default/tests/Common/logmonitoring/clusterrole-logmonitoring_test.yaml +++ b/config/helm/chart/default/tests/Common/logmonitoring/clusterrole-logmonitoring_test.yaml @@ -4,7 +4,8 @@ templates: tests: - it: logmonitoring ClusterRole should exist set: - platform: openshift + rbac.logMonitoring.create: true + rbac.oneagent.create: false documentIndex: 0 asserts: - isKind: @@ -28,7 +29,8 @@ tests: - it: logmonitoring ClusterRoleBinding should exist documentIndex: 1 set: - platform: openshift + rbac.logMonitoring.create: true + rbac.oneagent.create: false asserts: - isKind: of: ClusterRoleBinding @@ -37,16 +39,23 @@ tests: value: dynatrace-logmonitoring - isNotEmpty: path: metadata.labels - - it: shouldn't exist if not openshift + - it: extra binding should exist for fullstack + documentIndex: 2 set: rbac.logMonitoring.create: true - platform: NOT-openshift + rbac.oneagent.create: true asserts: - - hasDocuments: - count: 0 + - isKind: + of: ClusterRoleBinding + - equal: + path: metadata.name + value: dynatrace-logmonitoring-fullstack + - isNotEmpty: + path: metadata.labels - it: shouldn't exist if turned off set: rbac.logMonitoring.create: false + rbac.oneagent.create: true asserts: - hasDocuments: - count: 0 + count: 0 diff --git a/config/helm/chart/default/tests/Common/logmonitoring/serviceaccount-logmonitoring_test.yaml b/config/helm/chart/default/tests/Common/logmonitoring/serviceaccount-logmonitoring_test.yaml index 9ed5a243aa..9aaf740a57 100644 --- a/config/helm/chart/default/tests/Common/logmonitoring/serviceaccount-logmonitoring_test.yaml +++ b/config/helm/chart/default/tests/Common/logmonitoring/serviceaccount-logmonitoring_test.yaml @@ -31,4 +31,4 @@ tests: rbac.logMonitoring.create: false asserts: - hasDocuments: - count: 0 + count: 0 diff --git a/config/helm/chart/default/tests/Common/oneagent/clusterrole-oneagent_test.yaml b/config/helm/chart/default/tests/Common/oneagent/clusterrole-oneagent_test.yaml index 990a9f000a..cf55fb5e94 100644 --- a/config/helm/chart/default/tests/Common/oneagent/clusterrole-oneagent_test.yaml +++ b/config/helm/chart/default/tests/Common/oneagent/clusterrole-oneagent_test.yaml @@ -47,4 +47,4 @@ tests: rbac.oneAgent.create: false asserts: - hasDocuments: - count: 0 + count: 0 diff --git a/config/helm/chart/default/tests/Common/oneagent/serviceaccount-oneagent_test.yaml b/config/helm/chart/default/tests/Common/oneagent/serviceaccount-oneagent_test.yaml index 021b1f83f2..71c03aa60f 100644 --- a/config/helm/chart/default/tests/Common/oneagent/serviceaccount-oneagent_test.yaml +++ b/config/helm/chart/default/tests/Common/oneagent/serviceaccount-oneagent_test.yaml @@ -2,7 +2,7 @@ suite: test serviceaccount for oneagent templates: - Common/oneagent/serviceaccount-oneagent.yaml tests: - - it: should exist + - it: should exist on kubernetes set: platform: kubernetes asserts: @@ -16,8 +16,7 @@ tests: value: NAMESPACE - isNull: path: imagePullSecrets - - - it: should exist + - it: should exist on openshift set: platform: openshift asserts: @@ -26,9 +25,9 @@ tests: - equal: path: metadata.name value: dynatrace-dynakube-oneagent - - - it: should exist + - it: should add user annotations set: + rbac.oneAgent.create: true rbac.oneAgent.annotations: test: test asserts: @@ -43,4 +42,24 @@ tests: rbac.oneAgent.create: false asserts: - hasDocuments: - count: 0 + count: 0 + - it: should have automountServiceAccountToken set to TRUE, incase of log-monitoring is available + set: + rbac.oneAgent.create: true + rbac.logMonitoring.create: true + asserts: + - isKind: + of: ServiceAccount + - equal: + path: automountServiceAccountToken + value: true + - it: should have automountServiceAccountToken set to FALSE, incase of log-monitoring is NOT available + set: + rbac.oneAgent.create: true + rbac.logMonitoring.create: false + asserts: + - isKind: + of: ServiceAccount + - equal: + path: automountServiceAccountToken + value: false diff --git a/config/helm/chart/default/tests/Common/opentelemetry-collector/clusterrole-telemetry-endpoints_test.yaml b/config/helm/chart/default/tests/Common/opentelemetry-collector/clusterrole-telemetry-endpoints_test.yaml new file mode 100644 index 0000000000..b8ec64d51d --- /dev/null +++ b/config/helm/chart/default/tests/Common/opentelemetry-collector/clusterrole-telemetry-endpoints_test.yaml @@ -0,0 +1,85 @@ +suite: test clusterrole for the dynatrace OpenTelemetry collector endpoints +templates: + - Common/opentelemetry-collector/clusterole-telemetry-endpoints.yaml +tests: + - it: ClusterRole and ClusterRoleBinding exists + asserts: + - hasDocuments: + count: 2 + + - it: ClusterRole has correct permissions + documentIndex: 0 + asserts: + - isKind: + of: ClusterRole + - equal: + path: metadata.name + value: dynatrace-telemetry-ingest + - isNotEmpty: + path: metadata.labels + - isNotEmpty: + path: rules + - contains: + path: rules + content: + apiGroups: + - "" + resources: + - pods + - namespaces + - nodes + verbs: + - get + - watch + - list + - contains: + path: rules + content: + apiGroups: + - apps + resources: + - replicasets + verbs: + - get + - list + - watch + - it: ClusterRole should exist with extra permissions for openshift + documentIndex: 0 + set: + platform: openshift + asserts: + - isKind: + of: ClusterRole + - equal: + path: metadata.name + value: dynatrace-telemetry-ingest + - isNotEmpty: + path: metadata.labels + - contains: + path: rules + content: + apiGroups: + - security.openshift.io + resourceNames: + - privileged + resources: + - securitycontextconstraints + verbs: + - use + - it: ClusterRoleBinding exists + documentIndex: 1 + asserts: + - isKind: + of: ClusterRoleBinding + - equal: + path: metadata.name + value: dynatrace-telemetry-ingest + - isNotEmpty: + path: metadata.labels + + - it: shouldn't exist if turned off + set: + rbac.telemetryIngest.create: false + asserts: + - hasDocuments: + count: 0 diff --git a/config/helm/chart/default/tests/Common/extensions-opentelemetry-collector/serviceaccount-extensions-opentelemetry-collector_test.yaml b/config/helm/chart/default/tests/Common/opentelemetry-collector/serviceaccount-opentelemetry-collector_test.yaml similarity index 62% rename from config/helm/chart/default/tests/Common/extensions-opentelemetry-collector/serviceaccount-extensions-opentelemetry-collector_test.yaml rename to config/helm/chart/default/tests/Common/opentelemetry-collector/serviceaccount-opentelemetry-collector_test.yaml index 5c977365e1..b5591fc128 100644 --- a/config/helm/chart/default/tests/Common/extensions-opentelemetry-collector/serviceaccount-extensions-opentelemetry-collector_test.yaml +++ b/config/helm/chart/default/tests/Common/opentelemetry-collector/serviceaccount-opentelemetry-collector_test.yaml @@ -1,6 +1,6 @@ -suite: test service account for extensions OpenTelemetry collector +suite: test service account for dynatrace OpenTelemetry collector templates: - - Common/extensions-opentelemetry-collector/serviceaccount-extensions-opentelemetry-collector.yaml + - Common/opentelemetry-collector/serviceaccount-opentelemetry-collector.yaml tests: - it: should exist set: @@ -10,7 +10,7 @@ tests: of: ServiceAccount - equal: path: metadata.name - value: dynatrace-extensions-collector + value: dynatrace-opentelemetry-collector - equal: path: metadata.namespace value: NAMESPACE @@ -20,6 +20,7 @@ tests: - it: shouldn't exist if turned off set: rbac.extensions.create: false + rbac.telemetryIngest.create: false asserts: - hasDocuments: - count: 0 + count: 0 diff --git a/config/helm/chart/default/tests/Common/operator/allowlistsynchronizer_test.yaml b/config/helm/chart/default/tests/Common/operator/allowlistsynchronizer_test.yaml new file mode 100644 index 0000000000..6d81944beb --- /dev/null +++ b/config/helm/chart/default/tests/Common/operator/allowlistsynchronizer_test.yaml @@ -0,0 +1,20 @@ +suite: test allowlistsynchronizer for GKE-Autopilot +templates: + - Common/operator/allowlistsynchronizer.yaml +tests: + - it: shouldn't exist by default + asserts: + - hasDocuments: + count: 0 + - it: should exist on GKE-Autopilot + capabilities: + apiVersions: + - auto.gke.io/v1/AllowlistSynchronizer + asserts: + - isKind: + of: AllowlistSynchronizer + - isAPIVersion: + of: auto.gke.io/v1 + - equal: + path: metadata.name + value: dynatrace-operator diff --git a/config/helm/chart/default/tests/Common/operator/deployment-operator_test.yaml b/config/helm/chart/default/tests/Common/operator/deployment-operator_test.yaml index e6cb89352a..e18bbfb6a0 100644 --- a/config/helm/chart/default/tests/Common/operator/deployment-operator_test.yaml +++ b/config/helm/chart/default/tests/Common/operator/deployment-operator_test.yaml @@ -86,9 +86,18 @@ tests: valueFrom: fieldRef: fieldPath: metadata.name - envFrom: - - configMapRef: - name: install-config + - name: modules.json + value: | + { + "csiDriver": true, + "activeGate": true, + "oneAgent": true, + "extensions": true, + "logMonitoring": true, + "edgeConnect": true, + "supportability": true, + "kspm": true + } ports: - containerPort: 10080 name: livez @@ -264,9 +273,18 @@ tests: valueFrom: fieldRef: fieldPath: metadata.name - envFrom: - - configMapRef: - name: install-config + - name: modules.json + value: | + { + "csiDriver": true, + "activeGate": true, + "oneAgent": true, + "extensions": true, + "logMonitoring": true, + "edgeConnect": true, + "supportability": true, + "kspm": true + } ports: - containerPort: 10080 name: livez diff --git a/config/helm/chart/default/tests/Common/operator/install-config_test.yaml b/config/helm/chart/default/tests/Common/operator/install-config_test.yaml deleted file mode 100644 index fb34fb7c25..0000000000 --- a/config/helm/chart/default/tests/Common/operator/install-config_test.yaml +++ /dev/null @@ -1,61 +0,0 @@ -suite: test role for oneagent on kubernetes -templates: - - Common/operator/install-config.yaml -tests: - - it: ConfigMap should exist - asserts: - - equal: - path: metadata.name - value: install-config - - equal: - path: metadata.namespace - value: NAMESPACE - - isNotEmpty: - path: metadata.labels - - equal: - path: data - value: - modules.json: | - { - "csiDriver": true, - "activeGate": true, - "oneAgent": true, - "extensions": true, - "logMonitoring": true, - "edgeConnect": true, - "supportability": true, - "kspm": true - } - - it: ConfigMap should respect the set values - set: - csidriver.enabled: false - rbac.oneAgent.create: false - rbac.logMonitoring.create: false - rbac.edgeConnect.create: false - rbac.activeGate.create: false - rbac.extensions.create: false - rbac.supportability: false - rbac.kspm.create: false - asserts: - - equal: - path: metadata.name - value: install-config - - equal: - path: metadata.namespace - value: NAMESPACE - - isNotEmpty: - path: metadata.labels - - equal: - path: data - value: - modules.json: | - { - "csiDriver": false, - "activeGate": false, - "oneAgent": false, - "extensions": false, - "logMonitoring": false, - "edgeConnect": false, - "supportability": false, - "kspm": false - } diff --git a/config/helm/chart/default/tests/Common/operator/role-operator_test.yaml b/config/helm/chart/default/tests/Common/operator/role-operator_test.yaml index 9352fc658e..9c6bd13c26 100644 --- a/config/helm/chart/default/tests/Common/operator/role-operator_test.yaml +++ b/config/helm/chart/default/tests/Common/operator/role-operator_test.yaml @@ -39,27 +39,7 @@ tests: - apps resources: - statefulsets - verbs: - - get - - list - - watch - - create - - update - - delete - - apiGroups: - - apps - resources: - daemonsets - verbs: - - get - - list - - watch - - create - - update - - delete - - apiGroups: - - apps - resources: - replicasets - deployments verbs: @@ -79,6 +59,8 @@ tests: - "" resources: - configmaps + - secrets + - services verbs: - get - list @@ -94,17 +76,6 @@ tests: - get - list - watch - - apiGroups: - - "" - resources: - - secrets - verbs: - - get - - list - - watch - - create - - update - - delete - apiGroups: - "" resources: @@ -113,17 +84,6 @@ tests: - create - get - list - - apiGroups: - - "" - resources: - - services - verbs: - - create - - update - - delete - - get - - list - - watch - apiGroups: - networking.istio.io resources: diff --git a/config/helm/chart/default/tests/Common/webhook/clusterrole-webhook_test.yaml b/config/helm/chart/default/tests/Common/webhook/clusterrole-webhook_test.yaml index dc53e803eb..7a4f3422cc 100644 --- a/config/helm/chart/default/tests/Common/webhook/clusterrole-webhook_test.yaml +++ b/config/helm/chart/default/tests/Common/webhook/clusterrole-webhook_test.yaml @@ -40,6 +40,7 @@ tests: - "" resourceNames: - dynatrace-dynakube-config + - dynatrace-bootstrapper-config - dynatrace-metadata-enrichment-endpoint resources: - secrets diff --git a/config/helm/chart/default/tests/Common/webhook/deployment-webhook_test.yaml b/config/helm/chart/default/tests/Common/webhook/deployment-webhook_test.yaml index 213ccff512..c3c2cbaea5 100644 --- a/config/helm/chart/default/tests/Common/webhook/deployment-webhook_test.yaml +++ b/config/helm/chart/default/tests/Common/webhook/deployment-webhook_test.yaml @@ -116,9 +116,24 @@ tests: valueFrom: fieldRef: fieldPath: metadata.name - envFrom: - - configMapRef: - name: install-config + - name: WEBHOOK_PORT + value: "8443" + - name: HEALTH_PROBE_BIND_ADDRESS + value: :10080 + - name: METRICS_BIND_ADDRESS + value: :8383 + - name: modules.json + value: | + { + "csiDriver": true, + "activeGate": true, + "oneAgent": true, + "extensions": true, + "logMonitoring": true, + "edgeConnect": true, + "supportability": true, + "kspm": true + } livenessProbe: httpGet: path: /livez @@ -131,7 +146,7 @@ tests: name: server-port - containerPort: 10080 name: livez - - containerPort: 8080 + - containerPort: 8383 name: metrics readinessProbe: httpGet: @@ -297,9 +312,24 @@ tests: valueFrom: fieldRef: fieldPath: metadata.name - envFrom: - - configMapRef: - name: install-config + - name: WEBHOOK_PORT + value: "8443" + - name: HEALTH_PROBE_BIND_ADDRESS + value: :10080 + - name: METRICS_BIND_ADDRESS + value: :8383 + - name: modules.json + value: | + { + "csiDriver": true, + "activeGate": true, + "oneAgent": true, + "extensions": true, + "logMonitoring": true, + "edgeConnect": true, + "supportability": true, + "kspm": true + } livenessProbe: httpGet: path: /livez @@ -312,7 +342,7 @@ tests: name: server-port - containerPort: 10080 name: livez - - containerPort: 8080 + - containerPort: 8383 name: metrics readinessProbe: httpGet: @@ -443,9 +473,24 @@ tests: valueFrom: fieldRef: fieldPath: metadata.name - envFrom: - - configMapRef: - name: install-config + - name: WEBHOOK_PORT + value: "8443" + - name: HEALTH_PROBE_BIND_ADDRESS + value: :10080 + - name: METRICS_BIND_ADDRESS + value: :8383 + - name: modules.json + value: | + { + "csiDriver": true, + "activeGate": true, + "oneAgent": true, + "extensions": true, + "logMonitoring": true, + "edgeConnect": true, + "supportability": true, + "kspm": true + } livenessProbe: httpGet: path: /livez @@ -458,7 +503,7 @@ tests: name: server-port - containerPort: 10080 name: livez - - containerPort: 8080 + - containerPort: 8383 name: metrics readinessProbe: httpGet: diff --git a/config/helm/chart/default/tests/Common/webhook/poddisruptionbudget-webhook_test.yaml b/config/helm/chart/default/tests/Common/webhook/poddisruptionbudget-webhook_test.yaml index 7dba9ec551..99cda458ed 100644 --- a/config/helm/chart/default/tests/Common/webhook/poddisruptionbudget-webhook_test.yaml +++ b/config/helm/chart/default/tests/Common/webhook/poddisruptionbudget-webhook_test.yaml @@ -19,4 +19,4 @@ tests: webhook.highAvailability: false asserts: - hasDocuments: - count: 0 + count: 0 diff --git a/config/helm/chart/default/tests/Common/webhook/validatingwebhookconfiguration_test.yaml b/config/helm/chart/default/tests/Common/webhook/validatingwebhookconfiguration_test.yaml index e52f61671f..70232be82e 100644 --- a/config/helm/chart/default/tests/Common/webhook/validatingwebhookconfiguration_test.yaml +++ b/config/helm/chart/default/tests/Common/webhook/validatingwebhookconfiguration_test.yaml @@ -79,6 +79,27 @@ tests: timeoutSeconds: 10 sideEffects: None matchPolicy: Exact + - admissionReviewVersions: + - v1 + clientConfig: + service: + name: dynatrace-webhook + namespace: NAMESPACE + path: /validate-dynatrace-com-v1beta4-dynakube + rules: + - operations: + - CREATE + - UPDATE + apiGroups: + - dynatrace.com + apiVersions: + - v1beta4 + resources: + - dynakubes + name: v1beta4.dynakube.webhook.dynatrace.com + timeoutSeconds: 10 + sideEffects: None + matchPolicy: Exact - admissionReviewVersions: - v1 clientConfig: diff --git a/config/helm/chart/default/values.yaml b/config/helm/chart/default/values.yaml index c85f309905..c6bf800c9d 100644 --- a/config/helm/chart/default/values.yaml +++ b/config/helm/chart/default/values.yaml @@ -57,6 +57,10 @@ operator: webhook: hostNetwork: false + ports: + server: 8443 + metrics: 8383 + healthProbe: 10080 nodeSelector: {} tolerations: [] labels: {} @@ -96,7 +100,7 @@ csidriver: kubeletPath: "/var/lib/kubelet" existingPriorityClassName: "" # if defined, use this priorityclass instead of creating a new one priorityClassValue: "1000000" - maxUnmountedVolumeAge: "" # defined in days, must be a plain number + cleanupPeriod: "" # defined in the Golang time.Duration format, like "30m" == 30 minutes tolerations: - effect: NoSchedule key: node-role.kubernetes.io/master @@ -204,6 +208,9 @@ rbac: extensions: create: true annotations: {} + telemetryIngest: + create: true + annotations: {} logMonitoring: create: true annotations: {} diff --git a/config/helm/repos/stable/index.yaml b/config/helm/repos/stable/index.yaml index 739353b5dc..9457ef6d58 100644 --- a/config/helm/repos/stable/index.yaml +++ b/config/helm/repos/stable/index.yaml @@ -1,6 +1,50 @@ apiVersion: v1 entries: dynatrace-operator: + - apiVersion: v2 + appVersion: 1.4.1 + created: '2025-02-12T16:45:34.569679796Z' + description: The Dynatrace Operator Helm chart for Kubernetes and OpenShift + digest: 1389f21f5a1b26ef24b6759177a44c5ebd46666ddc1cff8c7c64fdb4dd00f759 + home: https://www.dynatrace.com/ + icon: https://assets.dynatrace.com/global/resources/Signet_Logo_RGB_CP_512x512px.png + kubeVersion: '>=1.19.0-0' + maintainers: + - email: marcell.sevcsik@dynatrace.com + name: 0sewa0 + - email: christoph.muellner@dynatrace.com + name: chrismuellner + - email: stefan.hauth@dynatrace.com + name: StefanHauth + name: dynatrace-operator + sources: + - https://github.com/Dynatrace/dynatrace-operator + type: application + urls: + - https://github.com/Dynatrace/dynatrace-operator/releases/download/v1.4.1/dynatrace-operator-1.4.1.tgz + version: 1.4.1 + - apiVersion: v2 + appVersion: 1.4.0 + created: '2024-12-09T09:04:42.588691061Z' + description: The Dynatrace Operator Helm chart for Kubernetes and OpenShift + digest: d09c178806648400d76955a4c0af249cf769016eda768899f4ba5a700e1cdc58 + home: https://www.dynatrace.com/ + icon: https://assets.dynatrace.com/global/resources/Signet_Logo_RGB_CP_512x512px.png + kubeVersion: '>=1.19.0-0' + maintainers: + - email: marcell.sevcsik@dynatrace.com + name: 0sewa0 + - email: christoph.muellner@dynatrace.com + name: chrismuellner + - email: lukas.hinterreiter@dynatrace.com + name: luhi-DT + name: dynatrace-operator + sources: + - https://github.com/Dynatrace/dynatrace-operator + type: application + urls: + - https://github.com/Dynatrace/dynatrace-operator/releases/download/v1.4.0/dynatrace-operator-1.4.0.tgz + version: 1.4.0 - apiVersion: v2 appVersion: 1.3.2 created: '2024-10-24T11:28:10.332056983Z' @@ -1038,4 +1082,4 @@ entries: urls: - https://raw.githubusercontent.com/Dynatrace/helm-charts/master/repos/stable/dynatrace-operator-0.1.0.tgz version: 0.1.0 -generated: '2024-10-31T07:13:31.055858571Z' +generated: '2025-02-12T16:45:34.566587927Z' diff --git a/config/helm/repos/stable/index.yaml.previous b/config/helm/repos/stable/index.yaml.previous index 60252e85b7..ad5f93fb80 100644 --- a/config/helm/repos/stable/index.yaml.previous +++ b/config/helm/repos/stable/index.yaml.previous @@ -1,6 +1,116 @@ apiVersion: v1 entries: dynatrace-operator: + - apiVersion: v2 + appVersion: 1.4.0 + created: '2024-12-09T09:04:42.588691061Z' + description: The Dynatrace Operator Helm chart for Kubernetes and OpenShift + digest: d09c178806648400d76955a4c0af249cf769016eda768899f4ba5a700e1cdc58 + home: https://www.dynatrace.com/ + icon: https://assets.dynatrace.com/global/resources/Signet_Logo_RGB_CP_512x512px.png + kubeVersion: '>=1.19.0-0' + maintainers: + - email: marcell.sevcsik@dynatrace.com + name: 0sewa0 + - email: christoph.muellner@dynatrace.com + name: chrismuellner + - email: lukas.hinterreiter@dynatrace.com + name: luhi-DT + name: dynatrace-operator + sources: + - https://github.com/Dynatrace/dynatrace-operator + type: application + urls: + - https://github.com/Dynatrace/dynatrace-operator/releases/download/v1.4.0/dynatrace-operator-1.4.0.tgz + version: 1.4.0 + - apiVersion: v2 + appVersion: 1.3.2 + created: '2024-10-24T11:28:10.332056983Z' + description: The Dynatrace Operator Helm chart for Kubernetes and OpenShift + digest: 81d6a0741113147b425fb41ce38933fb0697a56bb8f65bb9c67dccda33ae4507 + home: https://www.dynatrace.com/ + icon: https://assets.dynatrace.com/global/resources/Signet_Logo_RGB_CP_512x512px.png + kubeVersion: '>=1.19.0-0' + maintainers: + - email: marcell.sevcsik@dynatrace.com + name: 0sewa0 + - email: christoph.muellner@dynatrace.com + name: chrismuellner + - email: lukas.hinterreiter@dynatrace.com + name: luhi-DT + name: dynatrace-operator + sources: + - https://github.com/Dynatrace/dynatrace-operator + type: application + urls: + - https://github.com/Dynatrace/dynatrace-operator/releases/download/v1.3.2/dynatrace-operator-1.3.2.tgz + version: 1.3.2 + - apiVersion: v2 + appVersion: 1.3.1 + created: '2024-10-14T06:47:34.922235129Z' + description: The Dynatrace Operator Helm chart for Kubernetes and OpenShift + digest: 3cfb2e75151a07ef484fb5b431e8d0337b9468be0803c5bfea8ba091ffdded59 + home: https://www.dynatrace.com/ + icon: https://assets.dynatrace.com/global/resources/Signet_Logo_RGB_CP_512x512px.png + kubeVersion: '>=1.19.0-0' + maintainers: + - email: marcell.sevcsik@dynatrace.com + name: 0sewa0 + - email: christoph.muellner@dynatrace.com + name: chrismuellner + - email: lukas.hinterreiter@dynatrace.com + name: luhi-DT + name: dynatrace-operator + sources: + - https://github.com/Dynatrace/dynatrace-operator + type: application + urls: + - https://github.com/Dynatrace/dynatrace-operator/releases/download/v1.3.1/dynatrace-operator-1.3.1.tgz + version: 1.3.1 + - apiVersion: v2 + appVersion: 1.3.0 + created: '2024-09-23T13:14:16.373659553Z' + description: The Dynatrace Operator Helm chart for Kubernetes and OpenShift + digest: b4e29af761babcb8fb12737d4ffca1875b45abc682085b379666a3657acc97b1 + home: https://www.dynatrace.com/ + icon: https://assets.dynatrace.com/global/resources/Signet_Logo_RGB_CP_512x512px.png + kubeVersion: '>=1.19.0-0' + maintainers: + - email: marcell.sevcsik@dynatrace.com + name: 0sewa0 + - email: christoph.muellner@dynatrace.com + name: chrismuellner + - email: lukas.hinterreiter@dynatrace.com + name: luhi-DT + name: dynatrace-operator + sources: + - https://github.com/Dynatrace/dynatrace-operator + type: application + urls: + - https://github.com/Dynatrace/dynatrace-operator/releases/download/v1.3.0/dynatrace-operator-1.3.0.tgz + version: 1.3.0 + - apiVersion: v2 + appVersion: 1.2.3 + created: '2024-10-31T07:13:31.057866792Z' + description: The Dynatrace Operator Helm chart for Kubernetes and OpenShift + digest: d2e190574c30372f9d9c799ea6100d773a6ccfc581ba1fca163afba1ea114519 + home: https://www.dynatrace.com/ + icon: https://assets.dynatrace.com/global/resources/Signet_Logo_RGB_CP_512x512px.png + kubeVersion: '>=1.19.0-0' + maintainers: + - email: marcell.sevcsik@dynatrace.com + name: 0sewa0 + - email: christoph.muellner@dynatrace.com + name: chrismuellner + - email: lukas.hinterreiter@dynatrace.com + name: luhi-DT + name: dynatrace-operator + sources: + - https://github.com/Dynatrace/dynatrace-operator + type: application + urls: + - https://github.com/Dynatrace/dynatrace-operator/releases/download/v1.2.3/dynatrace-operator-1.2.3.tgz + version: 1.2.3 - apiVersion: v2 appVersion: 1.2.2 created: '2024-08-08T11:38:53.628185919Z' @@ -98,18 +208,18 @@ entries: icon: https://assets.dynatrace.com/global/resources/Signet_Logo_RGB_CP_512x512px.png kubeVersion: '>=1.19.0-0' maintainers: - - email: marcell.sevcsik@dynatrace.com - name: 0sewa0 - - email: christoph.muellner@dynatrace.com - name: chrismuellner - - email: lukas.hinterreiter@dynatrace.com - name: luhi-DT + - email: marcell.sevcsik@dynatrace.com + name: 0sewa0 + - email: christoph.muellner@dynatrace.com + name: chrismuellner + - email: lukas.hinterreiter@dynatrace.com + name: luhi-DT name: dynatrace-operator sources: - - https://github.com/Dynatrace/dynatrace-operator + - https://github.com/Dynatrace/dynatrace-operator type: application urls: - - https://github.com/Dynatrace/dynatrace-operator/releases/download/v1.1.0/dynatrace-operator-1.1.0.tgz + - https://github.com/Dynatrace/dynatrace-operator/releases/download/v1.1.0/dynatrace-operator-1.1.0.tgz version: 1.1.0 - apiVersion: v2 appVersion: 1.0.1 @@ -950,4 +1060,4 @@ entries: urls: - https://raw.githubusercontent.com/Dynatrace/helm-charts/master/repos/stable/dynatrace-operator-0.1.0.tgz version: 0.1.0 -generated: '2024-08-08T11:38:53.625316605Z' +generated: '2024-12-09T09:04:42.585436968Z' diff --git a/doc/api/dynakube-api-ref.md b/doc/api/dynakube-api-ref.md index b7b913e9de..31f2787526 100644 --- a/doc/api/dynakube-api-ref.md +++ b/doc/api/dynakube-api-ref.md @@ -4,18 +4,17 @@ |Parameter|Description|Default value|Data type| |:-|:-|:-|:-| -|`apiUrl`|Dynatrace apiUrl, including the /api path at the end. For SaaS, set YOUR_ENVIRONMENT_ID to your environment ID. For Managed, change the apiUrl address.
For instructions on how to determine the environment ID and how to configure the apiUrl address, see Environment ID (To define a custom pull secret and learn about the expected behavior, see Configure customPullSecret
(VirtualService and ServiceEntry objects to allow access to the Dynatrace Cluster from the OneAgent or ActiveGate.
Disabled by default.|-|boolean| -|`extensions`|When an (empty) ExtensionsSpec is provided, the extensions related components (extensions controller and extensions collector)
are deployed by the operator.|-|object| +|`enableIstio`|When enabled, and if Istio is installed on the Kubernetes environment, Dynatrace Operator will...|-|boolean| +|`extensions`|When an (empty) ExtensionsSpec is provided, the extensions related components (extensions...|-|object| |`kspm`|General configuration about the KSPM feature.|-|object| -|`logMonitoring`|General configuration about the LogMonitoring feature.|-|object| |`networkZone`|Sets a network zone for the OneAgent and ActiveGate pods.|-|string| -|`proxy`|Set custom proxy settings either directly or from a secret with the field proxy.
Note: Applies to Dynatrace Operator, ActiveGate, and OneAgents.|-|object| -|`skipCertCheck`|Disable certificate check for the connection between Dynatrace Operator and the Dynatrace Cluster.
Set to true if you want to skip certification validation checks.|-|boolean| +|`proxy`|Set custom proxy settings either directly or from a secret with the field proxy.|-|object| +|`skipCertCheck`|Disable certificate check for the connection between Dynatrace Operator and the Dynatrace Cluster.|-|boolean| |`tokens`|Name of the secret holding the tokens used for connecting to Dynatrace.|-|string| -|`trustedCAs`|Adds custom RootCAs from a configmap. Put the certificate under certs within your configmap.
Note: Applies to Dynatrace Operator, OneAgent and ActiveGate.|-|string| +|`trustedCAs`|Adds custom RootCAs from a configmap. Put the certificate under certs within your configmap.|-|string| ### .spec.oneAgent @@ -29,19 +28,35 @@ |:-|:-|:-|:-| |`annotations`|Adds additional annotations to the ActiveGate pods|-|object| |`capabilities`|Activegate capabilities enabled (routing, kubernetes-monitoring, metrics-ingest, dynatrace-api)|-|array| -|`customProperties`|Add a custom properties file by providing it as a value or reference it from a secret
If referenced from a secret, make sure the key is called 'customProperties'|-|object| +|`customProperties`|Add a custom properties file by providing it as a value or reference it from a secret
If referenced...|-|object| |`dnsPolicy`|Sets DNS Policy for the ActiveGate pods|-|string| |`env`|List of environment variables to set for the ActiveGate|-|array| |`group`|Set activation group for ActiveGate|-|string| -|`image`|The ActiveGate container image. Defaults to the latest ActiveGate image provided by the registry on the tenant|-|string| +|`image`|The ActiveGate container image.|-|string| |`labels`|Adds additional labels for the ActiveGate pods|-|object| |`nodeSelector`|Node selector to control the selection of nodes|-|object| -|`priorityClassName`|If specified, indicates the pod's priority. Name must be defined by creating a PriorityClass object with that
name. If not specified the setting will be removed from the StatefulSet.|-|string| +|`priorityClassName`|If specified, indicates the pod's priority.|-|string| |`replicas`|Amount of replicas for your ActiveGates|-|integer| |`resources`|Define resources requests and limits for single ActiveGate pods|-|object| -|`tlsSecretName`|The name of a secret containing ActiveGate TLS cert+key and password. If not set, self-signed certificate is used.
server.p12: certificate+key pair in pkcs12 format
password: passphrase to read server.p12|-|string| +|`terminationGracePeriodSeconds`|Configures the terminationGracePeriodSeconds parameter of the ActiveGate pod.|-|integer| +|`tlsSecretName`|The name of a secret containing ActiveGate TLS cert+key and password.|-|string| |`tolerations`|Set tolerations for the ActiveGate pods|-|array| |`topologySpreadConstraints`|Adds TopologySpreadConstraints for the ActiveGate pods|-|array| +|`useEphemeralVolume`|UseEphemeralVolume|-|boolean| + +### .spec.logMonitoring + +|Parameter|Description|Default value|Data type| +|:-|:-|:-|:-| +|`ingestRuleMatchers`||-|array| + +### .spec.telemetryIngest + +|Parameter|Description|Default value|Data type| +|:-|:-|:-|:-| +|`protocols`||-|array| +|`serviceName`||-|string| +|`tlsRefName`||-|string| ### .spec.metadataEnrichment @@ -55,17 +70,18 @@ |Parameter|Description|Default value|Data type| |:-|:-|:-|:-| |`annotations`|Add custom OneAgent annotations.|-|object| -|`args`|Set additional arguments to the OneAgent installer.
For available options, see Linux custom installation ().
For the list of limitations, see Limitations ().
Enabled by default.|-|boolean| -|`dnsPolicy`|Set the DNS Policy for OneAgent pods. For details, see Pods DNS Policy ().|-|string| +|`args`|Set additional arguments to the OneAgent installer.|-|array| +|`autoUpdate`|Disables automatic restarts of OneAgent pods in case a new version is available (Note: resource.requests shows the values needed to run; resource.limits shows the maximum limits for the pod.|-|object| -|`priorityClassName`|Assign a priority class to the OneAgent pods. By default, no class is set.
For details, see Pod Priority and Preemption ().|-|string| +|`oneAgentResources`|Resource settings for OneAgent container.|-|object| +|`priorityClassName`|Assign a priority class to the OneAgent pods. By default, no class is set.|-|string| |`secCompProfile`|The SecComp Profile that will be configured in order to run in secure computing mode.|-|string| -|`tolerations`|Tolerations to include with the OneAgent DaemonSet. For details, see Taints and Tolerations ().|-|array| +|`storageHostPath`|StorageHostPath is the writable directory on the host filesystem where OneAgent configurations will...|-|string| +|`tolerations`|Tolerations to include with the OneAgent DaemonSet.|-|array| |`version`|Use a specific OneAgent version. Defaults to the latest version from the Dynatrace cluster.|-|string| ### .spec.templates.logMonitoring @@ -79,25 +95,38 @@ |`nodeSelector`|Node selector to control the selection of nodes for the LogMonitoring pods|-|object| |`priorityClassName`|Assign a priority class to the LogMonitoring pods. By default, no class is set|-|string| |`resources`|Define resources' requests and limits for all the LogMonitoring pods|-|object| -|`secCompProfile`|The SecComp Profile that will be configured in order to run in secure computing mode for the LogMonitoring pods|-|string| +|`secCompProfile`|The SecComp Profile that will be configured in order to run in secure computing mode for the...|-|string| |`tolerations`|Set tolerations for the LogMonitoring pods|-|array| +### .spec.templates.otelCollector + +|Parameter|Description|Default value|Data type| +|:-|:-|:-|:-| +|`annotations`|Adds additional annotations to the OtelCollector pods|-|object| +|`labels`|Adds additional labels for the OtelCollector pods|-|object| +|`replicas`|Number of replicas for your OtelCollector|-|integer| +|`resources`|Define resources' requests and limits for single OtelCollector pod|-|object| +|`tlsRefName`||-|string| +|`tolerations`|Set tolerations for the OtelCollector pods|-|array| +|`topologySpreadConstraints`|Adds TopologySpreadConstraints for the OtelCollector pods|-|array| + ### .spec.oneAgent.classicFullStack |Parameter|Description|Default value|Data type| |:-|:-|:-|:-| |`annotations`|Add custom OneAgent annotations.|-|object| -|`args`|Set additional arguments to the OneAgent installer.
For available options, see Linux custom installation ().
For the list of limitations, see Limitations ().
Enabled by default.|-|boolean| -|`dnsPolicy`|Set the DNS Policy for OneAgent pods. For details, see Pods DNS Policy ().|-|string| +|`args`|Set additional arguments to the OneAgent installer.|-|array| +|`autoUpdate`|Disables automatic restarts of OneAgent pods in case a new version is available (Note: resource.requests shows the values needed to run; resource.limits shows the maximum limits for the pod.|-|object| -|`priorityClassName`|Assign a priority class to the OneAgent pods. By default, no class is set.
For details, see Pod Priority and Preemption ().|-|string| +|`oneAgentResources`|Resource settings for OneAgent container.|-|object| +|`priorityClassName`|Assign a priority class to the OneAgent pods. By default, no class is set.|-|string| |`secCompProfile`|The SecComp Profile that will be configured in order to run in secure computing mode.|-|string| -|`tolerations`|Tolerations to include with the OneAgent DaemonSet. For details, see Taints and Tolerations ().|-|array| +|`storageHostPath`|StorageHostPath is the writable directory on the host filesystem where OneAgent configurations will...|-|string| +|`tolerations`|Tolerations to include with the OneAgent DaemonSet.|-|array| |`version`|Use a specific OneAgent version. Defaults to the latest version from the Dynatrace cluster.|-|string| ### .spec.oneAgent.cloudNativeFullStack @@ -105,20 +134,21 @@ |Parameter|Description|Default value|Data type| |:-|:-|:-|:-| |`annotations`|Add custom OneAgent annotations.|-|object| -|`args`|Set additional arguments to the OneAgent installer.
For available options, see Linux custom installation ().
For the list of limitations, see Limitations ().
Enabled by default.|-|boolean| +|`args`|Set additional arguments to the OneAgent installer.|-|array| +|`autoUpdate`|Disables automatic restarts of OneAgent pods in case a new version is available ().|-|string| +|`dnsPolicy`|Set the DNS Policy for OneAgent pods. For details, see Pods DNS Policy (().|-|object| +|`initResources`|Define resources requests and limits for the initContainer.|-|object| |`labels`|Your defined labels for OneAgent pods in order to structure workloads as desired.|-|object| -|`namespaceSelector`|Applicable only for applicationMonitoring or cloudNativeFullStack configuration types. The namespaces where you want Dynatrace Operator to inject.
For more information, see Configure monitoring for namespaces and pods (Note: resource.requests shows the values needed to run; resource.limits shows the maximum limits for the pod.|-|object| -|`priorityClassName`|Assign a priority class to the OneAgent pods. By default, no class is set.
For details, see Pod Priority and Preemption ().|-|string| +|`oneAgentResources`|Resource settings for OneAgent container.|-|object| +|`priorityClassName`|Assign a priority class to the OneAgent pods. By default, no class is set.|-|string| |`secCompProfile`|The SecComp Profile that will be configured in order to run in secure computing mode.|-|string| -|`tolerations`|Tolerations to include with the OneAgent DaemonSet. For details, see Taints and Tolerations ().|-|array| +|`storageHostPath`|StorageHostPath is the writable directory on the host filesystem where OneAgent configurations will...|-|string| +|`tolerations`|Tolerations to include with the OneAgent DaemonSet.|-|array| |`version`|Use a specific OneAgent version. Defaults to the latest version from the Dynatrace cluster.|-|string| ### .spec.oneAgent.applicationMonitoring @@ -126,10 +156,22 @@ |Parameter|Description|Default value|Data type| |:-|:-|:-|:-| |`codeModulesImage`|Use a custom OneAgent CodeModule image to download binaries.|-|string| -|`initResources`|Define resources requests and limits for the initContainer. For details, see Managing resources for containers
().|-|object| -|`namespaceSelector`|Applicable only for applicationMonitoring or cloudNativeFullStack configuration types. The namespaces where you want Dynatrace Operator to inject.
For more information, see Configure monitoring for namespaces and pods (More info: https://kubernetes.|-|array| +|`dataSource`|dataSource field can be used to specify either:
* An existing VolumeSnapshot object (snapshot.|-|object| +|`resources`|resources represents the minimum resources the volume should have.|-|object| +|`selector`|selector is a label query over volumes to consider for binding.|-|object| +|`storageClassName`|storageClassName is the name of the StorageClass required by the claim.|-|string| +|`volumeAttributesClassName`|volumeAttributesClassName may be used to set the VolumeAttributesClass used by this claim.|-|string| +|`volumeMode`|volumeMode defines what type of volume is required by the claim.|-|string| +|`volumeName`|volumeName is the binding reference to the PersistentVolume backing this claim.|-|string| ### .spec.templates.logMonitoring.imageRef @@ -138,17 +180,12 @@ |`repository`|Custom image repository|-|string| |`tag`|Indicates a tag of the image to use|-|string| -### .spec.templates.openTelemetryCollector +### .spec.templates.otelCollector.imageRef |Parameter|Description|Default value|Data type| |:-|:-|:-|:-| -|`annotations`|Adds additional annotations to the OtelCollector pods|-|object| -|`labels`|Adds additional labels for the OtelCollector pods|-|object| -|`replicas`|Number of replicas for your OtelCollector|-|integer| -|`resources`|Define resources' requests and limits for single OtelCollector pod|-|object| -|`tlsRefName`||-|string| -|`tolerations`|Set tolerations for the OtelCollector pods|-|array| -|`topologySpreadConstraints`|Adds TopologySpreadConstraints for the OtelCollector pods|-|array| +|`repository`|Custom image repository|-|string| +|`tag`|Indicates a tag of the image to use|-|string| ### .spec.templates.extensionExecutionController @@ -172,17 +209,19 @@ |`args`|Set additional arguments to the NodeConfigurationCollector pods|-|array| |`env`|Set additional environment variables for the NodeConfigurationCollector pods|-|array| |`labels`|Adds additional labels for the NodeConfigurationCollector pods|-|object| -|`nodeSelector`|Specify the node selector that controls on which nodes NodeConfigurationCollector pods will be deployed.|-|object| -|`priorityClassName`|If specified, indicates the pod's priority. Name must be defined by creating a PriorityClass object with that
name. If not specified the setting will be removed from the DaemonSet.|-|string| +|`nodeSelector`|Specify the node selector that controls on which nodes NodeConfigurationCollector pods will be...|-|object| +|`priorityClassName`|If specified, indicates the pod's priority.|-|string| |`resources`|Define resources' requests and limits for single NodeConfigurationCollector pod|-|object| |`tolerations`|Set tolerations for the NodeConfigurationCollector pods|-|array| -### .spec.templates.openTelemetryCollector.imageRef +### .spec.activeGate.persistentVolumeClaim.dataSourceRef |Parameter|Description|Default value|Data type| |:-|:-|:-|:-| -|`repository`|Custom image repository|-|string| -|`tag`|Indicates a tag of the image to use|-|string| +|`apiGroup`|APIGroup is the group for the resource being referenced.|-|string| +|`kind`|Kind is the type of resource being referenced|-|string| +|`name`|Name is the name of resource being referenced|-|string| +|`namespace`|Namespace is the namespace of resource being referenced
Note that when a namespace is specified, a...|-|string| ### .spec.templates.extensionExecutionController.imageRef @@ -202,8 +241,8 @@ |Parameter|Description|Default value|Data type| |:-|:-|:-|:-| -|`preferredDuringSchedulingIgnoredDuringExecution`|The scheduler will prefer to schedule pods to nodes that satisfy
the affinity expressions specified by this field, but it may choose
a node that violates one or more of the expressions. The node that is
most preferred is the one with the greatest sum of weights, i.e.|-|array| -|`requiredDuringSchedulingIgnoredDuringExecution`|If the affinity requirements specified by this field are not met at
scheduling time, the pod will not be scheduled onto the node.
If the affinity requirements specified by this field cease to be met
at some point during pod execution (e.g. due to an update), the system
may or may not try to eventually evict the pod from its node.|-|object| +|`preferredDuringSchedulingIgnoredDuringExecution`|The scheduler will prefer to schedule pods to nodes that satisfy
the affinity expressions specified...|-|array| +|`requiredDuringSchedulingIgnoredDuringExecution`|If the affinity requirements specified by this field are not met at
scheduling time, the pod will...|-|object| ### .spec.templates.kspmNodeConfigurationCollector.updateStrategy @@ -215,27 +254,27 @@ |Parameter|Description|Default value|Data type| |:-|:-|:-|:-| -|`accessModes`|accessModes contains the desired access modes the volume should have.
More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1|-|array| -|`dataSource`|dataSource field can be used to specify either:
* An existing VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot)
* An existing PVC (PersistentVolumeClaim)
If the provisioner or an external controller can support the specified data source,
it will create a new volume based on the contents of the specified data source.|-|object| -|`resources`|resources represents the minimum resources the volume should have.
If RecoverVolumeExpansionFailure feature is enabled users are allowed to specify resource requirements
that are lower than previous value but must still be higher than capacity recorded in the
status field of the claim.
More info: https://kubernetes.|-|object| +|`accessModes`|accessModes contains the desired access modes the volume should have.
More info: https://kubernetes.|-|array| +|`dataSource`|dataSource field can be used to specify either:
* An existing VolumeSnapshot object (snapshot.|-|object| +|`resources`|resources represents the minimum resources the volume should have.|-|object| |`selector`|selector is a label query over volumes to consider for binding.|-|object| -|`storageClassName`|storageClassName is the name of the StorageClass required by the claim.
More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1|-|string| -|`volumeAttributesClassName`|volumeAttributesClassName may be used to set the VolumeAttributesClass used by this claim.
If specified, the CSI driver will create or update the volume with the attributes defined
in the corresponding VolumeAttributesClass. This has a different purpose than storageClassName,
it can be changed after the claim is created.|-|string| -|`volumeMode`|volumeMode defines what type of volume is required by the claim.
Value of Filesystem is implied when not included in claim spec.|-|string| +|`storageClassName`|storageClassName is the name of the StorageClass required by the claim.|-|string| +|`volumeAttributesClassName`|volumeAttributesClassName may be used to set the VolumeAttributesClass used by this claim.|-|string| +|`volumeMode`|volumeMode defines what type of volume is required by the claim.|-|string| |`volumeName`|volumeName is the binding reference to the PersistentVolume backing this claim.|-|string| ### .spec.templates.kspmNodeConfigurationCollector.updateStrategy.rollingUpdate |Parameter|Description|Default value|Data type| |:-|:-|:-|:-| -|`maxSurge`|The maximum number of nodes with an existing available DaemonSet pod that
can have an updated DaemonSet pod during during an update.
Value can be an absolute number (ex: 5) or a percentage of desired pods (ex: 10%).
This can not be 0 if MaxUnavailable is 0.
Absolute number is calculated from percentage by rounding up to a minimum of 1.|-|integer or string| -|`maxUnavailable`|The maximum number of DaemonSet pods that can be unavailable during the
update. Value can be an absolute number (ex: 5) or a percentage of total
number of DaemonSet pods at the start of the update (ex: 10%). Absolute
number is calculated from percentage by rounding up.
This cannot be 0 if MaxSurge is 0
Default value is 1.|-|integer or string| +|`maxSurge`|The maximum number of nodes with an existing available DaemonSet pod that
can have an updated...|-|integer or string| +|`maxUnavailable`|The maximum number of DaemonSet pods that can be unavailable during the
update.|-|integer or string| ### .spec.templates.extensionExecutionController.persistentVolumeClaim.dataSourceRef |Parameter|Description|Default value|Data type| |:-|:-|:-|:-| -|`apiGroup`|APIGroup is the group for the resource being referenced.
If APIGroup is not specified, the specified Kind must be in the core API group.
For any other third-party types, APIGroup is required.|-|string| +|`apiGroup`|APIGroup is the group for the resource being referenced.|-|string| |`kind`|Kind is the type of resource being referenced|-|string| |`name`|Name is the name of resource being referenced|-|string| -|`namespace`|Namespace is the namespace of resource being referenced
Note that when a namespace is specified, a gateway.networking.k8s.io/ReferenceGrant object is required in the referent namespace to allow that namespace's owner to accept the reference. See the ReferenceGrant documentation for details.|-|string| +|`namespace`|Namespace is the namespace of resource being referenced
Note that when a namespace is specified, a...|-|string| diff --git a/doc/api/edgeconnect-api-ref.md b/doc/api/edgeconnect-api-ref.md index e476553807..afab83dc88 100644 --- a/doc/api/edgeconnect-api-ref.md +++ b/doc/api/edgeconnect-api-ref.md @@ -6,8 +6,8 @@ |:-|:-|:-|:-| |`annotations`|Adds additional annotations to the EdgeConnect pods|-|object| |`apiServer`|Location of the Dynatrace API to connect to, including your specific environment UUID|-|string| -|`autoUpdate`|Enables automatic restarts of EdgeConnect pods in case a new version is available (the default value is: true)|-|boolean| -|`caCertsRef`|Adds custom root certificate from a configmap. Put the certificate under certs within your configmap.|-|string| +|`autoUpdate`|Enables automatic restarts of EdgeConnect pods in case a new version is available (the default...|-|boolean| +|`caCertsRef`|Adds custom root certificate from a configmap.|-|string| |`customPullSecret`|Pull secret for your private registry|-|string| |`env`|Adds additional environment variables to the EdgeConnect pods|-|array| |`hostPatterns`|Host patterns to be set in the tenant, only considered when provisioning is enabled.|-|array| @@ -26,16 +26,16 @@ |:-|:-|:-|:-| |`clientSecret`|Name of the secret that holds oauth clientId/secret|-|string| |`endpoint`|Token endpoint URL of Dynatrace SSO|-|string| -|`provisioner`|Determines if the operator will create the EdgeConnect and light OAuth client on the cluster using the credentials provided. Requires more scopes than default behavior.|-|boolean| +|`provisioner`|Determines if the operator will create the EdgeConnect and light OAuth client on the cluster using...|-|boolean| |`resource`|URN identifying your account. You get the URN when creating the OAuth client|-|string| ### .spec.proxy |Parameter|Description|Default value|Data type| |:-|:-|:-|:-| -|`authRef`|Secret name which contains the username and password used for authentication with the proxy, using the
"Basic" HTTP authentication scheme.|-|string| +|`authRef`|Secret name which contains the username and password used for authentication with the proxy, using...|-|string| |`host`|Server address (hostname or IP address) of the proxy.|-|string| -|`noProxy`|NoProxy represents the NO_PROXY or no_proxy environment
variable. It specifies a string that contains comma-separated values
specifying hosts that should be excluded from proxying.|-|string| +|`noProxy`|NoProxy represents the NO_PROXY or no_proxy environment
variable.|-|string| |`port`|Port of the proxy.|-|integer| ### .spec.imageRef diff --git a/doc/api/feature-flags.md b/doc/api/feature-flags.md index 09c8560081..ab2d05b30a 100644 --- a/doc/api/feature-flags.md +++ b/doc/api/feature-flags.md @@ -3,7 +3,7 @@ # dynakube ```go -import "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/tmp" +import "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/tmp" ``` ## Index @@ -34,6 +34,8 @@ const ( AnnotationFeatureAutomaticK8sApiMonitoringClusterName = AnnotationFeaturePrefix + "automatic-kubernetes-api-monitoring-cluster-name" AnnotationFeatureK8sAppEnabled = AnnotationFeaturePrefix + "k8s-app-enabled" + AnnotationFeatureActiveGateAutomaticTLSCertificate = AnnotationFeaturePrefix + "automatic-tls-certificate" + AnnotationFeatureNoProxy = AnnotationFeaturePrefix + "no-proxy" // Deprecated: AnnotationFeatureOneAgentIgnoreProxy use AnnotationFeatureNoProxy instead. @@ -42,6 +44,7 @@ const ( AnnotationFeatureOneAgentMaxUnavailable = AnnotationFeaturePrefix + "oneagent-max-unavailable" AnnotationFeatureOneAgentInitialConnectRetry = AnnotationFeaturePrefix + "oneagent-initial-connect-retry-ms" AnnotationFeatureRunOneAgentContainerPrivileged = AnnotationFeaturePrefix + "oneagent-privileged" + AnnotationFeatureOneAgentSkipLivenessProbe = AnnotationFeaturePrefix + "oneagent-skip-liveness-probe" AnnotationFeatureIgnoreUnknownState = AnnotationFeaturePrefix + "ignore-unknown-state" AnnotationFeatureIgnoredNamespaces = AnnotationFeaturePrefix + "ignored-namespaces" @@ -50,12 +53,16 @@ const ( AnnotationInjectionFailurePolicy = AnnotationFeaturePrefix + "injection-failure-policy" AnnotationFeatureInitContainerSeccomp = AnnotationFeaturePrefix + "init-container-seccomp-profile" AnnotationFeatureEnforcementMode = AnnotationFeaturePrefix + "enforcement-mode" - AnnotationFeatureReadOnlyOneAgent = AnnotationFeaturePrefix + "oneagent-readonly-host-fs" // CSI. AnnotationFeatureMaxFailedCsiMountAttempts = AnnotationFeaturePrefix + "max-csi-mount-attempts" AnnotationFeatureMaxCsiMountTimeout = AnnotationFeaturePrefix + "max-csi-mount-timeout" AnnotationFeatureReadOnlyCsiVolume = AnnotationFeaturePrefix + "injection-readonly-volume" + AnnotationFeatureNodeImagePull = AnnotationFeaturePrefix + "node-image-pull" + + // AnnotationTechnologies can be set on a Pod or DynaKube to configure which code module technologies to download. It's set to + // "all" if not set. + AnnotationTechnologies = "oneagent.dynatrace.com/technologies" ) ``` @@ -72,7 +79,7 @@ const ( -## func [MountAttemptsToTimeout]() +## func [MountAttemptsToTimeout]() ```go func MountAttemptsToTimeout(maxAttempts int) string diff --git a/doc/debug.md b/doc/debug.md index a9c3611a45..1fefc74408 100644 --- a/doc/debug.md +++ b/doc/debug.md @@ -40,7 +40,7 @@ As the operator only sends requests to the Kubernetes API, but never receives an 1. Deploy the operator as usual: ```shell -make deploy/helm +make deploy ``` 2. Scale down the cluster operator: @@ -106,7 +106,7 @@ make debug/telepresence/uninstall 2. Deploy without debugging changes: ```shell -make deploy/helm +make deploy ``` ### CSI-Driver Server @@ -161,7 +161,7 @@ make debug/tunnel/stop 2. Deploy without debugging changes: ```shell -make deploy/helm +make deploy ``` ### CSI-Driver Provisioner diff --git a/doc/e2e/features.md b/doc/e2e/features.md index e7fb8e91a1..04139aa523 100644 --- a/doc/e2e/features.md +++ b/doc/e2e/features.md @@ -26,40 +26,84 @@ Setup: OneAgent disabled Verification if ActiveGate is rolled out successfully. All ActiveGate capabilities are enabled in Dynakube. The test checks if ActiveGate is able to communicate over a http proxy, related *Gateway* modules are active and that the *Gateway* process is reachable via *Gateway service*. -# edgeconnect +# applicationmonitoring ```go -import "github.com/Dynatrace/dynatrace-operator/test/features/edgeconnect" +import "github.com/Dynatrace/dynatrace-operator/test/features/applicationmonitoring" ``` ## Index -- [func AutomationModeFeature(t *testing.T) features.Feature](<#AutomationModeFeature>) -- [func NormalModeFeature(t *testing.T) features.Feature](<#NormalModeFeature>) -- [func ProvisionerModeFeature(t *testing.T) features.Feature](<#ProvisionerModeFeature>) +- [func LabelVersionDetection(t *testing.T) features.Feature](<#LabelVersionDetection>) +- [func MetadataEnrichment(t *testing.T) features.Feature](<#MetadataEnrichment>) +- [func ReadOnlyCSIVolume(t *testing.T) features.Feature](<#ReadOnlyCSIVolume>) +- [func WithoutCSI(t *testing.T) features.Feature](<#WithoutCSI>) - + -## func [AutomationModeFeature]() +## func [LabelVersionDetection]() ```go -func AutomationModeFeature(t *testing.T) features.Feature +func LabelVersionDetection(t *testing.T) features.Feature ``` - +Verification that build labels are created and set accordingly. The test checks: -## func [NormalModeFeature]() +- default behavior - feature flag exists, but no additional configuration so the default variables are added - custom mapping - feature flag exists, with additional configuration so all 4 build variables are added - preserved values of existing variables - build variables exist, feature flag exists, with additional configuration, values of build variables not get overwritten - incorrect custom mapping - invalid name of BUILD VERSION label, reference exists but actual label doesn't exist + + + +## func [MetadataEnrichment]() ```go -func NormalModeFeature(t *testing.T) features.Feature +func MetadataEnrichment(t *testing.T) features.Feature ``` - +Verification of the metadata enrichment part of the operator. The test checks that enrichment variables are added to the initContainer and dt_metadata.json file contains required fields. -## func [ProvisionerModeFeature]() + + +## func [ReadOnlyCSIVolume]() ```go -func ProvisionerModeFeature(t *testing.T) features.Feature +func ReadOnlyCSIVolume(t *testing.T) features.Feature +``` + + + +## func [WithoutCSI]() + +```go +func WithoutCSI(t *testing.T) features.Feature +``` + +ApplicationMonitoring deployment without CSI driver + +# bootstrapper + +```go +import "github.com/Dynatrace/dynatrace-operator/test/features/bootstrapper" +``` + +## Index + +- [func InstallWithCSI(t *testing.T) features.Feature](<#InstallWithCSI>) +- [func NoCSI(t *testing.T) features.Feature](<#NoCSI>) + + + +## func [InstallWithCSI]() + +```go +func InstallWithCSI(t *testing.T) features.Feature +``` + + + +## func [NoCSI]() + +```go +func NoCSI(t *testing.T) features.Feature ``` # classic @@ -102,188 +146,209 @@ import "github.com/Dynatrace/dynatrace-operator/test/features/classic/switch_mod func Feature(t *testing.T) features.Feature ``` -# extensions +# cloudnative ```go -import "github.com/Dynatrace/dynatrace-operator/test/features/extensions" +import "github.com/Dynatrace/dynatrace-operator/test/features/cloudnative" ``` ## Index -- [func Feature(t *testing.T) features.Feature](<#Feature>) +- [func AssessActiveGateContainer(builder *features.FeatureBuilder, dk *dynakube.DynaKube, trustedCAs []byte)](<#AssessActiveGateContainer>) +- [func AssessOneAgentContainer(builder *features.FeatureBuilder, agCrtFunc func() []byte, trustedCAs []byte)](<#AssessOneAgentContainer>) +- [func AssessSampleContainer(builder *features.FeatureBuilder, sampleApp *sample.App, agCrtFunc func() []byte, trustedCAs []byte)](<#AssessSampleContainer>) +- [func AssessSampleInitContainers(builder *features.FeatureBuilder, sampleApp *sample.App)](<#AssessSampleInitContainers>) +- [func DefaultCloudNativeSpec() *oneagent.CloudNativeFullStackSpec](<#DefaultCloudNativeSpec>) - + -## func [Feature]() +## func [AssessActiveGateContainer]() ```go -func Feature(t *testing.T) features.Feature +func AssessActiveGateContainer(builder *features.FeatureBuilder, dk *dynakube.DynaKube, trustedCAs []byte) ``` -# applicationmonitoring + + +## func [AssessOneAgentContainer]() ```go -import "github.com/Dynatrace/dynatrace-operator/test/features/applicationmonitoring" +func AssessOneAgentContainer(builder *features.FeatureBuilder, agCrtFunc func() []byte, trustedCAs []byte) ``` -## Index + -- [func LabelVersionDetection(t *testing.T) features.Feature](<#LabelVersionDetection>) -- [func MetadataEnrichment(t *testing.T) features.Feature](<#MetadataEnrichment>) -- [func ReadOnlyCSIVolume(t *testing.T) features.Feature](<#ReadOnlyCSIVolume>) -- [func WithoutCSI(t *testing.T) features.Feature](<#WithoutCSI>) +## func [AssessSampleContainer]() - +```go +func AssessSampleContainer(builder *features.FeatureBuilder, sampleApp *sample.App, agCrtFunc func() []byte, trustedCAs []byte) +``` -## func [LabelVersionDetection]() + + +## func [AssessSampleInitContainers]() ```go -func LabelVersionDetection(t *testing.T) features.Feature +func AssessSampleInitContainers(builder *features.FeatureBuilder, sampleApp *sample.App) ``` -Verification that build labels are created and set accordingly. The test checks: + -- default behavior - feature flag exists, but no additional configuration so the default variables are added - custom mapping - feature flag exists, with additional configuration so all 4 build variables are added - preserved values of existing variables - build variables exist, feature flag exists, with additional configuration, values of build variables not get overwritten - incorrect custom mapping - invalid name of BUILD VERSION label, reference exists but actual label doesn't exist +## func [DefaultCloudNativeSpec]() - +```go +func DefaultCloudNativeSpec() *oneagent.CloudNativeFullStackSpec +``` -## func [MetadataEnrichment]() +# codemodules ```go -func MetadataEnrichment(t *testing.T) features.Feature +import "github.com/Dynatrace/dynatrace-operator/test/features/cloudnative/codemodules" ``` -Verification of the metadata enrichment part of the operator. The test checks that enrichment variables are added to the initContainer and dt_metadata.json file contains required fields. +## Index - +- [func ImageHasBeenDownloaded(dk dynakube.DynaKube) features.Func](<#ImageHasBeenDownloaded>) +- [func InstallFromImage(t *testing.T) features.Feature](<#InstallFromImage>) +- [func VolumesAreMountedCorrectly(sampleApp sample.App) features.Func](<#VolumesAreMountedCorrectly>) +- [func WithProxy(t *testing.T, proxySpec *value.Source) features.Feature](<#WithProxy>) +- [func WithProxyAndAGCert(t *testing.T, proxySpec *value.Source) features.Feature](<#WithProxyAndAGCert>) +- [func WithProxyAndAutomaticAGCert(t *testing.T, proxySpec *value.Source) features.Feature](<#WithProxyAndAutomaticAGCert>) +- [func WithProxyCAAndAGCert(t *testing.T, proxySpec *value.Source) features.Feature](<#WithProxyCAAndAGCert>) +- [func WithProxyCAAndAutomaticAGCert(t *testing.T, proxySpec *value.Source) features.Feature](<#WithProxyCAAndAutomaticAGCert>) -## func [ReadOnlyCSIVolume]() + + +## func [ImageHasBeenDownloaded]() ```go -func ReadOnlyCSIVolume(t *testing.T) features.Feature +func ImageHasBeenDownloaded(dk dynakube.DynaKube) features.Func ``` - + -## func [WithoutCSI]() +## func [InstallFromImage]() ```go -func WithoutCSI(t *testing.T) features.Feature +func InstallFromImage(t *testing.T) features.Feature ``` -ApplicationMonitoring deployment without CSI driver +Verification that the storage in the CSI driver directory does not increase when there are multiple tenants and pods which are monitored. -# publicregistry + + +## func [VolumesAreMountedCorrectly]() ```go -import "github.com/Dynatrace/dynatrace-operator/test/features/publicregistry" +func VolumesAreMountedCorrectly(sampleApp sample.App) features.Func ``` -## Index - -- [func Feature(t *testing.T) features.Feature](<#Feature>) - - + -## func [Feature]() +## func [WithProxy]() ```go -func Feature(t *testing.T) features.Feature +func WithProxy(t *testing.T, proxySpec *value.Source) features.Feature ``` -Feature defines the e2e test to verify that public-registry images can be deployed by the operator and that they function This includes: +Prerequisites: istio service mesh -- ActiveGate StatefulSet gets ready -- CodeModules can be downloaded and mounted -- OneAgent DaemonSet gets ready +Setup: CloudNative deployment with CSI driver -It determines the latest version of each image using the registry. +Verification that the operator and all deployed OneAgents are able to communicate over a http proxy. -# support_archive +Connectivity in the dynatrace namespace and sample application namespace is restricted to the local cluster. Sample application is installed. The test checks if DT_PROXY environment variable is defined in the *dynakubeComponents-oneagent* container and the *application container*. -```go -import "github.com/Dynatrace/dynatrace-operator/test/features/support_archive" -``` + -## Index +## func [WithProxyAndAGCert]() -- [func Feature(t *testing.T) features.Feature](<#Feature>) -- [type CustomResources](<#CustomResources>) +```go +func WithProxyAndAGCert(t *testing.T, proxySpec *value.Source) features.Feature +``` - + -## func [Feature]() +## func [WithProxyAndAutomaticAGCert]() ```go -func Feature(t *testing.T) features.Feature +func WithProxyAndAutomaticAGCert(t *testing.T, proxySpec *value.Source) features.Feature ``` -Setup: DTO with CSI driver + -Verification if support-archive package created by the support-archive command and printed to the standard output is a valid tar.gz package and contains required *operator-version.txt* file. +## func [WithProxyCAAndAGCert]() - +```go +func WithProxyCAAndAGCert(t *testing.T, proxySpec *value.Source) features.Feature +``` -## type [CustomResources]() + + +## func [WithProxyCAAndAutomaticAGCert]() ```go -type CustomResources struct { - // contains filtered or unexported fields -} +func WithProxyCAAndAutomaticAGCert(t *testing.T, proxySpec *value.Source) features.Feature ``` -# cloudnative +# _default ```go -import "github.com/Dynatrace/dynatrace-operator/test/features/cloudnative" +import "github.com/Dynatrace/dynatrace-operator/test/features/cloudnative/default" ``` ## Index -- [func AssessActiveGateContainer(builder *features.FeatureBuilder, dk *dynakube.DynaKube, trustedCAs []byte)](<#AssessActiveGateContainer>) -- [func AssessOneAgentContainer(builder *features.FeatureBuilder, agCrt []byte, trustedCAs []byte)](<#AssessOneAgentContainer>) -- [func AssessSampleContainer(builder *features.FeatureBuilder, sampleApp *sample.App, agCrt []byte, trustedCAs []byte)](<#AssessSampleContainer>) -- [func AssessSampleInitContainers(builder *features.FeatureBuilder, sampleApp *sample.App)](<#AssessSampleInitContainers>) -- [func DefaultCloudNativeSpec() *dynakube.CloudNativeFullStackSpec](<#DefaultCloudNativeSpec>) +- [func Feature(t *testing.T, istioEnabled bool, withCSI bool) features.Feature](<#Feature>) - + -## func [AssessActiveGateContainer]() +## func [Feature]() ```go -func AssessActiveGateContainer(builder *features.FeatureBuilder, dk *dynakube.DynaKube, trustedCAs []byte) +func Feature(t *testing.T, istioEnabled bool, withCSI bool) features.Feature ``` - +### With istio enabled -## func [AssessOneAgentContainer]() +Prerequisites: istio service mesh -```go -func AssessOneAgentContainer(builder *features.FeatureBuilder, agCrt []byte, trustedCAs []byte) -``` +Setup: CloudNative deployment with CSI driver - +Verify that the operator is working as expected when istio service mesh is installed and pre-configured on the cluster. -## func [AssessSampleContainer]() +### Install -```go -func AssessSampleContainer(builder *features.FeatureBuilder, sampleApp *sample.App, agCrt []byte, trustedCAs []byte) -``` +Verification that OneAgent is injected to sample application pods and can communicate with the *Dynatrace Cluster*. - +### Upgrade -## func [AssessSampleInitContainers]() +Verification that a *released version* can be updated to the *current version*. The exact number of *released version* is updated manually. The *released version* is installed using configuration files from GitHub. + +Sample application Deployment is installed and restarted to check if OneAgent is injected and can communicate with the *Dynatrace Cluster*. + +### Specific Agent Version) + +Verification that the operator is able to set agent version which is given via the dynakube. Upgrading to a newer version of agent is also tested. + +Sample application Deployment is installed and restarted to check if OneAgent is injected and VERSION environment variable is correct. + +# disabled_auto_injection ```go -func AssessSampleInitContainers(builder *features.FeatureBuilder, sampleApp *sample.App) +import "github.com/Dynatrace/dynatrace-operator/test/features/cloudnative/disabled_auto_injection" ``` - +## Index -## func [DefaultCloudNativeSpec]() +- [func Feature(t *testing.T) features.Feature](<#Feature>) + + + +## func [Feature]() ```go -func DefaultCloudNativeSpec() *dynakube.CloudNativeFullStackSpec +func Feature(t *testing.T) features.Feature ``` # network_problems @@ -312,10 +377,10 @@ Verification that the CSI driver is able to recover from network issues, when us Connectivity for csi driver pods is restricted to the local k8s cluster (no outside connections allowed) and sample application is installed. The test checks if init container was attached, run successfully and that the sample pods are up and running. -# upgrade +# switch_modes ```go -import "github.com/Dynatrace/dynatrace-operator/test/features/cloudnative/upgrade" +import "github.com/Dynatrace/dynatrace-operator/test/features/cloudnative/switch_modes" ``` ## Index @@ -324,122 +389,156 @@ import "github.com/Dynatrace/dynatrace-operator/test/features/cloudnative/upgrad -## func [Feature]() +## func [Feature]() ```go func Feature(t *testing.T) features.Feature ``` -# codemodules +# upgrade ```go -import "github.com/Dynatrace/dynatrace-operator/test/features/cloudnative/codemodules" +import "github.com/Dynatrace/dynatrace-operator/test/features/cloudnative/upgrade" ``` ## Index -- [func InstallFromImage(t *testing.T) features.Feature](<#InstallFromImage>) -- [func WithProxy(t *testing.T, proxySpec *value.Source) features.Feature](<#WithProxy>) -- [func WithProxyAndAGCert(t *testing.T, proxySpec *value.Source) features.Feature](<#WithProxyAndAGCert>) -- [func WithProxyCA(t *testing.T, proxySpec *value.Source) features.Feature](<#WithProxyCA>) -- [func WithProxyCAAndAGCert(t *testing.T, proxySpec *value.Source) features.Feature](<#WithProxyCAAndAGCert>) +- [func Feature(t *testing.T) features.Feature](<#Feature>) - + -## func [InstallFromImage]() +## func [Feature]() ```go -func InstallFromImage(t *testing.T) features.Feature +func Feature(t *testing.T) features.Feature ``` -Verification that the storage in the CSI driver directory does not increase when there are multiple tenants and pods which are monitored. +# consts - +```go +import "github.com/Dynatrace/dynatrace-operator/test/features/consts" +``` + +## Index + +- [Constants](<#constants>) + +## Constants -## func [WithProxy]() + ```go -func WithProxy(t *testing.T, proxySpec *value.Source) features.Feature +const ( + AgCertificate = "custom-cas/agcrt.pem" + AgCertificateAndPrivateKey = "custom-cas/agcrtkey.p12" + AgCertificateAndPrivateKeyField = "server.p12" + AgSecretName = "ag-ca" + TelemetryIngestTLSSecretName = "telemetry-ingest-tls" + + DevRegistryPullSecretName = "devregistry" + EecImageRepo = "478983378254.dkr.ecr.us-east-1.amazonaws.com/dynatrace/dynatrace-eec" + EecImageTag = "1.303.0.20240930-183404" + LogMonitoringImageRepo = "public.ecr.aws/dynatrace/dynatrace-logmodule" + LogMonitoringImageTag = "1.309.59.20250319-140247" +) ``` -Prerequisites: istio service mesh +# edgeconnect -Setup: CloudNative deployment with CSI driver +```go +import "github.com/Dynatrace/dynatrace-operator/test/features/edgeconnect" +``` -Verification that the operator and all deployed OneAgents are able to communicate over a http proxy. +## Index -Connectivity in the dynatrace namespace and sample application namespace is restricted to the local cluster. Sample application is installed. The test checks if DT_PROXY environment variable is defined in the *dynakubeComponents-oneagent* container and the *application container*. +- [func AutomationModeFeature(t *testing.T) features.Feature](<#AutomationModeFeature>) +- [func NormalModeFeature(t *testing.T) features.Feature](<#NormalModeFeature>) +- [func ProvisionerModeFeature(t *testing.T) features.Feature](<#ProvisionerModeFeature>) - + -## func [WithProxyAndAGCert]() +## func [AutomationModeFeature]() ```go -func WithProxyAndAGCert(t *testing.T, proxySpec *value.Source) features.Feature +func AutomationModeFeature(t *testing.T) features.Feature ``` - + -## func [WithProxyCA]() +## func [NormalModeFeature]() ```go -func WithProxyCA(t *testing.T, proxySpec *value.Source) features.Feature +func NormalModeFeature(t *testing.T) features.Feature ``` - + -## func [WithProxyCAAndAGCert]() +## func [ProvisionerModeFeature]() ```go -func WithProxyCAAndAGCert(t *testing.T, proxySpec *value.Source) features.Feature +func ProvisionerModeFeature(t *testing.T) features.Feature ``` -# _default +# extensions ```go -import "github.com/Dynatrace/dynatrace-operator/test/features/cloudnative/default" +import "github.com/Dynatrace/dynatrace-operator/test/features/extensions" ``` ## Index -- [func Feature(t *testing.T, istioEnabled bool) features.Feature](<#Feature>) +- [func Feature(t *testing.T) features.Feature](<#Feature>) -## func [Feature]() +## func [Feature]() ```go -func Feature(t *testing.T, istioEnabled bool) features.Feature +func Feature(t *testing.T) features.Feature ``` -### With istio enabled +# hostmonitoring -Prerequisites: istio service mesh +```go +import "github.com/Dynatrace/dynatrace-operator/test/features/hostmonitoring" +``` -Setup: CloudNative deployment with CSI driver +## Index -Verify that the operator is working as expected when istio service mesh is installed and pre-configured on the cluster. +- [func WithoutCSI(t *testing.T) features.Feature](<#WithoutCSI>) -### Install + -Verification that OneAgent is injected to sample application pods and can communicate with the *Dynatrace Cluster*. +## func [WithoutCSI]() -### Upgrade +```go +func WithoutCSI(t *testing.T) features.Feature +``` -Verification that a *released version* can be updated to the *current version*. The exact number of *released version* is updated manually. The *released version* is installed using configuration files from GitHub. +ApplicationMonitoring deployment without CSI driver -Sample application Deployment is installed and restarted to check if OneAgent is injected and can communicate with the *Dynatrace Cluster*. +# logmonitoring -### Specific Agent Version) +```go +import "github.com/Dynatrace/dynatrace-operator/test/features/logmonitoring" +``` -Verification that the operator is able to set agent version which is given via the dynakube. Upgrading to a newer version of agent is also tested. +## Index -Sample application Deployment is installed and restarted to check if OneAgent is injected and VERSION environment variable is correct. +- [func Feature(t *testing.T) features.Feature](<#Feature>) -# disabled_auto_injection + + +## func [Feature]() ```go -import "github.com/Dynatrace/dynatrace-operator/test/features/cloudnative/disabled_auto_injection" +func Feature(t *testing.T) features.Feature +``` + +# publicregistry + +```go +import "github.com/Dynatrace/dynatrace-operator/test/features/publicregistry" ``` ## Index @@ -448,52 +547,114 @@ import "github.com/Dynatrace/dynatrace-operator/test/features/cloudnative/disabl -## func [Feature]() +## func [Feature]() ```go func Feature(t *testing.T) features.Feature ``` -# switch_modes +Feature defines the e2e test to verify that public-registry images can be deployed by the operator and that they function This includes: + +- ActiveGate StatefulSet gets ready +- CodeModules can be downloaded and mounted +- OneAgent DaemonSet gets ready + +It determines the latest version of each image using the registry. + +# support_archive ```go -import "github.com/Dynatrace/dynatrace-operator/test/features/cloudnative/switch_modes" +import "github.com/Dynatrace/dynatrace-operator/test/features/support_archive" ``` ## Index - [func Feature(t *testing.T) features.Feature](<#Feature>) +- [type CustomResources](<#CustomResources>) -## func [Feature]() +## func [Feature]() ```go func Feature(t *testing.T) features.Feature ``` -# consts +Setup: DTO with CSI driver + +Verification if support-archive package created by the support-archive command and printed to the standard output is a valid tar.gz package and contains required *operator-version.txt* file. + + + +## type [CustomResources]() ```go -import "github.com/Dynatrace/dynatrace-operator/test/features/consts" +type CustomResources struct { + // contains filtered or unexported fields +} +``` + +# telemetryingest + +```go +import "github.com/Dynatrace/dynatrace-operator/test/features/telemetryingest" ``` ## Index - [Constants](<#constants>) +- [func OtelCollectorConfigUpdate(t *testing.T) features.Feature](<#OtelCollectorConfigUpdate>) +- [func WithLocalActiveGateAndCleanup(t *testing.T) features.Feature](<#WithLocalActiveGateAndCleanup>) +- [func WithPublicActiveGate(t *testing.T) features.Feature](<#WithPublicActiveGate>) +- [func WithTelemetryIngestEndpointTLS(t *testing.T) features.Feature](<#WithTelemetryIngestEndpointTLS>) ## Constants - + ```go const ( - AgCertificate = "custom-cas/agcrt.pem" - AgCertificateAndPrivateKey = "custom-cas/agcrtkey.p12" - AgCertificateAndPrivateKeyField = "server.p12" - AgSecretName = "ag-ca" - DevRegistryPullSecretName = "devregistry" - EecImageRepo = "478983378254.dkr.ecr.us-east-1.amazonaws.com/dynatrace/dynatrace-eec" - EecImageTag = "1.303.0.20240930-183404" + TelemetryIngestTLSCrt = "custom-cas/tls-telemetry-ingest.crt" + TelemetryIngestTLSKey = "custom-cas/tls-telemetry-ingest.key" ) ``` + + + +## func [OtelCollectorConfigUpdate]() + +```go +func OtelCollectorConfigUpdate(t *testing.T) features.Feature +``` + +Make sure the Otel collector configuration is updated and pods are restarted when protocols for telemetryIngest change + + + +## func [WithLocalActiveGateAndCleanup]() + +```go +func WithLocalActiveGateAndCleanup(t *testing.T) features.Feature +``` + +Rollout of OTel collector and a local in-cluster ActiveGate. Make sure that components are cleaned up after telemetryIngest gets disabled. + + + +## func [WithPublicActiveGate]() + +```go +func WithPublicActiveGate(t *testing.T) features.Feature +``` + +Rollout of OTel collector when no ActiveGate is configured in the Dynakube + + + +## func [WithTelemetryIngestEndpointTLS]() + +```go +func WithTelemetryIngestEndpointTLS(t *testing.T) features.Feature +``` + +Rollout of OTel collector with TLS secret to secure the telemetryIngest endpoints diff --git a/doc/roles/operator-roles.md b/doc/roles/operator-roles.md new file mode 100644 index 0000000000..1df282de55 --- /dev/null +++ b/doc/roles/operator-roles.md @@ -0,0 +1,39 @@ +# Permissions required by the Operator Pod + +**Permissions for Operator:** + +| Resources | Verbs | Comments | +| ------------------------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| events | create, get, list | Required by operator SDK | +| services | create, update, delete, get, list, watch | Required for ActiveGate, OTEL Collector Operator TelemetryIngest, Extensions | +| serviceentries.networking.istio.io | get, list, create, update, delete | Required by Istio Reconciler | +| virtualservices.networking.istio.io | get, list, create, update, delete | Required by Istio Reconciler | +| configmaps | get, list, watch, create, update, delete | Required to access trustedCAs, edgeConnect CA certs, ActiveGate/OneAgent Connection Info, Extension Custom Configuration, NodesController cache | +| secrets | get, list, watch, create, update, delete | Required for webhook certificates, OneAgent/ActiveGate AuthToken, ProcessModuleConfig; To access ActiveGate TLS, CustomPullSecret; | +| daemonsets.apps | get, list, watch, create, update, delete | Required by KSPM, LogMonitoring, All Monitoring modes that require host agents | +| deployments.apps | get, list, watch, create, update, delete | Required by our unit & E2E tests | +| replicasets.apps | get, list, watch, create, update, delete | Required by the nodes controller to check the owner | +| statefulsets.apps | get, list, watch, create, update, delete | Required by Extensions, OtelCollector, ActiveGate | +| dynakubes.dynatrace.com | get, list, watch, update | Required for reconciliation | +| edgeconnects.dynatrace.com | get, list, watch, update | Required for reconciliation | +| pods | get, list, watch | Required for operator pod to check if deployed via olm | +| leases.coordination.k8s.io | get, update, create | Required by Operator to guarantee, that only one is running at the same time | +| deployments.apps/finalizers | update | | +| dynakubes.dynatrace.com/finalizers | update | Required for reconciliation | +| dynakubes.dynatrace.com/status | update | Required for reconciliation | +| edgeconnects.dynatrace.com/finalizers | update | Required for reconciliation | +| edgeconnects.dynatrace.com/status | update | Required for reconciliation | + +**ClusterRole Permissions for Operator:** + +| Resources | Resource Names | Verbs | Comments | +| ------------------------------------------------------------ | -------------------------------------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| secrets | | create | Required to create init secret in every namespace for CNFS and application monitoring / metadata enrichment | +| namespaces | | get, list, watch, update | Required for setting the injection labels; Required as soon as a DynaKube is reconciled.; Required by EdgeConnect and DynaKube for requesting the kubeSystem UID | +| nodes | | get, list, watch | Required by nodes controller for node cache and mark for termination handling | +| secrets | dynatrace-dynakube-config | get, update, delete, list | Required to create init secret in every namespace for CNFS and application monitoring / metadata enrichment | +| secrets | dynatrace-metadata-enrichment-endpoint | get, update, delete, list | Required to create init secret in every namespace for CNFS and application monitoring / metadata enrichment | +| mutatingwebhookconfigurations.admissionregistration.k8s.io | dynatrace-webhook | get, update | Required for setting the CABundles aka. public cert created by our webhook cert controller. These certs are used by the API-Server to create a secure connection to the webhook. | +| validatingwebhookconfigurations.admissionregistration.k8s.io | dynatrace-webhook | get, update | Required for setting the CABundles aka. public cert created by our webhook cert controller. These certs are used by the API-Server to create a secure connection to the webhook. | +| customresourcedefinitions.apiextensions.k8s.io | dynakubes.dynatrace.com | get, update | Required for webhook cert controller. | +| customresourcedefinitions.apiextensions.k8s.io | edgeconnects.dynatrace.com | get, update | Required for webhook cert controller. | diff --git a/go.mod b/go.mod index 73f632da39..324b6c4140 100644 --- a/go.mod +++ b/go.mod @@ -1,107 +1,168 @@ module github.com/Dynatrace/dynatrace-operator -go 1.23.3 +go 1.23.4 require ( + github.com/Dynatrace/dynatrace-bootstrapper v1.0.3 github.com/container-storage-interface/spec v1.11.0 - github.com/docker/cli v27.3.1+incompatible - github.com/evanphx/json-patch v5.9.0+incompatible + github.com/docker/cli v28.0.4+incompatible + github.com/evanphx/json-patch v5.9.11+incompatible github.com/go-logr/logr v1.4.2 - github.com/google/go-containerregistry v0.20.2 + github.com/google/go-containerregistry v0.20.3 github.com/google/uuid v1.6.0 - github.com/klauspost/compress v1.17.11 - github.com/mattn/go-sqlite3 v1.14.24 + github.com/klauspost/compress v1.18.0 github.com/opencontainers/go-digest v1.0.0 github.com/pkg/errors v0.9.1 - github.com/prometheus/client_golang v1.20.5 - github.com/prometheus/client_model v0.6.1 - github.com/spf13/afero v1.11.0 - github.com/spf13/cobra v1.8.1 + github.com/prometheus/client_golang v1.21.1 + github.com/spf13/afero v1.14.0 + github.com/spf13/cobra v1.9.1 github.com/stretchr/testify v1.10.0 + go.opentelemetry.io/collector/component v1.29.0 + go.opentelemetry.io/collector/confmap v1.29.0 + go.opentelemetry.io/collector/pipeline v0.123.0 + go.opentelemetry.io/collector/service v0.123.0 go.uber.org/zap v1.27.0 - golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 - golang.org/x/mod v0.22.0 - golang.org/x/net v0.31.0 - golang.org/x/oauth2 v0.24.0 - golang.org/x/sys v0.27.0 - google.golang.org/grpc v1.68.0 + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 + golang.org/x/mod v0.24.0 + golang.org/x/net v0.38.0 + golang.org/x/oauth2 v0.28.0 + golang.org/x/sys v0.32.0 + google.golang.org/grpc v1.71.1 gopkg.in/yaml.v3 v3.0.1 - istio.io/api v1.24.1 - istio.io/client-go v1.24.1 - k8s.io/api v0.31.3 - k8s.io/apiextensions-apiserver v0.31.3 - k8s.io/apimachinery v0.31.3 - k8s.io/client-go v0.31.3 - k8s.io/mount-utils v0.31.3 - k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 - sigs.k8s.io/controller-runtime v0.19.2 + istio.io/api v1.25.2 + istio.io/client-go v1.25.2 + k8s.io/api v0.32.4 + k8s.io/apiextensions-apiserver v0.32.4 + k8s.io/apimachinery v0.32.4 + k8s.io/client-go v0.32.4 + k8s.io/mount-utils v0.32.4 + k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 + sigs.k8s.io/controller-runtime v0.20.4 sigs.k8s.io/e2e-framework v0.3.0 sigs.k8s.io/yaml v1.4.0 ) require ( github.com/beorn7/perks v1.0.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect - github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/docker/distribution v2.8.2+incompatible // indirect - github.com/docker/docker-credential-helpers v0.8.0 // indirect + github.com/docker/distribution v2.8.3+incompatible // indirect + github.com/docker/docker-credential-helpers v0.8.2 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect - github.com/evanphx/json-patch/v5 v5.9.0 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/zapr v1.3.0 // indirect - github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect - github.com/go-openapi/swag v0.22.4 // indirect - github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect + github.com/google/btree v1.1.3 // indirect github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect - github.com/imdario/mergo v0.3.15 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/kylelemons/godebug v1.1.0 // indirect + github.com/knadh/koanf/maps v0.1.1 // indirect + github.com/knadh/koanf/providers/confmap v0.1.0 // indirect + github.com/knadh/koanf/v2 v2.1.2 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/moby/spdystream v0.4.0 // indirect - github.com/moby/sys/mountinfo v0.7.1 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/moby/spdystream v0.5.0 // indirect + github.com/moby/sys/mountinfo v0.7.2 // indirect + github.com/moby/sys/userns v0.1.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect - github.com/opencontainers/image-spec v1.1.0-rc4 // indirect - github.com/opencontainers/runc v1.1.13 // indirect - github.com/opencontainers/runtime-spec v1.0.3-0.20220909204839-494a5a6aca78 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/sirupsen/logrus v1.9.3 // indirect - github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/pflag v1.0.6 // indirect github.com/stretchr/objx v0.5.2 // indirect - github.com/vbatts/tar-split v0.11.5 // indirect + github.com/vbatts/tar-split v0.11.6 // indirect github.com/vladimirvivien/gexe v0.2.0 // indirect github.com/x448/float16 v0.8.4 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/collector/component/componentstatus v0.123.0 // indirect + go.opentelemetry.io/collector/component/componenttest v0.123.0 // indirect + go.opentelemetry.io/collector/config/configtelemetry v0.123.0 // indirect + go.opentelemetry.io/collector/connector v0.123.0 // indirect + go.opentelemetry.io/collector/connector/connectortest v0.123.0 // indirect + go.opentelemetry.io/collector/connector/xconnector v0.123.0 // indirect + go.opentelemetry.io/collector/consumer v1.29.0 // indirect + go.opentelemetry.io/collector/consumer/consumererror v0.123.0 // indirect + go.opentelemetry.io/collector/consumer/consumertest v0.123.0 // indirect + go.opentelemetry.io/collector/consumer/xconsumer v0.123.0 // indirect + go.opentelemetry.io/collector/exporter v0.123.0 // indirect + go.opentelemetry.io/collector/exporter/exportertest v0.123.0 // indirect + go.opentelemetry.io/collector/exporter/xexporter v0.123.0 // indirect + go.opentelemetry.io/collector/extension v1.29.0 // indirect + go.opentelemetry.io/collector/extension/extensioncapabilities v0.123.0 // indirect + go.opentelemetry.io/collector/extension/extensiontest v0.123.0 // indirect + go.opentelemetry.io/collector/featuregate v1.29.0 // indirect + go.opentelemetry.io/collector/internal/fanoutconsumer v0.123.0 // indirect + go.opentelemetry.io/collector/internal/telemetry v0.123.0 // indirect + go.opentelemetry.io/collector/pdata v1.29.0 // indirect + go.opentelemetry.io/collector/pdata/pprofile v0.123.0 // indirect + go.opentelemetry.io/collector/pdata/testdata v0.123.0 // indirect + go.opentelemetry.io/collector/pipeline/xpipeline v0.123.0 // indirect + go.opentelemetry.io/collector/processor v1.29.0 // indirect + go.opentelemetry.io/collector/processor/processortest v0.123.0 // indirect + go.opentelemetry.io/collector/processor/xprocessor v0.123.0 // indirect + go.opentelemetry.io/collector/receiver v1.29.0 // indirect + go.opentelemetry.io/collector/receiver/receivertest v0.123.0 // indirect + go.opentelemetry.io/collector/receiver/xreceiver v0.123.0 // indirect + go.opentelemetry.io/contrib/bridges/otelzap v0.10.0 // indirect + go.opentelemetry.io/contrib/otelconf v0.15.0 // indirect + go.opentelemetry.io/contrib/propagators/b3 v1.35.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.11.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.11.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect + go.opentelemetry.io/otel/exporters/prometheus v0.57.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.11.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 // indirect + go.opentelemetry.io/otel/log v0.11.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/sdk v1.35.0 // indirect + go.opentelemetry.io/otel/sdk/log v0.11.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect + go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/sync v0.9.0 // indirect - golang.org/x/term v0.26.0 // indirect - golang.org/x/text v0.20.0 // indirect - golang.org/x/time v0.5.0 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/term v0.30.0 // indirect + golang.org/x/text v0.23.0 // indirect + golang.org/x/time v0.11.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect - google.golang.org/protobuf v1.34.2 // indirect + gonum.org/v1/gonum v0.16.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect + google.golang.org/protobuf v1.36.6 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect - sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect + sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.5.0 // indirect ) diff --git a/go.sum b/go.sum index d6745dada5..8f0b64444a 100644 --- a/go.sum +++ b/go.sum @@ -1,77 +1,86 @@ +github.com/Dynatrace/dynatrace-bootstrapper v1.0.3 h1:rjYpJfoD8k42sAdQlEkZOGQXGgpCZNu+v2ZYHKWul3s= +github.com/Dynatrace/dynatrace-bootstrapper v1.0.3/go.mod h1:LHHsJDxADbZ/41Sf+VVrRuhMhQeua4/y5f8N9My5Hk0= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= +github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/container-storage-interface/spec v1.11.0 h1:H/YKTOeUZwHtyPOr9raR+HgFmGluGCklulxDYxSdVNM= github.com/container-storage-interface/spec v1.11.0/go.mod h1:DtUvaQszPml1YJfIK7c00mlv6/g4wNMLanLgiUbKFRI= -github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k= -github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o= -github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8= +github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/docker/cli v27.3.1+incompatible h1:qEGdFBF3Xu6SCvCYhc7CzaQTlBmqDuzxPDpigSyeKQQ= -github.com/docker/cli v27.3.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= -github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker-credential-helpers v0.8.0 h1:YQFtbBQb4VrpoPxhFuzEBPQ9E16qz5SpHLS+uswaCp8= -github.com/docker/docker-credential-helpers v0.8.0/go.mod h1:UGFXcuoQ5TxPiB54nHOZ32AWRqQdECoh/Mg0AlEYb40= +github.com/docker/cli v28.0.4+incompatible h1:pBJSJeNd9QeIWPjRcV91RVJihd/TXB77q1ef64XEu4A= +github.com/docker/cli v28.0.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= +github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= +github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= -github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= -github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= +github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= -github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= -github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= -github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-containerregistry v0.20.2 h1:B1wPJ1SN/S7pB+ZAimcciVD+r+yV/l/DSArMxlbwseo= -github.com/google/go-containerregistry v0.20.2/go.mod h1:z38EKdKh4h7IP2gSfUUqEvalZBqs6AoLeWfUy34nQC8= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI= +github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af h1:kmjWCqn2qkEml422C2Rrd27c3VGxi6a/6HNq8QmHRKM= -github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= -github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -80,8 +89,14 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= +github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/providers/confmap v0.1.0 h1:gOkxhHkemwG4LezxxN8DMOFopOPghxRVp7JbIvdvqzU= +github.com/knadh/koanf/providers/confmap v0.1.0/go.mod h1:2uLhxQzJnyHKfxG927awZC7+fyHFdQkd697K4MdLnIU= +github.com/knadh/koanf/v2 v2.1.2 h1:I2rtLRqXRy1p01m/utEtpZSSA6dcJbgGVuE27kW2PzQ= +github.com/knadh/koanf/v2 v2.1.2/go.mod h1:Gphfaen0q1Fc1HTgJgSTC4oRX9R2R5ErYMZJy8fLJBo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -93,14 +108,18 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= -github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/moby/spdystream v0.4.0 h1:Vy79D6mHeJJjiPdFEL2yku1kl0chZpJfZcPpb16BRl8= -github.com/moby/spdystream v0.4.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= -github.com/moby/sys/mountinfo v0.7.1 h1:/tTvQaSJRr2FshkhXiIpux6fQ2Zvc4j7tAhMTStAG2g= -github.com/moby/sys/mountinfo v0.7.1/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= +github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= +github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= +github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -110,42 +129,38 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= -github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= -github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= -github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= +github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= +github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= +github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0-rc4 h1:oOxKUJWnFC4YGHCCMNql1x4YaDfYBTS5Y4x/Cgeo1E0= -github.com/opencontainers/image-spec v1.1.0-rc4/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= -github.com/opencontainers/runc v1.1.13 h1:98S2srgG9vw0zWcDpFMn5TRrh8kLxa/5OFUstuUhmRs= -github.com/opencontainers/runc v1.1.13/go.mod h1:R016aXacfp/gwQBYw2FDGa9m+n6atbLWrYY8hNMT/sA= -github.com/opencontainers/runtime-spec v1.0.3-0.20220909204839-494a5a6aca78 h1:R5M2qXZiK/mWPMT4VldCOiSL9HIAMuxQZWdG0CSM5+4= -github.com/opencontainers/runtime-spec v1.0.3-0.20220909204839-494a5a6aca78/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= -github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= +github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= -github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= +github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -158,14 +173,136 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts= -github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk= +github.com/vbatts/tar-split v0.11.6 h1:4SjTW5+PU11n6fZenf2IPoV8/tz3AaYHMWjf23envGs= +github.com/vbatts/tar-split v0.11.6/go.mod h1:dqKNtesIOr2j2Qv3W/cHjnvk9I8+G7oAkFDFN6TCBEI= github.com/vladimirvivien/gexe v0.2.0 h1:nbdAQ6vbZ+ZNsolCgSVb9Fno60kzSuvtzVh6Ytqi/xY= github.com/vladimirvivien/gexe v0.2.0/go.mod h1:LHQL00w/7gDUKIak24n801ABp8C+ni6eBht9vGVst8w= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/collector v0.123.0 h1:2aijJMaWk1Qn0ZLcJwEEfRWrQpvhxFFNIKZGUDw3QR0= +go.opentelemetry.io/collector v0.123.0/go.mod h1:HlBLICu+ZbxI9whVakFLg+h1yrco/n2M+aOIBhlq9Sk= +go.opentelemetry.io/collector/component v1.29.0 h1:SQDKqMb3EUNPyxZDrImkqgi8p682Hm986CMXBiiTkz0= +go.opentelemetry.io/collector/component v1.29.0/go.mod h1:qHoiz0iyBgyAYsT0F8FgtcJv8pTEQDvo1Ox3eO3qVlY= +go.opentelemetry.io/collector/component/componentstatus v0.123.0 h1:KL7nNmRoCLt8dVOr5kuvWDly2W5fLE5J6qZ8OdhEzw0= +go.opentelemetry.io/collector/component/componentstatus v0.123.0/go.mod h1:xZxBHdoU8bunn8NpcpRF9eEL1HhYFKXUFTqdMZRTqVk= +go.opentelemetry.io/collector/component/componenttest v0.123.0 h1:h0B/kBj0URKq+i9iMbMuLhc6/dZ2GWL0y9L6tqHRNuA= +go.opentelemetry.io/collector/component/componenttest v0.123.0/go.mod h1:4Y6EMvsgE9fUNM98G0eW5+LFXfcxdhTHQDhaOxJrgN8= +go.opentelemetry.io/collector/config/configretry v1.29.0 h1:Gm140dYwQk9TRQBQHF4hYybdvE2KvUhCLndPvZupTxM= +go.opentelemetry.io/collector/config/configretry v1.29.0/go.mod h1:QNnb+MCk7aS1k2EuGJMtlNCltzD7b8uC7Xel0Dxm1wQ= +go.opentelemetry.io/collector/config/configtelemetry v0.123.0 h1:Sml/reJ8At27NU5dgUmR/PBujSLr5Oa8aDrQ8IyoFWQ= +go.opentelemetry.io/collector/config/configtelemetry v0.123.0/go.mod h1:WXmlNatI0vwjv7whh/qF1Xy+UufCZDk7VLtYqML7QmA= +go.opentelemetry.io/collector/confmap v1.29.0 h1:1d43r3gQApgRxjyiQ86/qQd7OHW/BEbM6m/L36O5gKU= +go.opentelemetry.io/collector/confmap v1.29.0/go.mod h1:g8DLibnb2MEUWMMlyxHl1VviwFKUdwyBE5M1bxY1Pwg= +go.opentelemetry.io/collector/confmap/xconfmap v0.123.0 h1:7c5kmfweyEZG2AP/kxVXtmQQTmuMU5S4C7NKrWDHOuc= +go.opentelemetry.io/collector/confmap/xconfmap v0.123.0/go.mod h1:pNLcOJ/65QE12P8iwmn5RnkvzdplHF6AvebXeRClT3w= +go.opentelemetry.io/collector/connector v0.123.0 h1:B5vjKx/LGUnIkIvhF+65XkRXdndn/6b/mmqhLkjGiCA= +go.opentelemetry.io/collector/connector v0.123.0/go.mod h1:wVYmWz2ETJXP5i/IB5IUWQWEZi+PE4pQFTr4dzNLvcc= +go.opentelemetry.io/collector/connector/connectortest v0.123.0 h1:wytLYrZqzKhL+tts6VrQBf6rMBwlNZUj5SbjB15mcUA= +go.opentelemetry.io/collector/connector/connectortest v0.123.0/go.mod h1:pzIR5vPUVcccddMcvch/1D70IxoAIyEUDCOLFLUBu28= +go.opentelemetry.io/collector/connector/xconnector v0.123.0 h1:VHS7KLIGLMNyAhifa3LKIrbTYF8N8rgnym1ybrzaQqA= +go.opentelemetry.io/collector/connector/xconnector v0.123.0/go.mod h1:zMsvWvuZVt2vh1S3BcF1OjmiDswdrfFqaE+WiPQuR4Q= +go.opentelemetry.io/collector/consumer v1.29.0 h1:WOtwc7bY0/L47mZPE6fu/txD6Fg2dPI3c/Cq+H1RRNY= +go.opentelemetry.io/collector/consumer v1.29.0/go.mod h1:SRcOjDfuvTEk6pchJ0HuOBLsmbRG26+mZLrwXb9T3TQ= +go.opentelemetry.io/collector/consumer/consumererror v0.123.0 h1:/X32KeSGTLX+/YRlR1Wx6/GQkL2m1A6l3B0BYErnxC0= +go.opentelemetry.io/collector/consumer/consumererror v0.123.0/go.mod h1:A9oAkn1emXnenyXiBb5iJNteWCidrM5remnzRqXHOKc= +go.opentelemetry.io/collector/consumer/consumertest v0.123.0 h1:IGBan9tb/QJOzGO8AaQai/jv2DQ7V1DbsFhrTHhDSe8= +go.opentelemetry.io/collector/consumer/consumertest v0.123.0/go.mod h1:MN6OZWsjF1JpvxEzGy7xFu0+PGtbzOASQnN6GgX7olA= +go.opentelemetry.io/collector/consumer/xconsumer v0.123.0 h1:UJhwYgtAe3O/8ilQj+AiyLBz+PXnw/hrpPk3wkB7u9g= +go.opentelemetry.io/collector/consumer/xconsumer v0.123.0/go.mod h1:+LAk3gs6kQ3O0+jLZhZAfgPEWUinxKkFqb3nZkYbC6w= +go.opentelemetry.io/collector/exporter v0.123.0 h1:Dda4AJl6GB+3s6syq45EGpP/kq3xnjcYBBd9SL5A0iA= +go.opentelemetry.io/collector/exporter v0.123.0/go.mod h1:ZeRhRP7AfWmkWkQIBn6A9sIzPkhpjFSjC6Cr78kugr0= +go.opentelemetry.io/collector/exporter/exportertest v0.123.0 h1:TR8pmnyUvy6BnF/suwplZ8l/s7ozetlE1sRYeNfvpoc= +go.opentelemetry.io/collector/exporter/exportertest v0.123.0/go.mod h1:GHt8/K/1evqJ84IUT1QjYrpmBS5dtQOjc7moxik7rA0= +go.opentelemetry.io/collector/exporter/xexporter v0.123.0 h1:7uAARN1ktH8OnhansCiJkhqYcfjp4UtdjUimeORJCYc= +go.opentelemetry.io/collector/exporter/xexporter v0.123.0/go.mod h1:k8TLjOKKm7RxoXIk/aA7CF7CdsoCFYx+4Sfnjd2rR5U= +go.opentelemetry.io/collector/extension v1.29.0 h1:xydxgo+UwDSZtcYGjEbj2UJtfevBSqRhPYyDuOC2VSI= +go.opentelemetry.io/collector/extension v1.29.0/go.mod h1:s/8CiRG1UdAEmovkO7zGACoxBqhqo0UaHciK4Ud+5P4= +go.opentelemetry.io/collector/extension/extensioncapabilities v0.123.0 h1:0VCC+lxhrO+ugsW+m3yNm0Wj6nAzKmR0YOHIJyAbmio= +go.opentelemetry.io/collector/extension/extensioncapabilities v0.123.0/go.mod h1:aoh1Br9rFL+FHUr+pMPgXfM2Tk1u78q2X056P3NFgks= +go.opentelemetry.io/collector/extension/extensiontest v0.123.0 h1:QankUyQPcmx0UIBGWJX8ObpdESHULhCo0JDhXtagb0s= +go.opentelemetry.io/collector/extension/extensiontest v0.123.0/go.mod h1:JSnbeMyH55jFQqT77wZh04Xx8EnwcIY39A+MqnwDawk= +go.opentelemetry.io/collector/extension/xextension v0.123.0 h1:GA0Hp9+3aY+bCXlPTsSSkQKgRpK3TMJ7lke19DGyqXA= +go.opentelemetry.io/collector/extension/xextension v0.123.0/go.mod h1:qMBSQaxnFDL74poKWeVifhhuF5UCZ73GGD0mgyTI6SE= +go.opentelemetry.io/collector/featuregate v1.29.0 h1:DTDeaokT53AA+SiYBH8GC893cmppThU65b3XyFfLPWw= +go.opentelemetry.io/collector/featuregate v1.29.0/go.mod h1:Y/KsHbvREENKvvN9RlpiWk/IGBK+CATBYzIIpU7nccc= +go.opentelemetry.io/collector/internal/fanoutconsumer v0.123.0 h1:1wM3XB46Lp0svAPJmL74ST3CMVJZpHIDxxqSBPFSBy8= +go.opentelemetry.io/collector/internal/fanoutconsumer v0.123.0/go.mod h1:gTENSpqzDirGKm/9NM8WiHYVYpu9fFCdPiJqTJhZ4Kg= +go.opentelemetry.io/collector/internal/telemetry v0.123.0 h1:K6UMv22ulxcv/G8BkwywMN4LprpwuLl1rhTMWpnujqE= +go.opentelemetry.io/collector/internal/telemetry v0.123.0/go.mod h1:Qwh5XcqAlAT9NorjaaSaNcilDgaecs8S+OKljJ9hFno= +go.opentelemetry.io/collector/pdata v1.29.0 h1:ZXSZ2fROdAEbv4JKKiCspBpjIjYZ5XaNt71LNH4RpQw= +go.opentelemetry.io/collector/pdata v1.29.0/go.mod h1:9kb3zMtLFXBPA6WGWkBHbkFwlwwYL/OHk1m0ASWZpeY= +go.opentelemetry.io/collector/pdata/pprofile v0.123.0 h1:NjwE1btsQbzOWsIFYt7vfgrqDNCROO3hWiZwT1/KaZU= +go.opentelemetry.io/collector/pdata/pprofile v0.123.0/go.mod h1:af2WDdbCc+JA+FokOnsW3l+agsnW5ed8LkLn6CfneII= +go.opentelemetry.io/collector/pdata/testdata v0.123.0 h1:daMcuywv7L/O0pJMCKzlgsm/En8o7SeMSPWUZsPt+JM= +go.opentelemetry.io/collector/pdata/testdata v0.123.0/go.mod h1:lyARYT+2evYj7EdB0LLuo81lb48P7/yKKT86lRCEsmA= +go.opentelemetry.io/collector/pipeline v0.123.0 h1:LDcuCrwhCTx2yROJZqhNmq2v0CFkCkUEvxvvcRW0+2c= +go.opentelemetry.io/collector/pipeline v0.123.0/go.mod h1:TO02zju/K6E+oFIOdi372Wk0MXd+Szy72zcTsFQwXl4= +go.opentelemetry.io/collector/pipeline/xpipeline v0.123.0 h1:YLymvvo0xdOljzZk7A1XYwtf+v4v6A+yu0GA33MZafs= +go.opentelemetry.io/collector/pipeline/xpipeline v0.123.0/go.mod h1:RmjT+wsknmOnI4L87BfMmAnQP0SO5cl8WXQJhT9kIys= +go.opentelemetry.io/collector/processor v1.29.0 h1:adymt63B45hxkMLm5Gj+XkB9hat6UafZtF3Leel6DfU= +go.opentelemetry.io/collector/processor v1.29.0/go.mod h1:slWQP4bt5rQzT05UM7BPGOOEK/6cvNBg33aY4CjKEHw= +go.opentelemetry.io/collector/processor/processortest v0.123.0 h1:oDC4eum91br/p7FfpKRpMp2k5tXE488foW+yUHzwudU= +go.opentelemetry.io/collector/processor/processortest v0.123.0/go.mod h1:Moj5M7Vm8NqGO6qCMhPPQVX4jWFJYvz/fWHdhbi7m6g= +go.opentelemetry.io/collector/processor/xprocessor v0.123.0 h1:9L9ssdId6DDeaiW7e3b+AVBlNX4ldVW9629bu7Atskw= +go.opentelemetry.io/collector/processor/xprocessor v0.123.0/go.mod h1:s51Bsyu5b4uXiB2tN4IaulqRhzYMnmglVzPzE6PHsTI= +go.opentelemetry.io/collector/receiver v1.29.0 h1:HK0bzpRlcqqdWBG2v/N76f0eauN8woIU91LloLLWeGM= +go.opentelemetry.io/collector/receiver v1.29.0/go.mod h1:4QoZQZl4r9bIrKa7VLRw3Rp7++ezc5QoXn9VoPZwfoI= +go.opentelemetry.io/collector/receiver/receivertest v0.123.0 h1:MB2x4v7Pg+npLyIQxBn/7rrW4RliV1JtcKqWM75iz1U= +go.opentelemetry.io/collector/receiver/receivertest v0.123.0/go.mod h1:eDcm8N4yFz//V5dSa6MeEdjuK5Nq1X+j9xP9kPJr8BU= +go.opentelemetry.io/collector/receiver/xreceiver v0.123.0 h1:oRExkR/d5axuvAUa3lcOf1ea+zL0hK+iGAhewqZz1NU= +go.opentelemetry.io/collector/receiver/xreceiver v0.123.0/go.mod h1:M3a0ACBOPHag90EV+GbIWKZhJkHXW7Ka2xabBGY25aw= +go.opentelemetry.io/collector/semconv v0.123.0 h1:hFjhLU1SSmsZ67pXVCVbIaejonkYf5XD/6u4qCQQPtc= +go.opentelemetry.io/collector/semconv v0.123.0/go.mod h1:te6VQ4zZJO5Lp8dM2XIhDxDiL45mwX0YAQQWRQ0Qr9U= +go.opentelemetry.io/collector/service v0.123.0 h1:KPWMsbxsuesMGcyif4ulZ3JT8my5tq6MZXsJSvGjBhc= +go.opentelemetry.io/collector/service v0.123.0/go.mod h1:5VfeoadoI6wxbB11Fxu81mow9cxD05e+IL5pl+LtcJk= +go.opentelemetry.io/contrib/bridges/otelzap v0.10.0 h1:ojdSRDvjrnm30beHOmwsSvLpoRF40MlwNCA+Oo93kXU= +go.opentelemetry.io/contrib/bridges/otelzap v0.10.0/go.mod h1:oTTm4g7NEtHSV2i/0FeVdPaPgUIZPfQkFbq0vbzqnv0= +go.opentelemetry.io/contrib/otelconf v0.15.0 h1:BLNiIUsrNcqhSKpsa6CnhE6LdrpY1A8X0szMVsu99eo= +go.opentelemetry.io/contrib/otelconf v0.15.0/go.mod h1:OPH1seO5z9dp1P26gnLtoM9ht7JDvh3Ws6XRHuXqImY= +go.opentelemetry.io/contrib/propagators/b3 v1.35.0 h1:DpwKW04LkdFRFCIgM3sqwTJA/QREHMeMHYPWP1WeaPQ= +go.opentelemetry.io/contrib/propagators/b3 v1.35.0/go.mod h1:9+SNxwqvCWo1qQwUpACBY5YKNVxFJn5mlbXg/4+uKBg= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.11.0 h1:HMUytBT3uGhPKYY/u/G5MR9itrlSO2SMOsSD3Tk3k7A= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.11.0/go.mod h1:hdDXsiNLmdW/9BF2jQpnHHlhFajpWCEYfM6e5m2OAZg= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.11.0 h1:C/Wi2F8wEmbxJ9Kuzw/nhP+Z9XaHYMkyDmXy6yR2cjw= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.11.0/go.mod h1:0Lr9vmGKzadCTgsiBydxr6GEZ8SsZ7Ks53LzjWG5Ar4= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 h1:QcFwRrZLc82r8wODjvyCbP7Ifp3UANaBSmhDSFjnqSc= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0/go.mod h1:CXIWhUomyWBG/oY2/r/kLp6K/cmx9e/7DLpBuuGdLCA= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0 h1:0NIXxOCFx+SKbhCVxwl3ETG8ClLPAa0KuKV6p3yhxP8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0/go.mod h1:ChZSJbbfbl/DcRZNc9Gqh6DYGlfjw4PvO1pEOZH1ZsE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk= +go.opentelemetry.io/otel/exporters/prometheus v0.57.0 h1:AHh/lAP1BHrY5gBwk8ncc25FXWm/gmmY3BX258z5nuk= +go.opentelemetry.io/otel/exporters/prometheus v0.57.0/go.mod h1:QpFWz1QxqevfjwzYdbMb4Y1NnlJvqSGwyuU0B4iuc9c= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.11.0 h1:k6KdfZk72tVW/QVZf60xlDziDvYAePj5QHwoQvrB2m8= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.11.0/go.mod h1:5Y3ZJLqzi/x/kYtrSrPSx7TFI/SGsL7q2kME027tH6I= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 h1:PB3Zrjs1sG1GBX51SXyTSoOTqcDglmsk7nT6tkKPb/k= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0/go.mod h1:U2R3XyVPzn0WX7wOIypPuptulsMcPDPs/oiSVOMVnHY= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 h1:T0Ec2E+3YZf5bgTNQVet8iTDW7oIk03tXHq+wkwIDnE= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0/go.mod h1:30v2gqH+vYGJsesLWFov8u47EpYTcIQcBjKpI6pJThg= +go.opentelemetry.io/otel/log v0.11.0 h1:c24Hrlk5WJ8JWcwbQxdBqxZdOK7PcP/LFtOtwpDTe3Y= +go.opentelemetry.io/otel/log v0.11.0/go.mod h1:U/sxQ83FPmT29trrifhQg+Zj2lo1/IPN1PF6RTFqdwc= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= +go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/sdk/log v0.11.0 h1:7bAOpjpGglWhdEzP8z0VXc4jObOiDEwr3IYbhBnjk2c= +go.opentelemetry.io/otel/sdk/log v0.11.0/go.mod h1:dndLTxZbwBstZoqsJB3kGsRPkpAgaJrWfQg3lhlHFFY= +go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= +go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= +go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -175,60 +312,61 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= -golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= -golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= -golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= -golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= -golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= +golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= -golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= -golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= -golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= +golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc= -google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= -google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= -google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= +google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= +google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI= +google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -236,41 +374,38 @@ gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSP gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= -istio.io/api v1.24.1 h1:jF1I+ABGVS7ImVKzaAeiXHkFEbfXN2IEKDGJTw5UX0w= -istio.io/api v1.24.1/go.mod h1:MQnRok7RZ20/PE56v0LxmoWH0xVxnCQPNuf9O7PAN1I= -istio.io/client-go v1.24.1 h1:m1hYt+S7zZZpiWHVnJkp9SFfQ9EApBhKL0LUaviKh9c= -istio.io/client-go v1.24.1/go.mod h1:sCDBDJWQGJQz/1t3CHwUTDE5V7Nk6pFFkqBwhIg+LrI= -k8s.io/api v0.31.3 h1:umzm5o8lFbdN/hIXbrK9oRpOproJO62CV1zqxXrLgk8= -k8s.io/api v0.31.3/go.mod h1:UJrkIp9pnMOI9K2nlL6vwpxRzzEX5sWgn8kGQe92kCE= -k8s.io/apiextensions-apiserver v0.31.3 h1:+GFGj2qFiU7rGCsA5o+p/rul1OQIq6oYpQw4+u+nciE= -k8s.io/apiextensions-apiserver v0.31.3/go.mod h1:2DSpFhUZZJmn/cr/RweH1cEVVbzFw9YBu4T+U3mf1e4= -k8s.io/apimachinery v0.31.3 h1:6l0WhcYgasZ/wk9ktLq5vLaoXJJr5ts6lkaQzgeYPq4= -k8s.io/apimachinery v0.31.3/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= -k8s.io/client-go v0.31.3 h1:CAlZuM+PH2cm+86LOBemaJI/lQ5linJ6UFxKX/SoG+4= -k8s.io/client-go v0.31.3/go.mod h1:2CgjPUTpv3fE5dNygAr2NcM8nhHzXvxB8KL5gYc3kJs= +istio.io/api v1.25.2 h1:FCRQy7iaTreKJdLemlQ1vRJEsf1soCHoTAuSf68w5I8= +istio.io/api v1.25.2/go.mod h1:QFzEXv/IT582T0FHZVp1QoolvE4ws0zz/vVO55blmlE= +istio.io/client-go v1.25.2 h1:faupTqeMD0PkuNTZdFlpxxT35jBSQapK5k+MNAkXvqA= +istio.io/client-go v1.25.2/go.mod h1:E2LTxTcCVe4cqpKy4/9Y4VmwSoLiH6ff9MEG7EhfSDo= +k8s.io/api v0.32.4 h1:kw8Y/G8E7EpNy7gjB8gJZl3KJkNz8HM2YHrZPtAZsF4= +k8s.io/api v0.32.4/go.mod h1:5MYFvLvweRhyKylM3Es/6uh/5hGp0dg82vP34KifX4g= +k8s.io/apiextensions-apiserver v0.32.4 h1:IA+CoR63UDOijR/vEpow6wQnX4V6iVpzazJBskHrpHE= +k8s.io/apiextensions-apiserver v0.32.4/go.mod h1:Y06XO/b92H8ymOdG1HlA1submf7gIhbEDc3RjriqZOs= +k8s.io/apimachinery v0.32.4 h1:8EEksaxA7nd7xWJkkwLDN4SvWS5ot9g6Z/VZb3ju25I= +k8s.io/apimachinery v0.32.4/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/client-go v0.32.4 h1:zaGJS7xoYOYumoWIFXlcVrsiYioRPrXGO7dBfVC5R6M= +k8s.io/client-go v0.32.4/go.mod h1:k0jftcyYnEtwlFW92xC7MTtFv5BNcZBr+zn9jPlT9Ic= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= -k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= -k8s.io/mount-utils v0.31.3 h1:CANy3prUYvvDCc2X7ZKgpjpDhAidx4gjGh/WwDrCPq8= -k8s.io/mount-utils v0.31.3/go.mod h1:HV/VYBUGqYUj4vt82YltzpWvgv8FPg0G9ItyInT3NPU= -k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= -k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/controller-runtime v0.19.2 h1:3sPrF58XQEPzbE8T81TN6selQIMGbtYwuaJ6eDssDF8= -sigs.k8s.io/controller-runtime v0.19.2/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= +k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= +k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= +k8s.io/mount-utils v0.32.4 h1:Tgvgr70qGXY8zRftMAHHGVUky51NN7wvNyTBbMOMEsE= +k8s.io/mount-utils v0.32.4/go.mod h1:Kun5c2svjAPx0nnvJKYQWhfeNW+O0EpzHgRhDcYoSY0= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU= +sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= sigs.k8s.io/e2e-framework v0.3.0 h1:eqQALBtPCth8+ulTs6lcPK7ytV5rZSSHJzQHZph4O7U= sigs.k8s.io/e2e-framework v0.3.0/go.mod h1:C+ef37/D90Dc7Xq1jQnNbJYscrUGpxrWog9bx2KIa+c= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= -sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= -sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +sigs.k8s.io/structured-merge-diff/v4 v4.5.0 h1:nbCitCK2hfnhyiKo6uf2HxUPTCodY6Qaf85SbDIaMBk= +sigs.k8s.io/structured-merge-diff/v4 v4.5.0/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/hack/build/build_image.sh b/hack/build/build_image.sh index 71dfcdf5e8..fdfe2091d4 100755 --- a/hack/build/build_image.sh +++ b/hack/build/build_image.sh @@ -21,6 +21,7 @@ out_image="${image}:${tag}" # directory required by docker copy command mkdir -p third_party_licenses +touch dynatrace-operator-bin-sbom.cdx.json if ! command -v docker 2>/dev/null; then CONTAINER_CMD=podman @@ -41,3 +42,4 @@ ${CONTAINER_CMD} build "${OPERATOR_BUILD_PLATFORM}" . -f ./Dockerfile -t "${out_ --label "quay.expires-after=14d" rm -rf third_party_licenses +rm dynatrace-operator-bin-sbom.cdx.json diff --git a/hack/build/bundle.sh b/hack/build/bundle.sh index ea414ad1a9..cf4db051fa 100755 --- a/hack/build/bundle.sh +++ b/hack/build/bundle.sh @@ -62,12 +62,12 @@ grep -v '# Labels for testing.' "./config/olm/${PLATFORM}/bundle-${VERSION}.Dock mv "./config/olm/${PLATFORM}/bundle-${VERSION}.Dockerfile.output" "./config/olm/${PLATFORM}/bundle-${VERSION}.Dockerfile" if [ "${PLATFORM}" = "openshift" ]; then # shellcheck disable=SC2129 - echo 'LABEL com.redhat.openshift.versions="v4.9-v4.16"' >> "./config/olm/${PLATFORM}/bundle-${VERSION}.Dockerfile" + echo 'LABEL com.redhat.openshift.versions="v4.9-"' >> "./config/olm/${PLATFORM}/bundle-${VERSION}.Dockerfile" echo 'LABEL com.redhat.delivery.operator.bundle=true' >> "./config/olm/${PLATFORM}/bundle-${VERSION}.Dockerfile" echo 'LABEL com.redhat.delivery.backport=true' >> "./config/olm/${PLATFORM}/bundle-${VERSION}.Dockerfile" sed 's/\bkubectl\b/oc/g' "./config/olm/${PLATFORM}/${VERSION}/manifests/dynatrace-operator.v${VERSION}.clusterserviceversion.yaml" > "./config/olm/${PLATFORM}/${VERSION}/manifests/dynatrace-operator.v${VERSION}.clusterserviceversion.yaml.output" mv "./config/olm/${PLATFORM}/${VERSION}/manifests/dynatrace-operator.v${VERSION}.clusterserviceversion.yaml.output" "./config/olm/${PLATFORM}/${VERSION}/manifests/dynatrace-operator.v${VERSION}.clusterserviceversion.yaml" - echo ' com.redhat.openshift.versions: v4.9-v4.16' >> "./config/olm/${PLATFORM}/${VERSION}/metadata/annotations.yaml" + echo ' com.redhat.openshift.versions: v4.9-' >> "./config/olm/${PLATFORM}/${VERSION}/metadata/annotations.yaml" fi grep -v 'scorecard' "./config/olm/${PLATFORM}/${VERSION}/metadata/annotations.yaml" > "./config/olm/${PLATFORM}/${VERSION}/metadata/annotations.yaml.output" grep -v ' # Annotations for testing.' "./config/olm/${PLATFORM}/${VERSION}/metadata/annotations.yaml.output" > "./config/olm/${PLATFORM}/${VERSION}/metadata/annotations.yaml" diff --git a/hack/build/ci/create-manifest.sh b/hack/build/ci/create-manifest.sh index e54518cd79..dfb2422fa8 100755 --- a/hack/build/ci/create-manifest.sh +++ b/hack/build/ci/create-manifest.sh @@ -1,33 +1,31 @@ #!/bin/bash -if [ -z "$2" ] +if [ -z "$3" ] then - echo "Usage: $0 " + echo "Usage: $0 " exit 1 fi image_name=$1 image_tag=$2 -multiplatform=$3 +raw_platforms=$3 + image="${image_name}:${image_tag}" -if [ "$multiplatform" == "true" ] -then - supported_architectures=("amd64" "arm64" "ppc64le" "s390x") - images=() - echo "Creating manifest for ${supported_architectures[*]}" - - for architecture in "${supported_architectures[@]}" - do - docker pull "${image}-${architecture}" - images+=("${image}-${architecture}") - done - docker manifest create "${image}" "${images[@]}" -else - echo "Creating manifest for the AMD image " - docker pull "${image}-amd64" - docker manifest create "${image}" "${image}-amd64" -fi +platforms=($(echo "${raw_platforms}" | tr "," "\n")) + +echo "Creating manifest for ${platforms[*]}" + +images=() + +for platfrom in "${platforms[@]}" +do + docker pull "${image}-${platfrom}" + images+=("${image}-${platfrom}") +done + +docker manifest create "${image}" "${images[@]}" +docker manifest inspect "${image}" sha256=$(docker manifest push "${image}") echo "digest=${sha256}">> $GITHUB_OUTPUT diff --git a/hack/build/ci/install-cgo-dependencies.sh b/hack/build/ci/install-cgo-dependencies.sh deleted file mode 100755 index 37640cf7ba..0000000000 --- a/hack/build/ci/install-cgo-dependencies.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -sudo apt-get update -sudo apt-get install -y libdevmapper-dev libbtrfs-dev libgpgme-dev diff --git a/hack/build/ci/prepare-build-variables.sh b/hack/build/ci/prepare-build-variables.sh index b5648e9492..283ee77ca8 100755 --- a/hack/build/ci/prepare-build-variables.sh +++ b/hack/build/ci/prepare-build-variables.sh @@ -15,16 +15,16 @@ createDockerImageTag() { } createDockerImageLabels() { - if [[ "${GITHUB_REF_TYPE}" != "tag" ]] && [[ ! "${GITHUB_REF_NAME}" =~ ^release-* ]]; then + if [[ "${GITHUB_REF_TYPE}" != "tag" ]] && [[ ! "${GITHUB_REF_NAME}" =~ ^release-* ]] && [[ "${GITHUB_REF_NAME}" != "main" ]]; then echo "quay.expires-after=10d" fi echo "build-date=$(date --iso-8601)" + echo "vcs-ref=${GITHUB_SHA}" } printBuildRelatedVariables() { echo "go_linker_args=${go_linker_args}" - echo "go_build_tags=${go_build_tags}" echo "docker_image_labels=${docker_image_labels}" echo "docker_image_tag=${docker_image_tag}" echo "docker_image_tag_without_prefix=${docker_image_tag#v}" @@ -34,5 +34,4 @@ printBuildRelatedVariables() { docker_image_tag=$(createDockerImageTag) docker_image_labels=$(createDockerImageLabels) go_linker_args=$(hack/build/create_go_linker_args.sh "${docker_image_tag}" "${GITHUB_SHA}") -go_build_tags=$(hack/build/create_go_build_tags.sh false) printBuildRelatedVariables >> "$GITHUB_OUTPUT" diff --git a/hack/build/ci/update-e2e-ondemand-pipeline.py b/hack/build/ci/update-e2e-ondemand-pipeline.py index 6157a800f0..f25d6d330b 100644 --- a/hack/build/ci/update-e2e-ondemand-pipeline.py +++ b/hack/build/ci/update-e2e-ondemand-pipeline.py @@ -9,7 +9,7 @@ version = "" # read versions to list with open(version_file, "r") as f: - version = f.readline().strip().replace("origin/", "") + version = [v.strip().replace("origin/", "") for v in f.readlines()][-1] yaml = YAML() yaml.width = 4096 diff --git a/hack/build/create_go_build_tags.sh b/hack/build/create_go_build_tags.sh index caf4cfaebe..f89805a2b9 100755 --- a/hack/build/create_go_build_tags.sh +++ b/hack/build/create_go_build_tags.sh @@ -7,14 +7,7 @@ fi needs_e2e_tag=$1 -go_build_tags=( - # If CGO is enabled, certain standard libraries will also use CGO, these explicitly disallow that - "osusergo" - "netgo" - - # Disables the ability to add load extensions for sqlite3, needed to statically build when using the sqlite library. We never use extensions so its good to disable it. - "sqlite_omit_load_extension" -) +go_build_tags=() if "${needs_e2e_tag}"; then # Used for enabling e2e testing code diff --git a/hack/doc/gen_e2e_features.sh b/hack/doc/gen_e2e_features.sh index b6a54a0120..c4f08bc737 100755 --- a/hack/doc/gen_e2e_features.sh +++ b/hack/doc/gen_e2e_features.sh @@ -9,10 +9,13 @@ output="" # get dirs containing doc packages doc_dir_subdirs=$(find $doc_dir -type d) +# order dirs alphabetically +doc_dir_subdirs=$(echo "$doc_dir_subdirs" | sort) + # append all gomarkdoc outputs in a single variable for dir in $doc_dir_subdirs; do if [ "$dir" != "$doc_dir" ]; then - output="${output}$(GOARCH="e2e" gomarkdoc "${dir}" | sed 's/\\//g')" + output="${output}$(GOARCH="e2e" gomarkdoc --repository.url "https://github.com/Dynatrace/dynatrace-operator" --repository.path "/" --repository.default-branch "main" "${dir}" | sed 's/\\//g')" # remove gomarkdoc footer output=$(echo "${output}" | sed '$d') fi @@ -20,7 +23,8 @@ done # remove gomarkdoc headers and add custom one output=$(echo "${output}" | sed s/"$gomarkdoc_header"//) -output="${script_header}\n${output}" +output="${script_header} +${output}" # write output to file mkdir -p $output_dir diff --git a/hack/doc/gen_feature_flags.sh b/hack/doc/gen_feature_flags.sh index 2c51c98e3d..4b75645265 100755 --- a/hack/doc/gen_feature_flags.sh +++ b/hack/doc/gen_feature_flags.sh @@ -1,7 +1,7 @@ #!/bin/sh gomarkdoc_header="" script_header="" -doc_dir="./pkg/api/v1beta3/dynakube" +doc_dir="./pkg/api/v1beta4/dynakube" output_dir="./doc/api" output_file="feature-flags.md" output="" @@ -10,14 +10,15 @@ output="" mkdir ${doc_dir}/tmp cp ${doc_dir}/feature_flags.go ${doc_dir}/tmp -output="${output}$(gomarkdoc ${doc_dir}/tmp | sed 's/\\//g')" +output="${output}$(gomarkdoc --repository.url "https://github.com/Dynatrace/dynatrace-operator" --repository.path "/" --repository.default-branch "main" ${doc_dir}/tmp | sed 's/\\//g')" # remove gomarkdoc footer output=$(echo "${output}" | sed '$d') # remove gomarkdoc headers and add custom one output=$(echo "${output}" | sed s/"$gomarkdoc_header"//) -output="${script_header}\n${output}" +output="${script_header} +${output}" # write output to file mkdir -p $output_dir diff --git a/hack/helm/google-marketplace/README.md b/hack/helm/google-marketplace/README.md deleted file mode 100644 index 65bdf5fd35..0000000000 --- a/hack/helm/google-marketplace/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Hacking - -The `upload_test.sh` script will help you test your local changes. -It grabs your current selected gcloud project, an application name and a version, builds a new deployer image with all the corresponding files in your repository and pushes it. -Afterwards it applies the application CRD (from Google) and uses mpdev (needs to be part of $PATH) to deploy that new deployer image to your current Kubernetes cluster. - -The following environment variables needs to be provided: - -* gcloud project (REGISTRY bases on this) -* APP_NAME (defaults to "dynatrace-operator") -* VERSION (tag of the docker image - defaults to "test") - -The `verify.sh` script will run will build/push the necessary container then run `mpdev verify` on it. diff --git a/hack/helm/google-marketplace/upload_test.sh b/hack/helm/google-marketplace/upload_test.sh deleted file mode 100644 index f0b640a9c7..0000000000 --- a/hack/helm/google-marketplace/upload_test.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env bash - -set -eu - -gcloud config set project dynatrace-marketplace-dev -# Set the registry to your project GCR repo. -export REGISTRY=gcr.io/$(gcloud config get-value project | tr ':' '/') -export APP_NAME=dynatrace-operator -export VERSION=test - -docker build --tag $REGISTRY/$APP_NAME/deployer:$VERSION ./.. --no-cache -docker push $REGISTRY/$APP_NAME/deployer:$VERSION - -kubectl apply -f "https://raw.githubusercontent.com/GoogleCloudPlatform/marketplace-k8s-app-tools/master/crd/app-crd.yaml" - -if kubectl get Application/dynatrace-operator -n dynatrace &> /dev/null; then - kubectl delete application dynatrace-operator -n dynatrace -fi - -kubectl create ns dynatrace -kubectl apply -k https://github.com/Dynatrace/dynatrace-operator/config/crd -mpdev /scripts/install --deployer=$REGISTRY/$APP_NAME/deployer:$VERSION --parameters='{ "name": "dynatrace-operator","namespace": "dynatrace" }' diff --git a/hack/helm/google-marketplace/verify.sh b/hack/helm/google-marketplace/verify.sh deleted file mode 100644 index 7237c1aeb5..0000000000 --- a/hack/helm/google-marketplace/verify.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash - -set -eu - -gcloud config set project dynatrace-marketplace-dev -# Set the registry to your project GCR repo. -export REGISTRY=gcr.io/$(gcloud config get-value project | tr ':' '/') -export APP_NAME=dynatrace-operator -export VERSION=test - -docker build --tag $REGISTRY/$APP_NAME/deployer:$VERSION ./.. --no-cache -docker push $REGISTRY/$APP_NAME/deployer:$VERSION - -kubectl apply -f https://github.com/Dynatrace/dynatrace-operator/releases/latest/download/dynatrace.com_dynakubes.yaml -mpdev verify --deployer=$REGISTRY/$APP_NAME/deployer:$VERSION \ No newline at end of file diff --git a/hack/make/debug/debug.mk b/hack/make/debug/debug.mk index 3bb87cec1e..04b7b39b1d 100644 --- a/hack/make/debug/debug.mk +++ b/hack/make/debug/debug.mk @@ -19,7 +19,7 @@ debug/build: ## Install image with necessary changes to deployments. debug/deploy: - DEBUG=true make deploy/helm + DEBUG=true make deploy ## Install and setup Telepresence to intercept requests to the webhook debug/telepresence/install: diff --git a/hack/make/deploy/deploy.mk b/hack/make/deploy/deploy.mk index c9b70b49c5..7c1670f509 100644 --- a/hack/make/deploy/deploy.mk +++ b/hack/make/deploy/deploy.mk @@ -1,21 +1,28 @@ ENABLE_CSI ?= true PLATFORM ?= "kubernetes" -## Deploy the operator without the csi-driver, with platform specified in % (kubernetes or openshift) -deploy/%/no-csi: +## Deploy the operator without the csi-driver +deploy/no-csi: @make ENABLE_CSI=false $(@D) -## Deploy the operator with csi-driver, with platform specified in % (kubernetes or openshift) -deploy/%: - @make PLATFORM=$(@F) $(@D) - -## Deploy the operator with csi-driver, on kubernetes +## Deploy the operator with csi-driver deploy: manifests/crd/helm - kubectl get namespace dynatrace || kubectl create namespace dynatrace - helm template dynatrace-operator config/helm/chart/default \ + helm upgrade dynatrace-operator config/helm/chart/default \ + --install \ --namespace dynatrace \ + --create-namespace \ + --atomic \ --set installCRD=true \ - --set platform=$(PLATFORM) \ --set csidriver.enabled=$(ENABLE_CSI) \ --set manifests=true \ - --set image="$(IMAGE_URI)" | kubectl apply -f - + --set image="$(IMAGE_URI)" \ + --set debug=$(DEBUG) + +## Undeploy the current operator installation +undeploy: + kubectl delete dynakube --all -n dynatrace + kubectl delete edgeconnect --all -n dynatrace + kubectl -n dynatrace wait pod --for=delete -l app.kubernetes.io/managed-by=dynatrace-operator --timeout=300s + + helm uninstall dynatrace-operator \ + --namespace dynatrace diff --git a/hack/make/deploy/gke.mk b/hack/make/deploy/gke.mk deleted file mode 100644 index d3ca978f71..0000000000 --- a/hack/make/deploy/gke.mk +++ /dev/null @@ -1,9 +0,0 @@ -## Deploys the operator using a snapshot deployer image for a standard GKE cluster -deploy/gke/deployer: - kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/application/master/deploy/kube-app-manager-aio.yaml - ./hack/gcr/deploy.sh ":snapshot${SNAPSHOT_SUFFIX}" - -## Deploys the operator using a snapshot deployer image for an autopilot GKE cluster -deploy/gke-autopilot/deployer: - kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/application/master/deploy/kube-app-manager-aio.yaml - ./hack/gcr/deploy.sh ":snapshot${SNAPSHOT_SUFFIX}" "gke-autopilot" diff --git a/hack/make/deploy/helm.mk b/hack/make/deploy/helm.mk deleted file mode 100644 index abc4aa4949..0000000000 --- a/hack/make/deploy/helm.mk +++ /dev/null @@ -1,20 +0,0 @@ - -DEBUG ?= false - -## Deploy the operator in a cluster configured in ~/.kube/config where platform and version are autodetected -deploy/helm: manifests/crd/helm - helm upgrade dynatrace-operator config/helm/chart/default \ - --install \ - --namespace dynatrace \ - --create-namespace \ - --atomic \ - --set installCRD=true \ - --set csidriver.enabled=$(ENABLE_CSI) \ - --set manifests=true \ - --set image="$(IMAGE_URI)" \ - --set debug=$(DEBUG) - -## Undeploy the operator in a cluster configured in ~/.kube/config where platform and k8s version are autodetected -undeploy/helm: - helm uninstall dynatrace-operator \ - --namespace dynatrace diff --git a/hack/make/deploy/undeploy.mk b/hack/make/deploy/undeploy.mk deleted file mode 100644 index cdd24f40bb..0000000000 --- a/hack/make/deploy/undeploy.mk +++ /dev/null @@ -1,19 +0,0 @@ -## Remove the operator without the csi-driver, with platform specified in % (kubernetes or openshift) -undeploy/%/no-csi: - @make ENABLE_CSI=false $(@D) - -## Remove the operator with csi-driver, with platform specified in % (kubernetes or openshift) -undeploy/%: - @make PLATFORM=$(@F) $(@D) - -## Remove the operator with csi-driver, on kubernetes -undeploy: manifests/crd/helm - kubectl delete dynakube --all -n dynatrace - kubectl -n dynatrace wait pod --for=delete -l app.kubernetes.io/managed-by=dynatrace-operator --timeout=300s - helm template dynatrace-operator config/helm/chart/default \ - --namespace dynatrace \ - --set installCRD=true \ - --set platform=$(PLATFORM) \ - --set csidriver.enabled=$(ENABLE_CSI) \ - --set manifests=true \ - --set image="$(IMAGE_URI)" | kubectl delete -f - diff --git a/hack/make/go.mk b/hack/make/go.mk index b2e676826d..48bddc1594 100644 --- a/hack/make/go.mk +++ b/hack/make/go.mk @@ -45,7 +45,7 @@ go/coverage: go/test ## Runs go integration test go/integration_test: - go test -ldflags="-X 'github.com/Dynatrace/dynatrace-operator/pkg/version.Commit=$(shell git rev-parse HEAD)' -X 'github.com/Dynatrace/dynatrace-operator/pkg/version.Version=$(shell git branch --show-current)'" ./cmd/integration/* + go test -ldflags="-X 'github.com/Dynatrace/dynatrace-operator/pkg/version.Commit=$(shell git rev-parse HEAD)' -X 'github.com/Dynatrace/dynatrace-operator/pkg/version.Version=$(shell git branch --show-current)'" ./test/integration/* ## creates mocks from .mockery.yaml go/gen_mocks: prerequisites/mockery diff --git a/hack/make/manifests/config.mk b/hack/make/manifests/config.mk index f148f61315..516cca18a8 100644 --- a/hack/make/manifests/config.mk +++ b/hack/make/manifests/config.mk @@ -1,4 +1,4 @@ -CRD_OPTIONS ?= "crd:crdVersions=v1,maxDescLen=300,ignoreUnexportedFields=true" +CRD_OPTIONS ?= "crd:crdVersions=v1,maxDescLen=100,ignoreUnexportedFields=true" OLM ?= false diff --git a/hack/make/manifests/kubernetes.mk b/hack/make/manifests/kubernetes.mk index 8f43b999b1..8efc6d4fe2 100644 --- a/hack/make/manifests/kubernetes.mk +++ b/hack/make/manifests/kubernetes.mk @@ -1,4 +1,4 @@ -define generate_manifest +define generate_k8s_manifest helm template dynatrace-operator config/helm/chart/default \ --namespace dynatrace \ --set csidriver.enabled=$(1) \ @@ -11,15 +11,15 @@ endef ## Generates a Kubernetes manifest including CRD and CSI driver manifests/kubernetes/csi: manifests/crd/helm - $(call generate_manifest,true,$(KUBERNETES_CSIDRIVER_YAML)) + $(call generate_k8s_manifest,true,$(KUBERNETES_CSIDRIVER_YAML)) ## Generates a Kubernetes manifest including CRD without CSI driver manifests/kubernetes/core: manifests/crd/helm - $(call generate_manifest,false,$(KUBERNETES_CORE_YAML)) + $(call generate_k8s_manifest,false,$(KUBERNETES_CORE_YAML)) ## Generates a Kubernetes manifest including CRD and CSI driver with OLM set to true manifests/kubernetes/olm: manifests/crd/helm - OLM=true $(call generate_manifest,true,$(KUBERNETES_OLM_YAML)) + OLM=true $(call generate_k8s_manifest,true,$(KUBERNETES_OLM_YAML)) ## Generates a manifest for Kubernetes including a CRD, a CSI driver deployment manifests/kubernetes: manifests/kubernetes/core manifests/kubernetes/csi diff --git a/hack/make/manifests/openshift.mk b/hack/make/manifests/openshift.mk index c3116ee03f..b7665113f8 100644 --- a/hack/make/manifests/openshift.mk +++ b/hack/make/manifests/openshift.mk @@ -1,4 +1,4 @@ -define generate_manifest +define generate_openshift_manifest helm template dynatrace-operator config/helm/chart/default \ --namespace dynatrace \ --set csidriver.enabled=$(1) \ @@ -11,15 +11,15 @@ endef ## Generates an Openshift manifest including CRD and CSI driver manifests/openshift/csi: manifests/crd/helm - $(call generate_manifest,true,$(OPENSHIFT_CSIDRIVER_YAML)) + $(call generate_openshift_manifest,true,$(OPENSHIFT_CSIDRIVER_YAML)) ## Generates a Openshift manifest including CRD without CSI driver manifests/openshift/core: manifests/crd/helm - $(call generate_manifest,false,$(OPENSHIFT_CORE_YAML)) + $(call generate_openshift_manifest,false,$(OPENSHIFT_CORE_YAML)) ## Generates an Openshift manifest including CRD and CSI driver with OLM set to true manifests/openshift/olm: manifests/crd/helm - OLM=true $(call generate_manifest,true,$(OPENSHIFT_OLM_YAML)) + OLM=true $(call generate_openshift_manifest,true,$(OPENSHIFT_OLM_YAML)) ## Generates a manifest for OpenShift including a CRD and a CSI driver deployment manifests/openshift: manifests/openshift/core manifests/openshift/csi diff --git a/hack/make/prerequisites.mk b/hack/make/prerequisites.mk index 3a041dd9ed..14f24e5455 100644 --- a/hack/make/prerequisites.mk +++ b/hack/make/prerequisites.mk @@ -1,21 +1,23 @@ #renovate depName=sigs.k8s.io/kustomize/kustomize/v5 -kustomize_version=v5.5.0 +kustomize_version=v5.6.0 #renovate depName=sigs.k8s.io/controller-tools/cmd -controller_gen_version=v0.16.5 -# renovate depName=github.com/golangci/golangci-lint -golang_ci_cmd_version=v1.62.2 +controller_gen_version=v0.17.2 +# renovate depName=github.com/golangci/golangci-lint/v2 +golang_ci_cmd_version=v2.0.2 # renovate depName=github.com/daixiang0/gci -gci_version=v0.13.5 +gci_version=v0.13.6 # renovate depName=golang.org/x/tools -golang_tools_version=v0.27.0 +golang_tools_version=v0.31.0 # renovate depName=github.com/vektra/mockery -mockery_version=v2.49.0 +mockery_version=v2.53.3 # renovate depName=github.com/igorshubovych/markdownlint-cli -markdownlint_cli_version=v0.43.0 +markdownlint_cli_version=v0.44.0 # renovate depName=github.com/helm-unittest/helm-unittest -helmunittest_version=v0.7.0 +helmunittest_version=v0.8.1 # renovate depName=github.com/princjef/gomarkdoc gomarkdoc_version=v1.1.0 +# renovate depName=github.com/CycloneDX/cyclonedx-gomod/cmd/cyclonedx-gomod +cyclonedx_gomod_version=v1.9.0 # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) ifeq (,$(shell go env GOBIN)) @@ -37,7 +39,7 @@ CONTROLLER_GEN=$(shell hack/build/command.sh controller-gen) ## Install go linters prerequisites/go-linting: prerequisites/go-deadcode - go install github.com/golangci/golangci-lint/cmd/golangci-lint@$(golang_ci_cmd_version) + go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(golang_ci_cmd_version) go install github.com/daixiang0/gci@$(gci_version) go install golang.org/x/tools/cmd/goimports@$(golang_tools_version) go install github.com/bombsimon/wsl/v4/cmd...@master @@ -81,3 +83,7 @@ prerequisites/gomarkdoc: ## Install python dependencies prerequisites/python: python3 -m venv local/.venv && source local/.venv/bin/activate && pip3 install -r hack/requirements.txt + +## Install 'cyclonedx-gomod' if it is missing +prerequisites/cyclonedx-gomod: + go install github.com/CycloneDX/cyclonedx-gomod/cmd/cyclonedx-gomod@$(cyclonedx_gomod_version) diff --git a/hack/make/release/release.mk b/hack/make/release/release.mk new file mode 100644 index 0000000000..5e6abf3a6c --- /dev/null +++ b/hack/make/release/release.mk @@ -0,0 +1,3 @@ +## Generates SBOM of binary +release/gen-sbom: prerequisites/cyclonedx-gomod + cyclonedx-gomod app -licenses -assert-licenses -json -main cmd/ -output dynatrace-operator-bin-sbom.cdx.json diff --git a/hack/make/tests/e2e.mk b/hack/make/tests/e2e.mk index 0b91e4e347..895e64ea4e 100644 --- a/hack/make/tests/e2e.mk +++ b/hack/make/tests/e2e.mk @@ -8,18 +8,20 @@ test/e2e/%/publish: test/e2e/%/debug: @make SKIPCLEANUP="--fail-fast" $(@D) -## Run standard, istio and release e2e tests +## Run standard, no-csi, istio and release e2e tests test/e2e: RC=0; \ make test/e2e/standard || RC=1; \ + make test/e2e/no-csi || RC=1; \ make test/e2e/istio || RC=1; \ make test/e2e/release || RC=1; \ exit $$RC -## Run standard, istio and release e2e tests +## Run standard, no-csi, istio and release e2e tests with /publish test/e2e-publish: RC=0; \ make test/e2e/standard/publish || RC=1; \ + make test/e2e/no-csi/publish || RC=1; \ make test/e2e/istio/publish || RC=1; \ make test/e2e/release/publish || RC=1; \ exit $$RC @@ -32,13 +34,17 @@ test/e2e/standard: manifests/crd/helm test/e2e/istio: manifests/crd/helm $(GOTESTCMD) -v -tags "$(shell ./hack/build/create_go_build_tags.sh true)" -timeout 200m -count=1 ./test/scenarios/istio -args $(SKIPCLEANUP) +## Run no-csi e2e test only +test/e2e/no-csi: manifests/crd/helm + $(GOTESTCMD) -v -tags "$(shell ./hack/build/create_go_build_tags.sh true)" -timeout 200m -count=1 ./test/scenarios/no_csi -args $(SKIPCLEANUP) + ## Run release e2e test only test/e2e/release: manifests/crd/helm $(GOTESTCMD) -v -tags "$(shell ./hack/build/create_go_build_tags.sh true)" -timeout 20m -count=1 ./test/scenarios/release -args $(SKIPCLEANUP) ## Runs ActiveGate e2e test only test/e2e/activegate: manifests/crd/helm - go test -v -tags "$(shell ./hack/build/create_go_build_tags.sh true)" -timeout 20m -count=1 ./test/scenarios/standard -args --feature "activegate" $(SKIPCLEANUP) + go test -v -tags "$(shell ./hack/build/create_go_build_tags.sh true)" -timeout 20m -count=1 ./test/scenarios/no_csi -args --feature "activegate" $(SKIPCLEANUP) ## Runs ActiveGate proxy e2e test only test/e2e/activegate/proxy: manifests/crd/helm @@ -46,7 +52,7 @@ test/e2e/activegate/proxy: manifests/crd/helm ## Runs ClassicFullStack e2e test only test/e2e/classic: manifests/crd/helm - go test -v -tags "$(shell ./hack/build/create_go_build_tags.sh true)" -timeout 20m -count=1 ./test/scenarios/standard -args --feature "classic" $(SKIPCLEANUP) + go test -v -tags "$(shell ./hack/build/create_go_build_tags.sh true)" -timeout 20m -count=1 ./test/scenarios/no_csi -args --feature "classic" $(SKIPCLEANUP) ## Runs ClassicFullStack switch mode e2e test only test/e2e/classic/switchmodes: manifests/crd/helm @@ -58,20 +64,24 @@ test/e2e/cloudnative/codemodules: manifests/crd/helm ## Runs CloudNative codemodules-with-proxy e2e test only test/e2e/cloudnative/codemodules-with-proxy: manifests/crd/helm - go test -v -tags "$(shell ./hack/build/create_go_build_tags.sh true)" -timeout 20m -count=1 ./test/scenarios/istio -args --feature "codemodules-with-proxy" $(SKIPCLEANUP) - -## Runs CloudNative codemodules-with-proxy-custom-ca e2e test only -test/e2e/cloudnative/codemodules-with-custom-ca: manifests/crd/helm - go test -v -tags "$(shell ./hack/build/create_go_build_tags.sh true)" -timeout 20m -count=1 ./test/scenarios/istio -args --feature "codemodules-with-proxy-custom-ca" $(SKIPCLEANUP) + go test -v -tags "$(shell ./hack/build/create_go_build_tags.sh true)" -timeout 20m -count=1 ./test/scenarios/istio -args --feature "codemodules-with-proxy-no-certs" $(SKIPCLEANUP) ## Runs CloudNative codemodules e2e test with proxy and AG custom certificate test/e2e/cloudnative/codemodules-with-proxy-and-ag-cert: manifests/crd/helm go test -v -tags "$(shell ./hack/build/create_go_build_tags.sh true)" -timeout 20m -count=1 ./test/scenarios/istio -args --feature "codemodules-with-proxy-and-ag-cert" $(SKIPCLEANUP) +## Runs CloudNative codemodules e2e test with proxy and automatically created AG certificate +test/e2e/cloudnative/codemodules-with-proxy-and-auto-ag-cert: manifests/crd/helm + go test -v -tags "$(shell ./hack/build/create_go_build_tags.sh true)" -timeout 20m -count=1 ./test/scenarios/istio -args --feature "codemodules-with-proxy-and-auto-ag-cert" $(SKIPCLEANUP) + ## Runs CloudNative codemodules e2e test with proxy and AG custom certificates test/e2e/cloudnative/codemodules-with-proxy-custom-ca-ag-cert: manifests/crd/helm go test -v -tags "$(shell ./hack/build/create_go_build_tags.sh true)" -timeout 20m -count=1 ./test/scenarios/istio -args --feature "codemodules-with-proxy-custom-ca-ag-cert" $(SKIPCLEANUP) +## Runs CloudNative codemodules e2e test with proxy and automatically created AG certificates +test/e2e/cloudnative/codemodules-with-proxy-custom-ca-auto-ag-cert: manifests/crd/helm + go test -v -tags "$(shell ./hack/build/create_go_build_tags.sh true)" -timeout 20m -count=1 ./test/scenarios/istio -args --feature "codemodules-with-proxy-custom-ca-auto-ag-cert" $(SKIPCLEANUP) + ## Runs CloudNative automatic injection disabled e2e test only test/e2e/cloudnative/disabledautoinjection: manifests/crd/helm go test -v -tags "$(shell ./hack/build/create_go_build_tags.sh true)" -timeout 20m -count=1 ./test/scenarios/standard -args --feature "cloudnative-disabled-auto-inject" $(SKIPCLEANUP) @@ -98,11 +108,11 @@ test/e2e/cloudnative/upgrade: manifests/crd/helm ## Runs Application Monitoring metadata-enrichment e2e test only test/e2e/applicationmonitoring/metadataenrichment: manifests/crd/helm - go test -v -tags "$(shell ./hack/build/create_go_build_tags.sh true)" -timeout 20m -count=1 ./test/scenarios/standard -args --feature "metadata-enrichment" $(SKIPCLEANUP) + go test -v -tags "$(shell ./hack/build/create_go_build_tags.sh true)" -timeout 20m -count=1 ./test/scenarios/no_csi -args --feature "metadata-enrichment" $(SKIPCLEANUP) ## Runs Application Monitoring label versio detection e2e test only test/e2e/applicationmonitoring/labelversion: manifests/crd/helm - go test -v -tags "$(shell ./hack/build/create_go_build_tags.sh true)" -timeout 20m -count=1 ./test/scenarios/standard -args --feature "label-version" $(SKIPCLEANUP) + go test -v -tags "$(shell ./hack/build/create_go_build_tags.sh true)" -timeout 20m -count=1 ./test/scenarios/no_csi -args --feature "label-version" $(SKIPCLEANUP) ## Runs Application Monitoring readonly csi-volume e2e test only test/e2e/applicationmonitoring/readonlycsivolume: manifests/crd/helm @@ -110,7 +120,15 @@ test/e2e/applicationmonitoring/readonlycsivolume: manifests/crd/helm ## Runs Application Monitoring without CSI e2e test only test/e2e/applicationmonitoring/withoutcsi: manifests/crd/helm - go test -v -tags "$(shell ./hack/build/create_go_build_tags.sh true)" -timeout 20m -count=1 ./test/scenarios/standard -args --feature "app-monitoring-without-csi" $(SKIPCLEANUP) + go test -v -tags "$(shell ./hack/build/create_go_build_tags.sh true)" -timeout 20m -count=1 ./test/scenarios/no_csi -args --feature "app-monitoring-without-csi" $(SKIPCLEANUP) + +## Runs Application Monitoring bootstrapper with CSI e2e test only +test/e2e/applicationmonitoring/bootstrapper-csi: manifests/crd/helm + go test -v -tags "$(shell ./hack/build/create_go_build_tags.sh true)" -timeout 20m -count=1 ./test/scenarios/standard -args --feature "node-image-pull-with-csi" $(SKIPCLEANUP) + +## Runs Application Monitoring bootstrapper with no CSI e2e test only +test/e2e/applicationmonitoring/bootstrapper-no-csi: manifests/crd/helm + go test -v -tags "$(shell ./hack/build/create_go_build_tags.sh true)" -timeout 20m -count=1 ./test/scenarios/no_csi -args --feature "node-image-pull-with-no-csi" $(SKIPCLEANUP) ## Runs public registry images e2e test only test/e2e/publicregistry: manifests/crd/helm @@ -122,7 +140,7 @@ test/e2e/supportarchive: manifests/crd/helm ## Runs Edgeconnect e2e test only test/e2e/edgeconnect: manifests/crd/helm - go test -v -tags "$(shell ./hack/build/create_go_build_tags.sh true)" -timeout 20m -count=1 ./test/scenarios/standard -args --feature "edgeconnect-.*" $(SKIPCLEANUP) + go test -v -tags "$(shell ./hack/build/create_go_build_tags.sh true)" -timeout 20m -count=1 ./test/scenarios/no_csi -args --feature "edgeconnect-.*" $(SKIPCLEANUP) ## Runs e2e tests on gke-autopilot test/e2e/gke-autopilot: manifests/crd/helm @@ -130,4 +148,32 @@ test/e2e/gke-autopilot: manifests/crd/helm ## Runs extensions related e2e tests test/e2e/extensions: manifests/crd/helm - go test -v -tags "$(shell ./hack/build/create_go_build_tags.sh true)" -timeout 20m -count=1 ./test/scenarios/standard -args --feature "extensions-components-rollout" $(SKIPCLEANUP) + go test -v -tags "$(shell ./hack/build/create_go_build_tags.sh true)" -timeout 20m -count=1 ./test/scenarios/no_csi -args --feature "extensions-components-rollout" $(SKIPCLEANUP) + +## Runs LogMonitoring related e2e tests +test/e2e/logmonitoring: manifests/crd/helm + go test -v -tags "$(shell ./hack/build/create_go_build_tags.sh true)" -timeout 20m -count=1 ./test/scenarios/no_csi -args --feature "logmonitoring-components-rollout" $(SKIPCLEANUP) + +## Runs Host Monitoring without CSI e2e test only +test/e2e/hostmonitoring/withoutcsi: manifests/crd/helm + go test -v -tags "$(shell ./hack/build/create_go_build_tags.sh true)" -timeout 20m -count=1 ./test/scenarios/no_csi -args --feature "host-monitoring-without-csi" $(SKIPCLEANUP) + +## Runs CloudNative default e2e test only +test/e2e/cloudnative/withoutcsi: manifests/crd/helm + go test -v -tags "$(shell ./hack/build/create_go_build_tags.sh true)" -timeout 20m -count=1 ./test/scenarios/no_csi -args --feature "cloudnative" $(SKIPCLEANUP) + +## Runs TelemetryIngest related e2e tests +test/e2e/telemetryingest: manifests/crd/helm + go test -v -tags "$(shell ./hack/build/create_go_build_tags.sh true)" -timeout 20m -count=1 ./test/scenarios/no_csi -args --feature "telemetryingest-.*" $(SKIPCLEANUP) + +test/e2e/telemetryingest/public-active-gate: manifests/crd/helm + go test -v -tags "$(shell ./hack/build/create_go_build_tags.sh true)" -timeout 20m -count=1 ./test/scenarios/no_csi -args --feature "telemetryingest-with-public-ag-components-rollout" $(SKIPCLEANUP) + +test/e2e/telemetryingest/local-active-gate-and-cleanup: manifests/crd/helm + go test -v -tags "$(shell ./hack/build/create_go_build_tags.sh true)" -timeout 20m -count=1 ./test/scenarios/no_csi -args --feature "telemetryingest-with-local-active-gate-component-rollout-and-cleanup-after-disable" $(SKIPCLEANUP) + +test/e2e/telemetryingest/otel-collector-endpoint-tls: manifests/crd/helm + go test -v -tags "$(shell ./hack/build/create_go_build_tags.sh true)" -timeout 20m -count=1 ./test/scenarios/no_csi -args --feature "telemetryingest-with-otel-collector-endpoint-tls" $(SKIPCLEANUP) + +test/e2e/telemetryingest/otel-collector-config-udpate: manifests/crd/helm + go test -v -tags "$(shell ./hack/build/create_go_build_tags.sh true)" -timeout 20m -count=1 ./test/scenarios/no_csi -args --feature "telemetryingest-configuration-update" $(SKIPCLEANUP) diff --git a/hack/requirements.txt b/hack/requirements.txt index ed487d3843..eb0dc739bf 100644 --- a/hack/requirements.txt +++ b/hack/requirements.txt @@ -1,4 +1,4 @@ argparse==1.4.0 pyyaml==6.0.2 -json5==0.9.28 -ruamel.yaml==0.18.6 +json5==0.11.0 +ruamel.yaml==0.18.10 diff --git a/pkg/api/consts.go b/pkg/api/consts.go index 703e048bee..25bd0dbe1b 100644 --- a/pkg/api/consts.go +++ b/pkg/api/consts.go @@ -1,10 +1,8 @@ package api const ( - LatestTag = "latest" - RawTag = "raw" - AnnotationDynatraceExtensions = "dynatrace.com/extensions" - AnnotationDynatraceOpenTelemetryCollector = "dynatrace.com/openTelemetryCollector" - AnnotationDynatraceextEnsionExecutionController = "dynatrace.com/extensionExecutionController" - InternalFlagPrefix = "internal.operator.dynatrace.com/" + LatestTag = "latest" + RawTag = "raw" + InternalFlagPrefix = "internal.operator.dynatrace.com/" + AnnotationExtensionsSecretHash = InternalFlagPrefix + "extensions-secret-hash" ) diff --git a/pkg/api/scheme/scheme.go b/pkg/api/scheme/scheme.go index 0a9b70dc25..e00efc99de 100644 --- a/pkg/api/scheme/scheme.go +++ b/pkg/api/scheme/scheme.go @@ -27,6 +27,8 @@ import ( _ "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta2/dynakube" //nolint:staticcheck "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3" _ "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4" + _ "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" istiov1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" corev1 "k8s.io/api/core/v1" apiv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" @@ -45,6 +47,7 @@ func init() { utilruntime.Must(v1beta1.AddToScheme(Scheme)) utilruntime.Must(v1beta2.AddToScheme(Scheme)) utilruntime.Must(v1beta3.AddToScheme(Scheme)) + utilruntime.Must(v1beta4.AddToScheme(Scheme)) utilruntime.Must(istiov1beta1.AddToScheme(Scheme)) utilruntime.Must(corev1.AddToScheme(Scheme)) utilruntime.Must(apiv1.AddToScheme(Scheme)) diff --git a/pkg/api/shared/communication/types.go b/pkg/api/shared/communication/types.go index 437d762b55..6bbbbc9f80 100644 --- a/pkg/api/shared/communication/types.go +++ b/pkg/api/shared/communication/types.go @@ -14,4 +14,7 @@ type ConnectionInfo struct { // Available connection endpoints Endpoints string `json:"endpoints,omitempty"` + + // Hash of the tenant token + TenantTokenHash string `json:"tenantTokenHash,omitempty"` } diff --git a/pkg/api/v1alpha1/edgeconnect/convert_from_test.go b/pkg/api/v1alpha1/edgeconnect/convert_from_test.go index ae37af9b1d..dc5c38c5b6 100644 --- a/pkg/api/v1alpha1/edgeconnect/convert_from_test.go +++ b/pkg/api/v1alpha1/edgeconnect/convert_from_test.go @@ -10,11 +10,11 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/proxy" "github.com/Dynatrace/dynatrace-operator/pkg/api/status" "github.com/Dynatrace/dynatrace-operator/pkg/api/v1alpha2/edgeconnect" - "github.com/Dynatrace/dynatrace-operator/pkg/util/address" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" ) func TestConvertFrom(t *testing.T) { @@ -92,7 +92,7 @@ func getCurrentSpec() edgeconnect.EdgeConnectSpec { }, CustomPullSecret: "m", CaCertsRef: "n", - ServiceAccountName: address.Of("o"), + ServiceAccountName: ptr.To("o"), OAuth: edgeconnect.OAuthSpec{ ClientSecret: "p", Endpoint: "q", @@ -138,7 +138,7 @@ func getCurrentSpec() edgeconnect.EdgeConnectSpec { HostPatterns: []string{ "y", }, - AutoUpdate: address.Of(true), + AutoUpdate: ptr.To(true), } } diff --git a/pkg/api/v1alpha2/edgeconnect/edgeconnect_types_test.go b/pkg/api/v1alpha2/edgeconnect/edgeconnect_types_test.go index 2083267d4a..2a71b3088f 100644 --- a/pkg/api/v1alpha2/edgeconnect/edgeconnect_types_test.go +++ b/pkg/api/v1alpha2/edgeconnect/edgeconnect_types_test.go @@ -25,6 +25,6 @@ func TestHostMappings(t *testing.T) { To: KubernetesDefaultDNS, }, } - require.EqualValues(t, expected, got) + require.Equal(t, expected, got) }) } diff --git a/pkg/api/v1beta1/dynakube/convert_from.go b/pkg/api/v1beta1/dynakube/convert_from.go index 043e2265ea..117c119bcc 100644 --- a/pkg/api/v1beta1/dynakube/convert_from.go +++ b/pkg/api/v1beta1/dynakube/convert_from.go @@ -3,14 +3,13 @@ package dynakube import ( "strconv" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/util/address" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" "github.com/Dynatrace/dynatrace-operator/pkg/util/installconfig" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/conversion" ) -var isEnabledModules = installconfig.GetModules() - // ConvertFrom converts v1beta3 to v1beta1. func (dst *DynaKube) ConvertFrom(srcRaw conversion.Hub) error { src := srcRaw.(*dynakube.DynaKube) @@ -48,19 +47,19 @@ func (dst *DynaKube) fromOneAgentSpec(src *dynakube.DynaKube) { dst.Spec.OneAgent.HostGroup = src.Spec.OneAgent.HostGroup switch { - case src.HostMonitoringMode(): + case src.OneAgent().IsHostMonitoringMode(): dst.Spec.OneAgent.HostMonitoring = fromHostInjectSpec(*src.Spec.OneAgent.HostMonitoring) - case src.ClassicFullStackMode(): + case src.OneAgent().IsClassicFullStackMode(): dst.Spec.OneAgent.ClassicFullStack = fromHostInjectSpec(*src.Spec.OneAgent.ClassicFullStack) - case src.CloudNativeFullstackMode(): + case src.OneAgent().IsCloudNativeFullstackMode(): dst.Spec.OneAgent.CloudNativeFullStack = &CloudNativeFullStackSpec{} dst.Spec.OneAgent.CloudNativeFullStack.HostInjectSpec = *fromHostInjectSpec(src.Spec.OneAgent.CloudNativeFullStack.HostInjectSpec) dst.Spec.OneAgent.CloudNativeFullStack.AppInjectionSpec = *fromAppInjectSpec(src.Spec.OneAgent.CloudNativeFullStack.AppInjectionSpec) - case src.ApplicationMonitoringMode(): + case src.OneAgent().IsApplicationMonitoringMode(): dst.Spec.OneAgent.ApplicationMonitoring = &ApplicationMonitoringSpec{} dst.Spec.OneAgent.ApplicationMonitoring.AppInjectionSpec = *fromAppInjectSpec(src.Spec.OneAgent.ApplicationMonitoring.AppInjectionSpec) dst.Spec.OneAgent.ApplicationMonitoring.Version = src.Spec.OneAgent.ApplicationMonitoring.Version - dst.Spec.OneAgent.ApplicationMonitoring.UseCSIDriver = &isEnabledModules.CSIDriver + dst.Spec.OneAgent.ApplicationMonitoring.UseCSIDriver = ptr.To(installconfig.GetModules().CSIDriver) } } @@ -77,7 +76,7 @@ func (dst *DynaKube) fromActiveGateSpec(src *dynakube.DynaKube) { dst.Spec.ActiveGate.DNSPolicy = src.Spec.ActiveGate.DNSPolicy dst.Spec.ActiveGate.TopologySpreadConstraints = src.Spec.ActiveGate.TopologySpreadConstraints dst.Spec.ActiveGate.Resources = src.Spec.ActiveGate.Resources - dst.Spec.ActiveGate.Replicas = address.Of(src.Spec.ActiveGate.GetReplicas()) + dst.Spec.ActiveGate.Replicas = ptr.To(src.Spec.ActiveGate.GetReplicas()) for _, capability := range src.Spec.ActiveGate.Capabilities { dst.Spec.ActiveGate.Capabilities = append(dst.Spec.ActiveGate.Capabilities, CapabilityDisplayName(capability)) @@ -94,9 +93,9 @@ func (dst *DynaKube) fromActiveGateSpec(src *dynakube.DynaKube) { func (dst *DynaKube) fromMovedFields(src *dynakube.DynaKube) error { dst.Annotations[AnnotationFeatureMetadataEnrichment] = strconv.FormatBool(src.MetadataEnrichmentEnabled()) dst.Annotations[AnnotationFeatureApiRequestThreshold] = strconv.FormatInt(int64(src.GetDynatraceApiRequestThreshold()), 10) - dst.Annotations[AnnotationFeatureOneAgentSecCompProfile] = src.OneAgentSecCompProfile() + dst.Annotations[AnnotationFeatureOneAgentSecCompProfile] = src.OneAgent().GetSecCompProfile() - if selector := src.OneAgentNamespaceSelector(); selector != nil { + if selector := src.OneAgent().GetNamespaceSelector(); selector != nil { dst.Spec.NamespaceSelector = *selector } else { dst.Spec.NamespaceSelector = src.Spec.MetadataEnrichment.NamespaceSelector @@ -160,7 +159,7 @@ func (dst *DynaKube) fromActiveGateStatus(src dynakube.DynaKube) { dst.Status.ActiveGate.VersionStatus = src.Status.ActiveGate.VersionStatus } -func fromHostInjectSpec(src dynakube.HostInjectSpec) *HostInjectSpec { +func fromHostInjectSpec(src oneagent.HostInjectSpec) *HostInjectSpec { dst := &HostInjectSpec{} dst.AutoUpdate = src.AutoUpdate dst.OneAgentResources = src.OneAgentResources @@ -178,7 +177,7 @@ func fromHostInjectSpec(src dynakube.HostInjectSpec) *HostInjectSpec { return dst } -func fromAppInjectSpec(src dynakube.AppInjectionSpec) *AppInjectionSpec { +func fromAppInjectSpec(src oneagent.AppInjectionSpec) *AppInjectionSpec { dst := &AppInjectionSpec{} dst.CodeModulesImage = src.CodeModulesImage diff --git a/pkg/api/v1beta1/dynakube/convert_from_test.go b/pkg/api/v1beta1/dynakube/convert_from_test.go index 025c41d02e..7379c2d4ec 100644 --- a/pkg/api/v1beta1/dynakube/convert_from_test.go +++ b/pkg/api/v1beta1/dynakube/convert_from_test.go @@ -6,15 +6,17 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/communication" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/value" "github.com/Dynatrace/dynatrace-operator/pkg/api/status" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" - "github.com/Dynatrace/dynatrace-operator/pkg/util/address" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" + "github.com/Dynatrace/dynatrace-operator/pkg/util/installconfig" registryv1 "github.com/google/go-containerregistry/pkg/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" ) func TestConvertFrom(t *testing.T) { @@ -22,7 +24,8 @@ func TestConvertFrom(t *testing.T) { from := getNewDynakubeBase() to := DynaKube{} - to.ConvertFrom(&from) + err := to.ConvertFrom(&from) + require.NoError(t, err) compareBase(t, to, from) }) @@ -31,9 +34,15 @@ func TestConvertFrom(t *testing.T) { from := getNewDynakubeBase() hostSpec := getNewHostInjectSpec() from.Spec.OneAgent.HostMonitoring = &hostSpec + to := DynaKube{} + err := to.ConvertFrom(&from) + require.NoError(t, err) - to.ConvertFrom(&from) + assert.Nil(t, to.Spec.OneAgent.ClassicFullStack) + assert.Nil(t, to.Spec.OneAgent.CloudNativeFullStack) + assert.Nil(t, to.Spec.OneAgent.ApplicationMonitoring) + assert.Equal(t, from.Spec.OneAgent.HostGroup, to.Spec.OneAgent.HostGroup) compareHostInjectSpec(t, *to.Spec.OneAgent.HostMonitoring, *from.Spec.OneAgent.HostMonitoring) compareMovedFields(t, to, from) @@ -45,7 +54,13 @@ func TestConvertFrom(t *testing.T) { from.Spec.OneAgent.ClassicFullStack = &hostSpec to := DynaKube{} - to.ConvertFrom(&from) + err := to.ConvertFrom(&from) + require.NoError(t, err) + + assert.Nil(t, to.Spec.OneAgent.CloudNativeFullStack) + assert.Nil(t, to.Spec.OneAgent.ApplicationMonitoring) + assert.Nil(t, to.Spec.OneAgent.HostMonitoring) + assert.Equal(t, from.Spec.OneAgent.HostGroup, to.Spec.OneAgent.HostGroup) compareHostInjectSpec(t, *to.Spec.OneAgent.ClassicFullStack, *from.Spec.OneAgent.ClassicFullStack) compareMovedFields(t, to, from) @@ -57,7 +72,13 @@ func TestConvertFrom(t *testing.T) { from.Spec.OneAgent.CloudNativeFullStack = &spec to := DynaKube{} - to.ConvertFrom(&from) + err := to.ConvertFrom(&from) + require.NoError(t, err) + + assert.Nil(t, to.Spec.OneAgent.ClassicFullStack) + assert.Nil(t, to.Spec.OneAgent.ApplicationMonitoring) + assert.Nil(t, to.Spec.OneAgent.HostMonitoring) + assert.Equal(t, from.Spec.OneAgent.HostGroup, to.Spec.OneAgent.HostGroup) compareCloudNativeSpec(t, *to.Spec.OneAgent.CloudNativeFullStack, *from.Spec.OneAgent.CloudNativeFullStack) compareMovedFields(t, to, from) @@ -69,7 +90,13 @@ func TestConvertFrom(t *testing.T) { from.Spec.OneAgent.ApplicationMonitoring = &appSpec to := DynaKube{} - to.ConvertFrom(&from) + err := to.ConvertFrom(&from) + require.NoError(t, err) + + assert.Nil(t, to.Spec.OneAgent.ClassicFullStack) + assert.Nil(t, to.Spec.OneAgent.CloudNativeFullStack) + assert.Nil(t, to.Spec.OneAgent.HostMonitoring) + assert.Equal(t, from.Spec.OneAgent.HostGroup, to.Spec.OneAgent.HostGroup) compareApplicationMonitoringSpec(t, *to.Spec.OneAgent.ApplicationMonitoring, *from.Spec.OneAgent.ApplicationMonitoring) }) @@ -80,7 +107,8 @@ func TestConvertFrom(t *testing.T) { from.Spec.ActiveGate = agSpec to := DynaKube{} - to.ConvertFrom(&from) + err := to.ConvertFrom(&from) + require.NoError(t, err) compareActiveGateSpec(t, to.Spec.ActiveGate, from.Spec.ActiveGate) }) @@ -90,7 +118,8 @@ func TestConvertFrom(t *testing.T) { from.Status = getNewStatus() to := DynaKube{} - to.ConvertFrom(&from) + err := to.ConvertFrom(&from) + require.NoError(t, err) compareStatus(t, to.Status, from.Status) }) @@ -124,6 +153,7 @@ func compareBase(t *testing.T, oldDk DynaKube, newDk dynakube.DynaKube) { assert.Equal(t, oldDk.Spec.EnableIstio, newDk.Spec.EnableIstio) assert.Equal(t, oldDk.Spec.SkipCertCheck, newDk.Spec.SkipCertCheck) assert.Equal(t, oldDk.Spec.TrustedCAs, newDk.Spec.TrustedCAs) + assert.Equal(t, oldDk.Spec.NetworkZone, newDk.Spec.NetworkZone) if oldDk.Spec.Proxy != nil || newDk.Spec.Proxy != nil { // necessary so we don't explode with nil pointer when not set require.NotNil(t, oldDk.Spec.Proxy) @@ -135,7 +165,7 @@ func compareBase(t *testing.T, oldDk DynaKube, newDk dynakube.DynaKube) { func compareMovedFields(t *testing.T, oldDk DynaKube, newDk dynakube.DynaKube) { assert.Equal(t, oldDk.FeatureApiRequestThreshold(), newDk.ApiRequestThreshold()) - assert.Equal(t, oldDk.FeatureOneAgentSecCompProfile(), newDk.OneAgentSecCompProfile()) + assert.Equal(t, oldDk.FeatureOneAgentSecCompProfile(), newDk.OneAgent().GetSecCompProfile()) assert.Equal(t, !oldDk.FeatureDisableMetadataEnrichment(), newDk.MetadataEnrichmentEnabled()) assert.Equal(t, *oldDk.NamespaceSelector(), newDk.Spec.MetadataEnrichment.NamespaceSelector) @@ -143,12 +173,12 @@ func compareMovedFields(t *testing.T, oldDk DynaKube, newDk dynakube.DynaKube) { assert.Equal(t, dynakube.MountAttemptsToTimeout(oldDk.FeatureMaxFailedCsiMountAttempts()), newDk.FeatureMaxCSIRetryTimeout().String()) } - if newDk.NeedAppInjection() { - assert.Equal(t, oldDk.NamespaceSelector(), newDk.OneAgentNamespaceSelector()) + if newDk.OneAgent().IsAppInjectionNeeded() { + assert.Equal(t, oldDk.NamespaceSelector(), newDk.OneAgent().GetNamespaceSelector()) } } -func compareHostInjectSpec(t *testing.T, oldSpec HostInjectSpec, newSpec dynakube.HostInjectSpec) { +func compareHostInjectSpec(t *testing.T, oldSpec HostInjectSpec, newSpec oneagent.HostInjectSpec) { assert.Equal(t, oldSpec.Annotations, newSpec.Annotations) assert.Equal(t, oldSpec.Args, newSpec.Args) assert.Equal(t, *oldSpec.AutoUpdate, *newSpec.AutoUpdate) @@ -163,19 +193,19 @@ func compareHostInjectSpec(t *testing.T, oldSpec HostInjectSpec, newSpec dynakub assert.Equal(t, oldSpec.Version, newSpec.Version) } -func compareAppInjectionSpec(t *testing.T, oldSpec AppInjectionSpec, newSpec dynakube.AppInjectionSpec) { +func compareAppInjectionSpec(t *testing.T, oldSpec AppInjectionSpec, newSpec oneagent.AppInjectionSpec) { assert.Equal(t, oldSpec.CodeModulesImage, newSpec.CodeModulesImage) assert.Equal(t, oldSpec.InitResources, newSpec.InitResources) } -func compareCloudNativeSpec(t *testing.T, oldSpec CloudNativeFullStackSpec, newSpec dynakube.CloudNativeFullStackSpec) { +func compareCloudNativeSpec(t *testing.T, oldSpec CloudNativeFullStackSpec, newSpec oneagent.CloudNativeFullStackSpec) { compareAppInjectionSpec(t, oldSpec.AppInjectionSpec, newSpec.AppInjectionSpec) compareHostInjectSpec(t, oldSpec.HostInjectSpec, newSpec.HostInjectSpec) } -func compareApplicationMonitoringSpec(t *testing.T, oldSpec ApplicationMonitoringSpec, newSpec dynakube.ApplicationMonitoringSpec) { +func compareApplicationMonitoringSpec(t *testing.T, oldSpec ApplicationMonitoringSpec, newSpec oneagent.ApplicationMonitoringSpec) { compareAppInjectionSpec(t, oldSpec.AppInjectionSpec, newSpec.AppInjectionSpec) - assert.Equal(t, *oldSpec.UseCSIDriver, isEnabledModules.CSIDriver) + assert.Equal(t, *oldSpec.UseCSIDriver, installconfig.GetModules().CSIDriver) assert.Equal(t, oldSpec.Version, newSpec.Version) } @@ -270,23 +300,26 @@ func getNewDynakubeBase() dynakube.DynaKube { }, TrustedCAs: "trusted-ca", NetworkZone: "network-zone", - DynatraceApiRequestThreshold: address.Of(uint16(42)), + DynatraceApiRequestThreshold: ptr.To(uint16(42)), MetadataEnrichment: dynakube.MetadataEnrichment{ - Enabled: address.Of(true), + Enabled: ptr.To(true), NamespaceSelector: getTestNamespaceSelector(), }, + OneAgent: oneagent.Spec{ + HostGroup: "host-group", + }, }, } } -func getNewHostInjectSpec() dynakube.HostInjectSpec { - return dynakube.HostInjectSpec{ +func getNewHostInjectSpec() oneagent.HostInjectSpec { + return oneagent.HostInjectSpec{ Version: "host-inject-version", Image: "host-inject-image", Tolerations: []corev1.Toleration{ {Key: "host-inject-toleration-key", Operator: "In", Value: "host-inject-toleration-value"}, }, - AutoUpdate: address.Of(false), + AutoUpdate: ptr.To(false), DNSPolicy: corev1.DNSClusterFirstWithHostNet, Annotations: map[string]string{ "host-inject-annotation-key": "host-inject-annotation-value", @@ -322,8 +355,8 @@ func getNewHostInjectSpec() dynakube.HostInjectSpec { } } -func getNewAppInjectionSpec() dynakube.AppInjectionSpec { - return dynakube.AppInjectionSpec{ +func getNewAppInjectionSpec() oneagent.AppInjectionSpec { + return oneagent.AppInjectionSpec{ InitResources: &corev1.ResourceRequirements{ Limits: corev1.ResourceList{ corev1.ResourceCPU: *resource.NewScaledQuantity(2, 1)}, @@ -333,15 +366,15 @@ func getNewAppInjectionSpec() dynakube.AppInjectionSpec { } } -func getNewCloudNativeSpec() dynakube.CloudNativeFullStackSpec { - return dynakube.CloudNativeFullStackSpec{ +func getNewCloudNativeSpec() oneagent.CloudNativeFullStackSpec { + return oneagent.CloudNativeFullStackSpec{ AppInjectionSpec: getNewAppInjectionSpec(), HostInjectSpec: getNewHostInjectSpec(), } } -func getNewApplicationMonitoringSpec() dynakube.ApplicationMonitoringSpec { - return dynakube.ApplicationMonitoringSpec{ +func getNewApplicationMonitoringSpec() oneagent.ApplicationMonitoringSpec { + return oneagent.ApplicationMonitoringSpec{ AppInjectionSpec: getNewAppInjectionSpec(), Version: "app-monitoring-version", } @@ -380,7 +413,7 @@ func getNewActiveGateSpec() activegate.Spec { "activegate-node-selector-key": "activegate-node-selector-value", }, Image: "activegate-image", - Replicas: address.Of(int32(42)), + Replicas: ptr.To(int32(42)), Group: "activegate-group", CustomProperties: &value.Source{ Value: "activegate-cp-value", @@ -402,7 +435,7 @@ func getNewActiveGateSpec() activegate.Spec { func getNewStatus() dynakube.DynaKubeStatus { return dynakube.DynaKubeStatus{ - OneAgent: dynakube.OneAgentStatus{ + OneAgent: oneagent.Status{ VersionStatus: status.VersionStatus{ ImageID: "oa-image-id", Version: "oa-version", @@ -410,7 +443,7 @@ func getNewStatus() dynakube.DynaKubeStatus { Source: status.CustomImageVersionSource, LastProbeTimestamp: &testTime, }, - Instances: map[string]dynakube.OneAgentInstance{ + Instances: map[string]oneagent.Instance{ "oa-instance-key-1": { PodName: "oa-instance-pod-1", IPAddress: "oa-instance-ip-1", @@ -424,13 +457,13 @@ func getNewStatus() dynakube.DynaKubeStatus { Healthcheck: ®istryv1.HealthConfig{ Test: []string{"oa-health-check-test"}, }, - ConnectionInfoStatus: dynakube.OneAgentConnectionInfoStatus{ + ConnectionInfoStatus: oneagent.ConnectionInfoStatus{ ConnectionInfo: communication.ConnectionInfo{ LastRequest: testTime, TenantUUID: "oa-tenant-uuid", Endpoints: "oa-endpoints", }, - CommunicationHosts: []dynakube.CommunicationHostStatus{ + CommunicationHosts: []oneagent.CommunicationHostStatus{ { Protocol: "oa-protocol-1", Host: "oa-host-1", @@ -453,7 +486,7 @@ func getNewStatus() dynakube.DynaKubeStatus { LastProbeTimestamp: &testTime, }, }, - CodeModules: dynakube.CodeModulesStatus{ + CodeModules: oneagent.CodeModulesStatus{ VersionStatus: status.VersionStatus{ ImageID: "cm-image-id", Version: "cm-version", diff --git a/pkg/api/v1beta1/dynakube/convert_to.go b/pkg/api/v1beta1/dynakube/convert_to.go index de9411a5ef..e9c0a3438b 100644 --- a/pkg/api/v1beta1/dynakube/convert_to.go +++ b/pkg/api/v1beta1/dynakube/convert_to.go @@ -6,9 +6,10 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/communication" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/value" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" - "github.com/Dynatrace/dynatrace-operator/pkg/util/address" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/conversion" ) @@ -61,11 +62,11 @@ func (src *DynaKube) toOneAgentSpec(dst *dynakube.DynaKube) { case src.ClassicFullStackMode(): dst.Spec.OneAgent.ClassicFullStack = toHostInjectSpec(*src.Spec.OneAgent.ClassicFullStack) case src.CloudNativeFullstackMode(): - dst.Spec.OneAgent.CloudNativeFullStack = &dynakube.CloudNativeFullStackSpec{} + dst.Spec.OneAgent.CloudNativeFullStack = &oneagent.CloudNativeFullStackSpec{} dst.Spec.OneAgent.CloudNativeFullStack.HostInjectSpec = *toHostInjectSpec(src.Spec.OneAgent.CloudNativeFullStack.HostInjectSpec) dst.Spec.OneAgent.CloudNativeFullStack.AppInjectionSpec = *toAppInjectSpec(src.Spec.OneAgent.CloudNativeFullStack.AppInjectionSpec) case src.ApplicationMonitoringMode(): - dst.Spec.OneAgent.ApplicationMonitoring = &dynakube.ApplicationMonitoringSpec{} + dst.Spec.OneAgent.ApplicationMonitoring = &oneagent.ApplicationMonitoringSpec{} dst.Spec.OneAgent.ApplicationMonitoring.AppInjectionSpec = *toAppInjectSpec(src.Spec.OneAgent.ApplicationMonitoring.AppInjectionSpec) dst.Spec.OneAgent.ApplicationMonitoring.Version = src.Spec.OneAgent.ApplicationMonitoring.Version } @@ -102,10 +103,10 @@ func (src *DynaKube) toActiveGateSpec(dst *dynakube.DynaKube) { func (src *DynaKube) toMovedFields(dst *dynakube.DynaKube) error { if src.Annotations[AnnotationFeatureMetadataEnrichment] == "false" || !src.NeedAppInjection() { - dst.Spec.MetadataEnrichment = dynakube.MetadataEnrichment{Enabled: address.Of(false)} + dst.Spec.MetadataEnrichment = dynakube.MetadataEnrichment{Enabled: ptr.To(false)} delete(dst.Annotations, AnnotationFeatureMetadataEnrichment) } else { - dst.Spec.MetadataEnrichment = dynakube.MetadataEnrichment{Enabled: address.Of(true)} + dst.Spec.MetadataEnrichment = dynakube.MetadataEnrichment{Enabled: ptr.To(true)} delete(dst.Annotations, AnnotationFeatureMetadataEnrichment) } @@ -149,10 +150,10 @@ func (src *DynaKube) convertDynatraceApiRequestThreshold(dst *dynakube.DynaKube) if duration >= 0 { if math.MaxUint16 < duration { - dst.Spec.DynatraceApiRequestThreshold = address.Of(uint16(math.MaxUint16)) + dst.Spec.DynatraceApiRequestThreshold = ptr.To(uint16(math.MaxUint16)) } else { // linting disabled, handled in if - dst.Spec.DynatraceApiRequestThreshold = address.Of(uint16(duration)) //nolint:gosec + dst.Spec.DynatraceApiRequestThreshold = ptr.To(uint16(duration)) //nolint:gosec } } @@ -165,7 +166,7 @@ func (src *DynaKube) convertDynatraceApiRequestThreshold(dst *dynakube.DynaKube) func (src *DynaKube) toStatus(dst *dynakube.DynaKube) { src.toOneAgentStatus(dst) src.toActiveGateStatus(dst) - dst.Status.CodeModules = dynakube.CodeModulesStatus{ + dst.Status.CodeModules = oneagent.CodeModulesStatus{ VersionStatus: src.Status.CodeModules.VersionStatus, } @@ -180,11 +181,11 @@ func (src *DynaKube) toStatus(dst *dynakube.DynaKube) { } func (src *DynaKube) toOneAgentStatus(dst *dynakube.DynaKube) { - dst.Status.OneAgent.Instances = map[string]dynakube.OneAgentInstance{} + dst.Status.OneAgent.Instances = map[string]oneagent.Instance{} // Instance for key, instance := range src.Status.OneAgent.Instances { - tmp := dynakube.OneAgentInstance{ + tmp := oneagent.Instance{ PodName: instance.PodName, IPAddress: instance.IPAddress, } @@ -197,7 +198,7 @@ func (src *DynaKube) toOneAgentStatus(dst *dynakube.DynaKube) { dst.Status.OneAgent.ConnectionInfoStatus.ConnectionInfo = (communication.ConnectionInfo)(src.Status.OneAgent.ConnectionInfoStatus.ConnectionInfoStatus) for _, host := range src.Status.OneAgent.ConnectionInfoStatus.CommunicationHosts { - tmp := dynakube.CommunicationHostStatus{ + tmp := oneagent.CommunicationHostStatus{ Host: host.Host, Port: host.Port, Protocol: host.Protocol, @@ -216,8 +217,8 @@ func (src *DynaKube) toActiveGateStatus(dst *dynakube.DynaKube) { dst.Status.ActiveGate.VersionStatus = src.Status.ActiveGate.VersionStatus } -func toHostInjectSpec(src HostInjectSpec) *dynakube.HostInjectSpec { - dst := &dynakube.HostInjectSpec{} +func toHostInjectSpec(src HostInjectSpec) *oneagent.HostInjectSpec { + dst := &oneagent.HostInjectSpec{} dst.AutoUpdate = src.AutoUpdate dst.OneAgentResources = src.OneAgentResources dst.Args = src.Args @@ -234,8 +235,8 @@ func toHostInjectSpec(src HostInjectSpec) *dynakube.HostInjectSpec { return dst } -func toAppInjectSpec(src AppInjectionSpec) *dynakube.AppInjectionSpec { - dst := &dynakube.AppInjectionSpec{} +func toAppInjectSpec(src AppInjectionSpec) *oneagent.AppInjectionSpec { + dst := &oneagent.AppInjectionSpec{} dst.CodeModulesImage = src.CodeModulesImage dst.InitResources = src.InitResources diff --git a/pkg/api/v1beta1/dynakube/convert_to_test.go b/pkg/api/v1beta1/dynakube/convert_to_test.go index 5d1cc4a98c..14ee1a3248 100644 --- a/pkg/api/v1beta1/dynakube/convert_to_test.go +++ b/pkg/api/v1beta1/dynakube/convert_to_test.go @@ -4,13 +4,13 @@ import ( "testing" "github.com/Dynatrace/dynatrace-operator/pkg/api/status" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/util/address" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" registryv1 "github.com/google/go-containerregistry/pkg/v1" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" ) var testTime = metav1.Now() @@ -172,7 +172,7 @@ func getOldHostInjectSpec() HostInjectSpec { Tolerations: []corev1.Toleration{ {Key: "host-inject-toleration-key", Operator: "In", Value: "host-inject-toleration-value"}, }, - AutoUpdate: address.Of(false), + AutoUpdate: ptr.To(false), DNSPolicy: corev1.DNSClusterFirstWithHostNet, Annotations: map[string]string{ "host-inject-annotation-key": "host-inject-annotation-value", @@ -227,7 +227,7 @@ func getOldCloudNativeSpec() CloudNativeFullStackSpec { func getOldApplicationMonitoringSpec() ApplicationMonitoringSpec { return ApplicationMonitoringSpec{ AppInjectionSpec: getOldAppInjectionSpec(), - UseCSIDriver: address.Of(true), + UseCSIDriver: ptr.To(true), Version: "app-monitoring-version", } } @@ -265,7 +265,7 @@ func getOldActiveGateSpec() ActiveGateSpec { "activegate-node-selector-key": "activegate-node-selector-value", }, Image: "activegate-image", - Replicas: address.Of(int32(42)), + Replicas: ptr.To(int32(42)), Group: "activegate-group", CustomProperties: &DynaKubeValueSource{ Value: "activegate-cp-value", diff --git a/pkg/api/v1beta1/dynakube/dynakube_status.go b/pkg/api/v1beta1/dynakube/dynakube_status.go index dc43c4a84b..f1accb4f05 100644 --- a/pkg/api/v1beta1/dynakube/dynakube_status.go +++ b/pkg/api/v1beta1/dynakube/dynakube_status.go @@ -61,6 +61,9 @@ type ConnectionInfoStatus struct { // Available connection endpoints Endpoints string `json:"endpoints,omitempty"` + + // Hash of the tenant token + TenantTokenHash string `json:"tenantTokenHash,omitempty"` } type OneAgentConnectionInfoStatus struct { diff --git a/pkg/api/v1beta2/dynakube/convert_from.go b/pkg/api/v1beta2/dynakube/convert_from.go index a866099c25..f46c3dbc3d 100644 --- a/pkg/api/v1beta2/dynakube/convert_from.go +++ b/pkg/api/v1beta2/dynakube/convert_from.go @@ -1,16 +1,15 @@ package dynakube import ( - v1beta3 "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" "github.com/Dynatrace/dynatrace-operator/pkg/util/installconfig" "sigs.k8s.io/controller-runtime/pkg/conversion" ) -var isEnabledModules = installconfig.GetModules() - // ConvertFrom converts from the Hub version (v1beta3) to this version (v1beta3). func (dst *DynaKube) ConvertFrom(srcRaw conversion.Hub) error { - src := srcRaw.(*v1beta3.DynaKube) + src := srcRaw.(*dynakube.DynaKube) dst.fromBase(src) dst.fromOneAgentSpec(src) @@ -21,7 +20,7 @@ func (dst *DynaKube) ConvertFrom(srcRaw conversion.Hub) error { return nil } -func (dst *DynaKube) fromBase(src *v1beta3.DynaKube) { +func (dst *DynaKube) fromBase(src *dynakube.DynaKube) { if src.Annotations == nil { src.Annotations = map[string]string{} } @@ -39,27 +38,27 @@ func (dst *DynaKube) fromBase(src *v1beta3.DynaKube) { dst.Spec.DynatraceApiRequestThreshold = int(src.GetDynatraceApiRequestThreshold()) } -func (dst *DynaKube) fromOneAgentSpec(src *v1beta3.DynaKube) { +func (dst *DynaKube) fromOneAgentSpec(src *dynakube.DynaKube) { dst.Spec.OneAgent.HostGroup = src.Spec.OneAgent.HostGroup switch { - case src.HostMonitoringMode(): + case src.OneAgent().IsHostMonitoringMode(): dst.Spec.OneAgent.HostMonitoring = fromHostInjectSpec(*src.Spec.OneAgent.HostMonitoring) - case src.ClassicFullStackMode(): + case src.OneAgent().IsClassicFullStackMode(): dst.Spec.OneAgent.ClassicFullStack = fromHostInjectSpec(*src.Spec.OneAgent.ClassicFullStack) - case src.CloudNativeFullstackMode(): + case src.OneAgent().IsCloudNativeFullstackMode(): dst.Spec.OneAgent.CloudNativeFullStack = &CloudNativeFullStackSpec{} dst.Spec.OneAgent.CloudNativeFullStack.HostInjectSpec = *fromHostInjectSpec(src.Spec.OneAgent.CloudNativeFullStack.HostInjectSpec) dst.Spec.OneAgent.CloudNativeFullStack.AppInjectionSpec = *fromAppInjectSpec(src.Spec.OneAgent.CloudNativeFullStack.AppInjectionSpec) - case src.ApplicationMonitoringMode(): + case src.OneAgent().IsApplicationMonitoringMode(): dst.Spec.OneAgent.ApplicationMonitoring = &ApplicationMonitoringSpec{} dst.Spec.OneAgent.ApplicationMonitoring.AppInjectionSpec = *fromAppInjectSpec(src.Spec.OneAgent.ApplicationMonitoring.AppInjectionSpec) dst.Spec.OneAgent.ApplicationMonitoring.Version = src.Spec.OneAgent.ApplicationMonitoring.Version - dst.Spec.OneAgent.ApplicationMonitoring.UseCSIDriver = isEnabledModules.CSIDriver + dst.Spec.OneAgent.ApplicationMonitoring.UseCSIDriver = installconfig.GetModules().CSIDriver } } -func (dst *DynaKube) fromActiveGateSpec(src *v1beta3.DynaKube) { +func (dst *DynaKube) fromActiveGateSpec(src *dynakube.DynaKube) { dst.Spec.ActiveGate.Image = src.Spec.ActiveGate.Image dst.Spec.ActiveGate.PriorityClassName = src.Spec.ActiveGate.PriorityClassName dst.Spec.ActiveGate.TlsSecretName = src.Spec.ActiveGate.TlsSecretName @@ -87,7 +86,7 @@ func (dst *DynaKube) fromActiveGateSpec(src *v1beta3.DynaKube) { } } -func (dst *DynaKube) fromStatus(src *v1beta3.DynaKube) { +func (dst *DynaKube) fromStatus(src *dynakube.DynaKube) { dst.fromOneAgentStatus(*src) dst.fromActiveGateStatus(*src) dst.Status.CodeModules = CodeModulesStatus{ @@ -106,7 +105,7 @@ func (dst *DynaKube) fromStatus(src *v1beta3.DynaKube) { dst.Status.KubernetesClusterName = src.Status.KubernetesClusterName } -func (dst *DynaKube) fromOneAgentStatus(src v1beta3.DynaKube) { +func (dst *DynaKube) fromOneAgentStatus(src dynakube.DynaKube) { dst.Status.OneAgent.Instances = map[string]OneAgentInstance{} // Instance @@ -137,13 +136,13 @@ func (dst *DynaKube) fromOneAgentStatus(src v1beta3.DynaKube) { dst.Status.OneAgent.Healthcheck = src.Status.OneAgent.Healthcheck } -func (dst *DynaKube) fromActiveGateStatus(src v1beta3.DynaKube) { +func (dst *DynaKube) fromActiveGateStatus(src dynakube.DynaKube) { dst.Status.ActiveGate.ConnectionInfoStatus.ConnectionInfoStatus = (ConnectionInfoStatus)(src.Status.ActiveGate.ConnectionInfo) dst.Status.ActiveGate.ServiceIPs = src.Status.ActiveGate.ServiceIPs dst.Status.ActiveGate.VersionStatus = src.Status.ActiveGate.VersionStatus } -func fromHostInjectSpec(src v1beta3.HostInjectSpec) *HostInjectSpec { +func fromHostInjectSpec(src oneagent.HostInjectSpec) *HostInjectSpec { dst := &HostInjectSpec{} dst.AutoUpdate = src.AutoUpdate == nil || *src.AutoUpdate dst.OneAgentResources = src.OneAgentResources @@ -162,7 +161,7 @@ func fromHostInjectSpec(src v1beta3.HostInjectSpec) *HostInjectSpec { return dst } -func fromAppInjectSpec(src v1beta3.AppInjectionSpec) *AppInjectionSpec { +func fromAppInjectSpec(src oneagent.AppInjectionSpec) *AppInjectionSpec { dst := &AppInjectionSpec{} dst.CodeModulesImage = src.CodeModulesImage @@ -172,7 +171,7 @@ func fromAppInjectSpec(src v1beta3.AppInjectionSpec) *AppInjectionSpec { return dst } -func (dst *DynaKube) fromMetadataEnrichment(src *v1beta3.DynaKube) { +func (dst *DynaKube) fromMetadataEnrichment(src *dynakube.DynaKube) { dst.Spec.MetadataEnrichment.Enabled = src.MetadataEnrichmentEnabled() dst.Spec.MetadataEnrichment.NamespaceSelector = src.Spec.MetadataEnrichment.NamespaceSelector } diff --git a/pkg/api/v1beta2/dynakube/convert_from_test.go b/pkg/api/v1beta2/dynakube/convert_from_test.go index 50ea8678e4..591823c837 100644 --- a/pkg/api/v1beta2/dynakube/convert_from_test.go +++ b/pkg/api/v1beta2/dynakube/convert_from_test.go @@ -6,91 +6,120 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/communication" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/value" "github.com/Dynatrace/dynatrace-operator/pkg/api/status" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" - "github.com/Dynatrace/dynatrace-operator/pkg/util/address" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" + "github.com/Dynatrace/dynatrace-operator/pkg/util/installconfig" registryv1 "github.com/google/go-containerregistry/pkg/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" ) func TestConvertFrom(t *testing.T) { - t.Run("migrate base from v1beta3 to v1beta2", func(t *testing.T) { + t.Run("migrate base from v1beta4 to v1beta2", func(t *testing.T) { from := getNewDynakubeBase() to := DynaKube{} - to.ConvertFrom(&from) + err := to.ConvertFrom(&from) + require.NoError(t, err) compareBase(t, to, from) }) - t.Run("migrate host-monitoring from v1beta3 to v1beta2", func(t *testing.T) { + t.Run("migrate host-monitoring from v1beta4 to v1beta2", func(t *testing.T) { from := getNewDynakubeBase() hostSpec := getNewHostInjectSpec() from.Spec.OneAgent.HostMonitoring = &hostSpec to := DynaKube{} - to.ConvertFrom(&from) + err := to.ConvertFrom(&from) + require.NoError(t, err) + + assert.Nil(t, to.Spec.OneAgent.ClassicFullStack) + assert.Nil(t, to.Spec.OneAgent.CloudNativeFullStack) + assert.Nil(t, to.Spec.OneAgent.ApplicationMonitoring) + assert.Equal(t, from.Spec.OneAgent.HostGroup, to.Spec.OneAgent.HostGroup) compareHostInjectSpec(t, *to.Spec.OneAgent.HostMonitoring, *from.Spec.OneAgent.HostMonitoring) compareBase(t, to, from) }) - t.Run("migrate classic-fullstack from v1beta3 to v1beta2", func(t *testing.T) { + t.Run("migrate classic-fullstack from v1beta4 to v1beta2", func(t *testing.T) { from := getNewDynakubeBase() hostSpec := getNewHostInjectSpec() from.Spec.OneAgent.ClassicFullStack = &hostSpec to := DynaKube{} - to.ConvertFrom(&from) + err := to.ConvertFrom(&from) + require.NoError(t, err) + + assert.Nil(t, to.Spec.OneAgent.CloudNativeFullStack) + assert.Nil(t, to.Spec.OneAgent.ApplicationMonitoring) + assert.Nil(t, to.Spec.OneAgent.HostMonitoring) + assert.Equal(t, from.Spec.OneAgent.HostGroup, to.Spec.OneAgent.HostGroup) compareHostInjectSpec(t, *to.Spec.OneAgent.ClassicFullStack, *from.Spec.OneAgent.ClassicFullStack) compareBase(t, to, from) }) - t.Run("migrate cloud-native from v1beta3 to v1beta2", func(t *testing.T) { + t.Run("migrate cloud-native from v1beta4 to v1beta2", func(t *testing.T) { from := getNewDynakubeBase() spec := getNewCloudNativeSpec() from.Spec.OneAgent.CloudNativeFullStack = &spec to := DynaKube{} - to.ConvertFrom(&from) + err := to.ConvertFrom(&from) + require.NoError(t, err) + + assert.Nil(t, to.Spec.OneAgent.ClassicFullStack) + assert.Nil(t, to.Spec.OneAgent.ApplicationMonitoring) + assert.Nil(t, to.Spec.OneAgent.HostMonitoring) + assert.Equal(t, from.Spec.OneAgent.HostGroup, to.Spec.OneAgent.HostGroup) compareCloudNativeSpec(t, *to.Spec.OneAgent.CloudNativeFullStack, *from.Spec.OneAgent.CloudNativeFullStack) compareBase(t, to, from) }) - t.Run("migrate application-monitoring from v1beta3 to v1beta2", func(t *testing.T) { + t.Run("migrate application-monitoring from v1beta4 to v1beta2", func(t *testing.T) { from := getNewDynakubeBase() appSpec := getNewApplicationMonitoringSpec() from.Spec.OneAgent.ApplicationMonitoring = &appSpec to := DynaKube{} - to.ConvertFrom(&from) + err := to.ConvertFrom(&from) + require.NoError(t, err) + + assert.Nil(t, to.Spec.OneAgent.ClassicFullStack) + assert.Nil(t, to.Spec.OneAgent.CloudNativeFullStack) + assert.Nil(t, to.Spec.OneAgent.HostMonitoring) + assert.Equal(t, from.Spec.OneAgent.HostGroup, to.Spec.OneAgent.HostGroup) compareApplicationMonitoringSpec(t, *to.Spec.OneAgent.ApplicationMonitoring, *from.Spec.OneAgent.ApplicationMonitoring) }) - t.Run("migrate activegate from v1beta3 to v1beta2", func(t *testing.T) { + t.Run("migrate activegate from v1beta4 to v1beta2", func(t *testing.T) { from := getNewDynakubeBase() agSpec := getNewActiveGateSpec() from.Spec.ActiveGate = agSpec to := DynaKube{} - to.ConvertFrom(&from) + err := to.ConvertFrom(&from) + require.NoError(t, err) compareActiveGateSpec(t, to.Spec.ActiveGate, from.Spec.ActiveGate) }) - t.Run("migrate status from v1beta3 to v1beta2", func(t *testing.T) { + t.Run("migrate status from v1beta4 to v1beta2", func(t *testing.T) { from := getNewDynakubeBase() from.Status = getNewStatus() to := DynaKube{} - to.ConvertFrom(&from) + err := to.ConvertFrom(&from) + require.NoError(t, err) compareStatus(t, to.Status, from.Status) }) @@ -118,9 +147,10 @@ func compareBase(t *testing.T, oldDk DynaKube, newDk dynakube.DynaKube) { assert.Equal(t, oldDk.Spec.SkipCertCheck, newDk.Spec.SkipCertCheck) assert.Equal(t, oldDk.Spec.TrustedCAs, newDk.Spec.TrustedCAs) assert.Equal(t, uint16(oldDk.Spec.DynatraceApiRequestThreshold), *newDk.Spec.DynatraceApiRequestThreshold) //nolint:gosec + assert.Equal(t, oldDk.Spec.NetworkZone, newDk.Spec.NetworkZone) - if newDk.NeedAppInjection() { - assert.Equal(t, oldDk.OneAgentNamespaceSelector(), newDk.OneAgentNamespaceSelector()) + if newDk.OneAgent().IsAppInjectionNeeded() { + assert.Equal(t, oldDk.OneAgentNamespaceSelector(), newDk.OneAgent().GetNamespaceSelector()) } assert.Equal(t, oldDk.MetadataEnrichmentEnabled(), newDk.MetadataEnrichmentEnabled()) @@ -138,35 +168,36 @@ func compareBase(t *testing.T, oldDk DynaKube, newDk dynakube.DynaKube) { } } -func compareHostInjectSpec(t *testing.T, oldSpec HostInjectSpec, newSpec dynakube.HostInjectSpec) { +func compareHostInjectSpec(t *testing.T, oldSpec HostInjectSpec, newSpec oneagent.HostInjectSpec) { assert.Equal(t, oldSpec.Annotations, newSpec.Annotations) - assert.Equal(t, oldSpec.Args, newSpec.Args) - assert.Equal(t, oldSpec.AutoUpdate, *newSpec.AutoUpdate) - assert.Equal(t, oldSpec.DNSPolicy, newSpec.DNSPolicy) - assert.Equal(t, oldSpec.Env, newSpec.Env) - assert.Equal(t, oldSpec.Image, newSpec.Image) assert.Equal(t, oldSpec.Labels, newSpec.Labels) assert.Equal(t, oldSpec.NodeSelector, newSpec.NodeSelector) - assert.Equal(t, oldSpec.OneAgentResources, newSpec.OneAgentResources) - assert.Equal(t, oldSpec.PriorityClassName, newSpec.PriorityClassName) - assert.Equal(t, oldSpec.Tolerations, newSpec.Tolerations) + assert.Equal(t, oldSpec.AutoUpdate, *newSpec.AutoUpdate) assert.Equal(t, oldSpec.Version, newSpec.Version) + assert.Equal(t, oldSpec.Image, newSpec.Image) + assert.Equal(t, oldSpec.DNSPolicy, newSpec.DNSPolicy) + assert.Equal(t, oldSpec.PriorityClassName, newSpec.PriorityClassName) assert.Equal(t, oldSpec.SecCompProfile, newSpec.SecCompProfile) + assert.Equal(t, oldSpec.OneAgentResources, newSpec.OneAgentResources) + assert.Equal(t, oldSpec.Tolerations, newSpec.Tolerations) + assert.Equal(t, oldSpec.Env, newSpec.Env) + assert.Equal(t, oldSpec.Args, newSpec.Args) } -func compareAppInjectionSpec(t *testing.T, oldSpec AppInjectionSpec, newSpec dynakube.AppInjectionSpec) { +func compareAppInjectionSpec(t *testing.T, oldSpec AppInjectionSpec, newSpec oneagent.AppInjectionSpec) { assert.Equal(t, oldSpec.CodeModulesImage, newSpec.CodeModulesImage) assert.Equal(t, oldSpec.InitResources, newSpec.InitResources) + assert.Equal(t, oldSpec.NamespaceSelector, newSpec.NamespaceSelector) } -func compareCloudNativeSpec(t *testing.T, oldSpec CloudNativeFullStackSpec, newSpec dynakube.CloudNativeFullStackSpec) { +func compareCloudNativeSpec(t *testing.T, oldSpec CloudNativeFullStackSpec, newSpec oneagent.CloudNativeFullStackSpec) { compareAppInjectionSpec(t, oldSpec.AppInjectionSpec, newSpec.AppInjectionSpec) compareHostInjectSpec(t, oldSpec.HostInjectSpec, newSpec.HostInjectSpec) } -func compareApplicationMonitoringSpec(t *testing.T, oldSpec ApplicationMonitoringSpec, newSpec dynakube.ApplicationMonitoringSpec) { +func compareApplicationMonitoringSpec(t *testing.T, oldSpec ApplicationMonitoringSpec, newSpec oneagent.ApplicationMonitoringSpec) { compareAppInjectionSpec(t, oldSpec.AppInjectionSpec, newSpec.AppInjectionSpec) - assert.Equal(t, oldSpec.UseCSIDriver, isEnabledModules.CSIDriver) + assert.Equal(t, oldSpec.UseCSIDriver, installconfig.GetModules().CSIDriver) assert.Equal(t, oldSpec.Version, newSpec.Version) } @@ -261,23 +292,26 @@ func getNewDynakubeBase() dynakube.DynaKube { }, TrustedCAs: "trusted-ca", NetworkZone: "network-zone", - DynatraceApiRequestThreshold: address.Of(uint16(42)), + DynatraceApiRequestThreshold: ptr.To(uint16(42)), MetadataEnrichment: dynakube.MetadataEnrichment{ - Enabled: address.Of(true), + Enabled: ptr.To(true), NamespaceSelector: getTestNamespaceSelector(), }, + OneAgent: oneagent.Spec{ + HostGroup: "host-group", + }, }, } } -func getNewHostInjectSpec() dynakube.HostInjectSpec { - return dynakube.HostInjectSpec{ +func getNewHostInjectSpec() oneagent.HostInjectSpec { + return oneagent.HostInjectSpec{ Version: "host-inject-version", Image: "host-inject-image", Tolerations: []corev1.Toleration{ {Key: "host-inject-toleration-key", Operator: "In", Value: "host-inject-toleration-value"}, }, - AutoUpdate: address.Of(false), + AutoUpdate: ptr.To(false), DNSPolicy: corev1.DNSClusterFirstWithHostNet, Annotations: map[string]string{ "host-inject-annotation-key": "host-inject-annotation-value", @@ -313,8 +347,8 @@ func getNewHostInjectSpec() dynakube.HostInjectSpec { } } -func getNewAppInjectionSpec() dynakube.AppInjectionSpec { - return dynakube.AppInjectionSpec{ +func getNewAppInjectionSpec() oneagent.AppInjectionSpec { + return oneagent.AppInjectionSpec{ InitResources: &corev1.ResourceRequirements{ Limits: corev1.ResourceList{ corev1.ResourceCPU: *resource.NewScaledQuantity(2, 1)}, @@ -324,15 +358,15 @@ func getNewAppInjectionSpec() dynakube.AppInjectionSpec { } } -func getNewCloudNativeSpec() dynakube.CloudNativeFullStackSpec { - return dynakube.CloudNativeFullStackSpec{ +func getNewCloudNativeSpec() oneagent.CloudNativeFullStackSpec { + return oneagent.CloudNativeFullStackSpec{ AppInjectionSpec: getNewAppInjectionSpec(), HostInjectSpec: getNewHostInjectSpec(), } } -func getNewApplicationMonitoringSpec() dynakube.ApplicationMonitoringSpec { - return dynakube.ApplicationMonitoringSpec{ +func getNewApplicationMonitoringSpec() oneagent.ApplicationMonitoringSpec { + return oneagent.ApplicationMonitoringSpec{ AppInjectionSpec: getNewAppInjectionSpec(), Version: "app-monitoring-version", } @@ -371,7 +405,7 @@ func getNewActiveGateSpec() activegate.Spec { "activegate-node-selector-key": "activegate-node-selector-value", }, Image: "activegate-image", - Replicas: address.Of(int32(42)), + Replicas: ptr.To(int32(42)), Group: "activegate-group", CustomProperties: &value.Source{ Value: "activegate-cp-value", @@ -393,7 +427,7 @@ func getNewActiveGateSpec() activegate.Spec { func getNewStatus() dynakube.DynaKubeStatus { return dynakube.DynaKubeStatus{ - OneAgent: dynakube.OneAgentStatus{ + OneAgent: oneagent.Status{ VersionStatus: status.VersionStatus{ ImageID: "oa-image-id", Version: "oa-version", @@ -401,7 +435,7 @@ func getNewStatus() dynakube.DynaKubeStatus { Source: status.CustomImageVersionSource, LastProbeTimestamp: &testTime, }, - Instances: map[string]dynakube.OneAgentInstance{ + Instances: map[string]oneagent.Instance{ "oa-instance-key-1": { PodName: "oa-instance-pod-1", IPAddress: "oa-instance-ip-1", @@ -415,13 +449,13 @@ func getNewStatus() dynakube.DynaKubeStatus { Healthcheck: ®istryv1.HealthConfig{ Test: []string{"oa-health-check-test"}, }, - ConnectionInfoStatus: dynakube.OneAgentConnectionInfoStatus{ + ConnectionInfoStatus: oneagent.ConnectionInfoStatus{ ConnectionInfo: communication.ConnectionInfo{ LastRequest: testTime, TenantUUID: "oa-tenant-uuid", Endpoints: "oa-endpoints", }, - CommunicationHosts: []dynakube.CommunicationHostStatus{ + CommunicationHosts: []oneagent.CommunicationHostStatus{ { Protocol: "oa-protocol-1", Host: "oa-host-1", @@ -444,7 +478,7 @@ func getNewStatus() dynakube.DynaKubeStatus { LastProbeTimestamp: &testTime, }, }, - CodeModules: dynakube.CodeModulesStatus{ + CodeModules: oneagent.CodeModulesStatus{ VersionStatus: status.VersionStatus{ ImageID: "cm-image-id", Version: "cm-version", diff --git a/pkg/api/v1beta2/dynakube/convert_to.go b/pkg/api/v1beta2/dynakube/convert_to.go index c6515a67ae..2c36eafcf2 100644 --- a/pkg/api/v1beta2/dynakube/convert_to.go +++ b/pkg/api/v1beta2/dynakube/convert_to.go @@ -5,9 +5,10 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/communication" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/value" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" - "github.com/Dynatrace/dynatrace-operator/pkg/util/address" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/conversion" ) @@ -48,10 +49,10 @@ func (src *DynaKube) toBase(dst *dynakube.DynaKube) { func (src *DynaKube) convertDynatraceApiRequestThreshold(dst *dynakube.DynaKube) { if src.Spec.DynatraceApiRequestThreshold >= 0 { if math.MaxUint16 < src.Spec.DynatraceApiRequestThreshold { - dst.Spec.DynatraceApiRequestThreshold = address.Of(uint16(math.MaxUint16)) + dst.Spec.DynatraceApiRequestThreshold = ptr.To(uint16(math.MaxUint16)) } else { // linting disabled, handled in if - dst.Spec.DynatraceApiRequestThreshold = address.Of(uint16(src.Spec.DynatraceApiRequestThreshold)) //nolint:gosec + dst.Spec.DynatraceApiRequestThreshold = ptr.To(uint16(src.Spec.DynatraceApiRequestThreshold)) //nolint:gosec } } } @@ -72,11 +73,11 @@ func (src *DynaKube) toOneAgentSpec(dst *dynakube.DynaKube) { case src.ClassicFullStackMode(): dst.Spec.OneAgent.ClassicFullStack = toHostInjectSpec(*src.Spec.OneAgent.ClassicFullStack) case src.CloudNativeFullstackMode(): - dst.Spec.OneAgent.CloudNativeFullStack = &dynakube.CloudNativeFullStackSpec{} + dst.Spec.OneAgent.CloudNativeFullStack = &oneagent.CloudNativeFullStackSpec{} dst.Spec.OneAgent.CloudNativeFullStack.HostInjectSpec = *toHostInjectSpec(src.Spec.OneAgent.CloudNativeFullStack.HostInjectSpec) dst.Spec.OneAgent.CloudNativeFullStack.AppInjectionSpec = *toAppInjectSpec(src.Spec.OneAgent.CloudNativeFullStack.AppInjectionSpec) case src.ApplicationMonitoringMode(): - dst.Spec.OneAgent.ApplicationMonitoring = &dynakube.ApplicationMonitoringSpec{} + dst.Spec.OneAgent.ApplicationMonitoring = &oneagent.ApplicationMonitoringSpec{} dst.Spec.OneAgent.ApplicationMonitoring.AppInjectionSpec = *toAppInjectSpec(src.Spec.OneAgent.ApplicationMonitoring.AppInjectionSpec) dst.Spec.OneAgent.ApplicationMonitoring.Version = src.Spec.OneAgent.ApplicationMonitoring.Version } @@ -113,7 +114,7 @@ func (src *DynaKube) toActiveGateSpec(dst *dynakube.DynaKube) { func (src *DynaKube) toStatus(dst *dynakube.DynaKube) { src.toOneAgentStatus(dst) src.toActiveGateStatus(dst) - dst.Status.CodeModules = dynakube.CodeModulesStatus{ + dst.Status.CodeModules = oneagent.CodeModulesStatus{ VersionStatus: src.Status.CodeModules.VersionStatus, } @@ -130,11 +131,11 @@ func (src *DynaKube) toStatus(dst *dynakube.DynaKube) { } func (src *DynaKube) toOneAgentStatus(dst *dynakube.DynaKube) { - dst.Status.OneAgent.Instances = map[string]dynakube.OneAgentInstance{} + dst.Status.OneAgent.Instances = map[string]oneagent.Instance{} // Instance for key, instance := range src.Status.OneAgent.Instances { - tmp := dynakube.OneAgentInstance{ + tmp := oneagent.Instance{ PodName: instance.PodName, IPAddress: instance.IPAddress, } @@ -147,7 +148,7 @@ func (src *DynaKube) toOneAgentStatus(dst *dynakube.DynaKube) { dst.Status.OneAgent.ConnectionInfoStatus.ConnectionInfo = (communication.ConnectionInfo)(src.Status.OneAgent.ConnectionInfoStatus.ConnectionInfoStatus) for _, host := range src.Status.OneAgent.ConnectionInfoStatus.CommunicationHosts { - tmp := dynakube.CommunicationHostStatus{ + tmp := oneagent.CommunicationHostStatus{ Host: host.Host, Port: host.Port, Protocol: host.Protocol, @@ -166,8 +167,8 @@ func (src *DynaKube) toActiveGateStatus(dst *dynakube.DynaKube) { dst.Status.ActiveGate.VersionStatus = src.Status.ActiveGate.VersionStatus } -func toHostInjectSpec(src HostInjectSpec) *dynakube.HostInjectSpec { - dst := &dynakube.HostInjectSpec{} +func toHostInjectSpec(src HostInjectSpec) *oneagent.HostInjectSpec { + dst := &oneagent.HostInjectSpec{} dst.AutoUpdate = &src.AutoUpdate dst.OneAgentResources = src.OneAgentResources @@ -186,8 +187,8 @@ func toHostInjectSpec(src HostInjectSpec) *dynakube.HostInjectSpec { return dst } -func toAppInjectSpec(src AppInjectionSpec) *dynakube.AppInjectionSpec { - dst := &dynakube.AppInjectionSpec{} +func toAppInjectSpec(src AppInjectionSpec) *oneagent.AppInjectionSpec { + dst := &oneagent.AppInjectionSpec{} dst.CodeModulesImage = src.CodeModulesImage dst.InitResources = src.InitResources diff --git a/pkg/api/v1beta2/dynakube/convert_to_test.go b/pkg/api/v1beta2/dynakube/convert_to_test.go index cdcb9ef996..4231ca39d0 100644 --- a/pkg/api/v1beta2/dynakube/convert_to_test.go +++ b/pkg/api/v1beta2/dynakube/convert_to_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/Dynatrace/dynatrace-operator/pkg/api/status" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" registryv1 "github.com/google/go-containerregistry/pkg/v1" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" diff --git a/pkg/api/v1beta2/dynakube/dynakube_status.go b/pkg/api/v1beta2/dynakube/dynakube_status.go index cdb9c2d9fe..c1815173ec 100644 --- a/pkg/api/v1beta2/dynakube/dynakube_status.go +++ b/pkg/api/v1beta2/dynakube/dynakube_status.go @@ -66,6 +66,9 @@ type ConnectionInfoStatus struct { // Available connection endpoints Endpoints string `json:"endpoints,omitempty"` + + // Hash of the tenant token + TenantTokenHash string `json:"tenantTokenHash,omitempty"` } type OneAgentConnectionInfoStatus struct { diff --git a/pkg/api/v1beta3/dynakube/activegate/props.go b/pkg/api/v1beta3/dynakube/activegate/props.go index a347446494..ff51796e0b 100644 --- a/pkg/api/v1beta3/dynakube/activegate/props.go +++ b/pkg/api/v1beta3/dynakube/activegate/props.go @@ -10,6 +10,7 @@ import ( const ( TenantSecretSuffix = "-activegate-tenant-secret" + TlsSecretSuffix = "-activegate-tls-secret" ConnectionInfoConfigMapSuffix = "-activegate-connection-info" AuthTokenSecretSuffix = "-activegate-authtoken-secret" DefaultImageRegistrySubPath = "/linux/activegate" @@ -23,12 +24,12 @@ func (ag *Spec) SetName(name string) { ag.name = name } -func (ag *Spec) SetExtensionsDependency(isEnabled bool) { - ag.enabledDependencies.extensions = isEnabled +func (ag *Spec) SetAutomaticTLSCertificate(enabled bool) { + ag.automaticTLSCertificateEnabled = enabled } -func (ag *Spec) SetKSPMDependency(isEnabled bool) { - ag.enabledDependencies.kspm = isEnabled +func (ag *Spec) SetExtensionsDependency(isEnabled bool) { + ag.enabledDependencies.extensions = isEnabled } func (ag *Spec) apiUrlHost() string { @@ -92,33 +93,42 @@ func (ag *Spec) IsMetricsIngestEnabled() bool { return ag.IsMode(MetricsIngestCapability.DisplayName) } -func (ag *Spec) NeedsService() bool { - return ag.IsRoutingEnabled() || - ag.IsApiEnabled() || - ag.IsMetricsIngestEnabled() || - ag.enabledDependencies.extensions || - ag.enabledDependencies.kspm +func (ag *Spec) IsAutomaticTlsSecretEnabled() bool { + return ag.automaticTLSCertificateEnabled } func (ag *Spec) HasCaCert() bool { - return ag.IsEnabled() && ag.TlsSecretName != "" + return ag.IsEnabled() && (ag.TlsSecretName != "" || ag.IsAutomaticTlsSecretEnabled()) } -// ActivegateTenantSecret returns the name of the secret containing tenant UUID, token and communication endpoints for ActiveGate. +// GetTenantSecretName returns the name of the secret containing tenant UUID, token and communication endpoints for ActiveGate. func (ag *Spec) GetTenantSecretName() string { return ag.name + TenantSecretSuffix } -// ActiveGateAuthTokenSecret returns the name of the secret containing the ActiveGateAuthToken, which is mounted to the AGs. +// GetAuthTokenSecretName returns the name of the secret containing the ActiveGateAuthToken, which is mounted to the AGs. func (ag *Spec) GetAuthTokenSecretName() string { return ag.name + AuthTokenSecretSuffix } +// GetTLSSecretName returns the name of the AG TLS secret. +func (ag *Spec) GetTLSSecretName() string { + if ag.TlsSecretName != "" { + return ag.TlsSecretName + } + + if ag.IsAutomaticTlsSecretEnabled() { + return ag.name + TlsSecretSuffix + } + + return "" +} + func (ag *Spec) GetConnectionInfoConfigMapName() string { return ag.name + ConnectionInfoConfigMapSuffix } -// DefaultImage provides the image reference for the ActiveGate from tenant registry. +// GetDefaultImage provides the image reference for the ActiveGate from tenant registry. // Format: repo:tag. func (ag *Spec) GetDefaultImage(version string) string { apiUrlHost := ag.apiUrlHost() diff --git a/pkg/api/v1beta3/dynakube/activegate/spec.go b/pkg/api/v1beta3/dynakube/activegate/spec.go index 56b3f951de..bb95ff1a3a 100644 --- a/pkg/api/v1beta3/dynakube/activegate/spec.go +++ b/pkg/api/v1beta3/dynakube/activegate/spec.go @@ -43,6 +43,11 @@ var ( ShortName: "dynatrace-api", ArgumentName: "restInterface", } + DebuggingCapability = Capability{ + DisplayName: "debugging", + ShortName: "debugging", + ArgumentName: "debugging", + } ) var CapabilityDisplayNames = map[CapabilityDisplayName]struct{}{ @@ -50,6 +55,7 @@ var CapabilityDisplayNames = map[CapabilityDisplayName]struct{}{ KubeMonCapability.DisplayName: {}, MetricsIngestCapability.DisplayName: {}, DynatraceApiCapability.DisplayName: {}, + DebuggingCapability.DisplayName: {}, } type ActiveGate struct { @@ -60,17 +66,15 @@ type ActiveGate struct { // dependencies is a collection of possible other feature/components that need an ActiveGate, but is not directly configured in the ActiveGate section. type dependencies struct { extensions bool - kspm bool } func (d dependencies) Any() bool { - return d.extensions + return d.extensions // kspm is a dependency too, but blocked by validation webhook to not run standalone } // +kubebuilder:object:generate=true type Spec struct { - CapabilityProperties `json:",inline"` // Adds additional annotations to the ActiveGate pods // +kubebuilder:validation:Optional @@ -98,10 +102,14 @@ type Spec struct { // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Priority Class name",order=23,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:advanced","urn:alm:descriptor:io.kubernetes:PriorityClass"} PriorityClassName string `json:"priorityClassName,omitempty"` + CapabilityProperties `json:",inline"` + // Activegate capabilities enabled (routing, kubernetes-monitoring, metrics-ingest, dynatrace-api) Capabilities []CapabilityDisplayName `json:"capabilities,omitempty"` enabledDependencies dependencies + + automaticTLSCertificateEnabled bool } // +kubebuilder:object:generate=true diff --git a/pkg/api/v1beta3/dynakube/activegate/zz_generated.deepcopy.go b/pkg/api/v1beta3/dynakube/activegate/zz_generated.deepcopy.go index ee12d4da18..441afb5208 100644 --- a/pkg/api/v1beta3/dynakube/activegate/zz_generated.deepcopy.go +++ b/pkg/api/v1beta3/dynakube/activegate/zz_generated.deepcopy.go @@ -87,7 +87,6 @@ func (in *CapabilityProperties) DeepCopy() *CapabilityProperties { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Spec) DeepCopyInto(out *Spec) { *out = *in - in.CapabilityProperties.DeepCopyInto(&out.CapabilityProperties) if in.Annotations != nil { in, out := &in.Annotations, &out.Annotations *out = make(map[string]string, len(*in)) @@ -95,6 +94,7 @@ func (in *Spec) DeepCopyInto(out *Spec) { (*out)[key] = val } } + in.CapabilityProperties.DeepCopyInto(&out.CapabilityProperties) if in.Capabilities != nil { in, out := &in.Capabilities, &out.Capabilities *out = make([]CapabilityDisplayName, len(*in)) diff --git a/pkg/api/v1beta3/dynakube/activegate_props.go b/pkg/api/v1beta3/dynakube/activegate_props.go index 91347a70bd..30f9862abe 100644 --- a/pkg/api/v1beta3/dynakube/activegate_props.go +++ b/pkg/api/v1beta3/dynakube/activegate_props.go @@ -7,8 +7,8 @@ import ( func (dk *DynaKube) ActiveGate() *activegate.ActiveGate { dk.Spec.ActiveGate.SetApiUrl(dk.ApiUrl()) dk.Spec.ActiveGate.SetName(dk.Name) + dk.Spec.ActiveGate.SetAutomaticTLSCertificate(dk.FeatureActiveGateAutomaticTLSCertificate()) dk.Spec.ActiveGate.SetExtensionsDependency(dk.IsExtensionsEnabled()) - dk.Spec.ActiveGate.SetKSPMDependency(dk.KSPM().IsEnabled()) return &activegate.ActiveGate{ Spec: &dk.Spec.ActiveGate, diff --git a/pkg/api/v1beta3/dynakube/certs.go b/pkg/api/v1beta3/dynakube/certs.go index 12a1aa53d5..f6e5aa1068 100644 --- a/pkg/api/v1beta3/dynakube/certs.go +++ b/pkg/api/v1beta3/dynakube/certs.go @@ -48,7 +48,7 @@ func (dk *DynaKube) TrustedCAs(ctx context.Context, kubeReader client.Reader) ([ func (dk *DynaKube) ActiveGateTLSCert(ctx context.Context, kubeReader client.Reader) ([]byte, error) { if dk.ActiveGate().HasCaCert() { - secretName := dk.Spec.ActiveGate.TlsSecretName + secretName := dk.Spec.ActiveGate.GetTLSSecretName() var tlsSecret corev1.Secret diff --git a/pkg/api/v1beta3/dynakube/convert_from.go b/pkg/api/v1beta3/dynakube/convert_from.go new file mode 100644 index 0000000000..1322b23fcb --- /dev/null +++ b/pkg/api/v1beta3/dynakube/convert_from.go @@ -0,0 +1,293 @@ +package dynakube + +import ( + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/kspm" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/logmonitoring" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/oneagent" + dynakubev1beta4 "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + kspmv1beta4 "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/kspm" + logmonitoringv1beta4 "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/logmonitoring" + oneagentv1beta4 "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" + "sigs.k8s.io/controller-runtime/pkg/conversion" +) + +// ConvertFrom converts from the Hub version (v1beta4) to this version (v1beta3). +func (dst *DynaKube) ConvertFrom(srcRaw conversion.Hub) error { + src := srcRaw.(*dynakubev1beta4.DynaKube) + + dst.fromStatus(src) + + dst.fromBase(src) + dst.fromMetadataEnrichment(src) + dst.fromLogMonitoringSpec(src) + dst.fromKspmSpec(src) + dst.fromExtensionsSpec(src) + dst.fromOneAgentSpec(src) + dst.fromActiveGateSpec(src) + dst.fromTemplatesSpec(src) + + return nil +} + +func (dst *DynaKube) fromBase(src *dynakubev1beta4.DynaKube) { + if src.Annotations == nil { + src.Annotations = map[string]string{} + } + + dst.ObjectMeta = *src.ObjectMeta.DeepCopy() // DeepCopy mainly relevant for testing + + dst.Spec.Proxy = src.Spec.Proxy + dst.Spec.DynatraceApiRequestThreshold = src.Spec.DynatraceApiRequestThreshold + dst.Spec.APIURL = src.Spec.APIURL + dst.Spec.Tokens = src.Spec.Tokens + dst.Spec.TrustedCAs = src.Spec.TrustedCAs + dst.Spec.NetworkZone = src.Spec.NetworkZone + dst.Spec.CustomPullSecret = src.Spec.CustomPullSecret + dst.Spec.SkipCertCheck = src.Spec.SkipCertCheck + dst.Spec.EnableIstio = src.Spec.EnableIstio +} + +func (dst *DynaKube) fromLogMonitoringSpec(src *dynakubev1beta4.DynaKube) { + if src.Spec.LogMonitoring != nil { + dst.Spec.LogMonitoring = &logmonitoring.Spec{} + dst.Spec.LogMonitoring.IngestRuleMatchers = make([]logmonitoring.IngestRuleMatchers, 0) + + for _, rule := range src.Spec.LogMonitoring.IngestRuleMatchers { + dst.Spec.LogMonitoring.IngestRuleMatchers = append(dst.Spec.LogMonitoring.IngestRuleMatchers, logmonitoring.IngestRuleMatchers{ + Attribute: rule.Attribute, + Values: rule.Values, + }) + } + } +} + +func (dst *DynaKube) fromKspmSpec(src *dynakubev1beta4.DynaKube) { + if src.Spec.Kspm != nil { + dst.Spec.Kspm = &kspm.Spec{} + } +} + +func (dst *DynaKube) fromExtensionsSpec(src *dynakubev1beta4.DynaKube) { + if src.Spec.Extensions != nil { + dst.Spec.Extensions = &ExtensionsSpec{} + } +} + +func (dst *DynaKube) fromOneAgentSpec(src *dynakubev1beta4.DynaKube) { //nolint:dupl + switch { + case src.OneAgent().IsClassicFullStackMode(): + dst.Spec.OneAgent.ClassicFullStack = fromHostInjectSpec(*src.Spec.OneAgent.ClassicFullStack) + case src.OneAgent().IsCloudNativeFullstackMode(): + dst.Spec.OneAgent.CloudNativeFullStack = &oneagent.CloudNativeFullStackSpec{} + dst.Spec.OneAgent.CloudNativeFullStack.HostInjectSpec = *fromHostInjectSpec(src.Spec.OneAgent.CloudNativeFullStack.HostInjectSpec) + dst.Spec.OneAgent.CloudNativeFullStack.AppInjectionSpec = *fromAppInjectSpec(src.Spec.OneAgent.CloudNativeFullStack.AppInjectionSpec) + case src.OneAgent().IsApplicationMonitoringMode(): + dst.Spec.OneAgent.ApplicationMonitoring = &oneagent.ApplicationMonitoringSpec{} + dst.Spec.OneAgent.ApplicationMonitoring.Version = src.Spec.OneAgent.ApplicationMonitoring.Version + dst.Spec.OneAgent.ApplicationMonitoring.AppInjectionSpec = *fromAppInjectSpec(src.Spec.OneAgent.ApplicationMonitoring.AppInjectionSpec) + case src.OneAgent().IsHostMonitoringMode(): + dst.Spec.OneAgent.HostMonitoring = fromHostInjectSpec(*src.Spec.OneAgent.HostMonitoring) + } + + dst.Spec.OneAgent.HostGroup = src.Spec.OneAgent.HostGroup +} + +func (dst *DynaKube) fromTemplatesSpec(src *dynakubev1beta4.DynaKube) { + dst.Spec.Templates.LogMonitoring = fromLogMonitoringTemplate(src.Spec.Templates.LogMonitoring) + dst.Spec.Templates.KspmNodeConfigurationCollector = fromKspmNodeConfigurationCollectorTemplate(src.Spec.Templates.KspmNodeConfigurationCollector) + dst.Spec.Templates.OpenTelemetryCollector = fromOpenTelemetryCollectorTemplate(src.Spec.Templates.OpenTelemetryCollector) + dst.Spec.Templates.ExtensionExecutionController = fromExtensionControllerTemplate(src.Spec.Templates.ExtensionExecutionController) +} + +func fromLogMonitoringTemplate(src *logmonitoringv1beta4.TemplateSpec) *logmonitoring.TemplateSpec { + if src == nil { + return nil + } + + dst := &logmonitoring.TemplateSpec{} + + dst.Annotations = src.Annotations + dst.Labels = src.Labels + dst.NodeSelector = src.NodeSelector + dst.ImageRef = src.ImageRef + dst.DNSPolicy = src.DNSPolicy + dst.PriorityClassName = src.PriorityClassName + dst.SecCompProfile = src.SecCompProfile + dst.Resources = src.Resources + dst.Tolerations = src.Tolerations + dst.Args = src.Args + + return dst +} + +func fromKspmNodeConfigurationCollectorTemplate(src kspmv1beta4.NodeConfigurationCollectorSpec) kspm.NodeConfigurationCollectorSpec { + dst := kspm.NodeConfigurationCollectorSpec{} + + dst.UpdateStrategy = src.UpdateStrategy + dst.Labels = src.Labels + dst.Annotations = src.Annotations + dst.NodeSelector = src.NodeSelector + dst.ImageRef = src.ImageRef + dst.PriorityClassName = src.PriorityClassName + dst.Resources = src.Resources + dst.NodeAffinity = src.NodeAffinity + dst.Tolerations = src.Tolerations + dst.Args = src.Args + dst.Env = src.Env + + return dst +} + +func fromOpenTelemetryCollectorTemplate(src dynakubev1beta4.OpenTelemetryCollectorSpec) OpenTelemetryCollectorSpec { + dst := OpenTelemetryCollectorSpec{} + + dst.Labels = src.Labels + dst.Annotations = src.Annotations + dst.Replicas = src.Replicas + dst.ImageRef = src.ImageRef + dst.TlsRefName = src.TlsRefName + dst.Resources = src.Resources + dst.Tolerations = src.Tolerations + dst.TopologySpreadConstraints = src.TopologySpreadConstraints + + return dst +} + +func fromExtensionControllerTemplate(src dynakubev1beta4.ExtensionExecutionControllerSpec) ExtensionExecutionControllerSpec { + dst := ExtensionExecutionControllerSpec{} + + dst.PersistentVolumeClaim = src.PersistentVolumeClaim + dst.Labels = src.Labels + dst.Annotations = src.Annotations + dst.ImageRef = src.ImageRef + dst.TlsRefName = src.TlsRefName + dst.CustomConfig = src.CustomConfig + dst.CustomExtensionCertificates = src.CustomExtensionCertificates + dst.Resources = src.Resources + dst.Tolerations = src.Tolerations + dst.TopologySpreadConstraints = src.TopologySpreadConstraints + dst.UseEphemeralVolume = src.UseEphemeralVolume + + return dst +} + +func (dst *DynaKube) fromActiveGateSpec(src *dynakubev1beta4.DynaKube) { //nolint:dupl + dst.Spec.ActiveGate.Annotations = src.Spec.ActiveGate.Annotations + dst.Spec.ActiveGate.TlsSecretName = src.Spec.ActiveGate.TlsSecretName + dst.Spec.ActiveGate.DNSPolicy = src.Spec.ActiveGate.DNSPolicy + dst.Spec.ActiveGate.PriorityClassName = src.Spec.ActiveGate.PriorityClassName + + dst.Spec.ActiveGate.CapabilityProperties.CustomProperties = src.Spec.ActiveGate.CapabilityProperties.CustomProperties + dst.Spec.ActiveGate.CapabilityProperties.NodeSelector = src.Spec.ActiveGate.CapabilityProperties.NodeSelector + dst.Spec.ActiveGate.CapabilityProperties.Labels = src.Spec.ActiveGate.CapabilityProperties.Labels + dst.Spec.ActiveGate.CapabilityProperties.Replicas = src.Spec.ActiveGate.CapabilityProperties.Replicas + dst.Spec.ActiveGate.CapabilityProperties.Image = src.Spec.ActiveGate.CapabilityProperties.Image + dst.Spec.ActiveGate.CapabilityProperties.Group = src.Spec.ActiveGate.CapabilityProperties.Group + dst.Spec.ActiveGate.CapabilityProperties.Resources = src.Spec.ActiveGate.CapabilityProperties.Resources + dst.Spec.ActiveGate.CapabilityProperties.Tolerations = src.Spec.ActiveGate.CapabilityProperties.Tolerations + dst.Spec.ActiveGate.CapabilityProperties.Env = src.Spec.ActiveGate.CapabilityProperties.Env + dst.Spec.ActiveGate.CapabilityProperties.TopologySpreadConstraints = src.Spec.ActiveGate.CapabilityProperties.TopologySpreadConstraints + + dst.Spec.ActiveGate.Capabilities = make([]activegate.CapabilityDisplayName, 0) + for _, capability := range src.Spec.ActiveGate.Capabilities { + dst.Spec.ActiveGate.Capabilities = append(dst.Spec.ActiveGate.Capabilities, activegate.CapabilityDisplayName(capability)) + } +} + +func (dst *DynaKube) fromStatus(src *dynakubev1beta4.DynaKube) { + dst.fromOneAgentStatus(*src) + dst.fromActiveGateStatus(*src) + dst.Status.CodeModules = oneagent.CodeModulesStatus{ + VersionStatus: src.Status.CodeModules.VersionStatus, + } + + dst.Status.MetadataEnrichment.Rules = make([]EnrichmentRule, 0) + for _, rule := range src.Status.MetadataEnrichment.Rules { + dst.Status.MetadataEnrichment.Rules = append(dst.Status.MetadataEnrichment.Rules, + EnrichmentRule{ + Type: EnrichmentRuleType(rule.Type), + Source: rule.Source, + Target: rule.Target, + Enabled: rule.Enabled, + }) + } + + dst.Status.Kspm.TokenSecretHash = src.Status.Kspm.TokenSecretHash + dst.Status.UpdatedTimestamp = src.Status.UpdatedTimestamp + dst.Status.DynatraceApi = DynatraceApiStatus{ + LastTokenScopeRequest: src.Status.DynatraceApi.LastTokenScopeRequest, + } + dst.Status.Phase = src.Status.Phase + dst.Status.KubeSystemUUID = src.Status.KubeSystemUUID + dst.Status.KubernetesClusterMEID = src.Status.KubernetesClusterMEID + dst.Status.KubernetesClusterName = src.Status.KubernetesClusterName + dst.Status.Conditions = src.Status.Conditions +} + +func (dst *DynaKube) fromOneAgentStatus(src dynakubev1beta4.DynaKube) { //nolint:dupl + dst.Status.OneAgent.VersionStatus = src.Status.OneAgent.VersionStatus + + dst.Status.OneAgent.Instances = map[string]oneagent.Instance{} + for key, instance := range src.Status.OneAgent.Instances { + dst.Status.OneAgent.Instances[key] = oneagent.Instance{ + PodName: instance.PodName, + IPAddress: instance.IPAddress, + } + } + + dst.Status.OneAgent.LastInstanceStatusUpdate = src.Status.OneAgent.LastInstanceStatusUpdate + dst.Status.OneAgent.Healthcheck = src.Status.OneAgent.Healthcheck + dst.Status.OneAgent.ConnectionInfoStatus.ConnectionInfo = src.Status.OneAgent.ConnectionInfoStatus.ConnectionInfo + dst.Status.OneAgent.ConnectionInfoStatus.CommunicationHosts = make([]oneagent.CommunicationHostStatus, 0) + + for _, host := range src.Status.OneAgent.ConnectionInfoStatus.CommunicationHosts { + dst.Status.OneAgent.ConnectionInfoStatus.CommunicationHosts = + append(dst.Status.OneAgent.ConnectionInfoStatus.CommunicationHosts, oneagent.CommunicationHostStatus{ + Protocol: host.Protocol, + Host: host.Host, + Port: host.Port, + }) + } +} + +func (dst *DynaKube) fromActiveGateStatus(src dynakubev1beta4.DynaKube) { + dst.Status.ActiveGate.VersionStatus = src.Status.ActiveGate.VersionStatus + dst.Status.ActiveGate.ConnectionInfo = src.Status.ActiveGate.ConnectionInfo + dst.Status.ActiveGate.ServiceIPs = src.Status.ActiveGate.ServiceIPs +} + +func fromHostInjectSpec(src oneagentv1beta4.HostInjectSpec) *oneagent.HostInjectSpec { + dst := &oneagent.HostInjectSpec{} + + dst.Annotations = src.Annotations + dst.Labels = src.Labels + dst.NodeSelector = src.NodeSelector + dst.AutoUpdate = src.AutoUpdate + dst.Version = src.Version + dst.Image = src.Image + dst.DNSPolicy = src.DNSPolicy + dst.PriorityClassName = src.PriorityClassName + dst.SecCompProfile = src.SecCompProfile + dst.OneAgentResources = src.OneAgentResources + dst.Tolerations = src.Tolerations + dst.Env = src.Env + dst.Args = src.Args + + return dst +} + +func fromAppInjectSpec(src oneagentv1beta4.AppInjectionSpec) *oneagent.AppInjectionSpec { + dst := &oneagent.AppInjectionSpec{} + + dst.InitResources = src.InitResources + dst.CodeModulesImage = src.CodeModulesImage + dst.NamespaceSelector = src.NamespaceSelector + + return dst +} + +func (dst *DynaKube) fromMetadataEnrichment(src *dynakubev1beta4.DynaKube) { + dst.Spec.MetadataEnrichment.Enabled = src.Spec.MetadataEnrichment.Enabled + dst.Spec.MetadataEnrichment.NamespaceSelector = src.Spec.MetadataEnrichment.NamespaceSelector +} diff --git a/pkg/api/v1beta3/dynakube/convert_from_test.go b/pkg/api/v1beta3/dynakube/convert_from_test.go new file mode 100644 index 0000000000..3f87a99eb8 --- /dev/null +++ b/pkg/api/v1beta3/dynakube/convert_from_test.go @@ -0,0 +1,960 @@ +package dynakube + +import ( + "slices" + "testing" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/communication" + "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/image" + "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/value" + "github.com/Dynatrace/dynatrace-operator/pkg/api/status" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/kspm" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/logmonitoring" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/oneagent" + dynakubev1beta4 "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + activegatev1beta4 "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" + kspmv1beta4 "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/kspm" + logmonitoringv1beta4 "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/logmonitoring" + oneagentv1beta4 "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" + registryv1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/ptr" +) + +func TestConvertFrom(t *testing.T) { + t.Run("migrate base from v1beta4 to v1beta3", func(t *testing.T) { + from := getNewDynakubeBase() + to := DynaKube{} + + err := to.ConvertFrom(&from) + require.NoError(t, err) + + compareBase(t, to, from) + }) + + t.Run("migrate metadata-enrichment from v1beta4 to v1beta3", func(t *testing.T) { + from := getNewDynakubeBase() + to := DynaKube{} + + err := to.ConvertFrom(&from) + require.NoError(t, err) + + assert.True(t, to.MetadataEnrichmentEnabled()) + compareBase(t, to, from) + }) + + t.Run("migrate host-monitoring from v1beta4 to v1beta3", func(t *testing.T) { + from := getNewDynakubeBase() + hostSpec := getNewHostInjectSpec() + from.Spec.OneAgent.HostMonitoring = &hostSpec + to := DynaKube{} + + err := to.ConvertFrom(&from) + require.NoError(t, err) + + compareHostInjectSpec(t, *to.Spec.OneAgent.HostMonitoring, *from.Spec.OneAgent.HostMonitoring) + compareBase(t, to, from) + }) + + t.Run("migrate classic-fullstack from v1beta4 to v1beta3", func(t *testing.T) { + from := getNewDynakubeBase() + hostSpec := getNewHostInjectSpec() + from.Spec.OneAgent.ClassicFullStack = &hostSpec + to := DynaKube{} + + err := to.ConvertFrom(&from) + require.NoError(t, err) + + assert.Nil(t, to.Spec.OneAgent.CloudNativeFullStack) + assert.Nil(t, to.Spec.OneAgent.ApplicationMonitoring) + assert.Nil(t, to.Spec.OneAgent.HostMonitoring) + + compareHostInjectSpec(t, *to.Spec.OneAgent.ClassicFullStack, *from.Spec.OneAgent.ClassicFullStack) + compareBase(t, to, from) + }) + + t.Run("migrate cloud-native from v1beta4 to v1beta3", func(t *testing.T) { + from := getNewDynakubeBase() + spec := getNewCloudNativeSpec() + from.Spec.OneAgent.CloudNativeFullStack = &spec + to := DynaKube{} + + err := to.ConvertFrom(&from) + require.NoError(t, err) + + assert.Nil(t, to.Spec.OneAgent.ClassicFullStack) + assert.Nil(t, to.Spec.OneAgent.ApplicationMonitoring) + assert.Nil(t, to.Spec.OneAgent.HostMonitoring) + + compareCloudNativeSpec(t, *to.Spec.OneAgent.CloudNativeFullStack, *from.Spec.OneAgent.CloudNativeFullStack) + compareBase(t, to, from) + }) + + t.Run("migrate application-monitoring from v1beta4 to v1beta3", func(t *testing.T) { + from := getNewDynakubeBase() + appSpec := getNewApplicationMonitoringSpec() + from.Spec.OneAgent.ApplicationMonitoring = &appSpec + to := DynaKube{} + + err := to.ConvertFrom(&from) + require.NoError(t, err) + + assert.Nil(t, to.Spec.OneAgent.ClassicFullStack) + assert.Nil(t, to.Spec.OneAgent.CloudNativeFullStack) + assert.Nil(t, to.Spec.OneAgent.HostMonitoring) + + compareApplicationMonitoringSpec(t, *to.Spec.OneAgent.ApplicationMonitoring, *from.Spec.OneAgent.ApplicationMonitoring) + compareBase(t, to, from) + }) + + t.Run("migrate activegate from v1beta4 to v1beta3", func(t *testing.T) { + from := getNewDynakubeBase() + agSpec := getNewActiveGateSpec() + from.Spec.ActiveGate = agSpec + to := DynaKube{} + + err := to.ConvertFrom(&from) + require.NoError(t, err) + + compareActiveGateSpec(t, to.Spec.ActiveGate, from.Spec.ActiveGate) + compareBase(t, to, from) + }) + + t.Run("migrate extensions from v1beta4 to v1beta3", func(t *testing.T) { + from := getNewDynakubeBase() + from.Spec.Extensions = &dynakubev1beta4.ExtensionsSpec{} + to := DynaKube{} + + err := to.ConvertFrom(&from) + require.NoError(t, err) + + assert.NotNil(t, to.Spec.Extensions) + compareBase(t, to, from) + }) + + t.Run("migrate log-monitoring from v1beta4 to v1beta3", func(t *testing.T) { + from := getNewDynakubeBase() + from.Spec.LogMonitoring = getNewLogMonitoringSpec() + to := DynaKube{} + + err := to.ConvertFrom(&from) + require.NoError(t, err) + + compareLogMonitoringSpec(t, to.Spec.LogMonitoring, from.Spec.LogMonitoring) + compareBase(t, to, from) + }) + + t.Run("migrate kspm from v1beta4 to v1beta3", func(t *testing.T) { + from := getNewDynakubeBase() + from.Spec.Kspm = &kspmv1beta4.Spec{} + to := DynaKube{} + + err := to.ConvertFrom(&from) + require.NoError(t, err) + + assert.NotNil(t, to.Spec.Kspm) + compareBase(t, to, from) + }) + + t.Run("migrate extensions templates from v1beta4 to v1beta3", func(t *testing.T) { + from := getNewDynakubeBase() + from.Spec.Templates.OpenTelemetryCollector = getNewOpenTelemetryTemplateSpec() + from.Spec.Templates.ExtensionExecutionController = getNewExtensionExecutionControllerSpec() + + to := DynaKube{} + + err := to.ConvertFrom(&from) + require.NoError(t, err) + + compareOpenTelemetryTemplateSpec(t, to.Spec.Templates.OpenTelemetryCollector, from.Spec.Templates.OpenTelemetryCollector) + compareExtensionsExecutionControllerTemplateSpec(t, to.Spec.Templates.ExtensionExecutionController, from.Spec.Templates.ExtensionExecutionController) + + compareBase(t, to, from) + }) + + t.Run("migrate log-monitoring templates from v1beta4 to v1beta3", func(t *testing.T) { + from := getNewDynakubeBase() + from.Spec.Templates.LogMonitoring = getNewLogMonitoringTemplateSpec() + + to := DynaKube{} + + err := to.ConvertFrom(&from) + require.NoError(t, err) + + compareLogMonitoringTemplateSpec(t, to.Spec.Templates.LogMonitoring, from.Spec.Templates.LogMonitoring) + compareBase(t, to, from) + }) + + t.Run("migrate kspm templates from v1beta4 to v1beta3", func(t *testing.T) { + from := getNewDynakubeBase() + from.Spec.Templates.KspmNodeConfigurationCollector = getNewNodeConfigurationCollectorTemplateSpec() + + to := DynaKube{} + + err := to.ConvertFrom(&from) + require.NoError(t, err) + + compareNodeConfigurationCollectorTemplateSpec(t, to.Spec.Templates.KspmNodeConfigurationCollector, from.Spec.Templates.KspmNodeConfigurationCollector) + compareBase(t, to, from) + }) + + t.Run("migrate status from v1beta4 to v1beta3", func(t *testing.T) { + from := getNewDynakubeBase() + from.Status = getNewStatus() + to := DynaKube{} + + err := to.ConvertFrom(&from) + require.NoError(t, err) + + compareStatus(t, to.Status, from.Status) + }) + t.Run("migrate hostGroup", func(t *testing.T) { + from := getNewDynakubeBase() + from.Status = getNewStatus() + to := DynaKube{} + + err := to.ConvertFrom(&from) + require.NoError(t, err) + + assert.Equal(t, to.Spec.OneAgent.HostGroup, from.Spec.OneAgent.HostGroup) + }) +} + +func compareBase(t *testing.T, oldDk DynaKube, newDk dynakubev1beta4.DynaKube) { + require.NotEmpty(t, oldDk) + require.NotEmpty(t, newDk) + + // Some feature-flags are moved, so ObjectMeta will differ in that 1 field + oldAnnotations := oldDk.Annotations + newAnnotations := newDk.Annotations + oldDk.Annotations = nil + newDk.Annotations = nil + + assert.Equal(t, oldDk.ObjectMeta, newDk.ObjectMeta) + + oldDk.Annotations = oldAnnotations + newDk.Annotations = newAnnotations + + if oldDk.Spec.Proxy != nil || newDk.Spec.Proxy != nil { // necessary so we don't explode with nil pointer when not set + require.NotNil(t, oldDk.Spec.Proxy) + require.NotNil(t, newDk.Spec.Proxy) + assert.Equal(t, oldDk.Spec.Proxy.Value, newDk.Spec.Proxy.Value) + assert.Equal(t, oldDk.Spec.Proxy.ValueFrom, newDk.Spec.Proxy.ValueFrom) + } + + assert.Equal(t, oldDk.Spec.DynatraceApiRequestThreshold, newDk.Spec.DynatraceApiRequestThreshold) + assert.Equal(t, oldDk.Spec.APIURL, newDk.Spec.APIURL) + assert.Equal(t, oldDk.Spec.Tokens, newDk.Spec.Tokens) + assert.Equal(t, oldDk.Spec.TrustedCAs, newDk.Spec.TrustedCAs) + assert.Equal(t, oldDk.Spec.NetworkZone, newDk.Spec.NetworkZone) + assert.Equal(t, oldDk.Spec.CustomPullSecret, newDk.Spec.CustomPullSecret) + assert.Equal(t, oldDk.Spec.SkipCertCheck, newDk.Spec.SkipCertCheck) + assert.Equal(t, oldDk.Spec.EnableIstio, newDk.Spec.EnableIstio) + + if newDk.OneAgent().IsAppInjectionNeeded() { + assert.Equal(t, oldDk.OneAgent().GetNamespaceSelector(), newDk.OneAgent().GetNamespaceSelector()) + } + + assert.Equal(t, oldDk.MetadataEnrichmentEnabled(), newDk.MetadataEnrichmentEnabled()) + assert.Equal(t, oldDk.Spec.MetadataEnrichment.NamespaceSelector, newDk.Spec.MetadataEnrichment.NamespaceSelector) + + if oldDk.FeatureMaxFailedCsiMountAttempts() != DefaultMaxFailedCsiMountAttempts { + assert.Equal(t, dynakubev1beta4.MountAttemptsToTimeout(oldDk.FeatureMaxFailedCsiMountAttempts()), newDk.FeatureMaxCSIRetryTimeout().String()) + } +} + +func compareHostInjectSpec(t *testing.T, oldSpec oneagent.HostInjectSpec, newSpec oneagentv1beta4.HostInjectSpec) { + assert.Equal(t, oldSpec.Annotations, newSpec.Annotations) + assert.Equal(t, oldSpec.Args, newSpec.Args) + assert.Equal(t, *oldSpec.AutoUpdate, *newSpec.AutoUpdate) + assert.Equal(t, oldSpec.DNSPolicy, newSpec.DNSPolicy) + assert.Equal(t, oldSpec.Env, newSpec.Env) + assert.Equal(t, oldSpec.Image, newSpec.Image) + assert.Equal(t, oldSpec.Labels, newSpec.Labels) + assert.Equal(t, oldSpec.NodeSelector, newSpec.NodeSelector) + assert.Equal(t, oldSpec.OneAgentResources, newSpec.OneAgentResources) + assert.Equal(t, oldSpec.PriorityClassName, newSpec.PriorityClassName) + assert.Equal(t, oldSpec.Tolerations, newSpec.Tolerations) + assert.Equal(t, oldSpec.Version, newSpec.Version) + assert.Equal(t, oldSpec.SecCompProfile, newSpec.SecCompProfile) +} + +func compareAppInjectionSpec(t *testing.T, oldSpec oneagent.AppInjectionSpec, newSpec oneagentv1beta4.AppInjectionSpec) { + assert.Equal(t, oldSpec.InitResources, newSpec.InitResources) + assert.Equal(t, oldSpec.CodeModulesImage, newSpec.CodeModulesImage) + assert.Equal(t, oldSpec.NamespaceSelector, newSpec.NamespaceSelector) +} + +func compareCloudNativeSpec(t *testing.T, oldSpec oneagent.CloudNativeFullStackSpec, newSpec oneagentv1beta4.CloudNativeFullStackSpec) { + compareHostInjectSpec(t, oldSpec.HostInjectSpec, newSpec.HostInjectSpec) + compareAppInjectionSpec(t, oldSpec.AppInjectionSpec, newSpec.AppInjectionSpec) +} + +func compareApplicationMonitoringSpec(t *testing.T, oldSpec oneagent.ApplicationMonitoringSpec, newSpec oneagentv1beta4.ApplicationMonitoringSpec) { + assert.Equal(t, oldSpec.Version, newSpec.Version) + compareAppInjectionSpec(t, oldSpec.AppInjectionSpec, newSpec.AppInjectionSpec) +} + +func compareActiveGateSpec(t *testing.T, oldSpec activegate.Spec, newSpec activegatev1beta4.Spec) { + assert.Equal(t, oldSpec.Annotations, newSpec.Annotations) + assert.Equal(t, oldSpec.TlsSecretName, newSpec.TlsSecretName) + assert.Equal(t, oldSpec.DNSPolicy, newSpec.DNSPolicy) + assert.Equal(t, oldSpec.PriorityClassName, newSpec.PriorityClassName) + + if oldSpec.CapabilityProperties.CustomProperties != nil || newSpec.CapabilityProperties.CustomProperties != nil { // necessary so we don't explode with nil pointer when not set + require.NotNil(t, oldSpec.CapabilityProperties.CustomProperties) + require.NotNil(t, newSpec.CapabilityProperties.CustomProperties) + assert.Equal(t, oldSpec.CapabilityProperties.CustomProperties.Value, newSpec.CapabilityProperties.CustomProperties.Value) + assert.Equal(t, oldSpec.CapabilityProperties.CustomProperties.ValueFrom, newSpec.CapabilityProperties.CustomProperties.ValueFrom) + } + + assert.Equal(t, oldSpec.CapabilityProperties.NodeSelector, newSpec.CapabilityProperties.NodeSelector) + assert.Equal(t, oldSpec.CapabilityProperties.Labels, newSpec.CapabilityProperties.Labels) + assert.Equal(t, *oldSpec.CapabilityProperties.Replicas, *newSpec.CapabilityProperties.Replicas) + assert.Equal(t, oldSpec.CapabilityProperties.Image, newSpec.CapabilityProperties.Image) + assert.Equal(t, oldSpec.CapabilityProperties.Group, newSpec.CapabilityProperties.Group) + assert.Equal(t, oldSpec.CapabilityProperties.Resources, newSpec.CapabilityProperties.Resources) + assert.Equal(t, oldSpec.CapabilityProperties.Tolerations, newSpec.CapabilityProperties.Tolerations) + assert.Equal(t, oldSpec.CapabilityProperties.Env, newSpec.CapabilityProperties.Env) + assert.Equal(t, oldSpec.CapabilityProperties.TopologySpreadConstraints, newSpec.CapabilityProperties.TopologySpreadConstraints) + + assert.Len(t, newSpec.Capabilities, len(oldSpec.Capabilities)) + + for _, oldCapability := range oldSpec.Capabilities { + assert.Contains(t, newSpec.Capabilities, activegatev1beta4.CapabilityDisplayName(oldCapability)) + } +} + +func compareStatus(t *testing.T, oldStatus DynaKubeStatus, newStatus dynakubev1beta4.DynaKubeStatus) { + // Base + assert.Equal(t, oldStatus.Conditions, newStatus.Conditions) + assert.Equal(t, oldStatus.DynatraceApi.LastTokenScopeRequest, newStatus.DynatraceApi.LastTokenScopeRequest) + assert.Equal(t, oldStatus.KubeSystemUUID, newStatus.KubeSystemUUID) + assert.Equal(t, oldStatus.Phase, newStatus.Phase) + assert.Equal(t, oldStatus.UpdatedTimestamp, newStatus.UpdatedTimestamp) + + // CodeModule + assert.Equal(t, oldStatus.CodeModules.VersionStatus, newStatus.CodeModules.VersionStatus) + + // ActiveGate + assert.Equal(t, oldStatus.ActiveGate.VersionStatus, newStatus.ActiveGate.VersionStatus) + assert.Equal(t, oldStatus.ActiveGate.ConnectionInfo.Endpoints, newStatus.ActiveGate.ConnectionInfo.Endpoints) + assert.Equal(t, oldStatus.ActiveGate.ConnectionInfo.LastRequest, newStatus.ActiveGate.ConnectionInfo.LastRequest) + assert.Equal(t, oldStatus.ActiveGate.ConnectionInfo.TenantUUID, newStatus.ActiveGate.ConnectionInfo.TenantUUID) + + // OneAgent + assert.Equal(t, oldStatus.OneAgent.VersionStatus, newStatus.OneAgent.VersionStatus) + assert.Equal(t, oldStatus.OneAgent.ConnectionInfoStatus.Endpoints, newStatus.OneAgent.ConnectionInfoStatus.Endpoints) + assert.Equal(t, oldStatus.OneAgent.ConnectionInfoStatus.LastRequest, newStatus.OneAgent.ConnectionInfoStatus.LastRequest) + assert.Equal(t, oldStatus.OneAgent.ConnectionInfoStatus.TenantUUID, newStatus.OneAgent.ConnectionInfoStatus.TenantUUID) + assert.Equal(t, oldStatus.OneAgent.Healthcheck, newStatus.OneAgent.Healthcheck) + assert.Equal(t, oldStatus.OneAgent.LastInstanceStatusUpdate, newStatus.OneAgent.LastInstanceStatusUpdate) + + require.Equal(t, len(oldStatus.OneAgent.Instances), len(newStatus.OneAgent.Instances)) + + for key, value := range oldStatus.OneAgent.Instances { + assert.Equal(t, value.IPAddress, newStatus.OneAgent.Instances[key].IPAddress) + assert.Equal(t, value.PodName, newStatus.OneAgent.Instances[key].PodName) + } + + require.Equal(t, len(oldStatus.OneAgent.ConnectionInfoStatus.CommunicationHosts), len(newStatus.OneAgent.ConnectionInfoStatus.CommunicationHosts)) + + for i, oldHost := range oldStatus.OneAgent.ConnectionInfoStatus.CommunicationHosts { + newHost := newStatus.OneAgent.ConnectionInfoStatus.CommunicationHosts[i] + assert.Equal(t, oldHost.Host, newHost.Host) + assert.Equal(t, oldHost.Port, newHost.Port) + assert.Equal(t, oldHost.Protocol, newHost.Protocol) + } +} + +func compareLogMonitoringSpec(t *testing.T, oldSpec *logmonitoring.Spec, newSpec *logmonitoringv1beta4.Spec) { + if oldSpec == nil { + assert.Nil(t, newSpec) + + return + } else { + require.NotNil(t, newSpec) + } + + assert.Len(t, newSpec.IngestRuleMatchers, len(oldSpec.IngestRuleMatchers)) + + for _, oldMatchers := range oldSpec.IngestRuleMatchers { + assert.True(t, slices.ContainsFunc(newSpec.IngestRuleMatchers, func(newMatchers logmonitoringv1beta4.IngestRuleMatchers) bool { + return slices.Equal(newMatchers.Values, oldMatchers.Values) && newMatchers.Attribute == oldMatchers.Attribute + })) + } +} + +func compareOpenTelemetryTemplateSpec(t *testing.T, oldSpec OpenTelemetryCollectorSpec, newSpec dynakubev1beta4.OpenTelemetryCollectorSpec) { + assert.Equal(t, oldSpec.Labels, newSpec.Labels) + assert.Equal(t, oldSpec.Annotations, newSpec.Annotations) + assert.Equal(t, *oldSpec.Replicas, *newSpec.Replicas) + assert.Equal(t, oldSpec.ImageRef, newSpec.ImageRef) + assert.Equal(t, oldSpec.TlsRefName, newSpec.TlsRefName) + assert.Equal(t, oldSpec.Resources, newSpec.Resources) + assert.Equal(t, oldSpec.Tolerations, newSpec.Tolerations) + assert.Equal(t, oldSpec.TopologySpreadConstraints, newSpec.TopologySpreadConstraints) +} + +func compareExtensionsExecutionControllerTemplateSpec(t *testing.T, oldSpec ExtensionExecutionControllerSpec, newSpec dynakubev1beta4.ExtensionExecutionControllerSpec) { + assert.Equal(t, *oldSpec.PersistentVolumeClaim, *newSpec.PersistentVolumeClaim) + assert.Equal(t, oldSpec.Labels, newSpec.Labels) + assert.Equal(t, oldSpec.Annotations, newSpec.Annotations) + assert.Equal(t, oldSpec.ImageRef, newSpec.ImageRef) + assert.Equal(t, oldSpec.TlsRefName, newSpec.TlsRefName) + assert.Equal(t, oldSpec.CustomConfig, newSpec.CustomConfig) + assert.Equal(t, oldSpec.CustomExtensionCertificates, newSpec.CustomExtensionCertificates) + assert.Equal(t, oldSpec.Resources, newSpec.Resources) + assert.Equal(t, oldSpec.Tolerations, newSpec.Tolerations) + assert.Equal(t, oldSpec.TopologySpreadConstraints, newSpec.TopologySpreadConstraints) + assert.Equal(t, oldSpec.UseEphemeralVolume, newSpec.UseEphemeralVolume) +} + +func compareLogMonitoringTemplateSpec(t *testing.T, oldSpec *logmonitoring.TemplateSpec, newSpec *logmonitoringv1beta4.TemplateSpec) { + if oldSpec == nil { + assert.Nil(t, newSpec) + + return + } else { + require.NotNil(t, newSpec) + } + + assert.Equal(t, oldSpec.Annotations, newSpec.Annotations) + assert.Equal(t, oldSpec.Labels, newSpec.Labels) + assert.Equal(t, oldSpec.NodeSelector, newSpec.NodeSelector) + assert.Equal(t, oldSpec.ImageRef, newSpec.ImageRef) + assert.Equal(t, oldSpec.DNSPolicy, newSpec.DNSPolicy) + assert.Equal(t, oldSpec.PriorityClassName, newSpec.PriorityClassName) + assert.Equal(t, oldSpec.SecCompProfile, newSpec.SecCompProfile) + assert.Equal(t, oldSpec.Resources, newSpec.Resources) + assert.Equal(t, oldSpec.Tolerations, newSpec.Tolerations) + assert.Equal(t, oldSpec.Args, newSpec.Args) +} + +func compareNodeConfigurationCollectorTemplateSpec(t *testing.T, oldSpec kspm.NodeConfigurationCollectorSpec, newSpec kspmv1beta4.NodeConfigurationCollectorSpec) { + assert.Equal(t, oldSpec.UpdateStrategy, newSpec.UpdateStrategy) + assert.Equal(t, oldSpec.Labels, newSpec.Labels) + assert.Equal(t, oldSpec.Annotations, newSpec.Annotations) + assert.Equal(t, oldSpec.NodeSelector, newSpec.NodeSelector) + assert.Equal(t, oldSpec.ImageRef, newSpec.ImageRef) + assert.Equal(t, oldSpec.PriorityClassName, newSpec.PriorityClassName) + assert.Equal(t, oldSpec.Resources, newSpec.Resources) + assert.Equal(t, oldSpec.NodeAffinity, newSpec.NodeAffinity) + assert.Equal(t, oldSpec.Tolerations, newSpec.Tolerations) + assert.Equal(t, oldSpec.Args, newSpec.Args) + assert.Equal(t, oldSpec.Env, newSpec.Env) +} + +func getNewDynakubeBase() dynakubev1beta4.DynaKube { + return dynakubev1beta4.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Name: "name", + GenerateName: "generateName", + Namespace: "namespace", + Generation: 0xDEADBEEF, + Annotations: map[string]string{ + dynakubev1beta4.AnnotationFeatureActiveGateIgnoreProxy: "true", //nolint:staticcheck + dynakubev1beta4.AnnotationFeatureAutomaticK8sApiMonitoring: "true", + }, + Labels: map[string]string{ + "label": "label-value", + }, + Finalizers: []string{"finalizer1", "finalizer2"}, + }, + Spec: dynakubev1beta4.DynaKubeSpec{ + Proxy: &value.Source{ + Value: "proxy-value", + ValueFrom: "proxy-from", + }, + DynatraceApiRequestThreshold: ptr.To(uint16(42)), + APIURL: "api-url", + Tokens: "token", + TrustedCAs: "trusted-ca", + NetworkZone: "network-zone", + CustomPullSecret: "pull-secret", + SkipCertCheck: true, + EnableIstio: true, + MetadataEnrichment: dynakubev1beta4.MetadataEnrichment{ + Enabled: ptr.To(true), + NamespaceSelector: getTestNamespaceSelector(), + }, + }, + } +} + +func getNewHostInjectSpec() oneagentv1beta4.HostInjectSpec { + return oneagentv1beta4.HostInjectSpec{ + Version: "host-inject-version", + Image: "host-inject-image", + Tolerations: []corev1.Toleration{ + {Key: "host-inject-toleration-key", Operator: "In", Value: "host-inject-toleration-value"}, + }, + AutoUpdate: ptr.To(false), + DNSPolicy: corev1.DNSClusterFirstWithHostNet, + Annotations: map[string]string{ + "host-inject-annotation-key": "host-inject-annotation-value", + }, + Labels: map[string]string{ + "host-inject-label-key": "host-inject-label-value", + }, + Env: []corev1.EnvVar{ + {Name: "host-inject-env-1", Value: "host-inject-env-value-1", ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + Key: "host-inject-env-from-1", + }, + }}, + {Name: "host-inject-env-2", Value: "host-inject-env-value-2", ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + Key: "host-inject-env-from-2", + }, + }}, + }, + NodeSelector: map[string]string{ + "host-inject-node-selector-key": "host-inject-node-selector-value", + }, + PriorityClassName: "host-inject-priority-class", + Args: []string{ + "host-inject-arg-1", + "host-inject-arg-2", + }, + OneAgentResources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewScaledQuantity(1, 1)}, + }, + SecCompProfile: "seccomp", + } +} + +func getNewAppInjectionSpec() oneagentv1beta4.AppInjectionSpec { + return oneagentv1beta4.AppInjectionSpec{ + InitResources: &corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewScaledQuantity(2, 1)}, + }, + CodeModulesImage: "app-injection-image", + NamespaceSelector: getTestNamespaceSelector(), + } +} + +func getNewCloudNativeSpec() oneagentv1beta4.CloudNativeFullStackSpec { + return oneagentv1beta4.CloudNativeFullStackSpec{ + AppInjectionSpec: getNewAppInjectionSpec(), + HostInjectSpec: getNewHostInjectSpec(), + } +} + +func getNewApplicationMonitoringSpec() oneagentv1beta4.ApplicationMonitoringSpec { + return oneagentv1beta4.ApplicationMonitoringSpec{ + AppInjectionSpec: getNewAppInjectionSpec(), + Version: "app-monitoring-version", + } +} + +func getNewActiveGateSpec() activegatev1beta4.Spec { + return activegatev1beta4.Spec{ + DNSPolicy: corev1.DNSClusterFirstWithHostNet, + Annotations: map[string]string{ + "activegate-annotation-key": "activegate-annotation-value", + }, + TlsSecretName: "activegate-tls-secret-name", + PriorityClassName: "activegate-priority-class-name", + Capabilities: []activegatev1beta4.CapabilityDisplayName{ + activegatev1beta4.DynatraceApiCapability.DisplayName, + activegatev1beta4.KubeMonCapability.DisplayName, + activegatev1beta4.MetricsIngestCapability.DisplayName, + }, + CapabilityProperties: activegatev1beta4.CapabilityProperties{ + Labels: map[string]string{ + "activegate-label-key": "activegate-label-value", + }, + Env: []corev1.EnvVar{ + {Name: "host-inject-env-1", Value: "activegate-env-value-1", ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + Key: "activegate-env-from-1", + }, + }}, + {Name: "activegate-env-2", Value: "activegate-env-value-2", ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + Key: "activegate-env-from-2", + }, + }}, + }, + NodeSelector: map[string]string{ + "activegate-node-selector-key": "activegate-node-selector-value", + }, + Image: "activegate-image", + Replicas: ptr.To(int32(42)), + Group: "activegate-group", + CustomProperties: &value.Source{ + Value: "activegate-cp-value", + ValueFrom: "activegate-cp-value-from", + }, + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewScaledQuantity(3, 1)}, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewScaledQuantity(3, 1), + }, + Claims: []corev1.ResourceClaim{{ + Name: "claim-name", + Request: "claim-request", + }}, + }, + Tolerations: []corev1.Toleration{ + {Key: "activegate-toleration-key", Operator: "In", Value: "activegate-toleration-value"}, + }, + TopologySpreadConstraints: []corev1.TopologySpreadConstraint{ + {MaxSkew: 1}, + }, + }, + } +} + +func getNewLogMonitoringSpec() *logmonitoringv1beta4.Spec { + newSpec := logmonitoringv1beta4.Spec{ + IngestRuleMatchers: make([]logmonitoringv1beta4.IngestRuleMatchers, 0), + } + + newSpec.IngestRuleMatchers = append(newSpec.IngestRuleMatchers, logmonitoringv1beta4.IngestRuleMatchers{ + Attribute: "attribute1", + Values: []string{"matcher1", "matcher2", "matcher3"}, + }) + + newSpec.IngestRuleMatchers = append(newSpec.IngestRuleMatchers, logmonitoringv1beta4.IngestRuleMatchers{ + Attribute: "attribute2", + Values: []string{"matcher1", "matcher2", "matcher3"}, + }) + + newSpec.IngestRuleMatchers = append(newSpec.IngestRuleMatchers, logmonitoringv1beta4.IngestRuleMatchers{ + Attribute: "attribute3", + Values: []string{"matcher1", "matcher2", "matcher3"}, + }) + + return &newSpec +} + +func getNewOpenTelemetryTemplateSpec() dynakubev1beta4.OpenTelemetryCollectorSpec { + return dynakubev1beta4.OpenTelemetryCollectorSpec{ + Labels: map[string]string{ + "otelc-label-key1": "otelc-label-value1", + "otelc-label-key2": "otelc-label-value2", + }, + Annotations: map[string]string{ + "otelc-annotation-key1": "otelc-annotation-value1", + "otelc-annotation-key2": "otelc-annotation-value2", + }, + Replicas: ptr.To(int32(42)), + ImageRef: image.Ref{ + Repository: "image-repo.repohost.test/repo", + Tag: "image-tag", + }, + TlsRefName: "tls-ref-name", + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewScaledQuantity(3, 1), + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewScaledQuantity(3, 1), + }, + Claims: []corev1.ResourceClaim{{ + Name: "claim-name", + Request: "claim-request", + }}, + }, + Tolerations: []corev1.Toleration{ + {Key: "otelc-toleration-key", Operator: "In", Value: "otelc-toleration-value"}, + }, + TopologySpreadConstraints: []corev1.TopologySpreadConstraint{ + {MaxSkew: 1}, + }, + } +} + +func getNewExtensionExecutionControllerSpec() dynakubev1beta4.ExtensionExecutionControllerSpec { + return dynakubev1beta4.ExtensionExecutionControllerSpec{ + PersistentVolumeClaim: getPersistentVolumeClaimSpec(), + Labels: map[string]string{ + "eec-label-key1": "eec-label-value1", + "eec-label-key2": "eec-label-value2", + }, + Annotations: map[string]string{ + "eec-annotation-key1": "eec-annotation-value1", + "eec-annotation-key2": "eec-annotation-value2", + }, + ImageRef: image.Ref{ + Repository: "image-repo.repohost.test/repo", + Tag: "image-tag", + }, + TlsRefName: "tls-ref-name", + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewScaledQuantity(3, 1), + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewScaledQuantity(3, 1), + }, + Claims: []corev1.ResourceClaim{{ + Name: "claim-name", + Request: "claim-request", + }}, + }, + Tolerations: []corev1.Toleration{ + {Key: "otelc-toleration-key", Operator: "In", Value: "otelc-toleration-value"}, + }, + TopologySpreadConstraints: []corev1.TopologySpreadConstraint{ + {MaxSkew: 1}, + }, + CustomConfig: "custom-eec-config", + CustomExtensionCertificates: "custom-eec-certificates", + UseEphemeralVolume: true, + } +} + +func getPersistentVolumeClaimSpec() *corev1.PersistentVolumeClaimSpec { + return &corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "pvc-label-key1": "pvc-label-value1", + "pvc-label-key2": "pvc-label-value2", + }, + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "label-selector-key", + Operator: "label-selector-value", + Values: []string{"pvc-value-1", "pvc-value-key2"}, + }, + }, + }, + Resources: corev1.VolumeResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceStorage: *resource.NewScaledQuantity(3, 1), + }, + Requests: corev1.ResourceList{ + corev1.ResourceStorage: *resource.NewScaledQuantity(3, 1), + }, + }, + VolumeName: "volume-name", + StorageClassName: ptr.To("localstorage"), + VolumeMode: ptr.To(corev1.PersistentVolumeFilesystem), + VolumeAttributesClassName: ptr.To("volume-attributes-class-name"), + } +} + +func getNewLogMonitoringTemplateSpec() *logmonitoringv1beta4.TemplateSpec { + return &logmonitoringv1beta4.TemplateSpec{ + Labels: map[string]string{ + "logagent-label-key1": "logagent-label-value1", + "logagent-label-key2": "logagent-label-value2", + }, + Annotations: map[string]string{ + "logagent-annotation-key1": "logagent-annotation-value1", + "logagent-annotation-key2": "logagent-annotation-value2", + }, + NodeSelector: map[string]string{ + "selector1": "node1", + "selector2": "node2", + }, + ImageRef: image.Ref{ + Repository: "image-repo.repohost.test/repo", + Tag: "image-tag", + }, + DNSPolicy: "dns-policy", + PriorityClassName: "priority-class-name", + SecCompProfile: "sec-comp-profile", + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewScaledQuantity(3, 1), + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewScaledQuantity(3, 1), + }, + Claims: []corev1.ResourceClaim{{ + Name: "claim-name", + Request: "claim-request", + }}, + }, + Tolerations: []corev1.Toleration{ + {Key: "otelc-toleration-key", Operator: "In", Value: "otelc-toleration-value"}, + }, + Args: []string{"--log-level", "debug", "--log-format", "json"}, + } +} + +func getNewNodeConfigurationCollectorTemplateSpec() kspmv1beta4.NodeConfigurationCollectorSpec { + return kspmv1beta4.NodeConfigurationCollectorSpec{ + UpdateStrategy: &v1.DaemonSetUpdateStrategy{ + Type: "daemonset-update-strategy-type", + RollingUpdate: &v1.RollingUpdateDaemonSet{ + MaxUnavailable: &intstr.IntOrString{ + Type: 0, + IntVal: 42, + }, + MaxSurge: &intstr.IntOrString{ + Type: 1, + StrVal: "42", + }, + }, + }, + Labels: map[string]string{ + "ncc-label-key1": "ncc-label-value1", + "ncc-label-key2": "ncc-label-value2", + }, + Annotations: map[string]string{ + "ncc-annotation-key1": "ncc-annotation-value1", + "ncc-annotation-key2": "ncc-annotation-value2", + }, + NodeSelector: map[string]string{ + "selector1": "node1", + "selector2": "node2", + }, + ImageRef: image.Ref{ + Repository: "image-repo.repohost.test/repo", + Tag: "image-tag", + }, + PriorityClassName: "priority-class-name", + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewScaledQuantity(3, 1), + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewScaledQuantity(3, 1), + }, + Claims: []corev1.ResourceClaim{{ + Name: "claim-name", + Request: "claim-request", + }}, + }, + NodeAffinity: corev1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{ + NodeSelectorTerms: []corev1.NodeSelectorTerm{{ + MatchExpressions: []corev1.NodeSelectorRequirement{ + { + Key: "node-selector-match-key", + Operator: "node-selector-match-operator", + Values: []string{"node-match-value-1", "node-match-value2"}, + }, + }, + MatchFields: []corev1.NodeSelectorRequirement{ + { + Key: "node-selector-field-key", + Operator: "node-selector-field-operator", + Values: []string{"node-field-value-1", "node-field-value2"}, + }, + }, + }}, + }, + PreferredDuringSchedulingIgnoredDuringExecution: nil, + }, + Tolerations: []corev1.Toleration{ + {Key: "otelc-toleration-key", Operator: "In", Value: "otelc-toleration-value"}, + }, + Args: []string{"--log-level", "debug", "--log-format", "json"}, + Env: []corev1.EnvVar{ + { + Name: "ENV1", + Value: "VAL1", + }, + { + Name: "ENV2", + Value: "VAL2", + }, + { + Name: "ENV2", + Value: "VAL2", + }, + }, + } +} + +func getNewStatus() dynakubev1beta4.DynaKubeStatus { + return dynakubev1beta4.DynaKubeStatus{ + OneAgent: oneagentv1beta4.Status{ + VersionStatus: status.VersionStatus{ + ImageID: "oa-image-id", + Version: "oa-version", + Type: "oa-image-type", + Source: status.CustomImageVersionSource, + LastProbeTimestamp: &testTime, + }, + Instances: map[string]oneagentv1beta4.Instance{ + "oa-instance-key-1": { + PodName: "oa-instance-pod-1", + IPAddress: "oa-instance-ip-1", + }, + "oa-instance-key-2": { + PodName: "oa-instance-pod-2", + IPAddress: "oa-instance-ip-2", + }, + }, + LastInstanceStatusUpdate: &testTime, + Healthcheck: ®istryv1.HealthConfig{ + Test: []string{"oa-health-check-test"}, + }, + ConnectionInfoStatus: oneagentv1beta4.ConnectionInfoStatus{ + ConnectionInfo: communication.ConnectionInfo{ + LastRequest: testTime, + TenantUUID: "oa-tenant-uuid", + Endpoints: "oa-endpoints", + }, + CommunicationHosts: []oneagentv1beta4.CommunicationHostStatus{ + { + Protocol: "oa-protocol-1", + Host: "oa-host-1", + Port: 1, + }, + { + Protocol: "oa-protocol-2", + Host: "oa-host-2", + Port: 2, + }, + }, + }, + }, + ActiveGate: activegatev1beta4.Status{ + VersionStatus: status.VersionStatus{ + ImageID: "ag-image-id", + Version: "ag-version", + Type: "ag-image-type", + Source: status.CustomVersionVersionSource, + LastProbeTimestamp: &testTime, + }, + }, + CodeModules: oneagentv1beta4.CodeModulesStatus{ + VersionStatus: status.VersionStatus{ + ImageID: "cm-image-id", + Version: "cm-version", + Type: "cm-image-type", + Source: status.TenantRegistryVersionSource, + LastProbeTimestamp: &testTime, + }, + }, + DynatraceApi: dynakubev1beta4.DynatraceApiStatus{ + LastTokenScopeRequest: testTime, + }, + Conditions: []metav1.Condition{ + { + Type: "condition-type-1", + Status: "condition-status-1", + Reason: "condition-reason-1", + LastTransitionTime: testTime, + }, + { + Type: "condition-type-2", + Status: "condition-status-2", + Reason: "condition-reason-2", + LastTransitionTime: testTime, + }, + }, + KubeSystemUUID: "kube-system-uuid", + Phase: status.Deploying, + UpdatedTimestamp: testTime, + } +} diff --git a/pkg/api/v1beta3/dynakube/convert_to.go b/pkg/api/v1beta3/dynakube/convert_to.go new file mode 100644 index 0000000000..481fde97fe --- /dev/null +++ b/pkg/api/v1beta3/dynakube/convert_to.go @@ -0,0 +1,294 @@ +package dynakube + +import ( + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/kspm" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/logmonitoring" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/oneagent" + dynakubev1beta4 "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + activegatev1beta4 "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" + kspmv1beta4 "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/kspm" + logmonitoringv1beta4 "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/logmonitoring" + oneagentv1beta4 "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" + "sigs.k8s.io/controller-runtime/pkg/conversion" +) + +// Convertto converts this version (src=v1beta3) to the Hub version. +func (src *DynaKube) ConvertTo(dstRaw conversion.Hub) error { + dst := dstRaw.(*dynakubev1beta4.DynaKube) + + src.toStatus(dst) + + src.toBase(dst) + src.toMetadataEnrichment(dst) + src.toLogMonitoringSpec(dst) + src.toKspmSpec(dst) + src.toExtensionsSpec(dst) + src.toOneAgentSpec(dst) + src.toActiveGateSpec(dst) + src.toTemplatesSpec(dst) + + return nil +} + +func (src *DynaKube) toBase(dst *dynakubev1beta4.DynaKube) { + if src.Annotations == nil { + dst.Annotations = map[string]string{} + } + + dst.ObjectMeta = *src.ObjectMeta.DeepCopy() // DeepCopy mainly relevant for testing + + dst.Spec.Proxy = src.Spec.Proxy + dst.Spec.DynatraceApiRequestThreshold = src.Spec.DynatraceApiRequestThreshold + dst.Spec.APIURL = src.Spec.APIURL + dst.Spec.Tokens = src.Spec.Tokens + dst.Spec.TrustedCAs = src.Spec.TrustedCAs + dst.Spec.NetworkZone = src.Spec.NetworkZone + dst.Spec.CustomPullSecret = src.Spec.CustomPullSecret + dst.Spec.SkipCertCheck = src.Spec.SkipCertCheck + dst.Spec.EnableIstio = src.Spec.EnableIstio +} + +func (src *DynaKube) toLogMonitoringSpec(dst *dynakubev1beta4.DynaKube) { + if src.Spec.LogMonitoring != nil { + dst.Spec.LogMonitoring = &logmonitoringv1beta4.Spec{} + dst.Spec.LogMonitoring.IngestRuleMatchers = make([]logmonitoringv1beta4.IngestRuleMatchers, 0) + + for _, rule := range src.Spec.LogMonitoring.IngestRuleMatchers { + dst.Spec.LogMonitoring.IngestRuleMatchers = append(dst.Spec.LogMonitoring.IngestRuleMatchers, logmonitoringv1beta4.IngestRuleMatchers{ + Attribute: rule.Attribute, + Values: rule.Values, + }) + } + } +} + +func (src *DynaKube) toKspmSpec(dst *dynakubev1beta4.DynaKube) { + if src.Spec.Kspm != nil { + dst.Spec.Kspm = &kspmv1beta4.Spec{} + } +} + +func (src *DynaKube) toExtensionsSpec(dst *dynakubev1beta4.DynaKube) { + if src.Spec.Extensions != nil { + dst.Spec.Extensions = &dynakubev1beta4.ExtensionsSpec{} + } +} + +func (src *DynaKube) toOneAgentSpec(dst *dynakubev1beta4.DynaKube) { //nolint:dupl + switch { + case src.OneAgent().IsClassicFullStackMode(): + dst.Spec.OneAgent.ClassicFullStack = toHostInjectSpec(*src.Spec.OneAgent.ClassicFullStack) + case src.OneAgent().IsCloudNativeFullstackMode(): + dst.Spec.OneAgent.CloudNativeFullStack = &oneagentv1beta4.CloudNativeFullStackSpec{} + dst.Spec.OneAgent.CloudNativeFullStack.HostInjectSpec = *toHostInjectSpec(src.Spec.OneAgent.CloudNativeFullStack.HostInjectSpec) + dst.Spec.OneAgent.CloudNativeFullStack.AppInjectionSpec = *toAppInjectSpec(src.Spec.OneAgent.CloudNativeFullStack.AppInjectionSpec) + case src.OneAgent().IsApplicationMonitoringMode(): + dst.Spec.OneAgent.ApplicationMonitoring = &oneagentv1beta4.ApplicationMonitoringSpec{} + dst.Spec.OneAgent.ApplicationMonitoring.Version = src.Spec.OneAgent.ApplicationMonitoring.Version + dst.Spec.OneAgent.ApplicationMonitoring.AppInjectionSpec = *toAppInjectSpec(src.Spec.OneAgent.ApplicationMonitoring.AppInjectionSpec) + case src.OneAgent().IsHostMonitoringMode(): + dst.Spec.OneAgent.HostMonitoring = toHostInjectSpec(*src.Spec.OneAgent.HostMonitoring) + } + + dst.Spec.OneAgent.HostGroup = src.Spec.OneAgent.HostGroup +} + +func (src *DynaKube) toTemplatesSpec(dst *dynakubev1beta4.DynaKube) { + dst.Spec.Templates.LogMonitoring = toLogMonitoringTemplate(src.Spec.Templates.LogMonitoring) + dst.Spec.Templates.KspmNodeConfigurationCollector = toKspmNodeConfigurationCollectorTemplate(src.Spec.Templates.KspmNodeConfigurationCollector) + dst.Spec.Templates.OpenTelemetryCollector = toOpenTelemetryCollectorTemplate(src.Spec.Templates.OpenTelemetryCollector) + dst.Spec.Templates.ExtensionExecutionController = toExtensionControllerTemplate(src.Spec.Templates.ExtensionExecutionController) +} + +func toLogMonitoringTemplate(src *logmonitoring.TemplateSpec) *logmonitoringv1beta4.TemplateSpec { + if src == nil { + return nil + } + + dst := &logmonitoringv1beta4.TemplateSpec{} + + dst.Annotations = src.Annotations + dst.Labels = src.Labels + dst.NodeSelector = src.NodeSelector + dst.ImageRef = src.ImageRef + dst.DNSPolicy = src.DNSPolicy + dst.PriorityClassName = src.PriorityClassName + dst.SecCompProfile = src.SecCompProfile + dst.Resources = src.Resources + dst.Tolerations = src.Tolerations + dst.Args = src.Args + + return dst +} + +func toKspmNodeConfigurationCollectorTemplate(src kspm.NodeConfigurationCollectorSpec) kspmv1beta4.NodeConfigurationCollectorSpec { + dst := kspmv1beta4.NodeConfigurationCollectorSpec{} + + dst.UpdateStrategy = src.UpdateStrategy + dst.Labels = src.Labels + dst.Annotations = src.Annotations + dst.NodeSelector = src.NodeSelector + dst.ImageRef = src.ImageRef + dst.PriorityClassName = src.PriorityClassName + dst.Resources = src.Resources + dst.NodeAffinity = src.NodeAffinity + dst.Tolerations = src.Tolerations + dst.Args = src.Args + dst.Env = src.Env + + return dst +} + +func toOpenTelemetryCollectorTemplate(src OpenTelemetryCollectorSpec) dynakubev1beta4.OpenTelemetryCollectorSpec { + dst := dynakubev1beta4.OpenTelemetryCollectorSpec{} + + dst.Labels = src.Labels + dst.Annotations = src.Annotations + dst.Replicas = src.Replicas + dst.ImageRef = src.ImageRef + dst.TlsRefName = src.TlsRefName + dst.Resources = src.Resources + dst.Tolerations = src.Tolerations + dst.TopologySpreadConstraints = src.TopologySpreadConstraints + + return dst +} + +func toExtensionControllerTemplate(src ExtensionExecutionControllerSpec) dynakubev1beta4.ExtensionExecutionControllerSpec { + dst := dynakubev1beta4.ExtensionExecutionControllerSpec{} + + dst.PersistentVolumeClaim = src.PersistentVolumeClaim + dst.Labels = src.Labels + dst.Annotations = src.Annotations + dst.ImageRef = src.ImageRef + dst.TlsRefName = src.TlsRefName + dst.CustomConfig = src.CustomConfig + dst.CustomExtensionCertificates = src.CustomExtensionCertificates + dst.Resources = src.Resources + dst.Tolerations = src.Tolerations + dst.TopologySpreadConstraints = src.TopologySpreadConstraints + dst.UseEphemeralVolume = src.UseEphemeralVolume + + return dst +} + +func (src *DynaKube) toActiveGateSpec(dst *dynakubev1beta4.DynaKube) { //nolint:dupl + dst.Spec.ActiveGate.Annotations = src.Spec.ActiveGate.Annotations + dst.Spec.ActiveGate.TlsSecretName = src.Spec.ActiveGate.TlsSecretName + dst.Spec.ActiveGate.DNSPolicy = src.Spec.ActiveGate.DNSPolicy + dst.Spec.ActiveGate.PriorityClassName = src.Spec.ActiveGate.PriorityClassName + + dst.Spec.ActiveGate.CapabilityProperties.CustomProperties = src.Spec.ActiveGate.CapabilityProperties.CustomProperties + dst.Spec.ActiveGate.CapabilityProperties.NodeSelector = src.Spec.ActiveGate.CapabilityProperties.NodeSelector + dst.Spec.ActiveGate.CapabilityProperties.Labels = src.Spec.ActiveGate.CapabilityProperties.Labels + dst.Spec.ActiveGate.CapabilityProperties.Replicas = src.Spec.ActiveGate.CapabilityProperties.Replicas + dst.Spec.ActiveGate.CapabilityProperties.Image = src.Spec.ActiveGate.CapabilityProperties.Image + dst.Spec.ActiveGate.CapabilityProperties.Group = src.Spec.ActiveGate.CapabilityProperties.Group + dst.Spec.ActiveGate.CapabilityProperties.Resources = src.Spec.ActiveGate.CapabilityProperties.Resources + dst.Spec.ActiveGate.CapabilityProperties.Tolerations = src.Spec.ActiveGate.CapabilityProperties.Tolerations + dst.Spec.ActiveGate.CapabilityProperties.Env = src.Spec.ActiveGate.CapabilityProperties.Env + dst.Spec.ActiveGate.CapabilityProperties.TopologySpreadConstraints = src.Spec.ActiveGate.CapabilityProperties.TopologySpreadConstraints + + dst.Spec.ActiveGate.Capabilities = make([]activegatev1beta4.CapabilityDisplayName, 0) + for _, capability := range src.Spec.ActiveGate.Capabilities { + dst.Spec.ActiveGate.Capabilities = append(dst.Spec.ActiveGate.Capabilities, activegatev1beta4.CapabilityDisplayName(capability)) + } +} + +func (src *DynaKube) toStatus(dst *dynakubev1beta4.DynaKube) { + src.toOneAgentStatus(dst) + src.toActiveGateStatus(dst) + dst.Status.CodeModules = oneagentv1beta4.CodeModulesStatus{ + VersionStatus: src.Status.CodeModules.VersionStatus, + } + + dst.Status.MetadataEnrichment.Rules = make([]dynakubev1beta4.EnrichmentRule, 0) + for _, rule := range src.Status.MetadataEnrichment.Rules { + dst.Status.MetadataEnrichment.Rules = append(dst.Status.MetadataEnrichment.Rules, + dynakubev1beta4.EnrichmentRule{ + Type: dynakubev1beta4.EnrichmentRuleType(rule.Type), + Source: rule.Source, + Target: rule.Target, + Enabled: rule.Enabled, + }) + } + + dst.Status.Kspm.TokenSecretHash = src.Status.Kspm.TokenSecretHash + dst.Status.UpdatedTimestamp = src.Status.UpdatedTimestamp + dst.Status.DynatraceApi = dynakubev1beta4.DynatraceApiStatus{ + LastTokenScopeRequest: src.Status.DynatraceApi.LastTokenScopeRequest, + } + dst.Status.Phase = src.Status.Phase + dst.Status.KubeSystemUUID = src.Status.KubeSystemUUID + dst.Status.KubernetesClusterMEID = src.Status.KubernetesClusterMEID + dst.Status.KubernetesClusterName = src.Status.KubernetesClusterName + dst.Status.Conditions = src.Status.Conditions +} + +func (src *DynaKube) toOneAgentStatus(dst *dynakubev1beta4.DynaKube) { //nolint:dupl + dst.Status.OneAgent.VersionStatus = src.Status.OneAgent.VersionStatus + + dst.Status.OneAgent.Instances = map[string]oneagentv1beta4.Instance{} + for key, instance := range src.Status.OneAgent.Instances { + dst.Status.OneAgent.Instances[key] = oneagentv1beta4.Instance{ + PodName: instance.PodName, + IPAddress: instance.IPAddress, + } + } + + dst.Status.OneAgent.LastInstanceStatusUpdate = src.Status.OneAgent.LastInstanceStatusUpdate + dst.Status.OneAgent.Healthcheck = src.Status.OneAgent.Healthcheck + + dst.Status.OneAgent.ConnectionInfoStatus.ConnectionInfo = src.Status.OneAgent.ConnectionInfoStatus.ConnectionInfo + dst.Status.OneAgent.ConnectionInfoStatus.CommunicationHosts = make([]oneagentv1beta4.CommunicationHostStatus, 0) + + for _, host := range src.Status.OneAgent.ConnectionInfoStatus.CommunicationHosts { + dst.Status.OneAgent.ConnectionInfoStatus.CommunicationHosts = + append(dst.Status.OneAgent.ConnectionInfoStatus.CommunicationHosts, oneagentv1beta4.CommunicationHostStatus{ + Protocol: host.Protocol, + Host: host.Host, + Port: host.Port, + }) + } +} + +func (src *DynaKube) toActiveGateStatus(dst *dynakubev1beta4.DynaKube) { + dst.Status.ActiveGate.VersionStatus = src.Status.ActiveGate.VersionStatus + dst.Status.ActiveGate.ConnectionInfo = src.Status.ActiveGate.ConnectionInfo + dst.Status.ActiveGate.ServiceIPs = src.Status.ActiveGate.ServiceIPs +} + +func toHostInjectSpec(src oneagent.HostInjectSpec) *oneagentv1beta4.HostInjectSpec { + dst := &oneagentv1beta4.HostInjectSpec{} + + dst.Annotations = src.Annotations + dst.Labels = src.Labels + dst.NodeSelector = src.NodeSelector + dst.AutoUpdate = src.AutoUpdate + dst.Version = src.Version + dst.Image = src.Image + dst.DNSPolicy = src.DNSPolicy + dst.PriorityClassName = src.PriorityClassName + dst.SecCompProfile = src.SecCompProfile + dst.OneAgentResources = src.OneAgentResources + dst.Tolerations = src.Tolerations + dst.Env = src.Env + dst.Args = src.Args + + return dst +} + +func toAppInjectSpec(src oneagent.AppInjectionSpec) *oneagentv1beta4.AppInjectionSpec { + dst := &oneagentv1beta4.AppInjectionSpec{} + + dst.InitResources = src.InitResources + dst.CodeModulesImage = src.CodeModulesImage + dst.NamespaceSelector = src.NamespaceSelector + + return dst +} + +func (src *DynaKube) toMetadataEnrichment(dst *dynakubev1beta4.DynaKube) { + dst.Spec.MetadataEnrichment.Enabled = src.Spec.MetadataEnrichment.Enabled + dst.Spec.MetadataEnrichment.NamespaceSelector = src.Spec.MetadataEnrichment.NamespaceSelector +} diff --git a/pkg/api/v1beta3/dynakube/convert_to_test.go b/pkg/api/v1beta3/dynakube/convert_to_test.go new file mode 100644 index 0000000000..d6024ae2b2 --- /dev/null +++ b/pkg/api/v1beta3/dynakube/convert_to_test.go @@ -0,0 +1,707 @@ +package dynakube + +import ( + "testing" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/communication" + "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/image" + "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/value" + "github.com/Dynatrace/dynatrace-operator/pkg/api/status" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/kspm" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/logmonitoring" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/oneagent" + dynakubev1beta4 "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + registryv1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/ptr" +) + +var testTime = metav1.Now() + +func TestConvertTo(t *testing.T) { + t.Run("migrate from v1beta3 to v1beta4", func(t *testing.T) { + from := getOldDynakubeBase() + to := dynakubev1beta4.DynaKube{} + + err := from.ConvertTo(&to) + require.NoError(t, err) + + compareBase(t, from, to) + }) + + t.Run("migrate metadata-enrichment from v1beta3 to v1beta4", func(t *testing.T) { + from := getOldDynakubeBase() + to := dynakubev1beta4.DynaKube{} + + err := from.ConvertTo(&to) + require.NoError(t, err) + + assert.False(t, to.MetadataEnrichmentEnabled()) + compareBase(t, from, to) + }) + + t.Run("migrate host-monitoring from v1beta3 to v1beta4", func(t *testing.T) { + from := getOldDynakubeBase() + hostSpec := getOldHostInjectSpec() + from.Spec.OneAgent.HostMonitoring = &hostSpec + to := dynakubev1beta4.DynaKube{} + + err := from.ConvertTo(&to) + require.NoError(t, err) + + compareHostInjectSpec(t, *from.Spec.OneAgent.HostMonitoring, *to.Spec.OneAgent.HostMonitoring) + compareBase(t, from, to) + assert.False(t, to.MetadataEnrichmentEnabled()) + }) + + t.Run("migrate classic-fullstack from v1beta3 to v1beta4", func(t *testing.T) { + from := getOldDynakubeBase() + hostSpec := getOldHostInjectSpec() + from.Spec.OneAgent.ClassicFullStack = &hostSpec + to := dynakubev1beta4.DynaKube{} + + err := from.ConvertTo(&to) + require.NoError(t, err) + + assert.Nil(t, to.Spec.OneAgent.CloudNativeFullStack) + assert.Nil(t, to.Spec.OneAgent.ApplicationMonitoring) + assert.Nil(t, to.Spec.OneAgent.HostMonitoring) + compareHostInjectSpec(t, *from.Spec.OneAgent.ClassicFullStack, *to.Spec.OneAgent.ClassicFullStack) + compareBase(t, from, to) + assert.False(t, to.MetadataEnrichmentEnabled()) + }) + + t.Run("migrate cloud-native from v1beta3 to v1beta4", func(t *testing.T) { + from := getOldDynakubeBase() + spec := getOldCloudNativeSpec() + from.Spec.OneAgent.CloudNativeFullStack = &spec + to := dynakubev1beta4.DynaKube{} + + err := from.ConvertTo(&to) + require.NoError(t, err) + + assert.Nil(t, to.Spec.OneAgent.ClassicFullStack) + assert.Nil(t, to.Spec.OneAgent.ApplicationMonitoring) + assert.Nil(t, to.Spec.OneAgent.HostMonitoring) + compareCloudNativeSpec(t, *from.Spec.OneAgent.CloudNativeFullStack, *to.Spec.OneAgent.CloudNativeFullStack) + compareBase(t, from, to) + }) + + t.Run("migrate application-monitoring from v1beta3 to v1beta4", func(t *testing.T) { + from := getOldDynakubeBase() + appSpec := getOldApplicationMonitoringSpec() + from.Spec.OneAgent.ApplicationMonitoring = &appSpec + to := dynakubev1beta4.DynaKube{} + + err := from.ConvertTo(&to) + require.NoError(t, err) + + assert.Nil(t, to.Spec.OneAgent.ClassicFullStack) + assert.Nil(t, to.Spec.OneAgent.CloudNativeFullStack) + assert.Nil(t, to.Spec.OneAgent.HostMonitoring) + compareApplicationMonitoringSpec(t, *from.Spec.OneAgent.ApplicationMonitoring, *to.Spec.OneAgent.ApplicationMonitoring) + compareBase(t, from, to) + }) + + t.Run("migrate activegate from v1beta3 to v1beta4", func(t *testing.T) { + from := getOldDynakubeBase() + agSpec := getOldActiveGateSpec() + from.Spec.ActiveGate = agSpec + to := dynakubev1beta4.DynaKube{} + + err := from.ConvertTo(&to) + require.NoError(t, err) + + compareActiveGateSpec(t, from.Spec.ActiveGate, to.Spec.ActiveGate) + compareBase(t, from, to) + assert.False(t, to.MetadataEnrichmentEnabled()) + }) + + t.Run("migrate extensions from v1beta3 to v1beta4", func(t *testing.T) { + from := getOldDynakubeBase() + from.Spec.Extensions = &ExtensionsSpec{} + to := dynakubev1beta4.DynaKube{} + + err := from.ConvertTo(&to) + require.NoError(t, err) + + assert.NotNil(t, to.Spec.Extensions) + compareBase(t, from, to) + }) + + t.Run("migrate log-monitoring from v1beta3 to v1beta4", func(t *testing.T) { + from := getOldDynakubeBase() + from.Spec.LogMonitoring = getOldLogMonitoringSpec() + to := dynakubev1beta4.DynaKube{} + + err := from.ConvertTo(&to) + require.NoError(t, err) + + compareLogMonitoringSpec(t, from.Spec.LogMonitoring, to.Spec.LogMonitoring) + compareBase(t, from, to) + }) + + t.Run("migrate kspm from v1beta3 to v1beta4", func(t *testing.T) { + from := getOldDynakubeBase() + from.Spec.Kspm = &kspm.Spec{} + to := dynakubev1beta4.DynaKube{} + + err := from.ConvertTo(&to) + require.NoError(t, err) + + assert.NotNil(t, to.Spec.Kspm) + compareBase(t, from, to) + }) + + t.Run("migrate extensions templates from v1beta3 to v1beta4", func(t *testing.T) { + from := getOldDynakubeBase() + from.Spec.Templates.OpenTelemetryCollector = getOldOpenTelemetryTemplateSpec() + from.Spec.Templates.ExtensionExecutionController = getOldExtensionExecutionControllerSpec() + + to := dynakubev1beta4.DynaKube{} + + err := from.ConvertTo(&to) + require.NoError(t, err) + + compareOpenTelemetryTemplateSpec(t, from.Spec.Templates.OpenTelemetryCollector, to.Spec.Templates.OpenTelemetryCollector) + compareExtensionsExecutionControllerTemplateSpec(t, from.Spec.Templates.ExtensionExecutionController, to.Spec.Templates.ExtensionExecutionController) + + compareBase(t, from, to) + }) + + t.Run("migrate log-monitoring templates from v1beta3 to v1beta4", func(t *testing.T) { + from := getOldDynakubeBase() + from.Spec.Templates.LogMonitoring = getOldLogMonitoringTemplateSpec() + + to := dynakubev1beta4.DynaKube{} + + err := from.ConvertTo(&to) + require.NoError(t, err) + + compareLogMonitoringTemplateSpec(t, from.Spec.Templates.LogMonitoring, to.Spec.Templates.LogMonitoring) + compareBase(t, from, to) + }) + + t.Run("migrate kspm templates from v1beta4 to v1beta3", func(t *testing.T) { + from := getOldDynakubeBase() + from.Spec.Templates.KspmNodeConfigurationCollector = getOldNodeConfigurationCollectorTemplateSpec() + + to := dynakubev1beta4.DynaKube{} + + err := from.ConvertTo(&to) + require.NoError(t, err) + + compareNodeConfigurationCollectorTemplateSpec(t, from.Spec.Templates.KspmNodeConfigurationCollector, to.Spec.Templates.KspmNodeConfigurationCollector) + compareBase(t, from, to) + }) + + t.Run("migrate status from v1beta3 to v1beta4", func(t *testing.T) { + from := getOldDynakubeBase() + from.Status = getOldStatus() + to := dynakubev1beta4.DynaKube{} + + err := from.ConvertTo(&to) + require.NoError(t, err) + + compareStatus(t, from.Status, to.Status) + }) + + t.Run("migrate hostGroup", func(t *testing.T) { + from := getOldDynakubeBase() + from.Status = getOldStatus() + to := dynakubev1beta4.DynaKube{} + + err := from.ConvertTo(&to) + require.NoError(t, err) + + assert.Equal(t, from.Spec.OneAgent.HostGroup, to.Spec.OneAgent.HostGroup) + }) +} + +func getTestNamespaceSelector() metav1.LabelSelector { + return metav1.LabelSelector{ + MatchLabels: map[string]string{ + "match-label-key": "match-label-value", + }, + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "match-expression-key", + Operator: "In", + Values: []string{"match-expression-value-test-1", "match-expression-value-test-2"}, + }, + }, + } +} + +func getOldDynakubeBase() DynaKube { + return DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Name: "name", + Namespace: "namespace", + Annotations: map[string]string{ + dynakubev1beta4.AnnotationFeatureActiveGateIgnoreProxy: "true", //nolint:staticcheck + dynakubev1beta4.AnnotationFeatureAutomaticK8sApiMonitoring: "true", + }, + Labels: map[string]string{ + "label": "label-value", + }, + }, + Spec: DynaKubeSpec{ + OneAgent: oneagent.Spec{HostGroup: "hostgroup-value"}, + APIURL: "api-url", + Tokens: "token", + CustomPullSecret: "pull-secret", + EnableIstio: true, + SkipCertCheck: true, + Proxy: &value.Source{ + Value: "proxy-value", + ValueFrom: "proxy-from", + }, + TrustedCAs: "trusted-ca", + NetworkZone: "network-zone", + DynatraceApiRequestThreshold: ptr.To(uint16(42)), + MetadataEnrichment: MetadataEnrichment{ + Enabled: ptr.To(false), + }, + }, + } +} + +func getOldHostInjectSpec() oneagent.HostInjectSpec { + return oneagent.HostInjectSpec{ + Version: "host-inject-version", + Image: "host-inject-image", + Tolerations: []corev1.Toleration{ + {Key: "host-inject-toleration-key", Operator: "In", Value: "host-inject-toleration-value"}, + }, + AutoUpdate: ptr.To(false), + DNSPolicy: corev1.DNSClusterFirstWithHostNet, + Annotations: map[string]string{ + "host-inject-annotation-key": "host-inject-annotation-value", + }, + Labels: map[string]string{ + "host-inject-label-key": "host-inject-label-value", + }, + Env: []corev1.EnvVar{ + {Name: "host-inject-env-1", Value: "host-inject-env-value-1", ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + Key: "host-inject-env-from-1", + }, + }}, + {Name: "host-inject-env-2", Value: "host-inject-env-value-2", ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + Key: "host-inject-env-from-2", + }, + }}, + }, + NodeSelector: map[string]string{ + "host-inject-node-selector-key": "host-inject-node-selector-value", + }, + PriorityClassName: "host-inject-priority-class", + Args: []string{ + "host-inject-arg-1", + "host-inject-arg-2", + }, + OneAgentResources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewScaledQuantity(1, 1)}, + }, + SecCompProfile: "seccomp", + } +} + +func getOldAppInjectionSpec() oneagent.AppInjectionSpec { + return oneagent.AppInjectionSpec{ + InitResources: &corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewScaledQuantity(2, 1)}, + }, + CodeModulesImage: "app-injection-image", + NamespaceSelector: getTestNamespaceSelector(), + } +} + +func getOldCloudNativeSpec() oneagent.CloudNativeFullStackSpec { + return oneagent.CloudNativeFullStackSpec{ + AppInjectionSpec: getOldAppInjectionSpec(), + HostInjectSpec: getOldHostInjectSpec(), + } +} + +func getOldApplicationMonitoringSpec() oneagent.ApplicationMonitoringSpec { + return oneagent.ApplicationMonitoringSpec{ + AppInjectionSpec: getOldAppInjectionSpec(), + Version: "app-monitoring-version", + } +} + +func getOldActiveGateSpec() activegate.Spec { + return activegate.Spec{ + DNSPolicy: corev1.DNSClusterFirstWithHostNet, + Annotations: map[string]string{ + "activegate-annotation-key": "activegate-annotation-value", + }, + TlsSecretName: "activegate-tls-secret-name", + PriorityClassName: "activegate-priority-class-name", + Capabilities: []activegate.CapabilityDisplayName{ + activegate.DynatraceApiCapability.DisplayName, + activegate.KubeMonCapability.DisplayName, + activegate.MetricsIngestCapability.DisplayName, + }, + CapabilityProperties: activegate.CapabilityProperties{ + Labels: map[string]string{ + "activegate-label-key": "activegate-label-value", + }, + Env: []corev1.EnvVar{ + {Name: "host-inject-env-1", Value: "activegate-env-value-1", ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + Key: "activegate-env-from-1", + }, + }}, + {Name: "activegate-env-2", Value: "activegate-env-value-2", ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + Key: "activegate-env-from-2", + }, + }}, + }, + NodeSelector: map[string]string{ + "activegate-node-selector-key": "activegate-node-selector-value", + }, + Image: "activegate-image", + Replicas: ptr.To(int32(42)), + Group: "activegate-group", + CustomProperties: &value.Source{ + Value: "activegate-cp-value", + ValueFrom: "activegate-cp-value-from", + }, + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewScaledQuantity(3, 1)}, + }, + Tolerations: []corev1.Toleration{ + {Key: "activegate-toleration-key", Operator: "In", Value: "activegate-toleration-value"}, + }, + TopologySpreadConstraints: []corev1.TopologySpreadConstraint{ + {MaxSkew: 1}, + }, + }, + } +} + +func getOldLogMonitoringSpec() *logmonitoring.Spec { + oldSpec := logmonitoring.Spec{ + IngestRuleMatchers: make([]logmonitoring.IngestRuleMatchers, 0), + } + + oldSpec.IngestRuleMatchers = append(oldSpec.IngestRuleMatchers, logmonitoring.IngestRuleMatchers{ + Attribute: "attribute1", + Values: []string{"matcher1", "matcher2", "matcher3"}, + }) + + oldSpec.IngestRuleMatchers = append(oldSpec.IngestRuleMatchers, logmonitoring.IngestRuleMatchers{ + Attribute: "attribute2", + Values: []string{"matcher1", "matcher2", "matcher3"}, + }) + + oldSpec.IngestRuleMatchers = append(oldSpec.IngestRuleMatchers, logmonitoring.IngestRuleMatchers{ + Attribute: "attribute3", + Values: []string{"matcher1", "matcher2", "matcher3"}, + }) + + return &oldSpec +} + +func getOldOpenTelemetryTemplateSpec() OpenTelemetryCollectorSpec { + return OpenTelemetryCollectorSpec{ + Labels: map[string]string{ + "otelc-label-key1": "otelc-label-value1", + "otelc-label-key2": "otelc-label-value2", + }, + Annotations: map[string]string{ + "otelc-annotation-key1": "otelc-annotation-value1", + "otelc-annotation-key2": "otelc-annotation-value2", + }, + Replicas: ptr.To(int32(42)), + ImageRef: image.Ref{ + Repository: "image-repo.repohost.test/repo", + Tag: "image-tag", + }, + TlsRefName: "tls-ref-name", + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewScaledQuantity(3, 1), + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewScaledQuantity(3, 1), + }, + Claims: []corev1.ResourceClaim{{ + Name: "claim-name", + Request: "claim-request", + }}, + }, + Tolerations: []corev1.Toleration{ + {Key: "otelc-toleration-key", Operator: "In", Value: "otelc-toleration-value"}, + }, + TopologySpreadConstraints: []corev1.TopologySpreadConstraint{ + {MaxSkew: 1}, + }, + } +} + +func getOldExtensionExecutionControllerSpec() ExtensionExecutionControllerSpec { + return ExtensionExecutionControllerSpec{ + PersistentVolumeClaim: getPersistentVolumeClaimSpec(), + Labels: map[string]string{ + "eec-label-key1": "eec-label-value1", + "eec-label-key2": "eec-label-value2", + }, + Annotations: map[string]string{ + "eec-annotation-key1": "eec-annotation-value1", + "eec-annotation-key2": "eec-annotation-value2", + }, + ImageRef: image.Ref{ + Repository: "image-repo.repohost.test/repo", + Tag: "image-tag", + }, + TlsRefName: "tls-ref-name", + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewScaledQuantity(3, 1), + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewScaledQuantity(3, 1), + }, + Claims: []corev1.ResourceClaim{{ + Name: "claim-name", + Request: "claim-request", + }}, + }, + Tolerations: []corev1.Toleration{ + {Key: "otelc-toleration-key", Operator: "In", Value: "otelc-toleration-value"}, + }, + TopologySpreadConstraints: []corev1.TopologySpreadConstraint{ + {MaxSkew: 1}, + }, + CustomConfig: "custom-eec-config", + CustomExtensionCertificates: "custom-eec-certificates", + UseEphemeralVolume: true, + } +} + +func getOldLogMonitoringTemplateSpec() *logmonitoring.TemplateSpec { + return &logmonitoring.TemplateSpec{ + Labels: map[string]string{ + "logagent-label-key1": "logagent-label-value1", + "logagent-label-key2": "logagent-label-value2", + }, + Annotations: map[string]string{ + "logagent-annotation-key1": "logagent-annotation-value1", + "logagent-annotation-key2": "logagent-annotation-value2", + }, + NodeSelector: map[string]string{ + "selector1": "node1", + "selector2": "node2", + }, + ImageRef: image.Ref{ + Repository: "image-repo.repohost.test/repo", + Tag: "image-tag", + }, + DNSPolicy: "dns-policy", + PriorityClassName: "priority-class-name", + SecCompProfile: "sec-comp-profile", + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewScaledQuantity(3, 1), + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewScaledQuantity(3, 1), + }, + Claims: []corev1.ResourceClaim{{ + Name: "claim-name", + Request: "claim-request", + }}, + }, + Tolerations: []corev1.Toleration{ + {Key: "otelc-toleration-key", Operator: "In", Value: "otelc-toleration-value"}, + }, + Args: []string{"--log-level", "debug", "--log-format", "json"}, + } +} + +func getOldNodeConfigurationCollectorTemplateSpec() kspm.NodeConfigurationCollectorSpec { + return kspm.NodeConfigurationCollectorSpec{ + UpdateStrategy: &v1.DaemonSetUpdateStrategy{ + Type: "daemonset-update-strategy-type", + RollingUpdate: &v1.RollingUpdateDaemonSet{ + MaxUnavailable: &intstr.IntOrString{ + Type: 0, + IntVal: 42, + }, + MaxSurge: &intstr.IntOrString{ + Type: 1, + StrVal: "42", + }, + }, + }, + Labels: map[string]string{ + "ncc-label-key1": "ncc-label-value1", + "ncc-label-key2": "ncc-label-value2", + }, + Annotations: map[string]string{ + "ncc-annotation-key1": "ncc-annotation-value1", + "ncc-annotation-key2": "ncc-annotation-value2", + }, + NodeSelector: map[string]string{ + "selector1": "node1", + "selector2": "node2", + }, + ImageRef: image.Ref{ + Repository: "image-repo.repohost.test/repo", + Tag: "image-tag", + }, + PriorityClassName: "priority-class-name", + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewScaledQuantity(3, 1), + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewScaledQuantity(3, 1), + }, + Claims: []corev1.ResourceClaim{{ + Name: "claim-name", + Request: "claim-request", + }}, + }, + NodeAffinity: corev1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{ + NodeSelectorTerms: []corev1.NodeSelectorTerm{{ + MatchExpressions: []corev1.NodeSelectorRequirement{ + { + Key: "node-selector-match-key", + Operator: "node-selector-match-operator", + Values: []string{"node-match-value-1", "node-match-value2"}, + }, + }, + MatchFields: []corev1.NodeSelectorRequirement{ + { + Key: "node-selector-field-key", + Operator: "node-selector-field-operator", + Values: []string{"node-field-value-1", "node-field-value2"}, + }, + }, + }}, + }, + PreferredDuringSchedulingIgnoredDuringExecution: nil, + }, + Tolerations: []corev1.Toleration{ + {Key: "otelc-toleration-key", Operator: "In", Value: "otelc-toleration-value"}, + }, + Args: []string{"--log-level", "debug", "--log-format", "json"}, + Env: []corev1.EnvVar{ + { + Name: "ENV1", + Value: "VAL1", + }, + { + Name: "ENV2", + Value: "VAL2", + }, + { + Name: "ENV2", + Value: "VAL2", + }, + }, + } +} + +func getOldStatus() DynaKubeStatus { + return DynaKubeStatus{ + OneAgent: oneagent.Status{ + VersionStatus: status.VersionStatus{ + ImageID: "oa-image-id", + Version: "oa-version", + Type: "oa-image-type", + Source: status.CustomImageVersionSource, + LastProbeTimestamp: &testTime, + }, + Instances: map[string]oneagent.Instance{ + "oa-instance-key-1": { + PodName: "oa-instance-pod-1", + IPAddress: "oa-instance-ip-1", + }, + "oa-instance-key-2": { + PodName: "oa-instance-pod-2", + IPAddress: "oa-instance-ip-2", + }, + }, + LastInstanceStatusUpdate: &testTime, + Healthcheck: ®istryv1.HealthConfig{ + Test: []string{"oa-health-check-test"}, + }, + ConnectionInfoStatus: oneagent.ConnectionInfoStatus{ + ConnectionInfo: communication.ConnectionInfo{ + LastRequest: testTime, + TenantUUID: "oa-tenant-uuid", + Endpoints: "oa-endpoints", + }, + CommunicationHosts: []oneagent.CommunicationHostStatus{ + { + Protocol: "oa-protocol-1", + Host: "oa-host-1", + Port: 1, + }, + { + Protocol: "oa-protocol-2", + Host: "oa-host-2", + Port: 2, + }, + }, + }, + }, + ActiveGate: activegate.Status{ + VersionStatus: status.VersionStatus{ + ImageID: "ag-image-id", + Version: "ag-version", + Type: "ag-image-type", + Source: status.CustomVersionVersionSource, + LastProbeTimestamp: &testTime, + }, + }, + CodeModules: oneagent.CodeModulesStatus{ + VersionStatus: status.VersionStatus{ + ImageID: "cm-image-id", + Version: "cm-version", + Type: "cm-image-type", + Source: status.TenantRegistryVersionSource, + LastProbeTimestamp: &testTime, + }, + }, + DynatraceApi: DynatraceApiStatus{ + LastTokenScopeRequest: testTime, + }, + Conditions: []metav1.Condition{ + { + Type: "condition-type-1", + Status: "condition-status-1", + Reason: "condition-reason-1", + LastTransitionTime: testTime, + }, + { + Type: "condition-type-2", + Status: "condition-status-2", + Reason: "condition-reason-2", + LastTransitionTime: testTime, + }, + }, + KubeSystemUUID: "kube-system-uuid", + Phase: status.Deploying, + UpdatedTimestamp: testTime, + } +} diff --git a/pkg/api/v1beta3/dynakube/dynakube_props.go b/pkg/api/v1beta3/dynakube/dynakube_props.go index 26b9aca5ca..66b73ad319 100644 --- a/pkg/api/v1beta3/dynakube/dynakube_props.go +++ b/pkg/api/v1beta3/dynakube/dynakube_props.go @@ -2,7 +2,6 @@ package dynakube import ( "net/url" - "strings" "time" "github.com/Dynatrace/dynatrace-operator/pkg/util/timeprovider" @@ -80,37 +79,7 @@ func (dk *DynaKube) Tokens() string { return dk.Name } -func (dk *DynaKube) TenantUUIDFromApiUrl() (string, error) { - return tenantUUID(dk.ApiUrl()) -} - -func runeIs(wanted rune) func(rune) bool { - return func(actual rune) bool { - return actual == wanted - } -} - -func tenantUUID(apiUrl string) (string, error) { - parsedUrl, err := url.Parse(apiUrl) - if err != nil { - return "", errors.WithMessagef(err, "problem parsing tenant id from url %s", apiUrl) - } - - // Path = "/e//api" -> ["e", "", "api"] - subPaths := strings.FieldsFunc(parsedUrl.Path, runeIs('/')) - if len(subPaths) >= 3 && subPaths[0] == "e" && subPaths[2] == "api" { - return subPaths[1], nil - } - - hostnameWithDomains := strings.FieldsFunc(parsedUrl.Hostname(), runeIs('.')) - if len(hostnameWithDomains) >= 1 { - return hostnameWithDomains[0], nil - } - - return "", errors.Errorf("problem getting tenant id from API URL '%s'", apiUrl) -} - -func (dk *DynaKube) TenantUUIDFromConnectionInfoStatus() (string, error) { +func (dk *DynaKube) TenantUUID() (string, error) { if dk.Status.OneAgent.ConnectionInfoStatus.TenantUUID != "" { return dk.Status.OneAgent.ConnectionInfoStatus.TenantUUID, nil } else if dk.Status.ActiveGate.ConnectionInfo.TenantUUID != "" { diff --git a/pkg/api/v1beta3/dynakube/dynakube_props_test.go b/pkg/api/v1beta3/dynakube/dynakube_props_test.go index 592db3d418..bb470f80c9 100644 --- a/pkg/api/v1beta3/dynakube/dynakube_props_test.go +++ b/pkg/api/v1beta3/dynakube/dynakube_props_test.go @@ -20,15 +20,12 @@ import ( "testing" "time" - "github.com/Dynatrace/dynatrace-operator/pkg/util/address" "github.com/Dynatrace/dynatrace-operator/pkg/util/timeprovider" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" ) -const testAPIURL = "http://test-endpoint/api" - func TestTokens(t *testing.T) { testName := "test-name" testValue := "test-value" @@ -46,74 +43,6 @@ func TestTokens(t *testing.T) { }) } -func TestTenantUUID(t *testing.T) { - t.Run("happy path", func(t *testing.T) { - apiUrl := "https://demo.dev.dynatracelabs.com/api" - expectedTenantId := "demo" - - actualTenantId, err := tenantUUID(apiUrl) - - require.NoErrorf(t, err, "Expected that getting tenant id from '%s' will be successful", apiUrl) - assert.Equalf(t, expectedTenantId, actualTenantId, "Expected that tenant id of %s is %s, but found %s", - apiUrl, expectedTenantId, actualTenantId, - ) - }) - - t.Run("happy path (alternative)", func(t *testing.T) { - apiUrl := "https://dynakube-activegate.dynatrace/e/tenant/api/v2/metrics/ingest" - expectedTenantId := "tenant" - - actualTenantId, err := tenantUUID(apiUrl) - - require.NoErrorf(t, err, "Expected that getting tenant id from '%s' will be successful", apiUrl) - assert.Equalf(t, expectedTenantId, actualTenantId, "Expected that tenant id of %s is %s, but found %s", - apiUrl, expectedTenantId, actualTenantId, - ) - }) - - t.Run("happy path (alternative, no domain)", func(t *testing.T) { - apiUrl := "https://dynakube-activegate/e/tenant/api/v2/metrics/ingest" - expectedTenantId := "tenant" - - actualTenantId, err := tenantUUID(apiUrl) - - require.NoErrorf(t, err, "Expected that getting tenant id from '%s' will be successful", apiUrl) - assert.Equalf(t, expectedTenantId, actualTenantId, "Expected that tenant id of %s is %s, but found %s", - apiUrl, expectedTenantId, actualTenantId, - ) - }) - - t.Run("missing API URL protocol", func(t *testing.T) { - apiUrl := "demo.dev.dynatracelabs.com/api" - expectedTenantId := "" - expectedError := "problem getting tenant id from API URL 'demo.dev.dynatracelabs.com/api'" - - actualTenantId, err := tenantUUID(apiUrl) - - require.EqualErrorf(t, err, expectedError, "Expected that getting tenant id from '%s' will result in: '%v'", - apiUrl, expectedError, - ) - assert.Equalf(t, expectedTenantId, actualTenantId, "Expected that tenant id of %s is %s, but found %s", - apiUrl, expectedTenantId, actualTenantId, - ) - }) - - t.Run("suffix-only, relative API URL", func(t *testing.T) { - apiUrl := "/api" - expectedTenantId := "" - expectedError := "problem getting tenant id from API URL '/api'" - - actualTenantId, err := tenantUUID(apiUrl) - - require.EqualErrorf(t, err, expectedError, "Expected that getting tenant id from '%s' will result in: '%v'", - apiUrl, expectedError, - ) - assert.Equalf(t, expectedTenantId, actualTenantId, "Expected that tenant id of %s is %s, but found %s", - apiUrl, expectedTenantId, actualTenantId, - ) - }) -} - func TestIsTokenScopeVerificationAllowed(t *testing.T) { dk := DynaKube{ Status: DynaKubeStatus{ @@ -142,37 +71,37 @@ func TestIsTokenScopeVerificationAllowed(t *testing.T) { "Do not update after 3 minutes using 5m interval": { lastRequestTimeDeltaMinutes: -3, updateExpected: false, - threshold: address.Of(uint16(5)), + threshold: ptr.To(uint16(5)), }, "Do update after 7 minutes using 5m interval": { lastRequestTimeDeltaMinutes: -7, updateExpected: true, - threshold: address.Of(uint16(5)), + threshold: ptr.To(uint16(5)), }, "Do not update after 17 minutes using 20m interval": { lastRequestTimeDeltaMinutes: -17, updateExpected: false, - threshold: address.Of(uint16(20)), + threshold: ptr.To(uint16(20)), }, "Do update after 22 minutes using 20m interval": { lastRequestTimeDeltaMinutes: -22, updateExpected: true, - threshold: address.Of(uint16(20)), + threshold: ptr.To(uint16(20)), }, "Do update immediately using 0m interval": { lastRequestTimeDeltaMinutes: 0, updateExpected: true, - threshold: address.Of(uint16(0)), + threshold: ptr.To(uint16(0)), }, "Do update after 1 minute using 0m interval": { lastRequestTimeDeltaMinutes: -1, updateExpected: true, - threshold: address.Of(uint16(0)), + threshold: ptr.To(uint16(0)), }, "Do update after 20 minutes using 0m interval": { lastRequestTimeDeltaMinutes: -20, updateExpected: true, - threshold: address.Of(uint16(0)), + threshold: ptr.To(uint16(0)), }, } diff --git a/pkg/api/v1beta3/dynakube/dynakube_status.go b/pkg/api/v1beta3/dynakube/dynakube_status.go index 4a36247b7d..15dc8a585a 100644 --- a/pkg/api/v1beta3/dynakube/dynakube_status.go +++ b/pkg/api/v1beta3/dynakube/dynakube_status.go @@ -2,14 +2,11 @@ package dynakube import ( "context" - "fmt" - "time" - "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/communication" "github.com/Dynatrace/dynatrace-operator/pkg/api/status" "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/kspm" - containerv1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/oneagent" "github.com/pkg/errors" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -21,13 +18,13 @@ import ( type DynaKubeStatus struct { //nolint:revive // Observed state of OneAgent - OneAgent OneAgentStatus `json:"oneAgent,omitempty"` + OneAgent oneagent.Status `json:"oneAgent,omitempty"` // Observed state of ActiveGate ActiveGate activegate.Status `json:"activeGate,omitempty"` // Observed state of Code Modules - CodeModules CodeModulesStatus `json:"codeModules,omitempty"` + CodeModules oneagent.CodeModulesStatus `json:"codeModules,omitempty"` // Observed state of Metadata-Enrichment MetadataEnrichment MetadataEnrichmentStatus `json:"metadataEnrichment,omitempty"` @@ -65,65 +62,6 @@ type DynatraceApiStatus struct { LastTokenScopeRequest metav1.Time `json:"lastTokenScopeRequest,omitempty"` } -func GetCacheValidMessage(functionName string, lastRequestTimestamp metav1.Time, timeout time.Duration) string { - remaining := timeout - time.Since(lastRequestTimestamp.Time) - - return fmt.Sprintf("skipping %s, last request was made less than %d minutes ago, %d minutes remaining until next request", - functionName, - int(timeout.Minutes()), - int(remaining.Minutes())) -} - -type OneAgentConnectionInfoStatus struct { - // Information for communicating with the tenant - communication.ConnectionInfo `json:",inline"` - - // List of communication hosts - CommunicationHosts []CommunicationHostStatus `json:"communicationHosts,omitempty"` -} - -type CommunicationHostStatus struct { - // Connection protocol - Protocol string `json:"protocol,omitempty"` - - // Host domain - Host string `json:"host,omitempty"` - - // Connection port - Port uint32 `json:"port,omitempty"` -} - -type CodeModulesStatus struct { - status.VersionStatus `json:",inline"` -} - -type OneAgentStatus struct { - status.VersionStatus `json:",inline"` - - // List of deployed OneAgent instances - Instances map[string]OneAgentInstance `json:"instances,omitempty"` - - // Time of the last instance status update - LastInstanceStatusUpdate *metav1.Time `json:"lastInstanceStatusUpdate,omitempty"` - - // Commands used for OneAgent's readiness probe - // +kubebuilder:validation:Type=object - // +kubebuilder:validation:Schemaless - // +kubebuilder:pruning:PreserveUnknownFields - Healthcheck *containerv1.HealthConfig `json:"healthcheck,omitempty"` - - // Information about OneAgent's connections - ConnectionInfoStatus OneAgentConnectionInfoStatus `json:"connectionInfoStatus,omitempty"` -} - -type OneAgentInstance struct { - // Name of the OneAgent pod - PodName string `json:"podName,omitempty"` - - // IP address of the pod - IPAddress string `json:"ipAddress,omitempty"` -} - type EnrichmentRuleType string const ( diff --git a/pkg/api/v1beta3/dynakube/dynakube_types.go b/pkg/api/v1beta3/dynakube/dynakube_types.go index ac62f83857..0775e188bb 100644 --- a/pkg/api/v1beta3/dynakube/dynakube_types.go +++ b/pkg/api/v1beta3/dynakube/dynakube_types.go @@ -9,6 +9,7 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/kspm" "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/logmonitoring" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/oneagent" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -40,7 +41,6 @@ const ( // DynaKube is the Schema for the DynaKube API // +k8s:openapi-gen=true -// +kubebuilder:storageversion // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +kubebuilder:resource:path=dynakubes,scope=Namespaced,categories=dynatrace,shortName={dk,dks} @@ -94,7 +94,7 @@ type DynaKubeSpec struct { //nolint:revive // General configuration about OneAgent instances. // You can't enable more than one module (classicFullStack, cloudNativeFullStack, hostMonitoring, or applicationMonitoring). // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="OneAgent",xDescriptors="urn:alm:descriptor:com.tectonic.ui:text" - OneAgent OneAgentSpec `json:"oneAgent,omitempty"` + OneAgent oneagent.Spec `json:"oneAgent,omitempty"` // Dynatrace apiUrl, including the /api path at the end. For SaaS, set YOUR_ENVIRONMENT_ID to your environment ID. For Managed, change the apiUrl address. // For instructions on how to determine the environment ID and how to configure the apiUrl address, see Environment ID (https://www.dynatrace.com/support/help/get-started/monitoring-environment/environment-id). @@ -154,7 +154,7 @@ type TemplatesSpec struct { // +kubebuilder:validation:Optional KspmNodeConfigurationCollector kspm.NodeConfigurationCollectorSpec `json:"kspmNodeConfigurationCollector,omitempty"` // +kubebuilder:validation:Optional - OpenTelemetryCollector OpenTelemetryCollectorSpec `json:"openTelemetryCollector,omitempty"` + OpenTelemetryCollector OpenTelemetryCollectorSpec `json:"otelCollector,omitempty"` // +kubebuilder:validation:Optional ExtensionExecutionController ExtensionExecutionControllerSpec `json:"extensionExecutionController,omitempty"` } diff --git a/pkg/api/v1beta3/dynakube/extensions_props.go b/pkg/api/v1beta3/dynakube/extensions_props.go index 53601efc32..acdd7a3911 100644 --- a/pkg/api/v1beta3/dynakube/extensions_props.go +++ b/pkg/api/v1beta3/dynakube/extensions_props.go @@ -1,6 +1,8 @@ package dynakube -import "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/extension/consts" +import ( + "github.com/Dynatrace/dynatrace-operator/pkg/consts" +) func (dk *DynaKube) IsExtensionsEnabled() bool { return dk.Spec.Extensions != nil @@ -16,20 +18,32 @@ func (dk *DynaKube) ExtensionsNeedsSelfSignedTLS() bool { func (dk *DynaKube) ExtensionsTLSSecretName() string { if dk.ExtensionsNeedsSelfSignedTLS() { - return dk.Name + consts.ExtensionsSelfSignedTLSSecretSuffix + return dk.ExtensionsSelfSignedTLSSecretName() } return dk.ExtensionsTLSRefName() } -func (dk *DynaKube) ExtensionsExecutionControllerStatefulsetName() string { - return dk.Name + "-extensions-controller" +func (dk *DynaKube) ExtensionsSelfSignedTLSSecretName() string { + return dk.Name + consts.ExtensionsSelfSignedTLSSecretSuffix } -func (dk *DynaKube) ExtensionsCollectorStatefulsetName() string { - return dk.Name + "-extensions-collector" +func (dk *DynaKube) ExtensionsExecutionControllerStatefulsetName() string { + return dk.Name + "-extensions-controller" } func (dk *DynaKube) ExtensionsTokenSecretName() string { return dk.Name + "-extensions-token" } + +func (dk *DynaKube) ExtensionsPortName() string { + return "dynatrace" + consts.ExtensionsControllerSuffix + "-" + consts.ExtensionsCollectorTargetPortName +} + +func (dk *DynaKube) ExtensionsServiceNameFQDN() string { + return dk.ExtensionsServiceName() + "." + dk.Namespace +} + +func (dk *DynaKube) ExtensionsServiceName() string { + return dk.Name + consts.ExtensionsControllerSuffix +} diff --git a/pkg/api/v1beta3/dynakube/feature_flags.go b/pkg/api/v1beta3/dynakube/feature_flags.go index 94f0a741fd..8464fe7099 100644 --- a/pkg/api/v1beta3/dynakube/feature_flags.go +++ b/pkg/api/v1beta3/dynakube/feature_flags.go @@ -46,6 +46,8 @@ const ( AnnotationFeatureAutomaticK8sApiMonitoringClusterName = AnnotationFeaturePrefix + "automatic-kubernetes-api-monitoring-cluster-name" AnnotationFeatureK8sAppEnabled = AnnotationFeaturePrefix + "k8s-app-enabled" + AnnotationFeatureActiveGateAutomaticTLSCertificate = AnnotationFeaturePrefix + "automatic-tls-certificate" + // dtClient. AnnotationFeatureNoProxy = AnnotationFeaturePrefix + "no-proxy" @@ -58,6 +60,7 @@ const ( AnnotationFeatureOneAgentMaxUnavailable = AnnotationFeaturePrefix + "oneagent-max-unavailable" AnnotationFeatureOneAgentInitialConnectRetry = AnnotationFeaturePrefix + "oneagent-initial-connect-retry-ms" AnnotationFeatureRunOneAgentContainerPrivileged = AnnotationFeaturePrefix + "oneagent-privileged" + AnnotationFeatureOneAgentSkipLivenessProbe = AnnotationFeaturePrefix + "oneagent-skip-liveness-probe" AnnotationFeatureIgnoreUnknownState = AnnotationFeaturePrefix + "ignore-unknown-state" AnnotationFeatureIgnoredNamespaces = AnnotationFeaturePrefix + "ignored-namespaces" @@ -126,6 +129,11 @@ func (dk *DynaKube) FeatureNoProxy() string { return dk.getFeatureFlagRaw(AnnotationFeatureNoProxy) } +// FeatureActiveGateAutomaticTLSCertificate is a feature flag to disable automatic creation of ActiveGate TLS certificate. +func (dk *DynaKube) FeatureActiveGateAutomaticTLSCertificate() bool { + return dk.getFeatureFlagRaw(AnnotationFeatureActiveGateAutomaticTLSCertificate) != falsePhrase +} + // FeatureOneAgentMaxUnavailable is a feature flag to configure maxUnavailable on the OneAgent DaemonSets rolling upgrades. func (dk *DynaKube) FeatureOneAgentMaxUnavailable() int { return dk.getFeatureFlagInt(AnnotationFeatureOneAgentMaxUnavailable, 1) @@ -228,6 +236,10 @@ func (dk *DynaKube) FeatureOneAgentPrivileged() bool { return dk.getFeatureFlagRaw(AnnotationFeatureRunOneAgentContainerPrivileged) == truePhrase } +func (dk *DynaKube) FeatureOneAgentSkipLivenessProbe() bool { + return dk.getFeatureFlagRaw(AnnotationFeatureOneAgentSkipLivenessProbe) == truePhrase +} + func (dk *DynaKube) FeatureMaxFailedCsiMountAttempts() int { maxCsiMountAttemptsValue := dk.getFeatureFlagInt(AnnotationFeatureMaxFailedCsiMountAttempts, DefaultMaxFailedCsiMountAttempts) if maxCsiMountAttemptsValue < 0 { diff --git a/pkg/api/v1beta3/dynakube/feature_flags_test.go b/pkg/api/v1beta3/dynakube/feature_flags_test.go index dabab4eea2..6d2692cfbb 100644 --- a/pkg/api/v1beta3/dynakube/feature_flags_test.go +++ b/pkg/api/v1beta3/dynakube/feature_flags_test.go @@ -289,3 +289,33 @@ func TestAgentInitialConnectRetry(t *testing.T) { require.Equal(t, 5, initialRetry) }) } + +func TestIsOneAgentPrivileged(t *testing.T) { + t.Run("is false by default", func(t *testing.T) { + dk := DynaKube{} + + assert.False(t, dk.FeatureOneAgentPrivileged()) + }) + t.Run("is true when annotation is set to true", func(t *testing.T) { + dk := DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + AnnotationFeatureRunOneAgentContainerPrivileged: "true", + }, + }, + } + + assert.True(t, dk.FeatureOneAgentPrivileged()) + }) + t.Run("is false when annotation is set to false", func(t *testing.T) { + dk := DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + AnnotationFeatureRunOneAgentContainerPrivileged: "false", + }, + }, + } + + assert.False(t, dk.FeatureOneAgentPrivileged()) + }) +} diff --git a/pkg/api/v1beta3/dynakube/logmonitoring/props.go b/pkg/api/v1beta3/dynakube/logmonitoring/props.go index 9716b9d84d..3b014065c2 100644 --- a/pkg/api/v1beta3/dynakube/logmonitoring/props.go +++ b/pkg/api/v1beta3/dynakube/logmonitoring/props.go @@ -24,6 +24,14 @@ func (lm *LogMonitoring) IsStandalone() bool { return lm.IsEnabled() && !lm.enabledDependencies.hostAgents } +func (lm *LogMonitoring) GetNodeSelector() map[string]string { + if lm.IsStandalone() && lm.TemplateSpec != nil { + return lm.TemplateSpec.NodeSelector + } + + return nil +} + // Template is a nil-safe way to access the underlying TemplateSpec. func (lm *LogMonitoring) Template() TemplateSpec { if lm.TemplateSpec == nil { diff --git a/pkg/api/v1beta3/dynakube/logmonitoring_props.go b/pkg/api/v1beta3/dynakube/logmonitoring_props.go index c7fc6df26d..af3c8cf036 100644 --- a/pkg/api/v1beta3/dynakube/logmonitoring_props.go +++ b/pkg/api/v1beta3/dynakube/logmonitoring_props.go @@ -10,7 +10,7 @@ func (dk *DynaKube) LogMonitoring() *logmonitoring.LogMonitoring { TemplateSpec: dk.Spec.Templates.LogMonitoring, } lm.SetName(dk.Name) - lm.SetHostAgentDependency(dk.NeedsOneAgent()) + lm.SetHostAgentDependency(dk.OneAgent().IsDaemonsetRequired()) return lm } diff --git a/pkg/api/v1beta3/dynakube/oneagent/props.go b/pkg/api/v1beta3/dynakube/oneagent/props.go new file mode 100644 index 0000000000..3e65f767cd --- /dev/null +++ b/pkg/api/v1beta3/dynakube/oneagent/props.go @@ -0,0 +1,327 @@ +package oneagent + +import ( + "fmt" + "strings" + + "github.com/Dynatrace/dynatrace-operator/pkg/api" + "github.com/Dynatrace/dynatrace-operator/pkg/util/dtversion" + "github.com/Dynatrace/dynatrace-operator/pkg/util/installconfig" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + OneAgentTenantSecretSuffix = "-oneagent-tenant-secret" + OneAgentConnectionInfoConfigMapSuffix = "-oneagent-connection-info" + PodNameOsAgent = "oneagent" + DefaultOneAgentImageRegistrySubPath = "/linux/oneagent" +) + +func NewOneAgent(spec *Spec, status *Status, codeModulesStatus *CodeModulesStatus, //nolint:revive + name, apiUrlHost string, + featureOneAgentPrivileged, featureOneAgentSkipLivenessProbe bool) *OneAgent { + return &OneAgent{ + Spec: spec, + Status: status, + CodeModulesStatus: codeModulesStatus, + + name: name, + apiUrlHost: apiUrlHost, + + featureOneAgentPrivileged: featureOneAgentPrivileged, + featureOneAgentSkipLivenessProbe: featureOneAgentSkipLivenessProbe, + } +} + +func (oa *OneAgent) IsCSIAvailable() bool { + return installconfig.GetModules().CSIDriver +} + +// IsApplicationMonitoringMode returns true when application only section is used. +func (oa *OneAgent) IsApplicationMonitoringMode() bool { + return oa.ApplicationMonitoring != nil +} + +// IsCloudNativeFullstackMode returns true when cloud native fullstack section is used. +func (oa *OneAgent) IsCloudNativeFullstackMode() bool { + return oa.CloudNativeFullStack != nil +} + +// IsHostMonitoringMode returns true when host monitoring section is used. +func (oa *OneAgent) IsHostMonitoringMode() bool { + return oa.HostMonitoring != nil +} + +// IsClassicFullStackMode returns true when classic fullstack section is used. +func (oa *OneAgent) IsClassicFullStackMode() bool { + return oa.ClassicFullStack != nil +} + +// IsDaemonsetRequired returns true when a feature requires OneAgent instances. +func (oa *OneAgent) IsDaemonsetRequired() bool { + return oa.IsClassicFullStackMode() || oa.IsCloudNativeFullstackMode() || oa.IsHostMonitoringMode() +} + +func (oa *OneAgent) GetDaemonsetName() string { + return fmt.Sprintf("%s-%s", oa.name, PodNameOsAgent) +} + +func (oa *OneAgent) IsPrivilegedNeeded() bool { + return oa.featureOneAgentPrivileged +} + +func (oa *OneAgent) IsReadinessProbeNeeded() bool { + return oa.Healthcheck != nil +} + +func (oa *OneAgent) IsLivenessProbeNeeded() bool { + return oa.Healthcheck != nil && !oa.featureOneAgentSkipLivenessProbe +} + +// IsAutoUpdateEnabled returns true if the Operator should update OneAgent instances automatically. +func (oa *OneAgent) IsAutoUpdateEnabled() bool { + switch { + case oa.IsCloudNativeFullstackMode(): + return oa.CloudNativeFullStack.AutoUpdate == nil || *oa.CloudNativeFullStack.AutoUpdate + case oa.IsHostMonitoringMode(): + return oa.HostMonitoring.AutoUpdate == nil || *oa.HostMonitoring.AutoUpdate + case oa.IsClassicFullStackMode(): + return oa.ClassicFullStack.AutoUpdate == nil || *oa.ClassicFullStack.AutoUpdate + default: + return false + } +} + +// GetTenantSecret returns the name of the secret containing the token for the OneAgent. +func (oa *OneAgent) GetTenantSecret() string { + return oa.name + OneAgentTenantSecretSuffix +} + +func (oa *OneAgent) GetConnectionInfoConfigMapName() string { + return oa.name + OneAgentConnectionInfoConfigMapSuffix +} + +func (oa *OneAgent) IsReadOnlyOneAgentsMode() bool { + return oa.IsCloudNativeFullstackMode() || (oa.IsHostMonitoringMode() && oa.IsCSIAvailable()) +} + +func (oa *OneAgent) IsAppInjectionNeeded() bool { + return oa.IsCloudNativeFullstackMode() || oa.IsApplicationMonitoringMode() +} + +func (oa *OneAgent) GetInitResources() *corev1.ResourceRequirements { + if oa.IsApplicationMonitoringMode() { + return oa.ApplicationMonitoring.InitResources + } else if oa.IsCloudNativeFullstackMode() { + return oa.CloudNativeFullStack.InitResources + } + + return nil +} + +func (oa *OneAgent) GetNamespaceSelector() *metav1.LabelSelector { + switch { + case oa.IsCloudNativeFullstackMode(): + return &oa.CloudNativeFullStack.NamespaceSelector + case oa.IsApplicationMonitoringMode(): + return &oa.ApplicationMonitoring.NamespaceSelector + } + + return nil +} + +func (oa *OneAgent) GetSecCompProfile() string { + switch { + case oa.IsCloudNativeFullstackMode(): + return oa.CloudNativeFullStack.SecCompProfile + case oa.IsHostMonitoringMode(): + return oa.HostMonitoring.SecCompProfile + case oa.IsClassicFullStackMode(): + return oa.ClassicFullStack.SecCompProfile + default: + return "" + } +} + +func (oa *OneAgent) GetNodeSelector(fallbackNodeSelector map[string]string) map[string]string { + switch { + case oa.IsClassicFullStackMode(): + return oa.ClassicFullStack.NodeSelector + case oa.IsHostMonitoringMode(): + return oa.HostMonitoring.NodeSelector + case oa.IsCloudNativeFullstackMode(): + return oa.CloudNativeFullStack.NodeSelector + } + + return fallbackNodeSelector +} + +// GetImage provides the image reference set in Status for the OneAgent. +// Format: repo@sha256:digest. +func (oa *OneAgent) GetImage() string { + return oa.Status.ImageID +} + +// GetVersion provides version set in Status for the OneAgent. +func (oa *OneAgent) GetVersion() string { + return oa.Status.Version +} + +// GetCustomVersion provides the version for the OneAgent provided in the Spec. +func (oa *OneAgent) GetCustomVersion() string { + switch { + case oa.IsClassicFullStackMode(): + return oa.ClassicFullStack.Version + case oa.IsCloudNativeFullstackMode(): + return oa.CloudNativeFullStack.Version + case oa.IsApplicationMonitoringMode(): + return oa.ApplicationMonitoring.Version + case oa.IsHostMonitoringMode(): + return oa.HostMonitoring.Version + } + + return "" +} + +// GetCustomImage provides the image reference for the OneAgent provided in the Spec. +func (oa *OneAgent) GetCustomImage() string { + switch { + case oa.IsClassicFullStackMode(): + return oa.ClassicFullStack.Image + case oa.IsHostMonitoringMode(): + return oa.HostMonitoring.Image + case oa.IsCloudNativeFullstackMode(): + return oa.CloudNativeFullStack.Image + } + + return "" +} + +// GetDefaultImage provides the image reference for the OneAgent from tenant registry. +func (oa *OneAgent) GetDefaultImage(version string) string { + if oa.apiUrlHost == "" { + return "" + } + + truncatedVersion := dtversion.ToImageTag(version) + tag := truncatedVersion + + if !strings.Contains(tag, api.RawTag) { + tag += "-" + api.RawTag + } + + return oa.apiUrlHost + DefaultOneAgentImageRegistrySubPath + ":" + tag +} + +func (oa *OneAgent) GetHostGroup() string { + if oa.HostGroup != "" { + return oa.HostGroup + } + + return oa.GetHostGroupAsParam() +} + +func (oa *OneAgent) GetArguments() []string { + switch { + case oa.IsCloudNativeFullstackMode() && oa.CloudNativeFullStack.Args != nil: + return oa.CloudNativeFullStack.Args + case oa.IsClassicFullStackMode() && oa.ClassicFullStack.Args != nil: + return oa.ClassicFullStack.Args + case oa.IsHostMonitoringMode() && oa.HostMonitoring.Args != nil: + return oa.HostMonitoring.Args + } + + return []string{} +} + +func (oa *OneAgent) GetHostGroupAsParam() string { + var hostGroup string + + args := oa.GetArguments() + + for _, arg := range args { + key, value := splitArg(arg) + if key == "--set-host-group" { + hostGroup = value + + break + } + } + + return hostGroup +} + +func splitArg(arg string) (string, string) { + key, value, found := strings.Cut(arg, "=") + if !found { + return arg, "" + } + + return key, value +} + +func (oa *OneAgent) IsCommunicationRouteClear() bool { + return len(oa.ConnectionInfoStatus.CommunicationHosts) > 0 +} + +func (oa *OneAgent) GetEnvironment() []corev1.EnvVar { + switch { + case oa.IsCloudNativeFullstackMode(): + return oa.CloudNativeFullStack.Env + case oa.IsClassicFullStackMode(): + return oa.ClassicFullStack.Env + case oa.IsHostMonitoringMode(): + return oa.HostMonitoring.Env + } + + return []corev1.EnvVar{} +} + +func (oa *OneAgent) GetEndpoints() string { + return oa.ConnectionInfoStatus.Endpoints +} + +// GetCustomCodeModulesImage provides the image reference for the CodeModules provided in the Spec. +func (oa *OneAgent) GetCustomCodeModulesImage() string { + if oa.IsCloudNativeFullstackMode() { + return oa.CloudNativeFullStack.CodeModulesImage + } else if oa.IsApplicationMonitoringMode() && oa.IsCSIAvailable() { + return oa.ApplicationMonitoring.CodeModulesImage + } + + return "" +} + +// GetCustomCodeModulesVersion provides the version for the CodeModules provided in the Spec. +func (oa *OneAgent) GetCustomCodeModulesVersion() string { + return oa.GetCustomVersion() +} + +// GetCodeModulesVersion provides version set in Status for the CodeModules. +func (oa *OneAgent) GetCodeModulesVersion() string { + return oa.CodeModulesStatus.Version +} + +// GetCodeModulesImage provides the image reference set in Status for the CodeModules. +// Format: repo@sha256:digest. +func (oa *OneAgent) GetCodeModulesImage() string { + return oa.CodeModulesStatus.ImageID +} + +func (oa *OneAgent) GetArgumentsMap() map[string][]string { + args := oa.GetArguments() + + argMap := make(map[string][]string) + + for _, arg := range args { + key, value := splitArg(arg) + if _, exists := argMap[key]; !exists { + argMap[key] = []string{value} + } else { + argMap[key] = append(argMap[key], value) + } + } + + return argMap +} diff --git a/pkg/api/v1beta3/dynakube/oneagent/props_test.go b/pkg/api/v1beta3/dynakube/oneagent/props_test.go new file mode 100644 index 0000000000..6dff931c89 --- /dev/null +++ b/pkg/api/v1beta3/dynakube/oneagent/props_test.go @@ -0,0 +1,386 @@ +/* +Copyright 2021 Dynatrace LLC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package oneagent + +import ( + "net/url" + "testing" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/status" + "github.com/Dynatrace/dynatrace-operator/pkg/util/installconfig" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" +) + +const testAPIURL = "http://test-endpoint/api" + +func TestNeedsReadonlyOneagent(t *testing.T) { + t.Run("cloud native fullstack always use readonly host agent", func(t *testing.T) { + oneagent := OneAgent{ + Spec: &Spec{ + CloudNativeFullStack: &CloudNativeFullStackSpec{}, + }, + } + assert.True(t, oneagent.IsReadOnlyOneAgentsMode()) + }) + + t.Run("host monitoring with readonly host agent", func(t *testing.T) { + oneAgent := OneAgent{ + Spec: &Spec{ + HostMonitoring: &HostInjectSpec{}, + }, + } + assert.True(t, oneAgent.IsReadOnlyOneAgentsMode()) + }) + + t.Run("host monitoring without readonly host agent", func(t *testing.T) { + setupDisabledCSIEnv(t) + + oneAgent := OneAgent{ + Spec: &Spec{ + HostMonitoring: &HostInjectSpec{}, + }, + } + assert.False(t, oneAgent.IsReadOnlyOneAgentsMode()) + }) +} + +func TestDefaultOneAgentImage(t *testing.T) { + t.Run("OneAgentImage with no API URL", func(t *testing.T) { + oneAgent := OneAgent{} + assert.Empty(t, oneAgent.GetDefaultImage("")) + }) + + t.Run("OneAgentImage adds raw postfix", func(t *testing.T) { + hostUrl, _ := url.Parse(testAPIURL) + oneAgent := NewOneAgent(&Spec{}, &Status{}, &CodeModulesStatus{}, "", hostUrl.Host, false, false) + assert.Equal(t, "test-endpoint/linux/oneagent:1.234.5-raw", oneAgent.GetDefaultImage("1.234.5")) + }) + + t.Run("OneAgentImage doesn't add 'raw' postfix if present", func(t *testing.T) { + hostUrl, _ := url.Parse(testAPIURL) + oneAgent := NewOneAgent(&Spec{}, &Status{}, &CodeModulesStatus{}, "", hostUrl.Host, false, false) + assert.Equal(t, "test-endpoint/linux/oneagent:1.234.5-raw", oneAgent.GetDefaultImage("1.234.5-raw")) + }) + + t.Run("OneAgentImage with custom version truncates build date", func(t *testing.T) { + version := "1.239.14.20220325-164521" + expectedImage := "test-endpoint/linux/oneagent:1.239.14-raw" + hostUrl, _ := url.Parse(testAPIURL) + oneAgent := NewOneAgent(&Spec{}, &Status{}, &CodeModulesStatus{}, "", hostUrl.Host, false, false) + assert.Equal(t, expectedImage, oneAgent.GetDefaultImage(version)) + }) +} + +func TestCustomOneAgentImage(t *testing.T) { + t.Run("OneAgentImage with custom image", func(t *testing.T) { + customImg := "registry/my/oneagent:latest" + oneAgent := OneAgent{Spec: &Spec{ClassicFullStack: &HostInjectSpec{Image: customImg}}} + assert.Equal(t, customImg, oneAgent.GetCustomImage()) + }) + + t.Run("OneAgentImage with no custom image", func(t *testing.T) { + oneAgent := OneAgent{Spec: &Spec{ClassicFullStack: &HostInjectSpec{}}} + assert.Empty(t, oneAgent.GetCustomImage()) + }) +} + +func TestOneAgentDaemonsetName(t *testing.T) { + oneAgent := OneAgent{name: "test-name"} + assert.Equal(t, "test-name-oneagent", oneAgent.GetDaemonsetName()) +} + +func TestCodeModulesVersion(t *testing.T) { + testVersion := "1.2.3" + + t.Run("use status", func(t *testing.T) { + codeModulesStatus := &CodeModulesStatus{VersionStatus: status.VersionStatus{Version: testVersion}} + oneAgent := NewOneAgent(&Spec{}, &Status{}, codeModulesStatus, "", "", false, false) + version := oneAgent.GetCodeModulesVersion() + assert.Equal(t, testVersion, version) + }) + t.Run("use version ", func(t *testing.T) { + codeModulesStatus := &CodeModulesStatus{VersionStatus: status.VersionStatus{Version: "other"}} + oneAgent := NewOneAgent(&Spec{ + ApplicationMonitoring: &ApplicationMonitoringSpec{Version: testVersion}, + }, &Status{}, codeModulesStatus, "", "", false, false) + version := oneAgent.GetCustomCodeModulesVersion() + + assert.Equal(t, testVersion, version) + }) +} + +func TestGetOneAgentEnvironment(t *testing.T) { + t.Run("get environment from classicFullstack", func(t *testing.T) { + oneAgent := OneAgent{ + Spec: &Spec{ + ClassicFullStack: &HostInjectSpec{ + Env: []corev1.EnvVar{ + { + Name: "classicFullstack", + Value: "true", + }, + }, + }, + }, + } + env := oneAgent.GetEnvironment() + + require.Len(t, env, 1) + assert.Equal(t, "classicFullstack", env[0].Name) + }) + + t.Run("get environment from hostMonitoring", func(t *testing.T) { + oneAgent := OneAgent{ + Spec: &Spec{ + HostMonitoring: &HostInjectSpec{ + Env: []corev1.EnvVar{ + { + Name: "hostMonitoring", + Value: "true", + }, + }, + }, + }, + } + env := oneAgent.GetEnvironment() + + require.Len(t, env, 1) + assert.Equal(t, "hostMonitoring", env[0].Name) + }) + + t.Run("get environment from cloudNative", func(t *testing.T) { + oneAgent := OneAgent{ + Spec: &Spec{ + CloudNativeFullStack: &CloudNativeFullStackSpec{ + HostInjectSpec: HostInjectSpec{ + Env: []corev1.EnvVar{ + { + Name: "cloudNative", + Value: "true", + }, + }, + }, + }, + }, + } + env := oneAgent.GetEnvironment() + + require.Len(t, env, 1) + assert.Equal(t, "cloudNative", env[0].Name) + }) + + t.Run("get environment from applicationMonitoring", func(t *testing.T) { + oneAgent := OneAgent{Spec: &Spec{ + ApplicationMonitoring: &ApplicationMonitoringSpec{}, + }} + env := oneAgent.GetEnvironment() + + require.NotNil(t, env) + assert.Empty(t, env) + }) + + t.Run("get environment from unconfigured dynakube", func(t *testing.T) { + oneAgent := OneAgent{Spec: &Spec{}} + env := oneAgent.GetEnvironment() + + require.NotNil(t, env) + assert.Empty(t, env) + }) +} + +func TestOneAgentHostGroup(t *testing.T) { + t.Run("get host group from cloudNativeFullstack.args", func(t *testing.T) { + dk := OneAgent{Spec: &Spec{ + CloudNativeFullStack: &CloudNativeFullStackSpec{ + HostInjectSpec: HostInjectSpec{ + Args: []string{ + "--set-host-group=arg", + }, + }, + }, + }, + } + hostGroup := dk.GetHostGroup() + assert.Equal(t, "arg", hostGroup) + }) + + t.Run("get host group from oneagent.hostGroup", func(t *testing.T) { + dk := OneAgent{Spec: &Spec{ + HostGroup: "field", + }, + } + hostGroup := dk.GetHostGroup() + assert.Equal(t, "field", hostGroup) + }) + + t.Run("get host group if both methods used", func(t *testing.T) { + dk := OneAgent{Spec: &Spec{ + CloudNativeFullStack: &CloudNativeFullStackSpec{ + HostInjectSpec: HostInjectSpec{ + Args: []string{ + "--set-host-group=arg", + }, + }, + }, + HostGroup: "field", + }, + } + hostGroup := dk.GetHostGroup() + assert.Equal(t, "field", hostGroup) + }) +} + +func TestOneAgentArgumentsMap(t *testing.T) { + t.Run("straight forward argument list", func(t *testing.T) { + dk := OneAgent{Spec: &Spec{ + CloudNativeFullStack: &CloudNativeFullStackSpec{ + HostInjectSpec: HostInjectSpec{ + Args: []string{ + "--set-host-id-source=k8s-node-name", + "--set-host-property=OperatorVersion=$(DT_OPERATOR_VERSION)", + "--set-host-property=dt.security_context=kubernetes_clusters", + "--set-host-property=dynakube-name=$(CUSTOM_CRD_NAME)", + "--set-no-proxy=", + "--set-proxy=", + "--set-tenant=$(DT_TENANT)", + "--set-server=dynatrace.com", + "--set-host-property=prop1=val1", + "--set-host-property=prop2=val2", + "--set-host-property=prop3=val3", + "--set-host-tag=tag1", + "--set-host-tag=tag2", + "--set-host-tag=tag3", + }, + }, + }, + HostGroup: "field", + }, + } + argMap := dk.GetArgumentsMap() + require.Len(t, argMap, 7) + + require.Len(t, argMap["--set-host-id-source"], 1) + assert.Equal(t, "k8s-node-name", argMap["--set-host-id-source"][0]) + + require.Len(t, argMap["--set-host-property"], 6) + assert.Equal(t, "OperatorVersion=$(DT_OPERATOR_VERSION)", argMap["--set-host-property"][0]) + assert.Equal(t, "dt.security_context=kubernetes_clusters", argMap["--set-host-property"][1]) + assert.Equal(t, "dynakube-name=$(CUSTOM_CRD_NAME)", argMap["--set-host-property"][2]) + assert.Equal(t, "prop1=val1", argMap["--set-host-property"][3]) + assert.Equal(t, "prop2=val2", argMap["--set-host-property"][4]) + assert.Equal(t, "prop3=val3", argMap["--set-host-property"][5]) + + require.Len(t, argMap["--set-no-proxy"], 1) + assert.Empty(t, argMap["--set-no-proxy"][0]) + + require.Len(t, argMap["--set-proxy"], 1) + assert.Empty(t, argMap["--set-proxy"][0]) + + require.Len(t, argMap["--set-tenant"], 1) + assert.Equal(t, "$(DT_TENANT)", argMap["--set-tenant"][0]) + + require.Len(t, argMap["--set-server"], 1) + assert.Equal(t, "dynatrace.com", argMap["--set-server"][0]) + }) + + t.Run("multiple --set-host-property arguments", func(t *testing.T) { + dk := OneAgent{Spec: &Spec{ + CloudNativeFullStack: &CloudNativeFullStackSpec{ + HostInjectSpec: HostInjectSpec{ + Args: []string{ + "--set-host-property=prop1=val1", + "--set-host-property=prop2=val2", + "--set-host-property=prop3=val3", + "--set-host-property=prop3=val3", + }, + }, + }, + HostGroup: "field", + }, + } + argMap := dk.GetArgumentsMap() + require.Len(t, argMap, 1) + require.Len(t, argMap["--set-host-property"], 4) + + assert.Equal(t, "prop1=val1", argMap["--set-host-property"][0]) + assert.Equal(t, "prop2=val2", argMap["--set-host-property"][1]) + assert.Equal(t, "prop3=val3", argMap["--set-host-property"][2]) + }) + + t.Run("multiple --set-host-tag arguments", func(t *testing.T) { + dk := OneAgent{Spec: &Spec{ + CloudNativeFullStack: &CloudNativeFullStackSpec{ + HostInjectSpec: HostInjectSpec{ + Args: []string{ + "--set-host-tag=tag1=1", + "--set-host-tag=tag1=2", + "--set-host-tag=tag1=3", + "--set-host-tag=tag2", + "--set-host-tag=tag3", + }, + }, + }, + HostGroup: "field", + }, + } + argMap := dk.GetArgumentsMap() + require.Len(t, argMap, 1) + require.Len(t, argMap["--set-host-tag"], 5) + + assert.Equal(t, "tag1=1", argMap["--set-host-tag"][0]) + assert.Equal(t, "tag1=2", argMap["--set-host-tag"][1]) + assert.Equal(t, "tag1=3", argMap["--set-host-tag"][2]) + assert.Equal(t, "tag2", argMap["--set-host-tag"][3]) + assert.Equal(t, "tag3", argMap["--set-host-tag"][4]) + }) + + t.Run("arguments without value", func(t *testing.T) { + dk := OneAgent{Spec: &Spec{ + CloudNativeFullStack: &CloudNativeFullStackSpec{ + HostInjectSpec: HostInjectSpec{ + Args: []string{ + "--enable-feature-a", + "--enable-feature-b", + "--enable-feature-c", + }, + }, + }, + HostGroup: "field", + }, + } + argMap := dk.GetArgumentsMap() + require.Len(t, argMap, 3) + require.Len(t, argMap["--enable-feature-a"], 1) + require.Len(t, argMap["--enable-feature-b"], 1) + require.Len(t, argMap["--enable-feature-c"], 1) + }) +} + +func setupDisabledCSIEnv(t *testing.T) { + t.Helper() + installconfig.SetModulesOverride(t, installconfig.Modules{ + CSIDriver: false, + ActiveGate: true, + OneAgent: true, + Extensions: true, + LogMonitoring: true, + EdgeConnect: true, + Supportability: true, + }) +} diff --git a/pkg/api/v1beta3/dynakube/oneagent_types.go b/pkg/api/v1beta3/dynakube/oneagent/spec.go similarity index 94% rename from pkg/api/v1beta3/dynakube/oneagent_types.go rename to pkg/api/v1beta3/dynakube/oneagent/spec.go index c7a7fb727b..eb2c78fe4c 100644 --- a/pkg/api/v1beta3/dynakube/oneagent_types.go +++ b/pkg/api/v1beta3/dynakube/oneagent/spec.go @@ -1,13 +1,27 @@ -package dynakube +package oneagent import ( + "github.com/Dynatrace/dynatrace-operator/pkg/api/status" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -type OneAgentMode string +type OneAgent struct { + *Spec + *Status + *CodeModulesStatus -type OneAgentSpec struct { + name string + apiUrlHost string + + featureOneAgentPrivileged bool + featureOneAgentSkipLivenessProbe bool +} + +type Mode string + +// +kubebuilder:object:generate=true +type Spec struct { // Has a single OneAgent per node via DaemonSet. // Injection is performed via the same OneAgent DaemonSet. // +nullable @@ -35,11 +49,13 @@ type OneAgentSpec struct { HostGroup string `json:"hostGroup,omitempty"` } +// +kubebuilder:object:generate=true type CloudNativeFullStackSpec struct { HostInjectSpec `json:",inline"` AppInjectionSpec `json:",inline"` } +// +kubebuilder:object:generate=true type HostInjectSpec struct { // Add custom OneAgent annotations. @@ -113,6 +129,7 @@ type HostInjectSpec struct { Args []string `json:"args,omitempty"` } +// +kubebuilder:object:generate=true type ApplicationMonitoringSpec struct { // Use a specific OneAgent CodeModule version. Defaults to the latest version from the Dynatrace cluster. @@ -123,6 +140,7 @@ type ApplicationMonitoringSpec struct { AppInjectionSpec `json:",inline"` } +// +kubebuilder:object:generate=true type AppInjectionSpec struct { // Define resources requests and limits for the initContainer. For details, see Managing resources for containers // (https://kubernetes.io/docs/concepts/configuration/manage-resources-containers). @@ -140,3 +158,8 @@ type AppInjectionSpec struct { // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Namespace Selector",order=17,xDescriptors="urn:alm:descriptor:com.tectonic.ui:selector:core:v1:Namespace" NamespaceSelector metav1.LabelSelector `json:"namespaceSelector,omitempty"` } + +// +kubebuilder:object:generate=true +type CodeModulesStatus struct { + status.VersionStatus `json:",inline"` +} diff --git a/pkg/api/v1beta3/dynakube/oneagent/status.go b/pkg/api/v1beta3/dynakube/oneagent/status.go new file mode 100644 index 0000000000..b1a9427bdb --- /dev/null +++ b/pkg/api/v1beta3/dynakube/oneagent/status.go @@ -0,0 +1,58 @@ +package oneagent + +import ( + "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/communication" + "github.com/Dynatrace/dynatrace-operator/pkg/api/status" + containerv1 "github.com/google/go-containerregistry/pkg/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +kubebuilder:object:generate=true +type Status struct { + status.VersionStatus `json:",inline"` + + // List of deployed OneAgent instances + Instances map[string]Instance `json:"instances,omitempty"` + + // Time of the last instance status update + LastInstanceStatusUpdate *metav1.Time `json:"lastInstanceStatusUpdate,omitempty"` + + // Commands used for OneAgent's readiness probe + // +kubebuilder:validation:Type=object + // +kubebuilder:validation:Schemaless + // +kubebuilder:pruning:PreserveUnknownFields + Healthcheck *containerv1.HealthConfig `json:"healthcheck,omitempty"` + + // Information about OneAgent's connections + ConnectionInfoStatus ConnectionInfoStatus `json:"connectionInfoStatus,omitempty"` +} + +// +kubebuilder:object:generate=true +type Instance struct { + // Name of the OneAgent pod + PodName string `json:"podName,omitempty"` + + // IP address of the pod + IPAddress string `json:"ipAddress,omitempty"` +} + +// +kubebuilder:object:generate=true +type ConnectionInfoStatus struct { + // Information for communicating with the tenant + communication.ConnectionInfo `json:",inline"` + + // List of communication hosts + CommunicationHosts []CommunicationHostStatus `json:"communicationHosts,omitempty"` +} + +// +kubebuilder:object:generate=true +type CommunicationHostStatus struct { + // Connection protocol + Protocol string `json:"protocol,omitempty"` + + // Host domain + Host string `json:"host,omitempty"` + + // Connection port + Port uint32 `json:"port,omitempty"` +} diff --git a/pkg/api/v1beta3/dynakube/oneagent/zz_generated.deepcopy.go b/pkg/api/v1beta3/dynakube/oneagent/zz_generated.deepcopy.go new file mode 100644 index 0000000000..a2878735cc --- /dev/null +++ b/pkg/api/v1beta3/dynakube/oneagent/zz_generated.deepcopy.go @@ -0,0 +1,274 @@ +//go:build !ignore_autogenerated + +/* +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package oneagent + +import ( + pkgv1 "github.com/google/go-containerregistry/pkg/v1" + "k8s.io/api/core/v1" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AppInjectionSpec) DeepCopyInto(out *AppInjectionSpec) { + *out = *in + if in.InitResources != nil { + in, out := &in.InitResources, &out.InitResources + *out = new(v1.ResourceRequirements) + (*in).DeepCopyInto(*out) + } + in.NamespaceSelector.DeepCopyInto(&out.NamespaceSelector) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppInjectionSpec. +func (in *AppInjectionSpec) DeepCopy() *AppInjectionSpec { + if in == nil { + return nil + } + out := new(AppInjectionSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ApplicationMonitoringSpec) DeepCopyInto(out *ApplicationMonitoringSpec) { + *out = *in + in.AppInjectionSpec.DeepCopyInto(&out.AppInjectionSpec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationMonitoringSpec. +func (in *ApplicationMonitoringSpec) DeepCopy() *ApplicationMonitoringSpec { + if in == nil { + return nil + } + out := new(ApplicationMonitoringSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CloudNativeFullStackSpec) DeepCopyInto(out *CloudNativeFullStackSpec) { + *out = *in + in.HostInjectSpec.DeepCopyInto(&out.HostInjectSpec) + in.AppInjectionSpec.DeepCopyInto(&out.AppInjectionSpec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CloudNativeFullStackSpec. +func (in *CloudNativeFullStackSpec) DeepCopy() *CloudNativeFullStackSpec { + if in == nil { + return nil + } + out := new(CloudNativeFullStackSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CodeModulesStatus) DeepCopyInto(out *CodeModulesStatus) { + *out = *in + in.VersionStatus.DeepCopyInto(&out.VersionStatus) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CodeModulesStatus. +func (in *CodeModulesStatus) DeepCopy() *CodeModulesStatus { + if in == nil { + return nil + } + out := new(CodeModulesStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CommunicationHostStatus) DeepCopyInto(out *CommunicationHostStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommunicationHostStatus. +func (in *CommunicationHostStatus) DeepCopy() *CommunicationHostStatus { + if in == nil { + return nil + } + out := new(CommunicationHostStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConnectionInfoStatus) DeepCopyInto(out *ConnectionInfoStatus) { + *out = *in + in.ConnectionInfo.DeepCopyInto(&out.ConnectionInfo) + if in.CommunicationHosts != nil { + in, out := &in.CommunicationHosts, &out.CommunicationHosts + *out = make([]CommunicationHostStatus, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConnectionInfoStatus. +func (in *ConnectionInfoStatus) DeepCopy() *ConnectionInfoStatus { + if in == nil { + return nil + } + out := new(ConnectionInfoStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HostInjectSpec) DeepCopyInto(out *HostInjectSpec) { + *out = *in + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.NodeSelector != nil { + in, out := &in.NodeSelector, &out.NodeSelector + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.AutoUpdate != nil { + in, out := &in.AutoUpdate, &out.AutoUpdate + *out = new(bool) + **out = **in + } + in.OneAgentResources.DeepCopyInto(&out.OneAgentResources) + if in.Tolerations != nil { + in, out := &in.Tolerations, &out.Tolerations + *out = make([]v1.Toleration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Env != nil { + in, out := &in.Env, &out.Env + *out = make([]v1.EnvVar, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Args != nil { + in, out := &in.Args, &out.Args + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostInjectSpec. +func (in *HostInjectSpec) DeepCopy() *HostInjectSpec { + if in == nil { + return nil + } + out := new(HostInjectSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Instance) DeepCopyInto(out *Instance) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Instance. +func (in *Instance) DeepCopy() *Instance { + if in == nil { + return nil + } + out := new(Instance) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Spec) DeepCopyInto(out *Spec) { + *out = *in + if in.ClassicFullStack != nil { + in, out := &in.ClassicFullStack, &out.ClassicFullStack + *out = new(HostInjectSpec) + (*in).DeepCopyInto(*out) + } + if in.CloudNativeFullStack != nil { + in, out := &in.CloudNativeFullStack, &out.CloudNativeFullStack + *out = new(CloudNativeFullStackSpec) + (*in).DeepCopyInto(*out) + } + if in.ApplicationMonitoring != nil { + in, out := &in.ApplicationMonitoring, &out.ApplicationMonitoring + *out = new(ApplicationMonitoringSpec) + (*in).DeepCopyInto(*out) + } + if in.HostMonitoring != nil { + in, out := &in.HostMonitoring, &out.HostMonitoring + *out = new(HostInjectSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Spec. +func (in *Spec) DeepCopy() *Spec { + if in == nil { + return nil + } + out := new(Spec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Status) DeepCopyInto(out *Status) { + *out = *in + in.VersionStatus.DeepCopyInto(&out.VersionStatus) + if in.Instances != nil { + in, out := &in.Instances, &out.Instances + *out = make(map[string]Instance, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.LastInstanceStatusUpdate != nil { + in, out := &in.LastInstanceStatusUpdate, &out.LastInstanceStatusUpdate + *out = (*in).DeepCopy() + } + if in.Healthcheck != nil { + in, out := &in.Healthcheck, &out.Healthcheck + *out = new(pkgv1.HealthConfig) + (*in).DeepCopyInto(*out) + } + in.ConnectionInfoStatus.DeepCopyInto(&out.ConnectionInfoStatus) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Status. +func (in *Status) DeepCopy() *Status { + if in == nil { + return nil + } + out := new(Status) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/api/v1beta3/dynakube/oneagent_props.go b/pkg/api/v1beta3/dynakube/oneagent_props.go index ce8752faa6..dd69c90bbd 100644 --- a/pkg/api/v1beta3/dynakube/oneagent_props.go +++ b/pkg/api/v1beta3/dynakube/oneagent_props.go @@ -1,299 +1,18 @@ package dynakube import ( - "fmt" - "strings" - - "github.com/Dynatrace/dynatrace-operator/pkg/api" - "github.com/Dynatrace/dynatrace-operator/pkg/util/dtversion" - "github.com/Dynatrace/dynatrace-operator/pkg/util/installconfig" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -const ( - OneAgentTenantSecretSuffix = "-oneagent-tenant-secret" - OneAgentConnectionInfoConfigMapSuffix = "-oneagent-connection-info" - PodNameOsAgent = "oneagent" - DefaultOneAgentImageRegistrySubPath = "/linux/oneagent" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/oneagent" ) -func (dk *DynaKube) IsCSIAvailable() bool { - return installconfig.GetModules().CSIDriver -} - -// ApplicationMonitoringMode returns true when application only section is used. -func (dk *DynaKube) ApplicationMonitoringMode() bool { - return dk.Spec.OneAgent != OneAgentSpec{} && dk.Spec.OneAgent.ApplicationMonitoring != nil -} - -// CloudNativeFullstackMode returns true when cloud native fullstack section is used. -func (dk *DynaKube) CloudNativeFullstackMode() bool { - return dk.Spec.OneAgent != OneAgentSpec{} && dk.Spec.OneAgent.CloudNativeFullStack != nil -} - -// HostMonitoringMode returns true when host monitoring section is used. -func (dk *DynaKube) HostMonitoringMode() bool { - return dk.Spec.OneAgent != OneAgentSpec{} && dk.Spec.OneAgent.HostMonitoring != nil -} - -// ClassicFullStackMode returns true when classic fullstack section is used. -func (dk *DynaKube) ClassicFullStackMode() bool { - return dk.Spec.OneAgent != OneAgentSpec{} && dk.Spec.OneAgent.ClassicFullStack != nil -} - -// NeedsOneAgent returns true when a feature requires OneAgent instances. -func (dk *DynaKube) NeedsOneAgent() bool { - return dk.ClassicFullStackMode() || dk.CloudNativeFullstackMode() || dk.HostMonitoringMode() -} - -func (dk *DynaKube) OneAgentDaemonsetName() string { - return fmt.Sprintf("%s-%s", dk.Name, PodNameOsAgent) -} - -func (dk *DynaKube) NeedsOneAgentPrivileged() bool { - return dk.FeatureOneAgentPrivileged() -} - -func (dk *DynaKube) NeedsOneAgentProbe() bool { - return dk.Status.OneAgent.Healthcheck != nil -} - -// ShouldAutoUpdateOneAgent returns true if the Operator should update OneAgent instances automatically. -func (dk *DynaKube) ShouldAutoUpdateOneAgent() bool { - switch { - case dk.CloudNativeFullstackMode(): - return dk.Spec.OneAgent.CloudNativeFullStack.AutoUpdate == nil || *dk.Spec.OneAgent.CloudNativeFullStack.AutoUpdate - case dk.HostMonitoringMode(): - return dk.Spec.OneAgent.HostMonitoring.AutoUpdate == nil || *dk.Spec.OneAgent.HostMonitoring.AutoUpdate - case dk.ClassicFullStackMode(): - return dk.Spec.OneAgent.ClassicFullStack.AutoUpdate == nil || *dk.Spec.OneAgent.ClassicFullStack.AutoUpdate - default: - return false - } -} - -// OneagentTenantSecret returns the name of the secret containing the token for the OneAgent. -func (dk *DynaKube) OneagentTenantSecret() string { - return dk.Name + OneAgentTenantSecretSuffix -} - -func (dk *DynaKube) OneAgentConnectionInfoConfigMapName() string { - return dk.Name + OneAgentConnectionInfoConfigMapSuffix -} - -func (dk *DynaKube) UseReadOnlyOneAgents() bool { - return dk.CloudNativeFullstackMode() || (dk.HostMonitoringMode() && dk.IsCSIAvailable()) -} - -func (dk *DynaKube) NeedAppInjection() bool { - return dk.CloudNativeFullstackMode() || dk.ApplicationMonitoringMode() -} - -func (dk *DynaKube) InitResources() *corev1.ResourceRequirements { - if dk.ApplicationMonitoringMode() { - return dk.Spec.OneAgent.ApplicationMonitoring.InitResources - } else if dk.CloudNativeFullstackMode() { - return dk.Spec.OneAgent.CloudNativeFullStack.InitResources - } - - return nil -} - -func (dk *DynaKube) OneAgentNamespaceSelector() *metav1.LabelSelector { - switch { - case dk.CloudNativeFullstackMode(): - return &dk.Spec.OneAgent.CloudNativeFullStack.NamespaceSelector - case dk.ApplicationMonitoringMode(): - return &dk.Spec.OneAgent.ApplicationMonitoring.NamespaceSelector - } - - return nil -} - -func (dk *DynaKube) OneAgentSecCompProfile() string { - switch { - case dk.CloudNativeFullstackMode(): - return dk.Spec.OneAgent.CloudNativeFullStack.SecCompProfile - case dk.HostMonitoringMode(): - return dk.Spec.OneAgent.HostMonitoring.SecCompProfile - case dk.ClassicFullStackMode(): - return dk.Spec.OneAgent.ClassicFullStack.SecCompProfile - default: - return "" - } -} - -func (dk *DynaKube) OneAgentNodeSelector() map[string]string { - switch { - case dk.ClassicFullStackMode(): - return dk.Spec.OneAgent.ClassicFullStack.NodeSelector - case dk.HostMonitoringMode(): - return dk.Spec.OneAgent.HostMonitoring.NodeSelector - case dk.CloudNativeFullstackMode(): - return dk.Spec.OneAgent.CloudNativeFullStack.NodeSelector - case dk.LogMonitoring().IsStandalone(): - return dk.LogMonitoring().Template().NodeSelector - } - - return nil -} - -// CodeModulesVersion provides version set in Status for the CodeModules. -func (dk *DynaKube) CodeModulesVersion() string { - return dk.Status.CodeModules.Version -} - -// CodeModulesImage provides the image reference set in Status for the CodeModules. -// Format: repo@sha256:digest. -func (dk *DynaKube) CodeModulesImage() string { - return dk.Status.CodeModules.ImageID -} - -// CustomCodeModulesImage provides the image reference for the CodeModules provided in the Spec. -func (dk *DynaKube) CustomCodeModulesImage() string { - if dk.CloudNativeFullstackMode() { - return dk.Spec.OneAgent.CloudNativeFullStack.CodeModulesImage - } else if dk.ApplicationMonitoringMode() && dk.IsCSIAvailable() { - return dk.Spec.OneAgent.ApplicationMonitoring.CodeModulesImage - } - - return "" -} - -// CustomCodeModulesVersion provides the version for the CodeModules provided in the Spec. -func (dk *DynaKube) CustomCodeModulesVersion() string { - if !dk.ApplicationMonitoringMode() { - return "" - } - - return dk.CustomOneAgentVersion() -} - -// OneAgentImage provides the image reference set in Status for the OneAgent. -// Format: repo@sha256:digest. -func (dk *DynaKube) OneAgentImage() string { - return dk.Status.OneAgent.ImageID -} - -// OneAgentVersion provides version set in Status for the OneAgent. -func (dk *DynaKube) OneAgentVersion() string { - return dk.Status.OneAgent.Version -} - -// CustomOneAgentVersion provides the version for the OneAgent provided in the Spec. -func (dk *DynaKube) CustomOneAgentVersion() string { - switch { - case dk.ClassicFullStackMode(): - return dk.Spec.OneAgent.ClassicFullStack.Version - case dk.CloudNativeFullstackMode(): - return dk.Spec.OneAgent.CloudNativeFullStack.Version - case dk.ApplicationMonitoringMode(): - return dk.Spec.OneAgent.ApplicationMonitoring.Version - case dk.HostMonitoringMode(): - return dk.Spec.OneAgent.HostMonitoring.Version - } - - return "" -} - -// CustomOneAgentImage provides the image reference for the OneAgent provided in the Spec. -func (dk *DynaKube) CustomOneAgentImage() string { - switch { - case dk.ClassicFullStackMode(): - return dk.Spec.OneAgent.ClassicFullStack.Image - case dk.HostMonitoringMode(): - return dk.Spec.OneAgent.HostMonitoring.Image - case dk.CloudNativeFullstackMode(): - return dk.Spec.OneAgent.CloudNativeFullStack.Image - } - - return "" -} - -// DefaultOneAgentImage provides the image reference for the OneAgent from tenant registry. -func (dk *DynaKube) DefaultOneAgentImage(version string) string { - apiUrlHost := dk.ApiUrlHost() - if apiUrlHost == "" { - return "" - } - - truncatedVersion := dtversion.ToImageTag(version) - tag := truncatedVersion - - if !strings.Contains(tag, api.RawTag) { - tag += "-" + api.RawTag - } - - return apiUrlHost + DefaultOneAgentImageRegistrySubPath + ":" + tag -} - -func (dk *DynaKube) HostGroup() string { - if dk.Spec.OneAgent.HostGroup != "" { - return dk.Spec.OneAgent.HostGroup - } - - return dk.HostGroupAsParam() -} - -func (dk *DynaKube) HostGroupAsParam() string { - var hostGroup string - - var args []string - - switch { - case dk.CloudNativeFullstackMode() && dk.Spec.OneAgent.CloudNativeFullStack.Args != nil: - args = dk.Spec.OneAgent.CloudNativeFullStack.Args - case dk.ClassicFullStackMode() && dk.Spec.OneAgent.ClassicFullStack.Args != nil: - args = dk.Spec.OneAgent.ClassicFullStack.Args - case dk.HostMonitoringMode() && dk.Spec.OneAgent.HostMonitoring.Args != nil: - args = dk.Spec.OneAgent.HostMonitoring.Args - } - - for _, arg := range args { - key, value := splitArg(arg) - if key == "--set-host-group" { - hostGroup = value - - break - } - } - - return hostGroup -} - -func splitArg(arg string) (key, value string) { - split := strings.Split(arg, "=") - - const expectedLen = 2 - - if len(split) != expectedLen { - return - } - - key = split[0] - value = split[1] - - return -} - -func (dk *DynaKube) IsOneAgentCommunicationRouteClear() bool { - return len(dk.Status.OneAgent.ConnectionInfoStatus.CommunicationHosts) > 0 -} - -func (dk *DynaKube) GetOneAgentEnvironment() []corev1.EnvVar { - switch { - case dk.CloudNativeFullstackMode(): - return dk.Spec.OneAgent.CloudNativeFullStack.Env - case dk.ClassicFullStackMode(): - return dk.Spec.OneAgent.ClassicFullStack.Env - case dk.HostMonitoringMode(): - return dk.Spec.OneAgent.HostMonitoring.Env - } - - return []corev1.EnvVar{} -} - -func (dk *DynaKube) OneAgentEndpoints() string { - return dk.Status.OneAgent.ConnectionInfoStatus.Endpoints +func (dk *DynaKube) OneAgent() *oneagent.OneAgent { + oa := oneagent.NewOneAgent( + &dk.Spec.OneAgent, + &dk.Status.OneAgent, + &dk.Status.CodeModules, + dk.Name, + dk.ApiUrlHost(), + dk.FeatureOneAgentPrivileged(), + dk.FeatureOneAgentSkipLivenessProbe()) + + return oa } diff --git a/pkg/api/v1beta3/dynakube/oneagent_props_test.go b/pkg/api/v1beta3/dynakube/oneagent_props_test.go deleted file mode 100644 index 58f1ee879f..0000000000 --- a/pkg/api/v1beta3/dynakube/oneagent_props_test.go +++ /dev/null @@ -1,338 +0,0 @@ -/* -Copyright 2021 Dynatrace LLC. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package dynakube - -import ( - "testing" - - "github.com/Dynatrace/dynatrace-operator/pkg/api/status" - "github.com/Dynatrace/dynatrace-operator/pkg/util/installconfig" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func TestNeedsReadonlyOneagent(t *testing.T) { - t.Run("cloud native fullstack always use readonly host agent", func(t *testing.T) { - dk := DynaKube{ - Spec: DynaKubeSpec{ - OneAgent: OneAgentSpec{ - CloudNativeFullStack: &CloudNativeFullStackSpec{}, - }, - }, - } - assert.True(t, dk.UseReadOnlyOneAgents()) - }) - - t.Run("host monitoring with readonly host agent", func(t *testing.T) { - dk := DynaKube{ - Spec: DynaKubeSpec{ - OneAgent: OneAgentSpec{ - HostMonitoring: &HostInjectSpec{}, - }, - }, - } - assert.True(t, dk.UseReadOnlyOneAgents()) - }) - - t.Run("host monitoring without readonly host agent", func(t *testing.T) { - setupDisabledCSIEnv(t) - - dk := DynaKube{ - Spec: DynaKubeSpec{ - OneAgent: OneAgentSpec{ - HostMonitoring: &HostInjectSpec{}, - }, - }, - } - assert.False(t, dk.UseReadOnlyOneAgents()) - }) -} - -func TestDefaultOneAgentImage(t *testing.T) { - t.Run("OneAgentImage with no API URL", func(t *testing.T) { - dk := DynaKube{} - assert.Equal(t, "", dk.DefaultOneAgentImage("")) - }) - - t.Run("OneAgentImage adds raw postfix", func(t *testing.T) { - dk := DynaKube{Spec: DynaKubeSpec{APIURL: testAPIURL}} - assert.Equal(t, "test-endpoint/linux/oneagent:1.234.5-raw", dk.DefaultOneAgentImage("1.234.5")) - }) - - t.Run("OneAgentImage doesn't add 'raw' postfix if present", func(t *testing.T) { - dk := DynaKube{Spec: DynaKubeSpec{APIURL: testAPIURL}} - assert.Equal(t, "test-endpoint/linux/oneagent:1.234.5-raw", dk.DefaultOneAgentImage("1.234.5-raw")) - }) - - t.Run("OneAgentImage with custom version truncates build date", func(t *testing.T) { - version := "1.239.14.20220325-164521" - expectedImage := "test-endpoint/linux/oneagent:1.239.14-raw" - dk := DynaKube{Spec: DynaKubeSpec{APIURL: testAPIURL}} - - assert.Equal(t, expectedImage, dk.DefaultOneAgentImage(version)) - }) -} - -func TestCustomOneAgentImage(t *testing.T) { - t.Run("OneAgentImage with custom image", func(t *testing.T) { - customImg := "registry/my/oneagent:latest" - dk := DynaKube{Spec: DynaKubeSpec{OneAgent: OneAgentSpec{ClassicFullStack: &HostInjectSpec{Image: customImg}}}} - assert.Equal(t, customImg, dk.CustomOneAgentImage()) - }) - - t.Run("OneAgentImage with no custom image", func(t *testing.T) { - dk := DynaKube{Spec: DynaKubeSpec{OneAgent: OneAgentSpec{ClassicFullStack: &HostInjectSpec{}}}} - assert.Equal(t, "", dk.CustomOneAgentImage()) - }) -} - -func TestOneAgentDaemonsetName(t *testing.T) { - dk := &DynaKube{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-name", - }, - } - assert.Equal(t, "test-name-oneagent", dk.OneAgentDaemonsetName()) -} - -func TestCodeModulesVersion(t *testing.T) { - testVersion := "1.2.3" - - t.Run("use status", func(t *testing.T) { - dk := DynaKube{ - Spec: DynaKubeSpec{ - OneAgent: OneAgentSpec{ - CloudNativeFullStack: &CloudNativeFullStackSpec{}, - }, - }, - Status: DynaKubeStatus{ - CodeModules: CodeModulesStatus{ - VersionStatus: status.VersionStatus{ - Version: testVersion, - }, - }, - }, - } - version := dk.CodeModulesVersion() - assert.Equal(t, testVersion, version) - }) - t.Run("use version ", func(t *testing.T) { - dk := DynaKube{ - Spec: DynaKubeSpec{ - OneAgent: OneAgentSpec{ - ApplicationMonitoring: &ApplicationMonitoringSpec{ - Version: testVersion, - }, - }, - }, - Status: DynaKubeStatus{ - CodeModules: CodeModulesStatus{ - VersionStatus: status.VersionStatus{ - Version: "other", - }, - }, - }, - } - version := dk.CustomCodeModulesVersion() - assert.Equal(t, testVersion, version) - }) -} - -func TestIsOneAgentPrivileged(t *testing.T) { - t.Run("is false by default", func(t *testing.T) { - dk := DynaKube{} - - assert.False(t, dk.FeatureOneAgentPrivileged()) - }) - t.Run("is true when annotation is set to true", func(t *testing.T) { - dk := DynaKube{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ - AnnotationFeatureRunOneAgentContainerPrivileged: "true", - }, - }, - } - - assert.True(t, dk.FeatureOneAgentPrivileged()) - }) - t.Run("is false when annotation is set to false", func(t *testing.T) { - dk := DynaKube{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ - AnnotationFeatureRunOneAgentContainerPrivileged: "false", - }, - }, - } - - assert.False(t, dk.FeatureOneAgentPrivileged()) - }) -} - -func TestGetOneAgentEnvironment(t *testing.T) { - t.Run("get environment from classicFullstack", func(t *testing.T) { - dk := DynaKube{ - Spec: DynaKubeSpec{ - OneAgent: OneAgentSpec{ - ClassicFullStack: &HostInjectSpec{ - Env: []corev1.EnvVar{ - { - Name: "classicFullstack", - Value: "true", - }, - }, - }, - }, - }, - } - env := dk.GetOneAgentEnvironment() - - require.Len(t, env, 1) - assert.Equal(t, "classicFullstack", env[0].Name) - }) - - t.Run("get environment from hostMonitoring", func(t *testing.T) { - dk := DynaKube{ - Spec: DynaKubeSpec{ - OneAgent: OneAgentSpec{ - HostMonitoring: &HostInjectSpec{ - Env: []corev1.EnvVar{ - { - Name: "hostMonitoring", - Value: "true", - }, - }, - }, - }, - }, - } - env := dk.GetOneAgentEnvironment() - - require.Len(t, env, 1) - assert.Equal(t, "hostMonitoring", env[0].Name) - }) - - t.Run("get environment from cloudNative", func(t *testing.T) { - dk := DynaKube{ - Spec: DynaKubeSpec{ - OneAgent: OneAgentSpec{ - CloudNativeFullStack: &CloudNativeFullStackSpec{ - HostInjectSpec: HostInjectSpec{ - Env: []corev1.EnvVar{ - { - Name: "cloudNative", - Value: "true", - }, - }, - }, - }, - }, - }, - } - env := dk.GetOneAgentEnvironment() - - require.Len(t, env, 1) - assert.Equal(t, "cloudNative", env[0].Name) - }) - - t.Run("get environment from applicationMonitoring", func(t *testing.T) { - dk := DynaKube{ - Spec: DynaKubeSpec{ - OneAgent: OneAgentSpec{ - ApplicationMonitoring: &ApplicationMonitoringSpec{}, - }, - }, - } - env := dk.GetOneAgentEnvironment() - - require.NotNil(t, env) - assert.Empty(t, env) - }) - - t.Run("get environment from unconfigured dynakube", func(t *testing.T) { - dk := DynaKube{} - env := dk.GetOneAgentEnvironment() - - require.NotNil(t, env) - assert.Empty(t, env) - }) -} - -func TestOneAgentHostGroup(t *testing.T) { - t.Run("get host group from cloudNativeFullstack.args", func(t *testing.T) { - dk := DynaKube{ - Spec: DynaKubeSpec{ - OneAgent: OneAgentSpec{ - CloudNativeFullStack: &CloudNativeFullStackSpec{ - HostInjectSpec: HostInjectSpec{ - Args: []string{ - "--set-host-group=arg", - }, - }, - }, - }, - }, - } - hostGroup := dk.HostGroup() - assert.Equal(t, "arg", hostGroup) - }) - - t.Run("get host group from oneagent.hostGroup", func(t *testing.T) { - dk := DynaKube{ - Spec: DynaKubeSpec{ - OneAgent: OneAgentSpec{ - HostGroup: "field", - }, - }, - } - hostGroup := dk.HostGroup() - assert.Equal(t, "field", hostGroup) - }) - - t.Run("get host group if both methods used", func(t *testing.T) { - dk := DynaKube{ - Spec: DynaKubeSpec{ - OneAgent: OneAgentSpec{ - CloudNativeFullStack: &CloudNativeFullStackSpec{ - HostInjectSpec: HostInjectSpec{ - Args: []string{ - "--set-host-group=arg", - }, - }, - }, - HostGroup: "field", - }, - }, - } - hostGroup := dk.HostGroup() - assert.Equal(t, "field", hostGroup) - }) -} - -func setupDisabledCSIEnv(t *testing.T) { - t.Helper() - installconfig.SetModulesOverride(t, installconfig.Modules{ - CSIDriver: false, - ActiveGate: true, - OneAgent: true, - Extensions: true, - LogMonitoring: true, - EdgeConnect: true, - Supportability: true, - }) -} diff --git a/pkg/api/v1beta3/dynakube/test/proxy_test.go b/pkg/api/v1beta3/dynakube/test/proxy_test.go index 80c592c19c..58c2d3a8f5 100644 --- a/pkg/api/v1beta3/dynakube/test/proxy_test.go +++ b/pkg/api/v1beta3/dynakube/test/proxy_test.go @@ -36,7 +36,7 @@ func proxyValueTester(t *testing.T) { emptyDk := dynakube.DynaKube{} proxy, err = emptyDk.Proxy(context.TODO(), nil) require.NoError(t, err) - assert.Equal(t, "", proxy) + assert.Empty(t, proxy) } func proxyValueFromTester(t *testing.T) { @@ -57,5 +57,5 @@ func proxyValueFromTester(t *testing.T) { kubeReader = fake.NewClient() proxy, err = dk.Proxy(context.TODO(), kubeReader) require.Error(t, err) - assert.Equal(t, "", proxy) + assert.Empty(t, proxy) } diff --git a/pkg/api/v1beta3/dynakube/zz_generated.deepcopy.go b/pkg/api/v1beta3/dynakube/zz_generated.deepcopy.go index e5b6963c55..f9813d44b4 100644 --- a/pkg/api/v1beta3/dynakube/zz_generated.deepcopy.go +++ b/pkg/api/v1beta3/dynakube/zz_generated.deepcopy.go @@ -22,97 +22,11 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/value" "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/kspm" "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/logmonitoring" - pkgv1 "github.com/google/go-containerregistry/pkg/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AppInjectionSpec) DeepCopyInto(out *AppInjectionSpec) { - *out = *in - if in.InitResources != nil { - in, out := &in.InitResources, &out.InitResources - *out = new(corev1.ResourceRequirements) - (*in).DeepCopyInto(*out) - } - in.NamespaceSelector.DeepCopyInto(&out.NamespaceSelector) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppInjectionSpec. -func (in *AppInjectionSpec) DeepCopy() *AppInjectionSpec { - if in == nil { - return nil - } - out := new(AppInjectionSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ApplicationMonitoringSpec) DeepCopyInto(out *ApplicationMonitoringSpec) { - *out = *in - in.AppInjectionSpec.DeepCopyInto(&out.AppInjectionSpec) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationMonitoringSpec. -func (in *ApplicationMonitoringSpec) DeepCopy() *ApplicationMonitoringSpec { - if in == nil { - return nil - } - out := new(ApplicationMonitoringSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *CloudNativeFullStackSpec) DeepCopyInto(out *CloudNativeFullStackSpec) { - *out = *in - in.HostInjectSpec.DeepCopyInto(&out.HostInjectSpec) - in.AppInjectionSpec.DeepCopyInto(&out.AppInjectionSpec) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CloudNativeFullStackSpec. -func (in *CloudNativeFullStackSpec) DeepCopy() *CloudNativeFullStackSpec { - if in == nil { - return nil - } - out := new(CloudNativeFullStackSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *CodeModulesStatus) DeepCopyInto(out *CodeModulesStatus) { - *out = *in - in.VersionStatus.DeepCopyInto(&out.VersionStatus) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CodeModulesStatus. -func (in *CodeModulesStatus) DeepCopy() *CodeModulesStatus { - if in == nil { - return nil - } - out := new(CodeModulesStatus) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *CommunicationHostStatus) DeepCopyInto(out *CommunicationHostStatus) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommunicationHostStatus. -func (in *CommunicationHostStatus) DeepCopy() *CommunicationHostStatus { - if in == nil { - return nil - } - out := new(CommunicationHostStatus) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DynaKube) DeepCopyInto(out *DynaKube) { *out = *in @@ -341,67 +255,6 @@ func (in *ExtensionsSpec) DeepCopy() *ExtensionsSpec { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *HostInjectSpec) DeepCopyInto(out *HostInjectSpec) { - *out = *in - if in.Annotations != nil { - in, out := &in.Annotations, &out.Annotations - *out = make(map[string]string, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } - if in.Labels != nil { - in, out := &in.Labels, &out.Labels - *out = make(map[string]string, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } - if in.NodeSelector != nil { - in, out := &in.NodeSelector, &out.NodeSelector - *out = make(map[string]string, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } - if in.AutoUpdate != nil { - in, out := &in.AutoUpdate, &out.AutoUpdate - *out = new(bool) - **out = **in - } - in.OneAgentResources.DeepCopyInto(&out.OneAgentResources) - if in.Tolerations != nil { - in, out := &in.Tolerations, &out.Tolerations - *out = make([]corev1.Toleration, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - if in.Env != nil { - in, out := &in.Env, &out.Env - *out = make([]corev1.EnvVar, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - if in.Args != nil { - in, out := &in.Args, &out.Args - *out = make([]string, len(*in)) - copy(*out, *in) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostInjectSpec. -func (in *HostInjectSpec) DeepCopy() *HostInjectSpec { - if in == nil { - return nil - } - out := new(HostInjectSpec) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MetadataEnrichment) DeepCopyInto(out *MetadataEnrichment) { *out = *in @@ -443,110 +296,6 @@ func (in *MetadataEnrichmentStatus) DeepCopy() *MetadataEnrichmentStatus { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *OneAgentConnectionInfoStatus) DeepCopyInto(out *OneAgentConnectionInfoStatus) { - *out = *in - in.ConnectionInfo.DeepCopyInto(&out.ConnectionInfo) - if in.CommunicationHosts != nil { - in, out := &in.CommunicationHosts, &out.CommunicationHosts - *out = make([]CommunicationHostStatus, len(*in)) - copy(*out, *in) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OneAgentConnectionInfoStatus. -func (in *OneAgentConnectionInfoStatus) DeepCopy() *OneAgentConnectionInfoStatus { - if in == nil { - return nil - } - out := new(OneAgentConnectionInfoStatus) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *OneAgentInstance) DeepCopyInto(out *OneAgentInstance) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OneAgentInstance. -func (in *OneAgentInstance) DeepCopy() *OneAgentInstance { - if in == nil { - return nil - } - out := new(OneAgentInstance) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *OneAgentSpec) DeepCopyInto(out *OneAgentSpec) { - *out = *in - if in.ClassicFullStack != nil { - in, out := &in.ClassicFullStack, &out.ClassicFullStack - *out = new(HostInjectSpec) - (*in).DeepCopyInto(*out) - } - if in.CloudNativeFullStack != nil { - in, out := &in.CloudNativeFullStack, &out.CloudNativeFullStack - *out = new(CloudNativeFullStackSpec) - (*in).DeepCopyInto(*out) - } - if in.ApplicationMonitoring != nil { - in, out := &in.ApplicationMonitoring, &out.ApplicationMonitoring - *out = new(ApplicationMonitoringSpec) - (*in).DeepCopyInto(*out) - } - if in.HostMonitoring != nil { - in, out := &in.HostMonitoring, &out.HostMonitoring - *out = new(HostInjectSpec) - (*in).DeepCopyInto(*out) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OneAgentSpec. -func (in *OneAgentSpec) DeepCopy() *OneAgentSpec { - if in == nil { - return nil - } - out := new(OneAgentSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *OneAgentStatus) DeepCopyInto(out *OneAgentStatus) { - *out = *in - in.VersionStatus.DeepCopyInto(&out.VersionStatus) - if in.Instances != nil { - in, out := &in.Instances, &out.Instances - *out = make(map[string]OneAgentInstance, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } - if in.LastInstanceStatusUpdate != nil { - in, out := &in.LastInstanceStatusUpdate, &out.LastInstanceStatusUpdate - *out = (*in).DeepCopy() - } - if in.Healthcheck != nil { - in, out := &in.Healthcheck, &out.Healthcheck - *out = new(pkgv1.HealthConfig) - (*in).DeepCopyInto(*out) - } - in.ConnectionInfoStatus.DeepCopyInto(&out.ConnectionInfoStatus) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OneAgentStatus. -func (in *OneAgentStatus) DeepCopy() *OneAgentStatus { - if in == nil { - return nil - } - out := new(OneAgentStatus) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OpenTelemetryCollectorSpec) DeepCopyInto(out *OpenTelemetryCollectorSpec) { *out = *in diff --git a/pkg/api/v1beta4/dynakube/activegate/props.go b/pkg/api/v1beta4/dynakube/activegate/props.go new file mode 100644 index 0000000000..905a53797c --- /dev/null +++ b/pkg/api/v1beta4/dynakube/activegate/props.go @@ -0,0 +1,154 @@ +package activegate + +import ( + "net/url" + "strings" + + "github.com/Dynatrace/dynatrace-operator/pkg/api" + "github.com/Dynatrace/dynatrace-operator/pkg/util/dtversion" +) + +const ( + TenantSecretSuffix = "-activegate-tenant-secret" + TlsSecretSuffix = "-activegate-tls-secret" + ConnectionInfoConfigMapSuffix = "-activegate-connection-info" + AuthTokenSecretSuffix = "-activegate-authtoken-secret" + DefaultImageRegistrySubPath = "/linux/activegate" +) + +func (ag *Spec) SetApiUrl(apiUrl string) { + ag.apiUrl = apiUrl +} + +func (ag *Spec) SetName(name string) { + ag.name = name +} + +func (ag *Spec) SetAutomaticTLSCertificate(enabled bool) { + ag.automaticTLSCertificateEnabled = enabled +} +func (ag *Spec) SetExtensionsDependency(isEnabled bool) { + ag.enabledDependencies.extensions = isEnabled +} + +func (ag *Spec) apiUrlHost() string { + parsedUrl, err := url.Parse(ag.apiUrl) + if err != nil { + return "" + } + + return parsedUrl.Host +} + +// NeedsActiveGate returns true when a feature requires ActiveGate instances. +func (ag *Spec) IsEnabled() bool { + return len(ag.Capabilities) > 0 || ag.enabledDependencies.Any() +} + +func (ag *Spec) IsMode(mode CapabilityDisplayName) bool { + for _, capability := range ag.Capabilities { + if capability == mode { + return true + } + } + + return false +} + +func (ag *Spec) GetServiceAccountOwner() string { + if ag.IsKubernetesMonitoringEnabled() { + return string(KubeMonCapability.DisplayName) + } else { + return "activegate" + } +} + +func (ag *Spec) GetReplicas() int32 { + var defaultReplicas int32 = 1 + if ag.Replicas == nil { + return defaultReplicas + } + + return *ag.Replicas +} + +func (ag *Spec) GetServiceAccountName() string { + return "dynatrace-" + ag.GetServiceAccountOwner() +} + +func (ag *Spec) IsKubernetesMonitoringEnabled() bool { + return ag.IsMode(KubeMonCapability.DisplayName) +} + +func (ag *Spec) IsRoutingEnabled() bool { + return ag.IsMode(RoutingCapability.DisplayName) +} + +func (ag *Spec) IsApiEnabled() bool { + return ag.IsMode(DynatraceApiCapability.DisplayName) +} + +func (ag *Spec) IsMetricsIngestEnabled() bool { + return ag.IsMode(MetricsIngestCapability.DisplayName) +} + +func (ag *Spec) IsAutomaticTlsSecretEnabled() bool { + return ag.automaticTLSCertificateEnabled +} + +func (ag *Spec) HasCaCert() bool { + return ag.IsEnabled() && (ag.TlsSecretName != "" || ag.IsAutomaticTlsSecretEnabled()) +} + +// GetTenantSecretName returns the name of the secret containing tenant UUID, token and communication endpoints for ActiveGate. +func (ag *Spec) GetTenantSecretName() string { + return ag.name + TenantSecretSuffix +} + +// GetAuthTokenSecretName returns the name of the secret containing the ActiveGateAuthToken, which is mounted to the AGs. +func (ag *Spec) GetAuthTokenSecretName() string { + return ag.name + AuthTokenSecretSuffix +} + +// GetTLSSecretName returns the name of the AG TLS secret. +func (ag *Spec) GetTLSSecretName() string { + if ag.TlsSecretName != "" { + return ag.TlsSecretName + } + + if ag.IsAutomaticTlsSecretEnabled() { + return ag.name + TlsSecretSuffix + } + + return "" +} + +func (ag *Spec) GetConnectionInfoConfigMapName() string { + return ag.name + ConnectionInfoConfigMapSuffix +} + +// GetDefaultImage provides the image reference for the ActiveGate from tenant registry. +// Format: repo:tag. +func (ag *Spec) GetDefaultImage(version string) string { + apiUrlHost := ag.apiUrlHost() + if apiUrlHost == "" { + return "" + } + + truncatedVersion := dtversion.ToImageTag(version) + tag := truncatedVersion + + if !strings.Contains(tag, api.RawTag) { + tag += "-" + api.RawTag + } + + return apiUrlHost + DefaultImageRegistrySubPath + ":" + tag +} + +// CustomActiveGateImage provides the image reference for the ActiveGate provided in the Spec. +func (ag *Spec) GetCustomImage() string { + return ag.Image +} + +// GetTerminationGracePeriodSeconds provides the configured value for the terminatGracePeriodSeconds parameter of the pod. +func (ag *Spec) GetTerminationGracePeriodSeconds() *int64 { return ag.TerminationGracePeriodSeconds } diff --git a/pkg/api/v1beta4/dynakube/activegate/spec.go b/pkg/api/v1beta4/dynakube/activegate/spec.go new file mode 100644 index 0000000000..d32417a344 --- /dev/null +++ b/pkg/api/v1beta4/dynakube/activegate/spec.go @@ -0,0 +1,185 @@ +package activegate + +import ( + "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/value" + corev1 "k8s.io/api/core/v1" +) + +type CapabilityDisplayName string + +type Capability struct { + + // The name of the capability known by the user, mainly used in the CR + DisplayName CapabilityDisplayName + + // The name used for marking the pod for given capability + ShortName string + + // The string passed to the active gate image to enable a given capability + ArgumentName string +} + +var ( + RoutingCapability = Capability{ + DisplayName: "routing", + ShortName: "routing", + ArgumentName: "MSGrouter", + } + + KubeMonCapability = Capability{ + DisplayName: "kubernetes-monitoring", + ShortName: "kubemon", + ArgumentName: "kubernetes_monitoring", + } + + MetricsIngestCapability = Capability{ + DisplayName: "metrics-ingest", + ShortName: "metrics-ingest", + ArgumentName: "metrics_ingest", + } + + DynatraceApiCapability = Capability{ + DisplayName: "dynatrace-api", + ShortName: "dynatrace-api", + ArgumentName: "restInterface", + } + DebuggingCapability = Capability{ + DisplayName: "debugging", + ShortName: "debugging", + ArgumentName: "debugging", + } +) + +var CapabilityDisplayNames = map[CapabilityDisplayName]struct{}{ + RoutingCapability.DisplayName: {}, + KubeMonCapability.DisplayName: {}, + MetricsIngestCapability.DisplayName: {}, + DynatraceApiCapability.DisplayName: {}, + DebuggingCapability.DisplayName: {}, +} + +type ActiveGate struct { + *Spec + *Status +} + +// dependencies is a collection of possible other feature/components that need an ActiveGate, but is not directly configured in the ActiveGate section. +type dependencies struct { + extensions bool +} + +func (d dependencies) Any() bool { + return d.extensions // kspm is a dependency too, but blocked by validation webhook to not run standalone +} + +// +kubebuilder:object:generate=true + +type Spec struct { + + // Adds additional annotations to the ActiveGate pods + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Annotations",order=27,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:advanced","urn:alm:descriptor:com.tectonic.ui:text"} + Annotations map[string]string `json:"annotations,omitempty"` + + // Defines storage device + // +kubebuilder:validation:Optional + PersistentVolumeClaim *corev1.PersistentVolumeClaimSpec `json:"persistentVolumeClaim,omitempty"` + + // Configures the terminationGracePeriodSeconds parameter of the ActiveGate pod. Kubernetes defaults and rules apply. + // +kubebuild:validation:Optional + TerminationGracePeriodSeconds *int64 `json:"terminationGracePeriodSeconds,omitempty"` + + name string + apiUrl string + + // The name of a secret containing ActiveGate TLS cert+key and password. If not set, self-signed certificate is used. + // server.p12: certificate+key pair in pkcs12 format + // password: passphrase to read server.p12 + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="TlsSecretName",order=10,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:advanced","urn:alm:descriptor:com.tectonic.ui:text"} + TlsSecretName string `json:"tlsSecretName,omitempty"` + + // Sets DNS Policy for the ActiveGate pods + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="DNS Policy",order=24,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:advanced","urn:alm:descriptor:com.tectonic.ui:text"} + DNSPolicy corev1.DNSPolicy `json:"dnsPolicy,omitempty"` + + // If specified, indicates the pod's priority. Name must be defined by creating a PriorityClass object with that + // name. If not specified the setting will be removed from the StatefulSet. + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Priority Class name",order=23,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:advanced","urn:alm:descriptor:io.kubernetes:PriorityClass"} + PriorityClassName string `json:"priorityClassName,omitempty"` + + CapabilityProperties `json:",inline"` + + // Activegate capabilities enabled (routing, kubernetes-monitoring, metrics-ingest, dynatrace-api) + Capabilities []CapabilityDisplayName `json:"capabilities,omitempty"` + + enabledDependencies dependencies + + automaticTLSCertificateEnabled bool + + // UseEphemeralVolume + UseEphemeralVolume bool `json:"useEphemeralVolume,omitempty"` +} + +// +kubebuilder:object:generate=true + +// CapabilityProperties is a struct which can be embedded by ActiveGate capabilities +// Such as KubernetesMonitoring or Routing +// It encapsulates common properties. +type CapabilityProperties struct { + + // Add a custom properties file by providing it as a value or reference it from a secret + // +kubebuilder:validation:Optional + // If referenced from a secret, make sure the key is called 'customProperties' + CustomProperties *value.Source `json:"customProperties,omitempty"` + + // Node selector to control the selection of nodes + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Node Selector",order=35,xDescriptors="urn:alm:descriptor:com.tectonic.ui:selector:Node" + NodeSelector map[string]string `json:"nodeSelector,omitempty"` + + // Adds additional labels for the ActiveGate pods + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Labels",order=37,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:advanced","urn:alm:descriptor:com.tectonic.ui:text"} + Labels map[string]string `json:"labels,omitempty"` + + // Amount of replicas for your ActiveGates + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Replicas",order=30,xDescriptors="urn:alm:descriptor:com.tectonic.ui:podCount" + Replicas *int32 `json:"replicas,omitempty"` + + // The ActiveGate container image. Defaults to the latest ActiveGate image provided by the registry on the tenant + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Image",order=10,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:advanced","urn:alm:descriptor:com.tectonic.ui:text"} + Image string `json:"image,omitempty"` + + // Set activation group for ActiveGate + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Activation group",order=31,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:advanced","urn:alm:descriptor:com.tectonic.ui:text"} + Group string `json:"group,omitempty"` + + // Define resources requests and limits for single ActiveGate pods + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Resource Requirements",order=34,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:advanced","urn:alm:descriptor:com.tectonic.ui:resourceRequirements"} + Resources corev1.ResourceRequirements `json:"resources,omitempty"` + + // Set tolerations for the ActiveGate pods + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Tolerations",order=36,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:advanced","urn:alm:descriptor:com.tectonic.ui:hidden"} + Tolerations []corev1.Toleration `json:"tolerations,omitempty"` + + // List of environment variables to set for the ActiveGate + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Environment variables",order=39,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:advanced","urn:alm:descriptor:com.tectonic.ui:hidden"} + // +operator-sdk:gen-csv:customresourcedefinitions.specDescriptors=true + // +operator-sdk:gen-csv:customresourcedefinitions.specDescriptors.displayName="Environment variables" + // +operator-sdk:gen-csv:customresourcedefinitions.specDescriptors.x-descriptors="urn:alm:descriptor:com.tectonic.ui:advanced,urn:alm:descriptor:com.tectonic.ui:text" + Env []corev1.EnvVar `json:"env,omitempty"` + + // Adds TopologySpreadConstraints for the ActiveGate pods + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="topologySpreadConstraints",order=40,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:advanced","urn:alm:descriptor:com.tectonic.ui:hidden"} + TopologySpreadConstraints []corev1.TopologySpreadConstraint `json:"topologySpreadConstraints,omitempty"` +} diff --git a/pkg/api/v1beta4/dynakube/activegate/status.go b/pkg/api/v1beta4/dynakube/activegate/status.go new file mode 100644 index 0000000000..364f3c45bf --- /dev/null +++ b/pkg/api/v1beta4/dynakube/activegate/status.go @@ -0,0 +1,28 @@ +package activegate + +import ( + "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/communication" + "github.com/Dynatrace/dynatrace-operator/pkg/api/status" +) + +// +kubebuilder:object:generate=true +type Status struct { + status.VersionStatus `json:",inline"` + + // Information about Active Gate's connections + ConnectionInfo communication.ConnectionInfo `json:"connectionInfoStatus,omitempty"` + + // The ClusterIPs set by Kubernetes on the ActiveGate Service created by the Operator + ServiceIPs []string `json:"serviceIPs,omitempty"` +} + +// Image provides the image reference set in Status for the ActiveGate. +// Format: repo@sha256:digest. +func (ag *Status) GetImage() string { + return ag.ImageID +} + +// Version provides version set in Status for the ActiveGate. +func (ag *Status) GetVersion() string { + return ag.Version +} diff --git a/pkg/api/v1beta4/dynakube/activegate/zz_generated.deepcopy.go b/pkg/api/v1beta4/dynakube/activegate/zz_generated.deepcopy.go new file mode 100644 index 0000000000..4db276bb51 --- /dev/null +++ b/pkg/api/v1beta4/dynakube/activegate/zz_generated.deepcopy.go @@ -0,0 +1,146 @@ +//go:build !ignore_autogenerated + +/* +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package activegate + +import ( + "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/value" + "k8s.io/api/core/v1" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CapabilityProperties) DeepCopyInto(out *CapabilityProperties) { + *out = *in + if in.CustomProperties != nil { + in, out := &in.CustomProperties, &out.CustomProperties + *out = new(value.Source) + **out = **in + } + if in.NodeSelector != nil { + in, out := &in.NodeSelector, &out.NodeSelector + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Replicas != nil { + in, out := &in.Replicas, &out.Replicas + *out = new(int32) + **out = **in + } + in.Resources.DeepCopyInto(&out.Resources) + if in.Tolerations != nil { + in, out := &in.Tolerations, &out.Tolerations + *out = make([]v1.Toleration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Env != nil { + in, out := &in.Env, &out.Env + *out = make([]v1.EnvVar, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.TopologySpreadConstraints != nil { + in, out := &in.TopologySpreadConstraints, &out.TopologySpreadConstraints + *out = make([]v1.TopologySpreadConstraint, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CapabilityProperties. +func (in *CapabilityProperties) DeepCopy() *CapabilityProperties { + if in == nil { + return nil + } + out := new(CapabilityProperties) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Spec) DeepCopyInto(out *Spec) { + *out = *in + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.PersistentVolumeClaim != nil { + in, out := &in.PersistentVolumeClaim, &out.PersistentVolumeClaim + *out = new(v1.PersistentVolumeClaimSpec) + (*in).DeepCopyInto(*out) + } + if in.TerminationGracePeriodSeconds != nil { + in, out := &in.TerminationGracePeriodSeconds, &out.TerminationGracePeriodSeconds + *out = new(int64) + **out = **in + } + in.CapabilityProperties.DeepCopyInto(&out.CapabilityProperties) + if in.Capabilities != nil { + in, out := &in.Capabilities, &out.Capabilities + *out = make([]CapabilityDisplayName, len(*in)) + copy(*out, *in) + } + out.enabledDependencies = in.enabledDependencies +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Spec. +func (in *Spec) DeepCopy() *Spec { + if in == nil { + return nil + } + out := new(Spec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Status) DeepCopyInto(out *Status) { + *out = *in + in.VersionStatus.DeepCopyInto(&out.VersionStatus) + in.ConnectionInfo.DeepCopyInto(&out.ConnectionInfo) + if in.ServiceIPs != nil { + in, out := &in.ServiceIPs, &out.ServiceIPs + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Status. +func (in *Status) DeepCopy() *Status { + if in == nil { + return nil + } + out := new(Status) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/api/v1beta4/dynakube/activegate_props.go b/pkg/api/v1beta4/dynakube/activegate_props.go new file mode 100644 index 0000000000..2547e1c570 --- /dev/null +++ b/pkg/api/v1beta4/dynakube/activegate_props.go @@ -0,0 +1,17 @@ +package dynakube + +import ( + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" +) + +func (dk *DynaKube) ActiveGate() *activegate.ActiveGate { + dk.Spec.ActiveGate.SetApiUrl(dk.ApiUrl()) + dk.Spec.ActiveGate.SetName(dk.Name) + dk.Spec.ActiveGate.SetAutomaticTLSCertificate(dk.FeatureActiveGateAutomaticTLSCertificate()) + dk.Spec.ActiveGate.SetExtensionsDependency(dk.IsExtensionsEnabled()) + + return &activegate.ActiveGate{ + Spec: &dk.Spec.ActiveGate, + Status: &dk.Status.ActiveGate, + } +} diff --git a/pkg/api/v1beta4/dynakube/certs.go b/pkg/api/v1beta4/dynakube/certs.go new file mode 100644 index 0000000000..f6e5aa1068 --- /dev/null +++ b/pkg/api/v1beta4/dynakube/certs.go @@ -0,0 +1,66 @@ +/* +Copyright 2021 Dynatrace LLC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dynakube + +import ( + "context" + "fmt" + + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + TrustedCAKey = "certs" + TLSCertKey = "server.crt" +) + +func (dk *DynaKube) TrustedCAs(ctx context.Context, kubeReader client.Reader) ([]byte, error) { + configName := dk.Spec.TrustedCAs + if configName != "" { + var caConfigMap corev1.ConfigMap + + err := kubeReader.Get(ctx, client.ObjectKey{Name: configName, Namespace: dk.Namespace}, &caConfigMap) + if err != nil { + return nil, errors.WithMessage(err, fmt.Sprintf("failed to get trustedCa from %s configmap", configName)) + } + + return []byte(caConfigMap.Data[TrustedCAKey]), nil + } + + return nil, nil +} + +func (dk *DynaKube) ActiveGateTLSCert(ctx context.Context, kubeReader client.Reader) ([]byte, error) { + if dk.ActiveGate().HasCaCert() { + secretName := dk.Spec.ActiveGate.GetTLSSecretName() + + var tlsSecret corev1.Secret + + err := kubeReader.Get(ctx, client.ObjectKey{Name: secretName, Namespace: dk.Namespace}, &tlsSecret) + if err != nil { + return nil, errors.WithMessage(err, fmt.Sprintf("failed to get activeGate tlsCert from %s secret", secretName)) + } + + if tlsCertKey, ok := tlsSecret.Data[TLSCertKey]; ok { + return tlsCertKey, nil + } + } + + return nil, nil +} diff --git a/pkg/api/v1beta3/dynakube/conversion.go b/pkg/api/v1beta4/dynakube/conversion.go similarity index 100% rename from pkg/api/v1beta3/dynakube/conversion.go rename to pkg/api/v1beta4/dynakube/conversion.go diff --git a/pkg/api/v1beta4/dynakube/dynakube_props.go b/pkg/api/v1beta4/dynakube/dynakube_props.go new file mode 100644 index 0000000000..66b73ad319 --- /dev/null +++ b/pkg/api/v1beta4/dynakube/dynakube_props.go @@ -0,0 +1,106 @@ +package dynakube + +import ( + "net/url" + "time" + + "github.com/Dynatrace/dynatrace-operator/pkg/util/timeprovider" + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + // MaxNameLength is the maximum length of a DynaKube's name, we tend to add suffixes to the name to avoid name collisions for resources related to the DynaKube. (example: dkName-activegate-) + // The limit is necessary because kubernetes uses the name of some resources (ActiveGate StatefulSet) for the label value, which has a limit of 63 characters. (see https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#syntax-and-character-set) + MaxNameLength = 40 + + // PullSecretSuffix is the suffix appended to the DynaKube name to n. + PullSecretSuffix = "-pull-secret" +) + +// ApiUrl is a getter for dk.Spec.APIURL. +func (dk *DynaKube) ApiUrl() string { + return dk.Spec.APIURL +} + +func (dk *DynaKube) Conditions() *[]metav1.Condition { return &dk.Status.Conditions } + +// ApiUrlHost returns the host of dk.Spec.APIURL +// E.g. if the APIURL is set to "https://my-tenant.dynatrace.com/api", it returns "my-tenant.dynatrace.com" +// If the URL cannot be parsed, it returns an empty string. +func (dk *DynaKube) ApiUrlHost() string { + parsedUrl, err := url.Parse(dk.ApiUrl()) + if err != nil { + return "" + } + + return parsedUrl.Host +} + +// PullSecretName returns the name of the pull secret to be used for immutable images. +func (dk *DynaKube) PullSecretName() string { + if dk.Spec.CustomPullSecret != "" { + return dk.Spec.CustomPullSecret + } + + return dk.Name + PullSecretSuffix +} + +// PullSecretsNames returns the names of the pull secrets to be used for immutable images. +func (dk *DynaKube) PullSecretNames() []string { + names := []string{ + dk.Name + PullSecretSuffix, + } + if dk.Spec.CustomPullSecret != "" { + names = append(names, dk.Spec.CustomPullSecret) + } + + return names +} + +func (dk *DynaKube) ImagePullSecretReferences() []corev1.LocalObjectReference { + imagePullSecrets := make([]corev1.LocalObjectReference, 0) + for _, pullSecretName := range dk.PullSecretNames() { + imagePullSecrets = append(imagePullSecrets, corev1.LocalObjectReference{ + Name: pullSecretName, + }) + } + + return imagePullSecrets +} + +// Tokens returns the name of the Secret to be used for tokens. +func (dk *DynaKube) Tokens() string { + if tkns := dk.Spec.Tokens; tkns != "" { + return tkns + } + + return dk.Name +} + +func (dk *DynaKube) TenantUUID() (string, error) { + if dk.Status.OneAgent.ConnectionInfoStatus.TenantUUID != "" { + return dk.Status.OneAgent.ConnectionInfoStatus.TenantUUID, nil + } else if dk.Status.ActiveGate.ConnectionInfo.TenantUUID != "" { + return dk.Status.ActiveGate.ConnectionInfo.TenantUUID, nil + } + + return "", errors.New("tenant UUID not available") +} + +func (dk *DynaKube) GetDynatraceApiRequestThreshold() uint16 { + if dk.Spec.DynatraceApiRequestThreshold == nil { + return DefaultMinRequestThresholdMinutes + } + + return *dk.Spec.DynatraceApiRequestThreshold +} + +func (dk *DynaKube) ApiRequestThreshold() time.Duration { + return time.Duration(dk.GetDynatraceApiRequestThreshold()) * time.Minute +} + +func (dk *DynaKube) IsTokenScopeVerificationAllowed(timeProvider *timeprovider.Provider) bool { + return timeProvider.IsOutdated(&dk.Status.DynatraceApi.LastTokenScopeRequest, dk.ApiRequestThreshold()) +} diff --git a/pkg/api/v1beta4/dynakube/dynakube_props_test.go b/pkg/api/v1beta4/dynakube/dynakube_props_test.go new file mode 100644 index 0000000000..bb470f80c9 --- /dev/null +++ b/pkg/api/v1beta4/dynakube/dynakube_props_test.go @@ -0,0 +1,118 @@ +/* +Copyright 2021 Dynatrace LLC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dynakube + +import ( + "testing" + "time" + + "github.com/Dynatrace/dynatrace-operator/pkg/util/timeprovider" + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" +) + +func TestTokens(t *testing.T) { + testName := "test-name" + testValue := "test-value" + + t.Run(`GetTokensName returns custom token name`, func(t *testing.T) { + dk := DynaKube{ + ObjectMeta: metav1.ObjectMeta{Name: testName}, + Spec: DynaKubeSpec{Tokens: testValue}, + } + assert.Equal(t, dk.Tokens(), testValue) + }) + t.Run(`GetTokensName uses instance name as default value`, func(t *testing.T) { + dk := DynaKube{ObjectMeta: metav1.ObjectMeta{Name: testName}} + assert.Equal(t, dk.Tokens(), testName) + }) +} + +func TestIsTokenScopeVerificationAllowed(t *testing.T) { + dk := DynaKube{ + Status: DynaKubeStatus{ + DynatraceApi: DynatraceApiStatus{ + LastTokenScopeRequest: metav1.Time{}, + }, + }, + } + + timeProvider := timeprovider.New().Freeze() + tests := map[string]struct { + lastRequestTimeDeltaMinutes int + updateExpected bool + threshold *uint16 + }{ + "Do not update after 10 minutes using default interval": { + lastRequestTimeDeltaMinutes: -10, + updateExpected: false, + threshold: nil, + }, + "Do update after 20 minutes using default interval": { + lastRequestTimeDeltaMinutes: -20, + updateExpected: true, + threshold: nil, + }, + "Do not update after 3 minutes using 5m interval": { + lastRequestTimeDeltaMinutes: -3, + updateExpected: false, + threshold: ptr.To(uint16(5)), + }, + "Do update after 7 minutes using 5m interval": { + lastRequestTimeDeltaMinutes: -7, + updateExpected: true, + threshold: ptr.To(uint16(5)), + }, + "Do not update after 17 minutes using 20m interval": { + lastRequestTimeDeltaMinutes: -17, + updateExpected: false, + threshold: ptr.To(uint16(20)), + }, + "Do update after 22 minutes using 20m interval": { + lastRequestTimeDeltaMinutes: -22, + updateExpected: true, + threshold: ptr.To(uint16(20)), + }, + "Do update immediately using 0m interval": { + lastRequestTimeDeltaMinutes: 0, + updateExpected: true, + threshold: ptr.To(uint16(0)), + }, + "Do update after 1 minute using 0m interval": { + lastRequestTimeDeltaMinutes: -1, + updateExpected: true, + threshold: ptr.To(uint16(0)), + }, + "Do update after 20 minutes using 0m interval": { + lastRequestTimeDeltaMinutes: -20, + updateExpected: true, + threshold: ptr.To(uint16(0)), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + dk.Spec.DynatraceApiRequestThreshold = test.threshold + + lastRequestTime := timeProvider.Now().Add(time.Duration(test.lastRequestTimeDeltaMinutes) * time.Minute) + dk.Status.DynatraceApi.LastTokenScopeRequest.Time = lastRequestTime + + assert.Equal(t, test.updateExpected, dk.IsTokenScopeVerificationAllowed(timeProvider)) + }) + } +} diff --git a/pkg/api/v1beta4/dynakube/dynakube_status.go b/pkg/api/v1beta4/dynakube/dynakube_status.go new file mode 100644 index 0000000000..f5bbe6d317 --- /dev/null +++ b/pkg/api/v1beta4/dynakube/dynakube_status.go @@ -0,0 +1,123 @@ +package dynakube + +import ( + "context" + "fmt" + "time" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/status" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/kspm" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" + "github.com/pkg/errors" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// DynaKubeStatus defines the observed state of DynaKube +// +k8s:openapi-gen=true +type DynaKubeStatus struct { //nolint:revive + + // Observed state of OneAgent + OneAgent oneagent.Status `json:"oneAgent,omitempty"` + + // Observed state of ActiveGate + ActiveGate activegate.Status `json:"activeGate,omitempty"` + + // Observed state of Code Modules + CodeModules oneagent.CodeModulesStatus `json:"codeModules,omitempty"` + + // Observed state of Metadata-Enrichment + MetadataEnrichment MetadataEnrichmentStatus `json:"metadataEnrichment,omitempty"` + + // Observed state of Kspm + Kspm kspm.Status `json:"kspm,omitempty"` + + // UpdatedTimestamp indicates when the instance was last updated + // +operator-sdk:gen-csv:customresourcedefinitions.statusDescriptors=true + // +operator-sdk:gen-csv:customresourcedefinitions.specDescriptors.displayName="Last Updated" + // +operator-sdk:gen-csv:customresourcedefinitions.statusDescriptors.x-descriptors="urn:alm:descriptor:text" + UpdatedTimestamp metav1.Time `json:"updatedTimestamp,omitempty"` + + // Observed state of Dynatrace API + DynatraceApi DynatraceApiStatus `json:"dynatraceApi,omitempty"` + + // Defines the current state (Running, Updating, Error, ...) + Phase status.DeploymentPhase `json:"phase,omitempty"` + + // KubeSystemUUID contains the UUID of the current Kubernetes cluster + KubeSystemUUID string `json:"kubeSystemUUID,omitempty"` + + // KubernetesClusterMEID contains the ID of the monitored entity that points to the Kubernetes cluster + KubernetesClusterMEID string `json:"kubernetesClusterMEID,omitempty"` + + // KubernetesClusterName contains the display name (also know as label) of the monitored entity that points to the Kubernetes cluster + KubernetesClusterName string `json:"kubernetesClusterName,omitempty"` + + // Conditions includes status about the current state of the instance + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +type DynatraceApiStatus struct { + // Time of the last token request + LastTokenScopeRequest metav1.Time `json:"lastTokenScopeRequest,omitempty"` +} + +func GetCacheValidMessage(functionName string, lastRequestTimestamp metav1.Time, timeout time.Duration) string { + remaining := timeout - time.Since(lastRequestTimestamp.Time) + + return fmt.Sprintf("skipping %s, last request was made less than %d minutes ago, %d minutes remaining until next request", + functionName, + int(timeout.Minutes()), + int(remaining.Minutes())) +} + +type EnrichmentRuleType string + +const ( + EnrichmentLabelRule EnrichmentRuleType = "LABEL" + EnrichmentAnnotationRule EnrichmentRuleType = "ANNOTATION" +) + +const MetadataPrefix string = "metadata.dynatrace.com/" + +type MetadataEnrichmentStatus struct { + Rules []EnrichmentRule `json:"rules,omitempty"` +} + +type EnrichmentRule struct { + Type EnrichmentRuleType `json:"type,omitempty"` + Source string `json:"source,omitempty"` + Target string `json:"target,omitempty"` + Enabled bool `json:"enabled,omitempty"` +} + +func (rule EnrichmentRule) ToAnnotationKey() string { + if rule.Target == "" { + return "" + } + + return MetadataPrefix + rule.Target +} + +// SetPhase sets the status phase on the DynaKube object. +func (dk *DynaKubeStatus) SetPhase(phase status.DeploymentPhase) bool { + upd := phase != dk.Phase + dk.Phase = phase + + return upd +} + +func (dk *DynaKube) UpdateStatus(ctx context.Context, client client.Client) error { + dk.Status.UpdatedTimestamp = metav1.Now() + err := client.Status().Update(ctx, dk) + + if err != nil && k8serrors.IsConflict(err) { + log.Info("could not update dynakube due to conflict", "name", dk.Name) + + return nil + } + + return errors.WithStack(err) +} diff --git a/pkg/api/v1beta4/dynakube/dynakube_types.go b/pkg/api/v1beta4/dynakube/dynakube_types.go new file mode 100644 index 0000000000..e91ce74e9b --- /dev/null +++ b/pkg/api/v1beta4/dynakube/dynakube_types.go @@ -0,0 +1,180 @@ +// +kubebuilder:object:generate=true +// +groupName=dynatrace.com +// +versionName=v1beta4 +package dynakube + +import ( + "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/value" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/kspm" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/logmonitoring" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/telemetryingest" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// TODO: Move these conditions related consts to a place where they are used, so we don't bloat this package further. +const ( + // TokenConditionType identifies the token validity condition. + TokenConditionType string = "Tokens" + + // APITokenConditionType identifies the API Token validity condition. + APITokenConditionType string = "APIToken" + + // PaaSTokenConditionType identifies the PaaS Token validity condition. + PaaSTokenConditionType string = "PaaSToken" + + // DataIngestTokenConditionType identifies the DataIngest Token validity condition. + DataIngestTokenConditionType string = "DataIngestToken" +) + +// Possible reasons for ApiToken and PaaSToken conditions. +const ( + // ReasonTokenReady is set when a token has passed verifications. + ReasonTokenReady string = "TokenReady" + + // ReasonTokenError is set when an unknown error has been found when verifying the token. + ReasonTokenError string = "TokenError" +) + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// DynaKube is the Schema for the DynaKube API +// +k8s:openapi-gen=true +// +kubebuilder:storageversion +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:path=dynakubes,scope=Namespaced,categories=dynatrace,shortName={dk,dks} +// +kubebuilder:printcolumn:name="ApiUrl",type=string,JSONPath=`.spec.apiUrl` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` +// +operator-sdk:csv:customresourcedefinitions:displayName="Dynatrace DynaKube" +// +operator-sdk:csv:customresourcedefinitions:resources={{StatefulSet,v1,},{DaemonSet,v1,},{Pod,v1,}} +type DynaKube struct { + metav1.TypeMeta `json:",inline"` + + Status DynaKubeStatus `json:"status,omitempty"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec DynaKubeSpec `json:"spec,omitempty"` +} + +// DynaKubeSpec defines the desired state of DynaKube +// +k8s:openapi-gen=true +type DynaKubeSpec struct { //nolint:revive + // Add custom validation using kubebuilder tags: https://book-v1.book.kubebuilder.io/beyond_basics/generating_crd.html + + // Configuration for Metadata Enrichment. + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Metadata Enrichment",order=9,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:advanced"} + MetadataEnrichment MetadataEnrichment `json:"metadataEnrichment,omitempty"` + + // Set custom proxy settings either directly or from a secret with the field proxy. + // Note: Applies to Dynatrace Operator, ActiveGate, and OneAgents. + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Proxy",order=3,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:advanced","urn:alm:descriptor:com.tectonic.ui:booleanSwitch"} + Proxy *value.Source `json:"proxy,omitempty"` + + // General configuration about the LogMonitoring feature. + // +kubebuilder:validation:Optional + LogMonitoring *logmonitoring.Spec `json:"logMonitoring,omitempty"` + + // General configuration about the KSPM feature. + // +kubebuilder:validation:Optional + Kspm *kspm.Spec `json:"kspm,omitempty"` + + // Configuration for thresholding Dynatrace API requests. + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Dynatrace API Request Threshold",order=9,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:advanced"} + DynatraceApiRequestThreshold *uint16 `json:"dynatraceApiRequestThreshold,omitempty"` + + // When an (empty) ExtensionsSpec is provided, the extensions related components (extensions controller and extensions collector) + // are deployed by the operator. + // +kubebuilder:validation:Optional + Extensions *ExtensionsSpec `json:"extensions,omitempty"` + + // When a TelemetryIngestSpec is provided, the OTEL collector is deployed by the operator. + // +kubebuilder:validation:Optional + TelemetryIngest *telemetryingest.Spec `json:"telemetryIngest,omitempty"` + + // General configuration about OneAgent instances. + // You can't enable more than one module (classicFullStack, cloudNativeFullStack, hostMonitoring, or applicationMonitoring). + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="OneAgent",xDescriptors="urn:alm:descriptor:com.tectonic.ui:text" + OneAgent oneagent.Spec `json:"oneAgent,omitempty"` + + // Dynatrace apiUrl, including the /api path at the end. For SaaS, set YOUR_ENVIRONMENT_ID to your environment ID. For Managed, change the apiUrl address. + // For instructions on how to determine the environment ID and how to configure the apiUrl address, see Environment ID (https://www.dynatrace.com/support/help/get-started/monitoring-environment/environment-id). + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength=128 + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="API URL",order=1,xDescriptors="urn:alm:descriptor:com.tectonic.ui:text" + APIURL string `json:"apiUrl"` + + // Name of the secret holding the tokens used for connecting to Dynatrace. + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Tenant specific secrets",order=2,xDescriptors="urn:alm:descriptor:io.kubernetes:Secret" + Tokens string `json:"tokens,omitempty"` + + // Adds custom RootCAs from a configmap. Put the certificate under certs within your configmap. + // Note: Applies to Dynatrace Operator, OneAgent and ActiveGate. + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Trusted CAs",order=6,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:advanced","urn:alm:descriptor:io.kubernetes:ConfigMap"} + TrustedCAs string `json:"trustedCAs,omitempty"` + + // Sets a network zone for the OneAgent and ActiveGate pods. + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Network Zone",order=7,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:advanced","urn:alm:descriptor:com.tectonic.ui:text"} + NetworkZone string `json:"networkZone,omitempty"` + + // Defines a custom pull secret in case you use a private registry when pulling images from the Dynatrace environment. + // To define a custom pull secret and learn about the expected behavior, see Configure customPullSecret + // (https://www.dynatrace.com/support/help/setup-and-configuration/setup-on-container-platforms/kubernetes/get-started-with-kubernetes-monitoring/dto-config-options-k8s#custompullsecret). + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Custom PullSecret",order=8,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:advanced","urn:alm:descriptor:io.kubernetes:Secret"} + CustomPullSecret string `json:"customPullSecret,omitempty"` + + // +kubebuilder:validation:Optional + Templates TemplatesSpec `json:"templates,omitempty"` + + // General configuration about ActiveGate instances. + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="ActiveGate",xDescriptors="urn:alm:descriptor:com.tectonic.ui:text" + ActiveGate activegate.Spec `json:"activeGate,omitempty"` + + // Disable certificate check for the connection between Dynatrace Operator and the Dynatrace Cluster. + // Set to true if you want to skip certification validation checks. + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Skip Certificate Check",order=3,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:advanced","urn:alm:descriptor:com.tectonic.ui:booleanSwitch"} + SkipCertCheck bool `json:"skipCertCheck,omitempty"` + + // When enabled, and if Istio is installed on the Kubernetes environment, Dynatrace Operator will create the corresponding + // VirtualService and ServiceEntry objects to allow access to the Dynatrace Cluster from the OneAgent or ActiveGate. + // Disabled by default. + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Enable Istio automatic management",order=9,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:advanced","urn:alm:descriptor:com.tectonic.ui:booleanSwitch"} + EnableIstio bool `json:"enableIstio,omitempty"` +} + +type TemplatesSpec struct { + // Low-level configuration options for the LogMonitoring feature. + // +kubebuilder:validation:Optional + LogMonitoring *logmonitoring.TemplateSpec `json:"logMonitoring,omitempty"` + // +kubebuilder:validation:Optional + KspmNodeConfigurationCollector kspm.NodeConfigurationCollectorSpec `json:"kspmNodeConfigurationCollector,omitempty"` + // +kubebuilder:validation:Optional + OpenTelemetryCollector OpenTelemetryCollectorSpec `json:"otelCollector,omitempty"` + // +kubebuilder:validation:Optional + ExtensionExecutionController ExtensionExecutionControllerSpec `json:"extensionExecutionController,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// DynaKubeList contains a list of DynaKube +// +kubebuilder:object:root=true +type DynaKubeList struct { //nolint:revive + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []DynaKube `json:"items"` +} + +func init() { + v1beta4.SchemeBuilder.Register(&DynaKube{}, &DynaKubeList{}) +} diff --git a/pkg/api/v1beta4/dynakube/dynakube_webhook.go b/pkg/api/v1beta4/dynakube/dynakube_webhook.go new file mode 100644 index 0000000000..e09373cbc9 --- /dev/null +++ b/pkg/api/v1beta4/dynakube/dynakube_webhook.go @@ -0,0 +1,13 @@ +package dynakube + +import ( + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +func SetupWebhookWithManager(mgr ctrl.Manager, validator admission.CustomValidator) error { + return ctrl.NewWebhookManagedBy(mgr). + For(&DynaKube{}). + WithValidator(validator). // will create an endpoint at /validate-dynatrace-com-v1beta4-dynakube + Complete() +} diff --git a/pkg/api/v1beta4/dynakube/extensions.go b/pkg/api/v1beta4/dynakube/extensions.go new file mode 100644 index 0000000000..572a3db6d4 --- /dev/null +++ b/pkg/api/v1beta4/dynakube/extensions.go @@ -0,0 +1,88 @@ +package dynakube + +import ( + "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/image" + corev1 "k8s.io/api/core/v1" +) + +// +kubebuilder:validation:Optional +type ExtensionsSpec struct { +} + +type ExtensionExecutionControllerSpec struct { + + // Defines storage device + // +kubebuilder:validation:Optional + PersistentVolumeClaim *corev1.PersistentVolumeClaimSpec `json:"persistentVolumeClaim,omitempty"` + + // Adds additional labels for the ExtensionExecutionController pods + // +kubebuilder:validation:Optional + Labels map[string]string `json:"labels,omitempty"` + + // Adds additional annotations to the ExtensionExecutionController pods + Annotations map[string]string `json:"annotations,omitempty"` + + // Overrides the default image + // +kubebuilder:validation:Optional + ImageRef image.Ref `json:"imageRef,omitempty"` + + // +kubebuilder:validation:Optional + TlsRefName string `json:"tlsRefName,omitempty"` + + // Defines name of ConfigMap containing custom configuration file + // +kubebuilder:validation:Optional + CustomConfig string `json:"customConfig,omitempty"` + + // Defines name of Secret containing certificates for custom extensions signature validation + // +kubebuilder:validation:Optional + CustomExtensionCertificates string `json:"customExtensionCertificates,omitempty"` + + // Define resources' requests and limits for single ExtensionExecutionController pod + // +kubebuilder:validation:Optional + Resources corev1.ResourceRequirements `json:"resources,omitempty"` + + // Set tolerations for the ExtensionExecutionController pods + // +kubebuilder:validation:Optional + Tolerations []corev1.Toleration `json:"tolerations,omitempty"` + + // Adds TopologySpreadConstraints for the ExtensionExecutionController pods + // +kubebuilder:validation:Optional + TopologySpreadConstraints []corev1.TopologySpreadConstraint `json:"topologySpreadConstraints,omitempty"` + // Selects EmptyDir volume to be storage device + // +kubebuilder:validation:Optional + UseEphemeralVolume bool `json:"useEphemeralVolume,omitempty"` +} + +type OpenTelemetryCollectorSpec struct { + + // Adds additional labels for the OtelCollector pods + // +kubebuilder:validation:Optional + Labels map[string]string `json:"labels,omitempty"` + + // Adds additional annotations to the OtelCollector pods + // +kubebuilder:validation:Optional + Annotations map[string]string `json:"annotations,omitempty"` + + // Number of replicas for your OtelCollector + // +kubebuilder:validation:Optional + Replicas *int32 `json:"replicas"` + + // Overrides the default image + // +kubebuilder:validation:Optional + ImageRef image.Ref `json:"imageRef,omitempty"` + + // +kubebuilder:validation:Optional + TlsRefName string `json:"tlsRefName,omitempty"` + + // Define resources' requests and limits for single OtelCollector pod + // +kubebuilder:validation:Optional + Resources corev1.ResourceRequirements `json:"resources,omitempty"` + + // Set tolerations for the OtelCollector pods + // +kubebuilder:validation:Optional + Tolerations []corev1.Toleration `json:"tolerations,omitempty"` + + // Adds TopologySpreadConstraints for the OtelCollector pods + // +kubebuilder:validation:Optional + TopologySpreadConstraints []corev1.TopologySpreadConstraint `json:"topologySpreadConstraints,omitempty"` +} diff --git a/pkg/api/v1beta4/dynakube/extensions_props.go b/pkg/api/v1beta4/dynakube/extensions_props.go new file mode 100644 index 0000000000..acdd7a3911 --- /dev/null +++ b/pkg/api/v1beta4/dynakube/extensions_props.go @@ -0,0 +1,49 @@ +package dynakube + +import ( + "github.com/Dynatrace/dynatrace-operator/pkg/consts" +) + +func (dk *DynaKube) IsExtensionsEnabled() bool { + return dk.Spec.Extensions != nil +} + +func (dk *DynaKube) ExtensionsTLSRefName() string { + return dk.Spec.Templates.ExtensionExecutionController.TlsRefName +} + +func (dk *DynaKube) ExtensionsNeedsSelfSignedTLS() bool { + return dk.ExtensionsTLSRefName() == "" +} + +func (dk *DynaKube) ExtensionsTLSSecretName() string { + if dk.ExtensionsNeedsSelfSignedTLS() { + return dk.ExtensionsSelfSignedTLSSecretName() + } + + return dk.ExtensionsTLSRefName() +} + +func (dk *DynaKube) ExtensionsSelfSignedTLSSecretName() string { + return dk.Name + consts.ExtensionsSelfSignedTLSSecretSuffix +} + +func (dk *DynaKube) ExtensionsExecutionControllerStatefulsetName() string { + return dk.Name + "-extensions-controller" +} + +func (dk *DynaKube) ExtensionsTokenSecretName() string { + return dk.Name + "-extensions-token" +} + +func (dk *DynaKube) ExtensionsPortName() string { + return "dynatrace" + consts.ExtensionsControllerSuffix + "-" + consts.ExtensionsCollectorTargetPortName +} + +func (dk *DynaKube) ExtensionsServiceNameFQDN() string { + return dk.ExtensionsServiceName() + "." + dk.Namespace +} + +func (dk *DynaKube) ExtensionsServiceName() string { + return dk.Name + consts.ExtensionsControllerSuffix +} diff --git a/pkg/api/v1beta4/dynakube/feature_flags.go b/pkg/api/v1beta4/dynakube/feature_flags.go new file mode 100644 index 0000000000..44c5ad0609 --- /dev/null +++ b/pkg/api/v1beta4/dynakube/feature_flags.go @@ -0,0 +1,310 @@ +/* +Copyright 2021 Dynatrace LLC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dynakube + +import ( + "encoding/json" + "fmt" + "math" + "strconv" + "time" + + "github.com/Dynatrace/dynatrace-operator/pkg/logd" +) + +const ( + AnnotationFeaturePrefix = "feature.dynatrace.com/" + + // General. + AnnotationFeaturePublicRegistry = AnnotationFeaturePrefix + "public-registry" + + // activeGate. + + // Deprecated: AnnotationFeatureDisableActiveGateUpdates use AnnotationFeatureActiveGateUpdates instead. + AnnotationFeatureDisableActiveGateUpdates = AnnotationFeaturePrefix + "disable-activegate-updates" + // Deprecated: AnnotationFeatureActiveGateIgnoreProxy use AnnotationFeatureNoProxy instead. + AnnotationFeatureActiveGateIgnoreProxy = AnnotationFeaturePrefix + "activegate-ignore-proxy" + + AnnotationFeatureActiveGateUpdates = AnnotationFeaturePrefix + "activegate-updates" + + AnnotationFeatureActiveGateAppArmor = AnnotationFeaturePrefix + "activegate-apparmor" + AnnotationFeatureAutomaticK8sApiMonitoring = AnnotationFeaturePrefix + "automatic-kubernetes-api-monitoring" + AnnotationFeatureAutomaticK8sApiMonitoringClusterName = AnnotationFeaturePrefix + "automatic-kubernetes-api-monitoring-cluster-name" + AnnotationFeatureK8sAppEnabled = AnnotationFeaturePrefix + "k8s-app-enabled" + + AnnotationFeatureActiveGateAutomaticTLSCertificate = AnnotationFeaturePrefix + "automatic-tls-certificate" + + // dtClient. + + AnnotationFeatureNoProxy = AnnotationFeaturePrefix + "no-proxy" + + // oneAgent. + + // Deprecated: AnnotationFeatureOneAgentIgnoreProxy use AnnotationFeatureNoProxy instead. + AnnotationFeatureOneAgentIgnoreProxy = AnnotationFeaturePrefix + "oneagent-ignore-proxy" + + AnnotationFeatureOneAgentMaxUnavailable = AnnotationFeaturePrefix + "oneagent-max-unavailable" + AnnotationFeatureOneAgentInitialConnectRetry = AnnotationFeaturePrefix + "oneagent-initial-connect-retry-ms" + AnnotationFeatureRunOneAgentContainerPrivileged = AnnotationFeaturePrefix + "oneagent-privileged" + AnnotationFeatureOneAgentSkipLivenessProbe = AnnotationFeaturePrefix + "oneagent-skip-liveness-probe" + + AnnotationFeatureIgnoreUnknownState = AnnotationFeaturePrefix + "ignore-unknown-state" + AnnotationFeatureIgnoredNamespaces = AnnotationFeaturePrefix + "ignored-namespaces" + AnnotationFeatureAutomaticInjection = AnnotationFeaturePrefix + "automatic-injection" + AnnotationFeatureLabelVersionDetection = AnnotationFeaturePrefix + "label-version-detection" + AnnotationInjectionFailurePolicy = AnnotationFeaturePrefix + "injection-failure-policy" + AnnotationFeatureInitContainerSeccomp = AnnotationFeaturePrefix + "init-container-seccomp-profile" + AnnotationFeatureEnforcementMode = AnnotationFeaturePrefix + "enforcement-mode" + + // CSI. + AnnotationFeatureMaxFailedCsiMountAttempts = AnnotationFeaturePrefix + "max-csi-mount-attempts" + AnnotationFeatureMaxCsiMountTimeout = AnnotationFeaturePrefix + "max-csi-mount-timeout" + AnnotationFeatureReadOnlyCsiVolume = AnnotationFeaturePrefix + "injection-readonly-volume" + AnnotationFeatureNodeImagePull = AnnotationFeaturePrefix + "node-image-pull" + + falsePhrase = "false" + truePhrase = "true" + silentPhrase = "silent" + failPhrase = "fail" + + // AnnotationTechnologies can be set on a Pod or DynaKube to configure which code module technologies to download. It's set to + // "all" if not set. + AnnotationTechnologies = "oneagent.dynatrace.com/technologies" +) + +const ( + DefaultMaxCsiMountTimeout = "10m" + DefaultMaxFailedCsiMountAttempts = 10 + DefaultMinRequestThresholdMinutes = 15 + IstioDefaultOneAgentInitialConnectRetry = 6000 +) + +var ( + log = logd.Get().WithName("dynakube-api") +) + +func (dk *DynaKube) getDisableFlagWithDeprecatedAnnotation(annotation string, deprecatedAnnotation string) bool { + return dk.getFeatureFlagRaw(annotation) == falsePhrase || + dk.getFeatureFlagRaw(deprecatedAnnotation) == truePhrase && dk.getFeatureFlagRaw(annotation) == "" +} + +func (dk *DynaKube) getFeatureFlagRaw(annotation string) string { + if raw, ok := dk.Annotations[annotation]; ok { + return raw + } + + return "" +} + +func (dk *DynaKube) getFeatureFlagInt(annotation string, defaultVal int) int { + raw := dk.getFeatureFlagRaw(annotation) + if raw == "" { + return defaultVal + } + + val, err := strconv.Atoi(raw) + if err != nil { + return defaultVal + } + + return val +} + +// FeatureDisableActiveGateUpdates is a feature flag to disable ActiveGate updates. +func (dk *DynaKube) FeatureDisableActiveGateUpdates() bool { + return dk.getDisableFlagWithDeprecatedAnnotation(AnnotationFeatureActiveGateUpdates, AnnotationFeatureDisableActiveGateUpdates) +} + +// FeatureNoProxy is a feature flag to set the NO_PROXY value to be used by the dtClient. +func (dk *DynaKube) FeatureNoProxy() string { + return dk.getFeatureFlagRaw(AnnotationFeatureNoProxy) +} + +// FeatureActiveGateAutomaticTLSCertificate is a feature flag to disable automatic creation of ActiveGate TLS certificate. +func (dk *DynaKube) FeatureActiveGateAutomaticTLSCertificate() bool { + return dk.getFeatureFlagRaw(AnnotationFeatureActiveGateAutomaticTLSCertificate) != falsePhrase +} + +// FeatureOneAgentMaxUnavailable is a feature flag to configure maxUnavailable on the OneAgent DaemonSets rolling upgrades. +func (dk *DynaKube) FeatureOneAgentMaxUnavailable() int { + return dk.getFeatureFlagInt(AnnotationFeatureOneAgentMaxUnavailable, 1) +} + +// FeatureIgnoreUnknownState is a feature flag that makes the operator inject into applications even when the dynakube is in an UNKNOWN state, +// this may cause extra host to appear in the tenant for each process. +func (dk *DynaKube) FeatureIgnoreUnknownState() bool { + return dk.getFeatureFlagRaw(AnnotationFeatureIgnoreUnknownState) == truePhrase +} + +// FeatureIgnoredNamespaces is a feature flag for ignoring certain namespaces. +// defaults to "[ \"^dynatrace$\", \"^kube-.*\", \"openshift(-.*)?\" ]". +func (dk *DynaKube) FeatureIgnoredNamespaces() []string { + raw := dk.getFeatureFlagRaw(AnnotationFeatureIgnoredNamespaces) + if raw == "" { + return dk.getDefaultIgnoredNamespaces() + } + + ignoredNamespaces := &[]string{} + + err := json.Unmarshal([]byte(raw), ignoredNamespaces) + if err != nil { + log.Error(err, "failed to unmarshal ignoredNamespaces feature-flag") + + return dk.getDefaultIgnoredNamespaces() + } + + return *ignoredNamespaces +} + +func (dk *DynaKube) getDefaultIgnoredNamespaces() []string { + defaultIgnoredNamespaces := []string{ + fmt.Sprintf("^%s$", dk.Namespace), + "^kube-.*", + "^openshift(-.*)?", + "^gke-.*", + "^gmp-.*", + } + + return defaultIgnoredNamespaces +} + +// FeatureAutomaticKubernetesApiMonitoring is a feature flag to enable automatic kubernetes api monitoring, +// which ensures that settings for this kubernetes cluster exist in Dynatrace. +func (dk *DynaKube) FeatureAutomaticKubernetesApiMonitoring() bool { + return dk.getFeatureFlagRaw(AnnotationFeatureAutomaticK8sApiMonitoring) != falsePhrase +} + +// FeatureAutomaticKubernetesApiMonitoringClusterName is a feature flag to set custom cluster name for automatic-kubernetes-api-monitoring. +func (dk *DynaKube) FeatureAutomaticKubernetesApiMonitoringClusterName() string { + return dk.getFeatureFlagRaw(AnnotationFeatureAutomaticK8sApiMonitoringClusterName) +} + +// FeatureEnableK8sAppEnabled is a feature flag to enable automatically enable current Kubernetes cluster for the Kubernetes app. +func (dk *DynaKube) FeatureEnableK8sAppEnabled() bool { + return dk.getFeatureFlagRaw(AnnotationFeatureK8sAppEnabled) == truePhrase +} + +// FeatureAutomaticInjection controls OneAgent is injected to pods in selected namespaces automatically ("automatic-injection=true" or flag not set) +// or if pods need to be opted-in one by one ("automatic-injection=false"). +func (dk *DynaKube) FeatureAutomaticInjection() bool { + return dk.getFeatureFlagRaw(AnnotationFeatureAutomaticInjection) != falsePhrase +} + +// FeatureActiveGateAppArmor is a feature flag to enable AppArmor in ActiveGate container. +func (dk *DynaKube) FeatureActiveGateAppArmor() bool { + return dk.getFeatureFlagRaw(AnnotationFeatureActiveGateAppArmor) == truePhrase +} + +// FeatureOneAgentIgnoreProxy is a feature flag to ignore the proxy for oneAgents when set in CR. +func (dk *DynaKube) FeatureOneAgentIgnoreProxy() bool { + return dk.getFeatureFlagRaw(AnnotationFeatureOneAgentIgnoreProxy) == truePhrase +} + +// FeatureActiveGateIgnoreProxy is a feature flag to ignore the proxy for ActiveGate when set in CR. +func (dk *DynaKube) FeatureActiveGateIgnoreProxy() bool { + return dk.getFeatureFlagRaw(AnnotationFeatureActiveGateIgnoreProxy) == truePhrase +} + +// FeatureLabelVersionDetection is a feature flag to enable injecting additional environment variables based on user labels. +func (dk *DynaKube) FeatureLabelVersionDetection() bool { + return dk.getFeatureFlagRaw(AnnotationFeatureLabelVersionDetection) == truePhrase +} + +// FeatureAgentInitialConnectRetry is a feature flag to configure startup delay of standalone agents. +func (dk *DynaKube) FeatureAgentInitialConnectRetry() int { + defaultValue := -1 + ffValue := dk.getFeatureFlagInt(AnnotationFeatureOneAgentInitialConnectRetry, defaultValue) + + // In case of istio, we want to have a longer initial delay for codemodules to ensure the DT service is created consistently + if ffValue == defaultValue && dk.Spec.EnableIstio { + ffValue = IstioDefaultOneAgentInitialConnectRetry + } + + return ffValue +} + +func (dk *DynaKube) FeatureOneAgentPrivileged() bool { + return dk.getFeatureFlagRaw(AnnotationFeatureRunOneAgentContainerPrivileged) == truePhrase +} + +func (dk *DynaKube) FeatureOneAgentSkipLivenessProbe() bool { + return dk.getFeatureFlagRaw(AnnotationFeatureOneAgentSkipLivenessProbe) == truePhrase +} + +func (dk *DynaKube) FeatureMaxFailedCsiMountAttempts() int { + maxCsiMountAttemptsValue := dk.getFeatureFlagInt(AnnotationFeatureMaxFailedCsiMountAttempts, DefaultMaxFailedCsiMountAttempts) + if maxCsiMountAttemptsValue < 0 { + return DefaultMaxFailedCsiMountAttempts + } + + return maxCsiMountAttemptsValue +} + +func (dk *DynaKube) FeatureMaxCSIRetryTimeout() time.Duration { + maxCsiMountTimeoutValue := dk.getFeatureFlagRaw(AnnotationFeatureMaxCsiMountTimeout) + + duration, err := time.ParseDuration(maxCsiMountTimeoutValue) + if err != nil || duration < 0 { + duration, _ = time.ParseDuration(DefaultMaxCsiMountTimeout) + } + + return duration +} + +// MountAttemptsToTimeout converts the (old) number of csi mount attempts into a time.Duration string. +// The converted value is based on the exponential backoff's algorithm. +// The output is string because it's main purpose is to convert the value of an annotation to another annotation. +func MountAttemptsToTimeout(maxAttempts int) string { + var baseDelay = time.Second / 2 + + delay := time.Duration(math.Exp2(float64(maxAttempts))) * baseDelay + + return delay.String() +} + +func (dk *DynaKube) FeatureReadOnlyCsiVolume() bool { + return dk.getFeatureFlagRaw(AnnotationFeatureReadOnlyCsiVolume) == truePhrase +} + +func (dk *DynaKube) FeatureNodeImagePull() bool { + return dk.getFeatureFlagRaw(AnnotationFeatureNodeImagePull) == truePhrase +} + +func (dk *DynaKube) FeatureNodeImagePullTechnology() string { + return dk.getFeatureFlagRaw(AnnotationTechnologies) +} +func (dk *DynaKube) FeatureInjectionFailurePolicy() string { + if dk.getFeatureFlagRaw(AnnotationInjectionFailurePolicy) == failPhrase { + return failPhrase + } + + return silentPhrase +} + +func (dk *DynaKube) FeaturePublicRegistry() bool { + return dk.getFeatureFlagRaw(AnnotationFeaturePublicRegistry) == truePhrase +} + +func (dk *DynaKube) FeatureInitContainerSeccomp() bool { + return dk.getFeatureFlagRaw(AnnotationFeatureInitContainerSeccomp) == truePhrase +} + +// FeatureEnforcementMode is a feature flag to control how the initContainer +// sets the tenantUUID to the container.conf file (always vs if oneAgent is present). +func (dk *DynaKube) FeatureEnforcementMode() bool { + return dk.getFeatureFlagRaw(AnnotationFeatureEnforcementMode) != falsePhrase +} diff --git a/pkg/api/v1beta4/dynakube/feature_flags_test.go b/pkg/api/v1beta4/dynakube/feature_flags_test.go new file mode 100644 index 0000000000..6d2692cfbb --- /dev/null +++ b/pkg/api/v1beta4/dynakube/feature_flags_test.go @@ -0,0 +1,321 @@ +package dynakube + +import ( + "regexp" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func createDynakubeWithAnnotation(keyValues ...string) DynaKube { + dk := DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{}, + }, + } + + for i := 0; i < len(keyValues); i += 2 { + dk.Annotations[keyValues[i]] = keyValues[i+1] + } + + return dk +} + +func createDynakubeEmptyDynakube() DynaKube { + dk := DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{}, + }, + } + + return dk +} + +func TestCreateDynakubeWithAnnotation(t *testing.T) { + dk := createDynakubeWithAnnotation("test", "true") + + assert.Contains(t, dk.Annotations, "test") + assert.Equal(t, "true", dk.Annotations["test"]) + + dk = createDynakubeWithAnnotation("other test", "false") + + assert.Contains(t, dk.Annotations, "other test") + assert.Equal(t, "false", dk.Annotations["other test"]) + assert.NotContains(t, dk.Annotations, "test") + + dk = createDynakubeWithAnnotation("test", "true", "other test", "false") + + assert.Contains(t, dk.Annotations, "other test") + assert.Equal(t, "false", dk.Annotations["other test"]) + assert.Contains(t, dk.Annotations, "test") + assert.Equal(t, "true", dk.Annotations["test"]) +} + +func testDeprecateDisableAnnotation(t *testing.T, + newAnnotation string, + deprecatedAnnotation string, + propertyFunction func(dk DynaKube) bool) { + // New annotation works + dk := createDynakubeWithAnnotation(newAnnotation, "false") + + assert.True(t, propertyFunction(dk)) + + dk = createDynakubeWithAnnotation(newAnnotation, "true") + + assert.False(t, propertyFunction(dk)) + + // Old annotation works + dk = createDynakubeWithAnnotation(deprecatedAnnotation, "true") + + assert.True(t, propertyFunction(dk)) + + dk = createDynakubeWithAnnotation(deprecatedAnnotation, "false") + + assert.False(t, propertyFunction(dk)) + + // New annotation takes precedent + dk = createDynakubeWithAnnotation( + newAnnotation, "true", + deprecatedAnnotation, "true") + + assert.False(t, propertyFunction(dk)) + + dk = createDynakubeWithAnnotation( + newAnnotation, "false", + deprecatedAnnotation, "false") + + assert.True(t, propertyFunction(dk)) + + // Default is false + dk = createDynakubeWithAnnotation() + + assert.False(t, propertyFunction(dk)) +} + +func TestDeprecatedDisableAnnotations(t *testing.T) { + t.Run(AnnotationFeatureActiveGateUpdates, func(t *testing.T) { + testDeprecateDisableAnnotation(t, + AnnotationFeatureActiveGateUpdates, + AnnotationFeatureDisableActiveGateUpdates, + func(dk DynaKube) bool { + return dk.FeatureDisableActiveGateUpdates() + }) + }) +} + +func TestDeprecatedEnableAnnotations(t *testing.T) { + dk := createDynakubeWithAnnotation(AnnotationInjectionFailurePolicy, "fail") + assert.Equal(t, "fail", dk.FeatureInjectionFailurePolicy()) +} + +func TestMaxMountAttempts(t *testing.T) { + dk := createDynakubeWithAnnotation( + AnnotationFeatureMaxFailedCsiMountAttempts, "5") + + assert.Equal(t, 5, dk.FeatureMaxFailedCsiMountAttempts()) + + dk = createDynakubeWithAnnotation( + AnnotationFeatureMaxFailedCsiMountAttempts, "3") + + assert.Equal(t, 3, dk.FeatureMaxFailedCsiMountAttempts()) + + dk = createDynakubeWithAnnotation() + + assert.Equal(t, DefaultMaxFailedCsiMountAttempts, dk.FeatureMaxFailedCsiMountAttempts()) + + dk = createDynakubeWithAnnotation( + AnnotationFeatureMaxFailedCsiMountAttempts, "a") + + assert.Equal(t, DefaultMaxFailedCsiMountAttempts, dk.FeatureMaxFailedCsiMountAttempts()) + + dk = createDynakubeWithAnnotation( + AnnotationFeatureMaxFailedCsiMountAttempts, "-5") + + assert.Equal(t, DefaultMaxFailedCsiMountAttempts, dk.FeatureMaxFailedCsiMountAttempts()) +} + +func TestMaxCSIMountTimeout(t *testing.T) { + type testCase struct { + title string + input string + expected time.Duration + } + + defaultDuration, err := time.ParseDuration(DefaultMaxCsiMountTimeout) + require.NoError(t, err) + + tests := []testCase{ + { + title: "no annotation -> use default", + input: "", + expected: defaultDuration, + }, + { + title: "incorrect annotation (format) -> use default", + input: "5", + expected: defaultDuration, + }, + { + title: "incorrect annotation (negative) -> use default", + input: "-5m", + expected: defaultDuration, + }, + { + title: "correct annotation -> use value", + input: "5m", + expected: time.Minute * 5, + }, + } + + for _, test := range tests { + t.Run(test.title, func(t *testing.T) { + dk := createDynakubeWithAnnotation(AnnotationFeatureMaxCsiMountTimeout, test.input) + + assert.Equal(t, test.expected, dk.FeatureMaxCSIRetryTimeout()) + }) + } +} + +func TestMountAttemptsToTimeout(t *testing.T) { + type testCase struct { + title string + input int + expected time.Duration + delta float64 + } + + defaultDuration, err := time.ParseDuration(DefaultMaxCsiMountTimeout) + require.NoError(t, err) + + tests := []testCase{ + { + title: "default attempts ~ default duration", // 10 attempts ==> ~8 minutes + input: DefaultMaxFailedCsiMountAttempts, + expected: defaultDuration, + delta: float64(time.Minute * 2), + }, + + { + title: "1/2 of default attempts ~ NOT 1/2 of default duration (so it is actually exponential)", // 5 attempts ==> ~15 seconds + input: DefaultMaxFailedCsiMountAttempts / 2, + expected: defaultDuration / DefaultMaxFailedCsiMountAttempts / 4, + delta: float64(time.Second * 5), + }, + } + + for _, test := range tests { + t.Run(test.title, func(t *testing.T) { + duration, err := time.ParseDuration(MountAttemptsToTimeout(test.input)) + require.NoError(t, err) + assert.InDelta(t, test.expected, duration, test.delta) + }) + } +} + +func TestDynaKube_FeatureIgnoredNamespaces(t *testing.T) { + dk := DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + }, + } + ignoredNamespaces := dk.getDefaultIgnoredNamespaces() + dynakubeNamespaceMatches := false + + for _, namespace := range ignoredNamespaces { + regex, err := regexp.Compile(namespace) + + require.NoError(t, err) + + match := regex.MatchString(dk.Namespace) + + if match { + dynakubeNamespaceMatches = true + } + } + + assert.True(t, dynakubeNamespaceMatches) +} + +func TestDefaultEnabledFeatureFlags(t *testing.T) { + dk := createDynakubeEmptyDynakube() + + assert.True(t, dk.FeatureAutomaticKubernetesApiMonitoring()) + assert.True(t, dk.FeatureAutomaticInjection()) + assert.Equal(t, "silent", dk.FeatureInjectionFailurePolicy()) + + assert.False(t, dk.FeatureDisableActiveGateUpdates()) + assert.False(t, dk.FeatureLabelVersionDetection()) +} + +func TestInjectionFailurePolicy(t *testing.T) { + dk := createDynakubeEmptyDynakube() + + modes := map[string]string{ + failPhrase: failPhrase, + silentPhrase: silentPhrase, + } + for configuredMode, expectedMode := range modes { + t.Run(`injection failure policy: `+configuredMode, func(t *testing.T) { + dk.Annotations[AnnotationInjectionFailurePolicy] = configuredMode + + assert.Equal(t, expectedMode, dk.FeatureInjectionFailurePolicy()) + }) + } +} + +func TestAgentInitialConnectRetry(t *testing.T) { + t.Run("default => not set", func(t *testing.T) { + dk := createDynakubeEmptyDynakube() + + initialRetry := dk.FeatureAgentInitialConnectRetry() + require.Equal(t, -1, initialRetry) + }) + t.Run("istio default => set", func(t *testing.T) { + dk := createDynakubeEmptyDynakube() + dk.Spec.EnableIstio = true + + initialRetry := dk.FeatureAgentInitialConnectRetry() + require.Equal(t, IstioDefaultOneAgentInitialConnectRetry, initialRetry) + }) + t.Run("istio default can be overruled", func(t *testing.T) { + dk := createDynakubeEmptyDynakube() + dk.Spec.EnableIstio = true + dk.Annotations[AnnotationFeatureOneAgentInitialConnectRetry] = "5" + + initialRetry := dk.FeatureAgentInitialConnectRetry() + require.Equal(t, 5, initialRetry) + }) +} + +func TestIsOneAgentPrivileged(t *testing.T) { + t.Run("is false by default", func(t *testing.T) { + dk := DynaKube{} + + assert.False(t, dk.FeatureOneAgentPrivileged()) + }) + t.Run("is true when annotation is set to true", func(t *testing.T) { + dk := DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + AnnotationFeatureRunOneAgentContainerPrivileged: "true", + }, + }, + } + + assert.True(t, dk.FeatureOneAgentPrivileged()) + }) + t.Run("is false when annotation is set to false", func(t *testing.T) { + dk := DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + AnnotationFeatureRunOneAgentContainerPrivileged: "false", + }, + }, + } + + assert.False(t, dk.FeatureOneAgentPrivileged()) + }) +} diff --git a/pkg/api/v1beta4/dynakube/kspm/props.go b/pkg/api/v1beta4/dynakube/kspm/props.go new file mode 100644 index 0000000000..0d59f9e68e --- /dev/null +++ b/pkg/api/v1beta4/dynakube/kspm/props.go @@ -0,0 +1,17 @@ +package kspm + +func (kspm *Kspm) SetName(name string) { + kspm.name = name +} + +func (kspm *Kspm) IsEnabled() bool { + return kspm.Spec != nil +} + +func (kspm *Kspm) GetTokenSecretName() string { + return kspm.name + "-" + TokenSecretKey +} + +func (kspm *Kspm) GetDaemonSetName() string { + return kspm.name + "-" + NodeCollectorNameSuffix +} diff --git a/pkg/api/v1beta4/dynakube/kspm/spec.go b/pkg/api/v1beta4/dynakube/kspm/spec.go new file mode 100644 index 0000000000..b8f2fb3848 --- /dev/null +++ b/pkg/api/v1beta4/dynakube/kspm/spec.go @@ -0,0 +1,80 @@ +package kspm + +import ( + "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/image" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" +) + +const ( + TokenSecretKey = "kspm-token" + NodeCollectorNameSuffix = "node-config-collector" +) + +type Kspm struct { + *Spec + *Status + *NodeConfigurationCollectorSpec + + name string +} + +// +kubebuilder:object:generate=true + +type Spec struct{} + +// +kubebuilder:object:generate=true +type Status struct { + // TokenSecretHash contains the hash of the token that is passed to both the ActiveGate and Node-Configuration-Collector. + // Meant to keep the two in sync. + TokenSecretHash string `json:"tokenSecretHash,omitempty"` +} + +// +kubebuilder:object:generate=true + +type NodeConfigurationCollectorSpec struct { + + // Define the NodeConfigurationCollector daemonSet updateStrategy + // +kubebuilder:validation:Optional + UpdateStrategy *appsv1.DaemonSetUpdateStrategy `json:"updateStrategy,omitempty"` + // Adds additional labels for the NodeConfigurationCollector pods + // +kubebuilder:validation:Optional + Labels map[string]string `json:"labels,omitempty"` + + // Adds additional annotations for the NodeConfigurationCollector pods + // +kubebuilder:validation:Optional + Annotations map[string]string `json:"annotations,omitempty"` + + // Specify the node selector that controls on which nodes NodeConfigurationCollector pods will be deployed. + // +kubebuilder:validation:Optional + NodeSelector map[string]string `json:"nodeSelector,omitempty"` + + // Overrides the default image + // +kubebuilder:validation:Optional + ImageRef image.Ref `json:"imageRef,omitempty"` + + // If specified, indicates the pod's priority. Name must be defined by creating a PriorityClass object with that + // name. If not specified the setting will be removed from the DaemonSet. + // +kubebuilder:validation:Optional + PriorityClassName string `json:"priorityClassName,omitempty"` + + // Define resources' requests and limits for single NodeConfigurationCollector pod + // +kubebuilder:validation:Optional + Resources corev1.ResourceRequirements `json:"resources,omitempty"` + + // Define the nodeAffinity for the DaemonSet of the NodeConfigurationCollector + // +kubebuilder:validation:Optional + NodeAffinity corev1.NodeAffinity `json:"nodeAffinity,omitempty"` + + // Set tolerations for the NodeConfigurationCollector pods + // +kubebuilder:validation:Optional + Tolerations []corev1.Toleration `json:"tolerations,omitempty"` + + // Set additional arguments to the NodeConfigurationCollector pods + // +kubebuilder:validation:Optional + Args []string `json:"args,omitempty"` + + // Set additional environment variables for the NodeConfigurationCollector pods + // +kubebuilder:validation:Optional + Env []corev1.EnvVar `json:"env,omitempty"` +} diff --git a/pkg/api/v1beta4/dynakube/kspm/zz_generated.deepcopy.go b/pkg/api/v1beta4/dynakube/kspm/zz_generated.deepcopy.go new file mode 100644 index 0000000000..502ac4f400 --- /dev/null +++ b/pkg/api/v1beta4/dynakube/kspm/zz_generated.deepcopy.go @@ -0,0 +1,117 @@ +//go:build !ignore_autogenerated + +/* +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package kspm + +import ( + "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NodeConfigurationCollectorSpec) DeepCopyInto(out *NodeConfigurationCollectorSpec) { + *out = *in + if in.UpdateStrategy != nil { + in, out := &in.UpdateStrategy, &out.UpdateStrategy + *out = new(v1.DaemonSetUpdateStrategy) + (*in).DeepCopyInto(*out) + } + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.NodeSelector != nil { + in, out := &in.NodeSelector, &out.NodeSelector + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + out.ImageRef = in.ImageRef + in.Resources.DeepCopyInto(&out.Resources) + in.NodeAffinity.DeepCopyInto(&out.NodeAffinity) + if in.Tolerations != nil { + in, out := &in.Tolerations, &out.Tolerations + *out = make([]corev1.Toleration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Args != nil { + in, out := &in.Args, &out.Args + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Env != nil { + in, out := &in.Env, &out.Env + *out = make([]corev1.EnvVar, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeConfigurationCollectorSpec. +func (in *NodeConfigurationCollectorSpec) DeepCopy() *NodeConfigurationCollectorSpec { + if in == nil { + return nil + } + out := new(NodeConfigurationCollectorSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Spec) DeepCopyInto(out *Spec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Spec. +func (in *Spec) DeepCopy() *Spec { + if in == nil { + return nil + } + out := new(Spec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Status) DeepCopyInto(out *Status) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Status. +func (in *Status) DeepCopy() *Status { + if in == nil { + return nil + } + out := new(Status) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/api/v1beta4/dynakube/kspm_props.go b/pkg/api/v1beta4/dynakube/kspm_props.go new file mode 100644 index 0000000000..ee0ac86fda --- /dev/null +++ b/pkg/api/v1beta4/dynakube/kspm_props.go @@ -0,0 +1,14 @@ +package dynakube + +import "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/kspm" + +func (dk *DynaKube) KSPM() *kspm.Kspm { + _kspm := &kspm.Kspm{ + Spec: dk.Spec.Kspm, + Status: &dk.Status.Kspm, + NodeConfigurationCollectorSpec: &dk.Spec.Templates.KspmNodeConfigurationCollector, + } + _kspm.SetName(dk.GetName()) + + return _kspm +} diff --git a/pkg/api/v1beta4/dynakube/logmonitoring/ingestrulematchers.go b/pkg/api/v1beta4/dynakube/logmonitoring/ingestrulematchers.go new file mode 100644 index 0000000000..f3bb407722 --- /dev/null +++ b/pkg/api/v1beta4/dynakube/logmonitoring/ingestrulematchers.go @@ -0,0 +1,10 @@ +package logmonitoring + +// +kubebuilder:object:generate=true +type IngestRuleMatchers struct { + // +kubebuilder:validation:Optional + Attribute string `json:"attribute,omitempty"` + + // +kubebuilder:validation:Optional + Values []string `json:"values,omitempty"` +} diff --git a/pkg/api/v1beta4/dynakube/logmonitoring/props.go b/pkg/api/v1beta4/dynakube/logmonitoring/props.go new file mode 100644 index 0000000000..3b014065c2 --- /dev/null +++ b/pkg/api/v1beta4/dynakube/logmonitoring/props.go @@ -0,0 +1,42 @@ +package logmonitoring + +const ( + daemonSetSuffix = "-logmonitoring" +) + +func (lm *LogMonitoring) SetName(name string) { + lm.name = name +} + +func (lm *LogMonitoring) SetHostAgentDependency(isEnabled bool) { + lm.enabledDependencies.hostAgents = isEnabled +} + +func (lm *LogMonitoring) IsEnabled() bool { + return lm.Spec != nil +} + +func (lm *LogMonitoring) GetDaemonSetName() string { + return lm.name + daemonSetSuffix +} + +func (lm *LogMonitoring) IsStandalone() bool { + return lm.IsEnabled() && !lm.enabledDependencies.hostAgents +} + +func (lm *LogMonitoring) GetNodeSelector() map[string]string { + if lm.IsStandalone() && lm.TemplateSpec != nil { + return lm.TemplateSpec.NodeSelector + } + + return nil +} + +// Template is a nil-safe way to access the underlying TemplateSpec. +func (lm *LogMonitoring) Template() TemplateSpec { + if lm.TemplateSpec == nil { + return TemplateSpec{} + } + + return *lm.TemplateSpec +} diff --git a/pkg/api/v1beta4/dynakube/logmonitoring/spec.go b/pkg/api/v1beta4/dynakube/logmonitoring/spec.go new file mode 100644 index 0000000000..78294d383a --- /dev/null +++ b/pkg/api/v1beta4/dynakube/logmonitoring/spec.go @@ -0,0 +1,66 @@ +package logmonitoring + +import ( + "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/image" + corev1 "k8s.io/api/core/v1" +) + +type dependencies struct { + hostAgents bool +} + +type LogMonitoring struct { + *Spec + *TemplateSpec + + name string + enabledDependencies dependencies +} + +// +kubebuilder:object:generate=true +type Spec struct { + IngestRuleMatchers []IngestRuleMatchers `json:"ingestRuleMatchers,omitempty"` +} + +// +kubebuilder:object:generate=true +type TemplateSpec struct { + // Add custom annotations to the LogMonitoring pods + // +kubebuilder:validation:Optional + Annotations map[string]string `json:"annotations,omitempty"` + + // Add custom labels to the LogMonitoring pods + // +kubebuilder:validation:Optional + Labels map[string]string `json:"labels,omitempty"` + + // Node selector to control the selection of nodes for the LogMonitoring pods + // +kubebuilder:validation:Optional + NodeSelector map[string]string `json:"nodeSelector,omitempty"` + + // Overrides the default image for the LogMonitoring pods + // +kubebuilder:validation:Optional + ImageRef image.Ref `json:"imageRef,omitempty"` + + // Sets DNS Policy for the LogMonitoring pods + // +kubebuilder:validation:Optional + DNSPolicy corev1.DNSPolicy `json:"dnsPolicy,omitempty"` + + // Assign a priority class to the LogMonitoring pods. By default, no class is set + // +kubebuilder:validation:Optional + PriorityClassName string `json:"priorityClassName,omitempty"` + + // The SecComp Profile that will be configured in order to run in secure computing mode for the LogMonitoring pods + // +kubebuilder:validation:Optional + SecCompProfile string `json:"secCompProfile,omitempty"` + + // Define resources' requests and limits for all the LogMonitoring pods + // +kubebuilder:validation:Optional + Resources corev1.ResourceRequirements `json:"resources,omitempty"` + + // Set tolerations for the LogMonitoring pods + // +kubebuilder:validation:Optional + Tolerations []corev1.Toleration `json:"tolerations,omitempty"` + + // Set additional arguments to the LogMonitoring main container + // +kubebuilder:validation:Optional + Args []string `json:"args,omitempty"` +} diff --git a/pkg/api/v1beta4/dynakube/logmonitoring/zz_generated.deepcopy.go b/pkg/api/v1beta4/dynakube/logmonitoring/zz_generated.deepcopy.go new file mode 100644 index 0000000000..2290bd8826 --- /dev/null +++ b/pkg/api/v1beta4/dynakube/logmonitoring/zz_generated.deepcopy.go @@ -0,0 +1,115 @@ +//go:build !ignore_autogenerated + +/* +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package logmonitoring + +import ( + "k8s.io/api/core/v1" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IngestRuleMatchers) DeepCopyInto(out *IngestRuleMatchers) { + *out = *in + if in.Values != nil { + in, out := &in.Values, &out.Values + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngestRuleMatchers. +func (in *IngestRuleMatchers) DeepCopy() *IngestRuleMatchers { + if in == nil { + return nil + } + out := new(IngestRuleMatchers) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Spec) DeepCopyInto(out *Spec) { + *out = *in + if in.IngestRuleMatchers != nil { + in, out := &in.IngestRuleMatchers, &out.IngestRuleMatchers + *out = make([]IngestRuleMatchers, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Spec. +func (in *Spec) DeepCopy() *Spec { + if in == nil { + return nil + } + out := new(Spec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TemplateSpec) DeepCopyInto(out *TemplateSpec) { + *out = *in + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.NodeSelector != nil { + in, out := &in.NodeSelector, &out.NodeSelector + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + out.ImageRef = in.ImageRef + in.Resources.DeepCopyInto(&out.Resources) + if in.Tolerations != nil { + in, out := &in.Tolerations, &out.Tolerations + *out = make([]v1.Toleration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Args != nil { + in, out := &in.Args, &out.Args + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TemplateSpec. +func (in *TemplateSpec) DeepCopy() *TemplateSpec { + if in == nil { + return nil + } + out := new(TemplateSpec) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/api/v1beta4/dynakube/logmonitoring_props.go b/pkg/api/v1beta4/dynakube/logmonitoring_props.go new file mode 100644 index 0000000000..f076f75805 --- /dev/null +++ b/pkg/api/v1beta4/dynakube/logmonitoring_props.go @@ -0,0 +1,16 @@ +package dynakube + +import ( + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/logmonitoring" +) + +func (dk *DynaKube) LogMonitoring() *logmonitoring.LogMonitoring { + lm := &logmonitoring.LogMonitoring{ + Spec: dk.Spec.LogMonitoring, + TemplateSpec: dk.Spec.Templates.LogMonitoring, + } + lm.SetName(dk.Name) + lm.SetHostAgentDependency(dk.OneAgent().IsDaemonsetRequired()) + + return lm +} diff --git a/pkg/api/v1beta4/dynakube/metada_enrichment.go b/pkg/api/v1beta4/dynakube/metada_enrichment.go new file mode 100644 index 0000000000..a14de184c0 --- /dev/null +++ b/pkg/api/v1beta4/dynakube/metada_enrichment.go @@ -0,0 +1,13 @@ +package dynakube + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +type MetadataEnrichment struct { + // Enables MetadataEnrichment, `false` by default. + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="MetaDataEnrichment",xDescriptors="urn:alm:descriptor:com.tectonic.ui:selector:booleanSwitch" + Enabled *bool `json:"enabled,omitempty"` + + // The namespaces where you want Dynatrace Operator to inject enrichment. + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Namespace Selector",xDescriptors="urn:alm:descriptor:com.tectonic.ui:selector:core:v1:Namespace" + NamespaceSelector metav1.LabelSelector `json:"namespaceSelector,omitempty"` +} diff --git a/pkg/api/v1beta4/dynakube/metadata_enrichment_props.go b/pkg/api/v1beta4/dynakube/metadata_enrichment_props.go new file mode 100644 index 0000000000..21ff3015eb --- /dev/null +++ b/pkg/api/v1beta4/dynakube/metadata_enrichment_props.go @@ -0,0 +1,13 @@ +package dynakube + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func (dk *DynaKube) MetadataEnrichmentEnabled() bool { + return dk.Spec.MetadataEnrichment.Enabled != nil && *dk.Spec.MetadataEnrichment.Enabled +} + +func (dk *DynaKube) MetadataEnrichmentNamespaceSelector() *metav1.LabelSelector { + return &dk.Spec.MetadataEnrichment.NamespaceSelector +} diff --git a/pkg/api/v1beta4/dynakube/oneagent/props.go b/pkg/api/v1beta4/dynakube/oneagent/props.go new file mode 100644 index 0000000000..0cb4645324 --- /dev/null +++ b/pkg/api/v1beta4/dynakube/oneagent/props.go @@ -0,0 +1,350 @@ +package oneagent + +import ( + "fmt" + "strings" + + "github.com/Dynatrace/dynatrace-operator/pkg/api" + "github.com/Dynatrace/dynatrace-operator/pkg/util/dtversion" + "github.com/Dynatrace/dynatrace-operator/pkg/util/installconfig" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + OneAgentTenantSecretSuffix = "-oneagent-tenant-secret" + OneAgentConnectionInfoConfigMapSuffix = "-oneagent-connection-info" + PodNameOsAgent = "oneagent" + DefaultOneAgentImageRegistrySubPath = "/linux/oneagent" + storageVolumeDefaultHostPath = "/var/opt/dynatrace" +) + +func NewOneAgent(spec *Spec, status *Status, codeModulesStatus *CodeModulesStatus, //nolint:revive + name, apiUrlHost string, + featureOneAgentPrivileged, featureOneAgentSkipLivenessProbe, featureBootstrapperInjection bool) *OneAgent { + return &OneAgent{ + Spec: spec, + Status: status, + CodeModulesStatus: codeModulesStatus, + + name: name, + apiUrlHost: apiUrlHost, + + featureOneAgentPrivileged: featureOneAgentPrivileged, + featureOneAgentSkipLivenessProbe: featureOneAgentSkipLivenessProbe, + featureBootstrapperInjection: featureBootstrapperInjection, + } +} + +func (oa *OneAgent) IsCSIAvailable() bool { + return installconfig.GetModules().CSIDriver +} + +// IsApplicationMonitoringMode returns true when application only section is used. +func (oa *OneAgent) IsApplicationMonitoringMode() bool { + return oa.ApplicationMonitoring != nil +} + +// IsCloudNativeFullstackMode returns true when cloud native fullstack section is used. +func (oa *OneAgent) IsCloudNativeFullstackMode() bool { + return oa.CloudNativeFullStack != nil +} + +// IsHostMonitoringMode returns true when host monitoring section is used. +func (oa *OneAgent) IsHostMonitoringMode() bool { + return oa.HostMonitoring != nil +} + +// IsClassicFullStackMode returns true when classic fullstack section is used. +func (oa *OneAgent) IsClassicFullStackMode() bool { + return oa.ClassicFullStack != nil +} + +// IsDaemonsetRequired returns true when a feature requires OneAgent instances. +func (oa *OneAgent) IsDaemonsetRequired() bool { + return oa.IsClassicFullStackMode() || oa.IsCloudNativeFullstackMode() || oa.IsHostMonitoringMode() +} + +func (oa *OneAgent) GetDaemonsetName() string { + return fmt.Sprintf("%s-%s", oa.name, PodNameOsAgent) +} + +func (oa *OneAgent) IsPrivilegedNeeded() bool { + return oa.featureOneAgentPrivileged +} + +func (oa *OneAgent) IsReadinessProbeNeeded() bool { + return oa.Healthcheck != nil +} + +func (oa *OneAgent) IsLivenessProbeNeeded() bool { + return oa.Healthcheck != nil && !oa.featureOneAgentSkipLivenessProbe +} + +// IsAutoUpdateEnabled returns true if the Operator should update OneAgent instances automatically. +func (oa *OneAgent) IsAutoUpdateEnabled() bool { + switch { + case oa.IsCloudNativeFullstackMode(): + return oa.CloudNativeFullStack.AutoUpdate == nil || *oa.CloudNativeFullStack.AutoUpdate + case oa.IsHostMonitoringMode(): + return oa.HostMonitoring.AutoUpdate == nil || *oa.HostMonitoring.AutoUpdate + case oa.IsClassicFullStackMode(): + return oa.ClassicFullStack.AutoUpdate == nil || *oa.ClassicFullStack.AutoUpdate + default: + return false + } +} + +// GetTenantSecret returns the name of the secret containing the token for the OneAgent. +func (oa *OneAgent) GetTenantSecret() string { + return oa.name + OneAgentTenantSecretSuffix +} + +func (oa *OneAgent) GetConnectionInfoConfigMapName() string { + return oa.name + OneAgentConnectionInfoConfigMapSuffix +} + +func (oa *OneAgent) IsReadOnlyFSSupported() bool { + return oa.IsCloudNativeFullstackMode() || oa.IsHostMonitoringMode() +} + +func (oa *OneAgent) IsAppInjectionNeeded() bool { + return oa.IsCloudNativeFullstackMode() || oa.IsApplicationMonitoringMode() +} + +func (oa *OneAgent) GetInitResources() *corev1.ResourceRequirements { + if oa.IsApplicationMonitoringMode() { + return oa.ApplicationMonitoring.InitResources + } else if oa.IsCloudNativeFullstackMode() { + return oa.CloudNativeFullStack.InitResources + } + + return nil +} + +func (oa *OneAgent) GetNamespaceSelector() *metav1.LabelSelector { + switch { + case oa.IsCloudNativeFullstackMode(): + return &oa.CloudNativeFullStack.NamespaceSelector + case oa.IsApplicationMonitoringMode(): + return &oa.ApplicationMonitoring.NamespaceSelector + } + + return nil +} + +func (oa *OneAgent) GetSecCompProfile() string { + switch { + case oa.IsCloudNativeFullstackMode(): + return oa.CloudNativeFullStack.SecCompProfile + case oa.IsHostMonitoringMode(): + return oa.HostMonitoring.SecCompProfile + case oa.IsClassicFullStackMode(): + return oa.ClassicFullStack.SecCompProfile + default: + return "" + } +} + +func (oa *OneAgent) GetNodeSelector(fallbackNodeSelector map[string]string) map[string]string { + switch { + case oa.IsClassicFullStackMode(): + return oa.ClassicFullStack.NodeSelector + case oa.IsHostMonitoringMode(): + return oa.HostMonitoring.NodeSelector + case oa.IsCloudNativeFullstackMode(): + return oa.CloudNativeFullStack.NodeSelector + } + + return fallbackNodeSelector +} + +// GetImage provides the image reference set in Status for the OneAgent. +// Format: repo@sha256:digest. +func (oa *OneAgent) GetImage() string { + return oa.Status.ImageID +} + +// GetVersion provides version set in Status for the OneAgent. +func (oa *OneAgent) GetVersion() string { + return oa.Status.Version +} + +// GetCustomVersion provides the version for the OneAgent provided in the Spec. +func (oa *OneAgent) GetCustomVersion() string { + switch { + case oa.IsClassicFullStackMode(): + return oa.ClassicFullStack.Version + case oa.IsCloudNativeFullstackMode(): + return oa.CloudNativeFullStack.Version + case oa.IsApplicationMonitoringMode(): + return oa.ApplicationMonitoring.Version + case oa.IsHostMonitoringMode(): + return oa.HostMonitoring.Version + } + + return "" +} + +// GetCustomImage provides the image reference for the OneAgent provided in the Spec. +func (oa *OneAgent) GetCustomImage() string { + switch { + case oa.IsClassicFullStackMode(): + return oa.ClassicFullStack.Image + case oa.IsHostMonitoringMode(): + return oa.HostMonitoring.Image + case oa.IsCloudNativeFullstackMode(): + return oa.CloudNativeFullStack.Image + } + + return "" +} + +// GetDefaultImage provides the image reference for the OneAgent from tenant registry. +func (oa *OneAgent) GetDefaultImage(version string) string { + if oa.apiUrlHost == "" { + return "" + } + + truncatedVersion := dtversion.ToImageTag(version) + tag := truncatedVersion + + if !strings.Contains(tag, api.RawTag) { + tag += "-" + api.RawTag + } + + return oa.apiUrlHost + DefaultOneAgentImageRegistrySubPath + ":" + tag +} + +func (oa *OneAgent) GetHostGroup() string { + if oa.HostGroup != "" { + return oa.HostGroup + } + + return oa.GetHostGroupAsParam() +} + +func (oa *OneAgent) GetArguments() []string { + switch { + case oa.IsCloudNativeFullstackMode() && oa.CloudNativeFullStack.Args != nil: + return oa.CloudNativeFullStack.Args + case oa.IsClassicFullStackMode() && oa.ClassicFullStack.Args != nil: + return oa.ClassicFullStack.Args + case oa.IsHostMonitoringMode() && oa.HostMonitoring.Args != nil: + return oa.HostMonitoring.Args + } + + return []string{} +} + +func (oa *OneAgent) GetHostGroupAsParam() string { + var hostGroup string + + args := oa.GetArguments() + + for _, arg := range args { + key, value := splitArg(arg) + if key == "--set-host-group" { + hostGroup = value + + break + } + } + + return hostGroup +} + +func splitArg(arg string) (string, string) { + key, value, found := strings.Cut(arg, "=") + if !found { + return arg, "" + } + + return key, value +} + +func (oa *OneAgent) IsCommunicationRouteClear() bool { + return len(oa.ConnectionInfoStatus.CommunicationHosts) > 0 +} + +func (oa *OneAgent) GetEnvironment() []corev1.EnvVar { + switch { + case oa.IsCloudNativeFullstackMode(): + return oa.CloudNativeFullStack.Env + case oa.IsClassicFullStackMode(): + return oa.ClassicFullStack.Env + case oa.IsHostMonitoringMode(): + return oa.HostMonitoring.Env + } + + return []corev1.EnvVar{} +} + +func (oa *OneAgent) GetEndpoints() string { + return oa.ConnectionInfoStatus.Endpoints +} + +// GetCustomCodeModulesImage provides the image reference for the CodeModules provided in the Spec. +func (oa *OneAgent) GetCustomCodeModulesImage() string { + if oa.IsCloudNativeFullstackMode() { + return oa.CloudNativeFullStack.CodeModulesImage + } else if oa.IsApplicationMonitoringMode() && (oa.IsCSIAvailable() || oa.featureBootstrapperInjection) { + return oa.ApplicationMonitoring.CodeModulesImage + } + + return "" +} + +// GetCustomCodeModulesVersion provides the version for the CodeModules provided in the Spec. +func (oa *OneAgent) GetCustomCodeModulesVersion() string { + return oa.GetCustomVersion() +} + +// GetCodeModulesVersion provides version set in Status for the CodeModules. +func (oa *OneAgent) GetCodeModulesVersion() string { + return oa.CodeModulesStatus.Version +} + +// GetCodeModulesImage provides the image reference set in Status for the CodeModules. +// Format: repo@sha256:digest. +func (oa *OneAgent) GetCodeModulesImage() string { + return oa.CodeModulesStatus.ImageID +} + +func (oa *OneAgent) GetArgumentsMap() map[string][]string { + args := oa.GetArguments() + + argMap := make(map[string][]string) + + for _, arg := range args { + key, value := splitArg(arg) + if _, exists := argMap[key]; !exists { + argMap[key] = []string{value} + } else { + argMap[key] = append(argMap[key], value) + } + } + + return argMap +} + +// GetHostPath provides the host path for the storage volume if CSI driver is absent. +func (oa *OneAgent) GetHostPath() string { + if oa.IsCloudNativeFullstackMode() { + if oa.CloudNativeFullStack.StorageHostPath != "" { + return oa.CloudNativeFullStack.StorageHostPath + } + + return storageVolumeDefaultHostPath + } + + if oa.IsHostMonitoringMode() { + if oa.HostMonitoring.StorageHostPath != "" { + return oa.HostMonitoring.StorageHostPath + } + + return storageVolumeDefaultHostPath + } + + return "" +} diff --git a/pkg/api/v1beta4/dynakube/oneagent/props_test.go b/pkg/api/v1beta4/dynakube/oneagent/props_test.go new file mode 100644 index 0000000000..f5b8a9c9a6 --- /dev/null +++ b/pkg/api/v1beta4/dynakube/oneagent/props_test.go @@ -0,0 +1,386 @@ +/* +Copyright 2021 Dynatrace LLC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package oneagent + +import ( + "net/url" + "testing" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/status" + "github.com/Dynatrace/dynatrace-operator/pkg/util/installconfig" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" +) + +const testAPIURL = "http://test-endpoint/api" + +func TestNeedsReadonlyOneagent(t *testing.T) { + t.Run("cloud native fullstack always use readonly host agent", func(t *testing.T) { + oneagent := OneAgent{ + Spec: &Spec{ + CloudNativeFullStack: &CloudNativeFullStackSpec{}, + }, + } + assert.True(t, oneagent.IsReadOnlyFSSupported()) + }) + + t.Run("host monitoring with readonly host agent", func(t *testing.T) { + oneAgent := OneAgent{ + Spec: &Spec{ + HostMonitoring: &HostInjectSpec{}, + }, + } + assert.True(t, oneAgent.IsReadOnlyFSSupported()) + }) + + t.Run("host monitoring without readonly host agent", func(t *testing.T) { + setupDisabledCSIEnv(t) + + oneAgent := OneAgent{ + Spec: &Spec{ + HostMonitoring: &HostInjectSpec{}, + }, + } + assert.True(t, oneAgent.IsReadOnlyFSSupported()) + }) +} + +func TestDefaultOneAgentImage(t *testing.T) { + t.Run("OneAgentImage with no API URL", func(t *testing.T) { + oneAgent := OneAgent{} + assert.Empty(t, oneAgent.GetDefaultImage("")) + }) + + t.Run("OneAgentImage adds raw postfix", func(t *testing.T) { + hostUrl, _ := url.Parse(testAPIURL) + oneAgent := NewOneAgent(&Spec{}, &Status{}, &CodeModulesStatus{}, "", hostUrl.Host, false, false, false) + assert.Equal(t, "test-endpoint/linux/oneagent:1.234.5-raw", oneAgent.GetDefaultImage("1.234.5")) + }) + + t.Run("OneAgentImage doesn't add 'raw' postfix if present", func(t *testing.T) { + hostUrl, _ := url.Parse(testAPIURL) + oneAgent := NewOneAgent(&Spec{}, &Status{}, &CodeModulesStatus{}, "", hostUrl.Host, false, false, false) + assert.Equal(t, "test-endpoint/linux/oneagent:1.234.5-raw", oneAgent.GetDefaultImage("1.234.5-raw")) + }) + + t.Run("OneAgentImage with custom version truncates build date", func(t *testing.T) { + version := "1.239.14.20220325-164521" + expectedImage := "test-endpoint/linux/oneagent:1.239.14-raw" + hostUrl, _ := url.Parse(testAPIURL) + oneAgent := NewOneAgent(&Spec{}, &Status{}, &CodeModulesStatus{}, "", hostUrl.Host, false, false, false) + assert.Equal(t, expectedImage, oneAgent.GetDefaultImage(version)) + }) +} + +func TestCustomOneAgentImage(t *testing.T) { + t.Run("OneAgentImage with custom image", func(t *testing.T) { + customImg := "registry/my/oneagent:latest" + oneAgent := OneAgent{Spec: &Spec{ClassicFullStack: &HostInjectSpec{Image: customImg}}} + assert.Equal(t, customImg, oneAgent.GetCustomImage()) + }) + + t.Run("OneAgentImage with no custom image", func(t *testing.T) { + oneAgent := OneAgent{Spec: &Spec{ClassicFullStack: &HostInjectSpec{}}} + assert.Empty(t, oneAgent.GetCustomImage()) + }) +} + +func TestOneAgentDaemonsetName(t *testing.T) { + oneAgent := OneAgent{name: "test-name"} + assert.Equal(t, "test-name-oneagent", oneAgent.GetDaemonsetName()) +} + +func TestCodeModulesVersion(t *testing.T) { + testVersion := "1.2.3" + + t.Run("use status", func(t *testing.T) { + codeModulesStatus := &CodeModulesStatus{VersionStatus: status.VersionStatus{Version: testVersion}} + oneAgent := NewOneAgent(&Spec{}, &Status{}, codeModulesStatus, "", "", false, false, false) + version := oneAgent.GetCodeModulesVersion() + assert.Equal(t, testVersion, version) + }) + t.Run("use version ", func(t *testing.T) { + codeModulesStatus := &CodeModulesStatus{VersionStatus: status.VersionStatus{Version: "other"}} + oneAgent := NewOneAgent(&Spec{ + ApplicationMonitoring: &ApplicationMonitoringSpec{Version: testVersion}, + }, &Status{}, codeModulesStatus, "", "", false, false, false) + version := oneAgent.GetCustomCodeModulesVersion() + + assert.Equal(t, testVersion, version) + }) +} + +func TestGetOneAgentEnvironment(t *testing.T) { + t.Run("get environment from classicFullstack", func(t *testing.T) { + oneAgent := OneAgent{ + Spec: &Spec{ + ClassicFullStack: &HostInjectSpec{ + Env: []corev1.EnvVar{ + { + Name: "classicFullstack", + Value: "true", + }, + }, + }, + }, + } + env := oneAgent.GetEnvironment() + + require.Len(t, env, 1) + assert.Equal(t, "classicFullstack", env[0].Name) + }) + + t.Run("get environment from hostMonitoring", func(t *testing.T) { + oneAgent := OneAgent{ + Spec: &Spec{ + HostMonitoring: &HostInjectSpec{ + Env: []corev1.EnvVar{ + { + Name: "hostMonitoring", + Value: "true", + }, + }, + }, + }, + } + env := oneAgent.GetEnvironment() + + require.Len(t, env, 1) + assert.Equal(t, "hostMonitoring", env[0].Name) + }) + + t.Run("get environment from cloudNative", func(t *testing.T) { + oneAgent := OneAgent{ + Spec: &Spec{ + CloudNativeFullStack: &CloudNativeFullStackSpec{ + HostInjectSpec: HostInjectSpec{ + Env: []corev1.EnvVar{ + { + Name: "cloudNative", + Value: "true", + }, + }, + }, + }, + }, + } + env := oneAgent.GetEnvironment() + + require.Len(t, env, 1) + assert.Equal(t, "cloudNative", env[0].Name) + }) + + t.Run("get environment from applicationMonitoring", func(t *testing.T) { + oneAgent := OneAgent{Spec: &Spec{ + ApplicationMonitoring: &ApplicationMonitoringSpec{}, + }} + env := oneAgent.GetEnvironment() + + require.NotNil(t, env) + assert.Empty(t, env) + }) + + t.Run("get environment from unconfigured dynakube", func(t *testing.T) { + oneAgent := OneAgent{Spec: &Spec{}} + env := oneAgent.GetEnvironment() + + require.NotNil(t, env) + assert.Empty(t, env) + }) +} + +func TestOneAgentHostGroup(t *testing.T) { + t.Run("get host group from cloudNativeFullstack.args", func(t *testing.T) { + dk := OneAgent{Spec: &Spec{ + CloudNativeFullStack: &CloudNativeFullStackSpec{ + HostInjectSpec: HostInjectSpec{ + Args: []string{ + "--set-host-group=arg", + }, + }, + }, + }, + } + hostGroup := dk.GetHostGroup() + assert.Equal(t, "arg", hostGroup) + }) + + t.Run("get host group from oneagent.hostGroup", func(t *testing.T) { + dk := OneAgent{Spec: &Spec{ + HostGroup: "field", + }, + } + hostGroup := dk.GetHostGroup() + assert.Equal(t, "field", hostGroup) + }) + + t.Run("get host group if both methods used", func(t *testing.T) { + dk := OneAgent{Spec: &Spec{ + CloudNativeFullStack: &CloudNativeFullStackSpec{ + HostInjectSpec: HostInjectSpec{ + Args: []string{ + "--set-host-group=arg", + }, + }, + }, + HostGroup: "field", + }, + } + hostGroup := dk.GetHostGroup() + assert.Equal(t, "field", hostGroup) + }) +} + +func TestOneAgentArgumentsMap(t *testing.T) { + t.Run("straight forward argument list", func(t *testing.T) { + dk := OneAgent{Spec: &Spec{ + CloudNativeFullStack: &CloudNativeFullStackSpec{ + HostInjectSpec: HostInjectSpec{ + Args: []string{ + "--set-host-id-source=k8s-node-name", + "--set-host-property=OperatorVersion=$(DT_OPERATOR_VERSION)", + "--set-host-property=dt.security_context=kubernetes_clusters", + "--set-host-property=dynakube-name=$(CUSTOM_CRD_NAME)", + "--set-no-proxy=", + "--set-proxy=", + "--set-tenant=$(DT_TENANT)", + "--set-server=dynatrace.com", + "--set-host-property=prop1=val1", + "--set-host-property=prop2=val2", + "--set-host-property=prop3=val3", + "--set-host-tag=tag1", + "--set-host-tag=tag2", + "--set-host-tag=tag3", + }, + }, + }, + HostGroup: "field", + }, + } + argMap := dk.GetArgumentsMap() + require.Len(t, argMap, 7) + + require.Len(t, argMap["--set-host-id-source"], 1) + assert.Equal(t, "k8s-node-name", argMap["--set-host-id-source"][0]) + + require.Len(t, argMap["--set-host-property"], 6) + assert.Equal(t, "OperatorVersion=$(DT_OPERATOR_VERSION)", argMap["--set-host-property"][0]) + assert.Equal(t, "dt.security_context=kubernetes_clusters", argMap["--set-host-property"][1]) + assert.Equal(t, "dynakube-name=$(CUSTOM_CRD_NAME)", argMap["--set-host-property"][2]) + assert.Equal(t, "prop1=val1", argMap["--set-host-property"][3]) + assert.Equal(t, "prop2=val2", argMap["--set-host-property"][4]) + assert.Equal(t, "prop3=val3", argMap["--set-host-property"][5]) + + require.Len(t, argMap["--set-no-proxy"], 1) + assert.Empty(t, argMap["--set-no-proxy"][0]) + + require.Len(t, argMap["--set-proxy"], 1) + assert.Empty(t, argMap["--set-proxy"][0]) + + require.Len(t, argMap["--set-tenant"], 1) + assert.Equal(t, "$(DT_TENANT)", argMap["--set-tenant"][0]) + + require.Len(t, argMap["--set-server"], 1) + assert.Equal(t, "dynatrace.com", argMap["--set-server"][0]) + }) + + t.Run("multiple --set-host-property arguments", func(t *testing.T) { + dk := OneAgent{Spec: &Spec{ + CloudNativeFullStack: &CloudNativeFullStackSpec{ + HostInjectSpec: HostInjectSpec{ + Args: []string{ + "--set-host-property=prop1=val1", + "--set-host-property=prop2=val2", + "--set-host-property=prop3=val3", + "--set-host-property=prop3=val3", + }, + }, + }, + HostGroup: "field", + }, + } + argMap := dk.GetArgumentsMap() + require.Len(t, argMap, 1) + require.Len(t, argMap["--set-host-property"], 4) + + assert.Equal(t, "prop1=val1", argMap["--set-host-property"][0]) + assert.Equal(t, "prop2=val2", argMap["--set-host-property"][1]) + assert.Equal(t, "prop3=val3", argMap["--set-host-property"][2]) + }) + + t.Run("multiple --set-host-tag arguments", func(t *testing.T) { + dk := OneAgent{Spec: &Spec{ + CloudNativeFullStack: &CloudNativeFullStackSpec{ + HostInjectSpec: HostInjectSpec{ + Args: []string{ + "--set-host-tag=tag1=1", + "--set-host-tag=tag1=2", + "--set-host-tag=tag1=3", + "--set-host-tag=tag2", + "--set-host-tag=tag3", + }, + }, + }, + HostGroup: "field", + }, + } + argMap := dk.GetArgumentsMap() + require.Len(t, argMap, 1) + require.Len(t, argMap["--set-host-tag"], 5) + + assert.Equal(t, "tag1=1", argMap["--set-host-tag"][0]) + assert.Equal(t, "tag1=2", argMap["--set-host-tag"][1]) + assert.Equal(t, "tag1=3", argMap["--set-host-tag"][2]) + assert.Equal(t, "tag2", argMap["--set-host-tag"][3]) + assert.Equal(t, "tag3", argMap["--set-host-tag"][4]) + }) + + t.Run("arguments without value", func(t *testing.T) { + dk := OneAgent{Spec: &Spec{ + CloudNativeFullStack: &CloudNativeFullStackSpec{ + HostInjectSpec: HostInjectSpec{ + Args: []string{ + "--enable-feature-a", + "--enable-feature-b", + "--enable-feature-c", + }, + }, + }, + HostGroup: "field", + }, + } + argMap := dk.GetArgumentsMap() + require.Len(t, argMap, 3) + require.Len(t, argMap["--enable-feature-a"], 1) + require.Len(t, argMap["--enable-feature-b"], 1) + require.Len(t, argMap["--enable-feature-c"], 1) + }) +} + +func setupDisabledCSIEnv(t *testing.T) { + t.Helper() + installconfig.SetModulesOverride(t, installconfig.Modules{ + CSIDriver: false, + ActiveGate: true, + OneAgent: true, + Extensions: true, + LogMonitoring: true, + EdgeConnect: true, + Supportability: true, + }) +} diff --git a/pkg/api/v1beta4/dynakube/oneagent/spec.go b/pkg/api/v1beta4/dynakube/oneagent/spec.go new file mode 100644 index 0000000000..0e2a09e4c0 --- /dev/null +++ b/pkg/api/v1beta4/dynakube/oneagent/spec.go @@ -0,0 +1,171 @@ +package oneagent + +import ( + "github.com/Dynatrace/dynatrace-operator/pkg/api/status" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type OneAgent struct { + *Spec + *Status + *CodeModulesStatus + + name string + apiUrlHost string + + featureOneAgentPrivileged bool + featureBootstrapperInjection bool + featureOneAgentSkipLivenessProbe bool +} + +type Mode string + +// +kubebuilder:object:generate=true +type Spec struct { + // Has a single OneAgent per node via DaemonSet. + // Injection is performed via the same OneAgent DaemonSet. + // +nullable + ClassicFullStack *HostInjectSpec `json:"classicFullStack,omitempty"` + + // Has a single OneAgent per node via DaemonSet. + // dynatrace-webhook injects into application pods based on labeled namespaces. + // Has a CSI driver per node via DaemonSet to provide binaries to pods. + // +nullable + CloudNativeFullStack *CloudNativeFullStackSpec `json:"cloudNativeFullStack,omitempty"` + + // dynatrace-webhook injects into application pods based on labeled namespaces. + // Has an optional CSI driver per node via DaemonSet to provide binaries to pods. + // +nullable + ApplicationMonitoring *ApplicationMonitoringSpec `json:"applicationMonitoring,omitempty"` + + // Has a single OneAgent per node via DaemonSet. + // Doesn't inject into application pods. + // +nullable + HostMonitoring *HostInjectSpec `json:"hostMonitoring,omitempty"` + + // Sets a host group for OneAgent. + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Host Group",order=5,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:advanced","urn:alm:descriptor:com.tectonic.ui:text"} + HostGroup string `json:"hostGroup,omitempty"` +} + +// +kubebuilder:object:generate=true +type CloudNativeFullStackSpec struct { + HostInjectSpec `json:",inline"` + AppInjectionSpec `json:",inline"` +} + +// +kubebuilder:object:generate=true +type HostInjectSpec struct { + + // Add custom OneAgent annotations. + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Annotations",order=27,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:advanced","urn:alm:descriptor:com.tectonic.ui:text"} + Annotations map[string]string `json:"annotations,omitempty"` + + // Your defined labels for OneAgent pods in order to structure workloads as desired. + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Labels",order=26,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:advanced","urn:alm:descriptor:com.tectonic.ui:text"} + Labels map[string]string `json:"labels,omitempty"` + + // Specify the node selector that controls on which nodes OneAgent will be deployed. + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Node Selector",order=17,xDescriptors="urn:alm:descriptor:com.tectonic.ui:selector:Node" + NodeSelector map[string]string `json:"nodeSelector,omitempty"` + + // Disables automatic restarts of OneAgent pods in case a new version is available (https://www.dynatrace.com/support/help/setup-and-configuration/setup-on-container-platforms/kubernetes/get-started-with-kubernetes-monitoring#disable-auto). + // Enabled by default. + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Automatically update Agent",order=13,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:advanced","urn:alm:descriptor:com.tectonic.ui:booleanSwitch"} + AutoUpdate *bool `json:"autoUpdate"` + + // Use a specific OneAgent version. Defaults to the latest version from the Dynatrace cluster. + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="OneAgent version",order=11,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:advanced","urn:alm:descriptor:com.tectonic.ui:text"} + Version string `json:"version,omitempty"` + + // Use a custom OneAgent image. Defaults to the latest image from the Dynatrace cluster. + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Image",order=12,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:advanced","urn:alm:descriptor:com.tectonic.ui:text"} + Image string `json:"image,omitempty"` + + // Set the DNS Policy for OneAgent pods. For details, see Pods DNS Policy (https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-s-dns-policy). + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="DNS Policy",order=24,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:advanced","urn:alm:descriptor:com.tectonic.ui:text"} + DNSPolicy corev1.DNSPolicy `json:"dnsPolicy,omitempty"` + + // Assign a priority class to the OneAgent pods. By default, no class is set. + // For details, see Pod Priority and Preemption (https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/). + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Priority Class name",order=23,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:advanced","urn:alm:descriptor:io.kubernetes:PriorityClass"} + PriorityClassName string `json:"priorityClassName,omitempty"` + + // The SecComp Profile that will be configured in order to run in secure computing mode. + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="OneAgent SecComp Profile",order=17,xDescriptors="urn:alm:descriptor:com.tectonic.ui:selector:core:v1:Namespace" + SecCompProfile string `json:"secCompProfile,omitempty"` + + // StorageHostPath is the writable directory on the host filesystem where OneAgent configurations will be stored. + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="StorageHostPath",order=28,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:advanced","urn:alm:descriptor:com.tectonic.ui:hidden"} + StorageHostPath string `json:"storageHostPath,omitempty"` + + // Resource settings for OneAgent container. Consumption of the OneAgent heavily depends on the workload to monitor. You can use the default settings in the CR. + // Note: resource.requests shows the values needed to run; resource.limits shows the maximum limits for the pod. + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Resource Requirements",order=20,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:advanced","urn:alm:descriptor:com.tectonic.ui:resourceRequirements"} + OneAgentResources corev1.ResourceRequirements `json:"oneAgentResources,omitempty"` + + // Tolerations to include with the OneAgent DaemonSet. For details, see Taints and Tolerations (https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/). + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Tolerations",order=18,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:advanced","urn:alm:descriptor:com.tectonic.ui:hidden"} + Tolerations []corev1.Toleration `json:"tolerations,omitempty"` + + // Set additional environment variables for the OneAgent pods. + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="OneAgent environment variable installer arguments",order=22,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:advanced","urn:alm:descriptor:com.tectonic.ui:hidden"} + Env []corev1.EnvVar `json:"env,omitempty"` + + // Set additional arguments to the OneAgent installer. + // For available options, see Linux custom installation (https://www.dynatrace.com/support/help/setup-and-configuration/dynatrace-oneagent/installation-and-operation/linux/installation/customize-oneagent-installation-on-linux). + // For the list of limitations, see Limitations (https://www.dynatrace.com/support/help/setup-and-configuration/setup-on-container-platforms/docker/set-up-dynatrace-oneagent-as-docker-container#limitations). + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="OneAgent installer arguments",order=21,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:advanced","urn:alm:descriptor:com.tectonic.ui:hidden"} + // +listType=set + Args []string `json:"args,omitempty"` +} + +// +kubebuilder:object:generate=true +type ApplicationMonitoringSpec struct { + + // Use a specific OneAgent CodeModule version. Defaults to the latest version from the Dynatrace cluster. + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="OneAgent version",order=11,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:advanced","urn:alm:descriptor:com.tectonic.ui:text"} + Version string `json:"version,omitempty"` + + AppInjectionSpec `json:",inline"` +} + +// +kubebuilder:object:generate=true +type AppInjectionSpec struct { + // Define resources requests and limits for the initContainer. For details, see Managing resources for containers + // (https://kubernetes.io/docs/concepts/configuration/manage-resources-containers). + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Resource Requirements",order=15,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:advanced","urn:alm:descriptor:com.tectonic.ui:resourceRequirements"} + InitResources *corev1.ResourceRequirements `json:"initResources,omitempty"` + + // Use a custom OneAgent CodeModule image to download binaries. + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="CodeModulesImage",order=12,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:advanced","urn:alm:descriptor:com.tectonic.ui:text"} + CodeModulesImage string `json:"codeModulesImage,omitempty"` + + // Applicable only for applicationMonitoring or cloudNativeFullStack configuration types. The namespaces where you want Dynatrace Operator to inject. + // For more information, see Configure monitoring for namespaces and pods (https://www.dynatrace.com/support/help/setup-and-configuration/setup-on-container-platforms/kubernetes/get-started-with-kubernetes-monitoring/dto-config-options-k8s#annotate). + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Namespace Selector",order=17,xDescriptors="urn:alm:descriptor:com.tectonic.ui:selector:core:v1:Namespace" + NamespaceSelector metav1.LabelSelector `json:"namespaceSelector,omitempty"` +} + +// +kubebuilder:object:generate=true +type CodeModulesStatus struct { + status.VersionStatus `json:",inline"` +} diff --git a/pkg/api/v1beta4/dynakube/oneagent/status.go b/pkg/api/v1beta4/dynakube/oneagent/status.go new file mode 100644 index 0000000000..b1a9427bdb --- /dev/null +++ b/pkg/api/v1beta4/dynakube/oneagent/status.go @@ -0,0 +1,58 @@ +package oneagent + +import ( + "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/communication" + "github.com/Dynatrace/dynatrace-operator/pkg/api/status" + containerv1 "github.com/google/go-containerregistry/pkg/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +kubebuilder:object:generate=true +type Status struct { + status.VersionStatus `json:",inline"` + + // List of deployed OneAgent instances + Instances map[string]Instance `json:"instances,omitempty"` + + // Time of the last instance status update + LastInstanceStatusUpdate *metav1.Time `json:"lastInstanceStatusUpdate,omitempty"` + + // Commands used for OneAgent's readiness probe + // +kubebuilder:validation:Type=object + // +kubebuilder:validation:Schemaless + // +kubebuilder:pruning:PreserveUnknownFields + Healthcheck *containerv1.HealthConfig `json:"healthcheck,omitempty"` + + // Information about OneAgent's connections + ConnectionInfoStatus ConnectionInfoStatus `json:"connectionInfoStatus,omitempty"` +} + +// +kubebuilder:object:generate=true +type Instance struct { + // Name of the OneAgent pod + PodName string `json:"podName,omitempty"` + + // IP address of the pod + IPAddress string `json:"ipAddress,omitempty"` +} + +// +kubebuilder:object:generate=true +type ConnectionInfoStatus struct { + // Information for communicating with the tenant + communication.ConnectionInfo `json:",inline"` + + // List of communication hosts + CommunicationHosts []CommunicationHostStatus `json:"communicationHosts,omitempty"` +} + +// +kubebuilder:object:generate=true +type CommunicationHostStatus struct { + // Connection protocol + Protocol string `json:"protocol,omitempty"` + + // Host domain + Host string `json:"host,omitempty"` + + // Connection port + Port uint32 `json:"port,omitempty"` +} diff --git a/pkg/api/v1beta4/dynakube/oneagent/zz_generated.deepcopy.go b/pkg/api/v1beta4/dynakube/oneagent/zz_generated.deepcopy.go new file mode 100644 index 0000000000..a2878735cc --- /dev/null +++ b/pkg/api/v1beta4/dynakube/oneagent/zz_generated.deepcopy.go @@ -0,0 +1,274 @@ +//go:build !ignore_autogenerated + +/* +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package oneagent + +import ( + pkgv1 "github.com/google/go-containerregistry/pkg/v1" + "k8s.io/api/core/v1" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AppInjectionSpec) DeepCopyInto(out *AppInjectionSpec) { + *out = *in + if in.InitResources != nil { + in, out := &in.InitResources, &out.InitResources + *out = new(v1.ResourceRequirements) + (*in).DeepCopyInto(*out) + } + in.NamespaceSelector.DeepCopyInto(&out.NamespaceSelector) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppInjectionSpec. +func (in *AppInjectionSpec) DeepCopy() *AppInjectionSpec { + if in == nil { + return nil + } + out := new(AppInjectionSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ApplicationMonitoringSpec) DeepCopyInto(out *ApplicationMonitoringSpec) { + *out = *in + in.AppInjectionSpec.DeepCopyInto(&out.AppInjectionSpec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationMonitoringSpec. +func (in *ApplicationMonitoringSpec) DeepCopy() *ApplicationMonitoringSpec { + if in == nil { + return nil + } + out := new(ApplicationMonitoringSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CloudNativeFullStackSpec) DeepCopyInto(out *CloudNativeFullStackSpec) { + *out = *in + in.HostInjectSpec.DeepCopyInto(&out.HostInjectSpec) + in.AppInjectionSpec.DeepCopyInto(&out.AppInjectionSpec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CloudNativeFullStackSpec. +func (in *CloudNativeFullStackSpec) DeepCopy() *CloudNativeFullStackSpec { + if in == nil { + return nil + } + out := new(CloudNativeFullStackSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CodeModulesStatus) DeepCopyInto(out *CodeModulesStatus) { + *out = *in + in.VersionStatus.DeepCopyInto(&out.VersionStatus) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CodeModulesStatus. +func (in *CodeModulesStatus) DeepCopy() *CodeModulesStatus { + if in == nil { + return nil + } + out := new(CodeModulesStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CommunicationHostStatus) DeepCopyInto(out *CommunicationHostStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommunicationHostStatus. +func (in *CommunicationHostStatus) DeepCopy() *CommunicationHostStatus { + if in == nil { + return nil + } + out := new(CommunicationHostStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConnectionInfoStatus) DeepCopyInto(out *ConnectionInfoStatus) { + *out = *in + in.ConnectionInfo.DeepCopyInto(&out.ConnectionInfo) + if in.CommunicationHosts != nil { + in, out := &in.CommunicationHosts, &out.CommunicationHosts + *out = make([]CommunicationHostStatus, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConnectionInfoStatus. +func (in *ConnectionInfoStatus) DeepCopy() *ConnectionInfoStatus { + if in == nil { + return nil + } + out := new(ConnectionInfoStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HostInjectSpec) DeepCopyInto(out *HostInjectSpec) { + *out = *in + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.NodeSelector != nil { + in, out := &in.NodeSelector, &out.NodeSelector + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.AutoUpdate != nil { + in, out := &in.AutoUpdate, &out.AutoUpdate + *out = new(bool) + **out = **in + } + in.OneAgentResources.DeepCopyInto(&out.OneAgentResources) + if in.Tolerations != nil { + in, out := &in.Tolerations, &out.Tolerations + *out = make([]v1.Toleration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Env != nil { + in, out := &in.Env, &out.Env + *out = make([]v1.EnvVar, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Args != nil { + in, out := &in.Args, &out.Args + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostInjectSpec. +func (in *HostInjectSpec) DeepCopy() *HostInjectSpec { + if in == nil { + return nil + } + out := new(HostInjectSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Instance) DeepCopyInto(out *Instance) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Instance. +func (in *Instance) DeepCopy() *Instance { + if in == nil { + return nil + } + out := new(Instance) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Spec) DeepCopyInto(out *Spec) { + *out = *in + if in.ClassicFullStack != nil { + in, out := &in.ClassicFullStack, &out.ClassicFullStack + *out = new(HostInjectSpec) + (*in).DeepCopyInto(*out) + } + if in.CloudNativeFullStack != nil { + in, out := &in.CloudNativeFullStack, &out.CloudNativeFullStack + *out = new(CloudNativeFullStackSpec) + (*in).DeepCopyInto(*out) + } + if in.ApplicationMonitoring != nil { + in, out := &in.ApplicationMonitoring, &out.ApplicationMonitoring + *out = new(ApplicationMonitoringSpec) + (*in).DeepCopyInto(*out) + } + if in.HostMonitoring != nil { + in, out := &in.HostMonitoring, &out.HostMonitoring + *out = new(HostInjectSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Spec. +func (in *Spec) DeepCopy() *Spec { + if in == nil { + return nil + } + out := new(Spec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Status) DeepCopyInto(out *Status) { + *out = *in + in.VersionStatus.DeepCopyInto(&out.VersionStatus) + if in.Instances != nil { + in, out := &in.Instances, &out.Instances + *out = make(map[string]Instance, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.LastInstanceStatusUpdate != nil { + in, out := &in.LastInstanceStatusUpdate, &out.LastInstanceStatusUpdate + *out = (*in).DeepCopy() + } + if in.Healthcheck != nil { + in, out := &in.Healthcheck, &out.Healthcheck + *out = new(pkgv1.HealthConfig) + (*in).DeepCopyInto(*out) + } + in.ConnectionInfoStatus.DeepCopyInto(&out.ConnectionInfoStatus) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Status. +func (in *Status) DeepCopy() *Status { + if in == nil { + return nil + } + out := new(Status) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/api/v1beta4/dynakube/oneagent_props.go b/pkg/api/v1beta4/dynakube/oneagent_props.go new file mode 100644 index 0000000000..d4b30e80a1 --- /dev/null +++ b/pkg/api/v1beta4/dynakube/oneagent_props.go @@ -0,0 +1,20 @@ +package dynakube + +import ( + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" +) + +func (dk *DynaKube) OneAgent() *oneagent.OneAgent { + oa := oneagent.NewOneAgent( + &dk.Spec.OneAgent, + &dk.Status.OneAgent, + &dk.Status.CodeModules, + dk.Name, + dk.ApiUrlHost(), + dk.FeatureOneAgentPrivileged(), + dk.FeatureOneAgentSkipLivenessProbe(), + dk.FeatureNodeImagePull(), + ) + + return oa +} diff --git a/pkg/api/v1beta4/dynakube/otelc_props.go b/pkg/api/v1beta4/dynakube/otelc_props.go new file mode 100644 index 0000000000..d237e883fb --- /dev/null +++ b/pkg/api/v1beta4/dynakube/otelc_props.go @@ -0,0 +1,21 @@ +package dynakube + +func (dk *DynaKube) OtelCollectorStatefulsetName() string { + return dk.Name + "-otel-collector" +} + +func (dk *DynaKube) IsAGCertificateNeeded() bool { + if dk.ActiveGate().IsEnabled() && dk.ActiveGate().HasCaCert() { + return true + } + + return false +} + +func (dk *DynaKube) IsCACertificateNeeded() bool { + if !dk.ActiveGate().IsEnabled() && dk.Spec.TrustedCAs != "" { + return true + } + + return false +} diff --git a/pkg/api/v1beta4/dynakube/otelc_props_test.go b/pkg/api/v1beta4/dynakube/otelc_props_test.go new file mode 100644 index 0000000000..3ded4c9f00 --- /dev/null +++ b/pkg/api/v1beta4/dynakube/otelc_props_test.go @@ -0,0 +1,96 @@ +package dynakube + +import ( + "testing" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" + "github.com/stretchr/testify/assert" +) + +func TestIsAGCertificateNeeded(t *testing.T) { + t.Run("remote AG and no trustedCAs", func(t *testing.T) { + dk := &DynaKube{ + Spec: DynaKubeSpec{}, + } + assert.False(t, dk.IsAGCertificateNeeded()) + }) + t.Run("remote AG and trustedCAs", func(t *testing.T) { + dk := &DynaKube{ + Spec: DynaKubeSpec{ + TrustedCAs: "test", + }, + } + assert.False(t, dk.IsAGCertificateNeeded()) + }) + t.Run("in-cluster AG and no trustedCAs", func(t *testing.T) { + dk := &DynaKube{ + Spec: DynaKubeSpec{ + ActiveGate: activegate.Spec{ + TlsSecretName: "test-ag-cert", + Capabilities: []activegate.CapabilityDisplayName{ + activegate.DynatraceApiCapability.DisplayName, + }, + }, + }, + } + assert.True(t, dk.IsAGCertificateNeeded()) + }) + t.Run("in-cluster AG and trustedCAs", func(t *testing.T) { + dk := &DynaKube{ + Spec: DynaKubeSpec{ + ActiveGate: activegate.Spec{ + TlsSecretName: "test-ag-cert", + Capabilities: []activegate.CapabilityDisplayName{ + activegate.DynatraceApiCapability.DisplayName, + }, + }, + TrustedCAs: "test", + }, + } + assert.True(t, dk.IsAGCertificateNeeded()) + }) +} + +func TestIsCACertificateNeeded(t *testing.T) { + t.Run("remote AG and no trustedCAs", func(t *testing.T) { + dk := &DynaKube{ + Spec: DynaKubeSpec{}, + } + assert.False(t, dk.IsCACertificateNeeded()) + }) + t.Run("remote AG and trustedCAs", func(t *testing.T) { + dk := &DynaKube{ + Spec: DynaKubeSpec{ + TrustedCAs: "test", + }, + } + assert.True(t, dk.IsCACertificateNeeded()) + }) + t.Run("in-cluster AG and no trustedCAs", func(t *testing.T) { + dk := &DynaKube{ + Spec: DynaKubeSpec{ + ActiveGate: activegate.Spec{ + TlsSecretName: "test-ag-cert", + Capabilities: []activegate.CapabilityDisplayName{ + activegate.DynatraceApiCapability.DisplayName, + }, + }, + }, + } + assert.False(t, dk.IsCACertificateNeeded()) + }) + t.Run("in-cluster AG and trustedCAs", func(t *testing.T) { + dk := &DynaKube{ + Spec: DynaKubeSpec{ + ActiveGate: activegate.Spec{ + TlsSecretName: "test-ag-cert", + Capabilities: []activegate.CapabilityDisplayName{ + activegate.DynatraceApiCapability.DisplayName, + }, + }, + TrustedCAs: "test", + }, + } + assert.False(t, dk.IsCACertificateNeeded()) + }) +} diff --git a/pkg/api/v1beta4/dynakube/proxy.go b/pkg/api/v1beta4/dynakube/proxy.go new file mode 100644 index 0000000000..bf83996403 --- /dev/null +++ b/pkg/api/v1beta4/dynakube/proxy.go @@ -0,0 +1,81 @@ +/* +Copyright 2021 Dynatrace LLC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dynakube + +import ( + "context" + "fmt" + + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + ProxyKey = "proxy" + NoProxyKey = "noProxy" +) + +func (dk *DynaKube) Proxy(ctx context.Context, kubeReader client.Reader) (string, error) { + if dk.Spec.Proxy == nil { + return "", nil + } + + if dk.Spec.Proxy.Value != "" { + return dk.Spec.Proxy.Value, nil + } else if dk.Spec.Proxy.ValueFrom != "" { + return dk.proxyUrlFromUserSecret(ctx, kubeReader) + } + + return "", nil +} + +func (dk *DynaKube) HasProxy() bool { + return dk.Spec.Proxy != nil && (dk.Spec.Proxy.Value != "" || dk.Spec.Proxy.ValueFrom != "") +} + +func (dk *DynaKube) NeedsCustomNoProxy() bool { + return dk.HasProxy() && dk.FeatureNoProxy() != "" +} + +func (dk *DynaKube) NeedsActiveGateProxy() bool { + return !dk.FeatureActiveGateIgnoreProxy() && dk.HasProxy() && dk.ActiveGate().IsEnabled() +} + +func (dk *DynaKube) NeedsOneAgentProxy() bool { + return !dk.FeatureOneAgentIgnoreProxy() && dk.HasProxy() +} + +func (dk *DynaKube) proxyUrlFromUserSecret(ctx context.Context, kubeReader client.Reader) (string, error) { + secretName := dk.Spec.Proxy.ValueFrom + + var proxySecret corev1.Secret + + err := kubeReader.Get(ctx, client.ObjectKey{Name: secretName, Namespace: dk.Namespace}, &proxySecret) + if err != nil { + return "", errors.WithMessage(err, fmt.Sprintf("failed to get proxy from %s secret", secretName)) + } + + proxy, hasKey := proxySecret.Data[ProxyKey] + if !hasKey { + err := errors.Errorf("missing token %s in proxy secret %s", ProxyKey, secretName) + + return "", err + } + + return string(proxy), nil +} diff --git a/pkg/api/v1beta4/dynakube/telemetryingest/props.go b/pkg/api/v1beta4/dynakube/telemetryingest/props.go new file mode 100644 index 0000000000..087865bbdc --- /dev/null +++ b/pkg/api/v1beta4/dynakube/telemetryingest/props.go @@ -0,0 +1,49 @@ +package telemetryingest + +import "github.com/Dynatrace/dynatrace-operator/pkg/otelcgen" + +const ( + ServiceNameSuffix = "-telemetry-ingest" +) + +func (spec *Spec) GetProtocols() otelcgen.Protocols { + if spec == nil { + return otelcgen.Protocols{} + } + + if len(spec.Protocols) == 0 { + return otelcgen.RegisteredProtocols + } + + protocols := make(otelcgen.Protocols, len(spec.Protocols)) + for i, proto := range spec.Protocols { + protocols[i] = otelcgen.Protocol(proto) + } + + return protocols +} + +func (ts *TelemetryIngest) SetName(name string) { + ts.name = name +} + +func (ts *TelemetryIngest) GetDefaultServiceName() string { + return ts.name + ServiceNameSuffix +} + +func (ts *TelemetryIngest) GetServiceName() string { + if ts.Spec == nil { + return ts.GetDefaultServiceName() + } + + serviceName := ts.Spec.ServiceName + if serviceName == "" { + serviceName = ts.GetDefaultServiceName() + } + + return serviceName +} + +func (ts *TelemetryIngest) IsEnabled() bool { + return ts.Spec != nil +} diff --git a/pkg/api/v1beta4/dynakube/telemetryingest/spec.go b/pkg/api/v1beta4/dynakube/telemetryingest/spec.go new file mode 100644 index 0000000000..e803e4dedc --- /dev/null +++ b/pkg/api/v1beta4/dynakube/telemetryingest/spec.go @@ -0,0 +1,20 @@ +package telemetryingest + +type TelemetryIngest struct { + *Spec + + name string +} + +// +kubebuilder:object:generate=true + +type Spec struct { + // +kubebuilder:validation:Optional + ServiceName string `json:"serviceName,omitempty"` + + // +kubebuilder:validation:Optional + TlsRefName string `json:"tlsRefName,omitempty"` + + // +kubebuilder:validation:Optional + Protocols []string `json:"protocols,omitempty"` +} diff --git a/pkg/api/v1beta4/dynakube/telemetryingest/zz_generated.deepcopy.go b/pkg/api/v1beta4/dynakube/telemetryingest/zz_generated.deepcopy.go new file mode 100644 index 0000000000..425966b834 --- /dev/null +++ b/pkg/api/v1beta4/dynakube/telemetryingest/zz_generated.deepcopy.go @@ -0,0 +1,41 @@ +//go:build !ignore_autogenerated + +/* +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package telemetryingest + +import () + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Spec) DeepCopyInto(out *Spec) { + *out = *in + if in.Protocols != nil { + in, out := &in.Protocols, &out.Protocols + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Spec. +func (in *Spec) DeepCopy() *Spec { + if in == nil { + return nil + } + out := new(Spec) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/api/v1beta4/dynakube/telemetryservice_props.go b/pkg/api/v1beta4/dynakube/telemetryservice_props.go new file mode 100644 index 0000000000..43e596bc5a --- /dev/null +++ b/pkg/api/v1beta4/dynakube/telemetryservice_props.go @@ -0,0 +1,14 @@ +package dynakube + +import ( + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/telemetryingest" +) + +func (dk *DynaKube) TelemetryIngest() *telemetryingest.TelemetryIngest { + ts := &telemetryingest.TelemetryIngest{ + Spec: dk.Spec.TelemetryIngest, + } + ts.SetName(dk.Name) + + return ts +} diff --git a/pkg/api/v1beta4/dynakube/test/certs_test.go b/pkg/api/v1beta4/dynakube/test/certs_test.go new file mode 100644 index 0000000000..e63788103d --- /dev/null +++ b/pkg/api/v1beta4/dynakube/test/certs_test.go @@ -0,0 +1,107 @@ +package test + +import ( + "context" + "testing" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + testConfigMapName = "test-config-map" + testConfigMapValue = "test-config-map-value" + + testSecretName = "test-secret" + testSecretValue = "test-secret-value" +) + +func TestCerts(t *testing.T) { + t.Run(`get trusted certificate authorities`, trustedCAsTester) + t.Run(`get no tls certificates`, activeGateTlsNoCertificateTester) + activeGateTLSCertificate(t) +} + +func trustedCAsTester(t *testing.T) { + kubeReader := fake.NewClient(&corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: testConfigMapName}, + Data: map[string]string{ + dynakube.TrustedCAKey: testConfigMapValue, + }, + }) + dk := dynakube.DynaKube{ + Spec: dynakube.DynaKubeSpec{ + TrustedCAs: testConfigMapName, + }, + } + trustedCAs, err := dk.TrustedCAs(context.TODO(), kubeReader) + require.NoError(t, err) + assert.Equal(t, []byte(testConfigMapValue), trustedCAs) + + kubeReader = fake.NewClient() + trustedCAs, err = dk.TrustedCAs(context.TODO(), kubeReader) + + require.Error(t, err) + assert.Empty(t, trustedCAs) + + emptyDk := dynakube.DynaKube{} + trustedCAs, err = emptyDk.TrustedCAs(context.TODO(), kubeReader) + require.NoError(t, err) + assert.Empty(t, trustedCAs) +} + +func activeGateTlsNoCertificateTester(t *testing.T) { + dk := dynakube.DynaKube{ + Spec: dynakube.DynaKubeSpec{ + ActiveGate: activegate.Spec{ + Capabilities: []activegate.CapabilityDisplayName{activegate.KubeMonCapability.DisplayName}, + TlsSecretName: testSecretName, + }, + }, + } + + kubeReader := fake.NewClient() + tlsCert, err := dk.ActiveGateTLSCert(context.TODO(), kubeReader) + + require.Error(t, err) + assert.Empty(t, tlsCert) + + emptyDk := dynakube.DynaKube{} + tlsCert, err = emptyDk.ActiveGateTLSCert(context.TODO(), kubeReader) + + require.NoError(t, err) + assert.Empty(t, tlsCert) +} + +func activeGateTLSCertificate(t *testing.T) { + testFunc := func(t *testing.T, data map[string][]byte) { + kubeReader := fake.NewClient(&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: testSecretName}, + Data: data, + }) + + dk := dynakube.DynaKube{ + Spec: dynakube.DynaKubeSpec{ + ActiveGate: activegate.Spec{ + Capabilities: []activegate.CapabilityDisplayName{activegate.KubeMonCapability.DisplayName}, + TlsSecretName: testSecretName, + }, + }, + } + tlsCert, err := dk.ActiveGateTLSCert(context.TODO(), kubeReader) + + require.NoError(t, err) + assert.Equal(t, testSecretValue, string(tlsCert)) + } + + t.Run("get tls certificates from server.crt", func(t *testing.T) { + testFunc(t, map[string][]byte{ + dynakube.TLSCertKey: []byte(testSecretValue), + }) + }) +} diff --git a/pkg/api/v1beta4/dynakube/test/proxy_test.go b/pkg/api/v1beta4/dynakube/test/proxy_test.go new file mode 100644 index 0000000000..cfb626a7bf --- /dev/null +++ b/pkg/api/v1beta4/dynakube/test/proxy_test.go @@ -0,0 +1,61 @@ +package test + +import ( + "context" + "testing" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" + "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/value" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + testProxyName = "test-proxy" + testProxyData = "test-proxy-value" +) + +func TestProxy(t *testing.T) { + t.Run(`get proxy value`, proxyValueTester) + t.Run(`get proxy value from secret`, proxyValueFromTester) +} + +func proxyValueTester(t *testing.T) { + dk := dynakube.DynaKube{ + Spec: dynakube.DynaKubeSpec{ + Proxy: &value.Source{Value: testProxyData}, + }, + } + proxy, err := dk.Proxy(context.TODO(), nil) + require.NoError(t, err) + assert.Equal(t, testProxyData, proxy) + + emptyDk := dynakube.DynaKube{} + proxy, err = emptyDk.Proxy(context.TODO(), nil) + require.NoError(t, err) + assert.Empty(t, proxy) +} + +func proxyValueFromTester(t *testing.T) { + kubeReader := fake.NewClient(&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: testProxyName}, + Data: map[string][]byte{ + dynakube.ProxyKey: []byte(testProxyData), + }}) + dk := dynakube.DynaKube{ + Spec: dynakube.DynaKubeSpec{ + Proxy: &value.Source{ValueFrom: testProxyName}, + }, + } + proxy, err := dk.Proxy(context.TODO(), kubeReader) + require.NoError(t, err) + assert.Equal(t, testProxyData, proxy) + + kubeReader = fake.NewClient() + proxy, err = dk.Proxy(context.TODO(), kubeReader) + require.Error(t, err) + assert.Empty(t, proxy) +} diff --git a/pkg/api/v1beta4/dynakube/zz_generated.deepcopy.go b/pkg/api/v1beta4/dynakube/zz_generated.deepcopy.go new file mode 100644 index 0000000000..6ab093947e --- /dev/null +++ b/pkg/api/v1beta4/dynakube/zz_generated.deepcopy.go @@ -0,0 +1,376 @@ +//go:build !ignore_autogenerated + +/* +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package dynakube + +import ( + "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/value" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/kspm" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/logmonitoring" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/telemetryingest" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DynaKube) DeepCopyInto(out *DynaKube) { + *out = *in + out.TypeMeta = in.TypeMeta + in.Status.DeepCopyInto(&out.Status) + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DynaKube. +func (in *DynaKube) DeepCopy() *DynaKube { + if in == nil { + return nil + } + out := new(DynaKube) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DynaKube) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DynaKubeList) DeepCopyInto(out *DynaKubeList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DynaKube, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DynaKubeList. +func (in *DynaKubeList) DeepCopy() *DynaKubeList { + if in == nil { + return nil + } + out := new(DynaKubeList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DynaKubeList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DynaKubeSpec) DeepCopyInto(out *DynaKubeSpec) { + *out = *in + in.MetadataEnrichment.DeepCopyInto(&out.MetadataEnrichment) + if in.Proxy != nil { + in, out := &in.Proxy, &out.Proxy + *out = new(value.Source) + **out = **in + } + if in.LogMonitoring != nil { + in, out := &in.LogMonitoring, &out.LogMonitoring + *out = new(logmonitoring.Spec) + (*in).DeepCopyInto(*out) + } + if in.Kspm != nil { + in, out := &in.Kspm, &out.Kspm + *out = new(kspm.Spec) + **out = **in + } + if in.DynatraceApiRequestThreshold != nil { + in, out := &in.DynatraceApiRequestThreshold, &out.DynatraceApiRequestThreshold + *out = new(uint16) + **out = **in + } + if in.Extensions != nil { + in, out := &in.Extensions, &out.Extensions + *out = new(ExtensionsSpec) + **out = **in + } + if in.TelemetryIngest != nil { + in, out := &in.TelemetryIngest, &out.TelemetryIngest + *out = new(telemetryingest.Spec) + (*in).DeepCopyInto(*out) + } + in.OneAgent.DeepCopyInto(&out.OneAgent) + in.Templates.DeepCopyInto(&out.Templates) + in.ActiveGate.DeepCopyInto(&out.ActiveGate) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DynaKubeSpec. +func (in *DynaKubeSpec) DeepCopy() *DynaKubeSpec { + if in == nil { + return nil + } + out := new(DynaKubeSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DynaKubeStatus) DeepCopyInto(out *DynaKubeStatus) { + *out = *in + in.OneAgent.DeepCopyInto(&out.OneAgent) + in.ActiveGate.DeepCopyInto(&out.ActiveGate) + in.CodeModules.DeepCopyInto(&out.CodeModules) + in.MetadataEnrichment.DeepCopyInto(&out.MetadataEnrichment) + out.Kspm = in.Kspm + in.UpdatedTimestamp.DeepCopyInto(&out.UpdatedTimestamp) + in.DynatraceApi.DeepCopyInto(&out.DynatraceApi) + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DynaKubeStatus. +func (in *DynaKubeStatus) DeepCopy() *DynaKubeStatus { + if in == nil { + return nil + } + out := new(DynaKubeStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DynatraceApiStatus) DeepCopyInto(out *DynatraceApiStatus) { + *out = *in + in.LastTokenScopeRequest.DeepCopyInto(&out.LastTokenScopeRequest) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DynatraceApiStatus. +func (in *DynatraceApiStatus) DeepCopy() *DynatraceApiStatus { + if in == nil { + return nil + } + out := new(DynatraceApiStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EnrichmentRule) DeepCopyInto(out *EnrichmentRule) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnrichmentRule. +func (in *EnrichmentRule) DeepCopy() *EnrichmentRule { + if in == nil { + return nil + } + out := new(EnrichmentRule) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExtensionExecutionControllerSpec) DeepCopyInto(out *ExtensionExecutionControllerSpec) { + *out = *in + if in.PersistentVolumeClaim != nil { + in, out := &in.PersistentVolumeClaim, &out.PersistentVolumeClaim + *out = new(corev1.PersistentVolumeClaimSpec) + (*in).DeepCopyInto(*out) + } + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + out.ImageRef = in.ImageRef + in.Resources.DeepCopyInto(&out.Resources) + if in.Tolerations != nil { + in, out := &in.Tolerations, &out.Tolerations + *out = make([]corev1.Toleration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.TopologySpreadConstraints != nil { + in, out := &in.TopologySpreadConstraints, &out.TopologySpreadConstraints + *out = make([]corev1.TopologySpreadConstraint, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtensionExecutionControllerSpec. +func (in *ExtensionExecutionControllerSpec) DeepCopy() *ExtensionExecutionControllerSpec { + if in == nil { + return nil + } + out := new(ExtensionExecutionControllerSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExtensionsSpec) DeepCopyInto(out *ExtensionsSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtensionsSpec. +func (in *ExtensionsSpec) DeepCopy() *ExtensionsSpec { + if in == nil { + return nil + } + out := new(ExtensionsSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MetadataEnrichment) DeepCopyInto(out *MetadataEnrichment) { + *out = *in + if in.Enabled != nil { + in, out := &in.Enabled, &out.Enabled + *out = new(bool) + **out = **in + } + in.NamespaceSelector.DeepCopyInto(&out.NamespaceSelector) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MetadataEnrichment. +func (in *MetadataEnrichment) DeepCopy() *MetadataEnrichment { + if in == nil { + return nil + } + out := new(MetadataEnrichment) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MetadataEnrichmentStatus) DeepCopyInto(out *MetadataEnrichmentStatus) { + *out = *in + if in.Rules != nil { + in, out := &in.Rules, &out.Rules + *out = make([]EnrichmentRule, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MetadataEnrichmentStatus. +func (in *MetadataEnrichmentStatus) DeepCopy() *MetadataEnrichmentStatus { + if in == nil { + return nil + } + out := new(MetadataEnrichmentStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OpenTelemetryCollectorSpec) DeepCopyInto(out *OpenTelemetryCollectorSpec) { + *out = *in + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Replicas != nil { + in, out := &in.Replicas, &out.Replicas + *out = new(int32) + **out = **in + } + out.ImageRef = in.ImageRef + in.Resources.DeepCopyInto(&out.Resources) + if in.Tolerations != nil { + in, out := &in.Tolerations, &out.Tolerations + *out = make([]corev1.Toleration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.TopologySpreadConstraints != nil { + in, out := &in.TopologySpreadConstraints, &out.TopologySpreadConstraints + *out = make([]corev1.TopologySpreadConstraint, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenTelemetryCollectorSpec. +func (in *OpenTelemetryCollectorSpec) DeepCopy() *OpenTelemetryCollectorSpec { + if in == nil { + return nil + } + out := new(OpenTelemetryCollectorSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TemplatesSpec) DeepCopyInto(out *TemplatesSpec) { + *out = *in + if in.LogMonitoring != nil { + in, out := &in.LogMonitoring, &out.LogMonitoring + *out = new(logmonitoring.TemplateSpec) + (*in).DeepCopyInto(*out) + } + in.KspmNodeConfigurationCollector.DeepCopyInto(&out.KspmNodeConfigurationCollector) + in.OpenTelemetryCollector.DeepCopyInto(&out.OpenTelemetryCollector) + in.ExtensionExecutionController.DeepCopyInto(&out.ExtensionExecutionController) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TemplatesSpec. +func (in *TemplatesSpec) DeepCopy() *TemplatesSpec { + if in == nil { + return nil + } + out := new(TemplatesSpec) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/api/v1beta4/groupversion_info.go b/pkg/api/v1beta4/groupversion_info.go new file mode 100644 index 0000000000..6afda4d829 --- /dev/null +++ b/pkg/api/v1beta4/groupversion_info.go @@ -0,0 +1,34 @@ +/* +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1beta4 contains API Schema definitions for the dynatrace v1beta4 API group +// +kubebuilder:object:generate=true +// +groupName=dynatrace.com +package v1beta4 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects. + GroupVersion = schema.GroupVersion{Group: "dynatrace.com", Version: "v1beta4"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme. + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/pkg/api/validation/dynakube/activegate.go b/pkg/api/validation/dynakube/activegate.go index da6c0c72c2..6025a9b4ba 100644 --- a/pkg/api/validation/dynakube/activegate.go +++ b/pkg/api/validation/dynakube/activegate.go @@ -4,8 +4,8 @@ import ( "context" "fmt" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" corev1 "k8s.io/api/core/v1" ) @@ -17,6 +17,8 @@ Make sure you correctly specify the ActiveGate capabilities in your custom resou errorDuplicateActiveGateCapability = `The DynaKube's specification tries to specify duplicate capabilities in the ActiveGate section, duplicate capability=%s. Make sure you don't duplicate an Activegate capability in your custom resource. ` + errorActiveGateInvalidPVCConfiguration = ` DynaKube specifies a PVC for the ActiveGate while ephemeral volume is also enabled. These settings are mutually exclusive, please choose only one.` + warningMissingActiveGateMemoryLimit = `ActiveGate specification missing memory limits. Can cause excess memory usage.` ) @@ -66,3 +68,17 @@ func missingActiveGateMemoryLimit(_ context.Context, _ *Validator, dk *dynakube. func memoryLimitSet(resources corev1.ResourceRequirements) bool { return resources.Limits != nil && resources.Limits.Memory() != nil } + +func activeGateMutuallyExclusivePVCSettings(dk *dynakube.DynaKube) bool { + return dk.Spec.ActiveGate.UseEphemeralVolume && dk.Spec.ActiveGate.PersistentVolumeClaim != nil +} + +func mutuallyExclusiveActiveGatePVsettings(_ context.Context, _ *Validator, dk *dynakube.DynaKube) string { + if activeGateMutuallyExclusivePVCSettings(dk) { + log.Info("requested dynakube specifies mutually exclusive PersistentVolumeClaim settings for ActiveGate.", "name", dk.Name, "namespace", dk.Namespace) + + return errorActiveGateInvalidPVCConfiguration + } + + return "" +} diff --git a/pkg/api/validation/dynakube/activegate_test.go b/pkg/api/validation/dynakube/activegate_test.go index e2cafff1d3..caa04adae2 100644 --- a/pkg/api/validation/dynakube/activegate_test.go +++ b/pkg/api/validation/dynakube/activegate_test.go @@ -4,8 +4,8 @@ import ( "fmt" "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" ) @@ -87,3 +87,46 @@ func TestMissingActiveGateMemoryLimit(t *testing.T) { }) }) } + +func TestActiveGatePVCSettings(t *testing.T) { + t.Run(`EphemeralVolume disabled and PVC specified`, func(t *testing.T) { + assertAllowed(t, + &dynakube.DynaKube{ + ObjectMeta: defaultDynakubeObjectMeta, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + ActiveGate: activegate.Spec{ + UseEphemeralVolume: false, + PersistentVolumeClaim: &corev1.PersistentVolumeClaimSpec{}, + }, + }, + }) + }) + t.Run(`EphemeralVolume enabled and no PVC specified`, func(t *testing.T) { + assertAllowed(t, + &dynakube.DynaKube{ + ObjectMeta: defaultDynakubeObjectMeta, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + ActiveGate: activegate.Spec{ + UseEphemeralVolume: true, + }, + }, + }) + }) + t.Run(`EphemeralVolume enabled and PVC specified`, func(t *testing.T) { + assertDenied(t, + []string{errorActiveGateInvalidPVCConfiguration}, + &dynakube.DynaKube{ + ObjectMeta: defaultDynakubeObjectMeta, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + Extensions: &dynakube.ExtensionsSpec{}, + ActiveGate: activegate.Spec{ + UseEphemeralVolume: true, + PersistentVolumeClaim: &corev1.PersistentVolumeClaimSpec{}, + }, + }, + }) + }) +} diff --git a/pkg/api/validation/dynakube/api_url.go b/pkg/api/validation/dynakube/api_url.go index ccb066ccdd..2336f6e681 100644 --- a/pkg/api/validation/dynakube/api_url.go +++ b/pkg/api/validation/dynakube/api_url.go @@ -5,7 +5,7 @@ import ( "net/url" "strings" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" ) const ( diff --git a/pkg/api/validation/dynakube/api_url_test.go b/pkg/api/validation/dynakube/api_url_test.go index 9d0edef20c..2d5dc9a4fa 100644 --- a/pkg/api/validation/dynakube/api_url_test.go +++ b/pkg/api/validation/dynakube/api_url_test.go @@ -5,7 +5,7 @@ import ( "strings" "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/stretchr/testify/assert" ) diff --git a/pkg/api/validation/dynakube/csi_daemonset.go b/pkg/api/validation/dynakube/csi_daemonset.go index 5d50e196b8..6f21cf480e 100644 --- a/pkg/api/validation/dynakube/csi_daemonset.go +++ b/pkg/api/validation/dynakube/csi_daemonset.go @@ -3,7 +3,7 @@ package validation import ( "context" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" ) const ( @@ -12,7 +12,7 @@ const ( ) func disabledCSIForReadonlyCSIVolume(_ context.Context, _ *Validator, dk *dynakube.DynaKube) string { - isCSINotUsed := !dk.IsCSIAvailable() || (!isCSIRequired(dk) && !isCSIOptional(dk)) + isCSINotUsed := !dk.OneAgent().IsCSIAvailable() || !isCSIOptional(dk) if dk.FeatureReadOnlyCsiVolume() && isCSINotUsed { log.Info("requested dynakube uses readonly csi volume, but csi driver is not enabled", "name", dk.Name, "namespace", dk.Namespace) @@ -24,5 +24,5 @@ func disabledCSIForReadonlyCSIVolume(_ context.Context, _ *Validator, dk *dynaku // IsCSIDriverOptional checks if the DynaKube may use the csi-driver if available, otherwise fallbacks exist to provide similar functionality. func isCSIOptional(dk *dynakube.DynaKube) bool { - return dk.HostMonitoringMode() || dk.ApplicationMonitoringMode() + return dk.OneAgent().IsCloudNativeFullstackMode() || dk.OneAgent().IsHostMonitoringMode() || dk.OneAgent().IsApplicationMonitoringMode() } diff --git a/pkg/api/validation/dynakube/csi_daemonset_test.go b/pkg/api/validation/dynakube/csi_daemonset_test.go index d726215c40..920d16f979 100644 --- a/pkg/api/validation/dynakube/csi_daemonset_test.go +++ b/pkg/api/validation/dynakube/csi_daemonset_test.go @@ -3,7 +3,8 @@ package validation import ( "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" ) func TestDisabledCSIForReadonlyCSIVolume(t *testing.T) { @@ -17,8 +18,8 @@ func TestDisabledCSIForReadonlyCSIVolume(t *testing.T) { ObjectMeta: *objectMeta, Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{ - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{}, + OneAgent: oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{}, }, }, }) @@ -31,8 +32,8 @@ func TestDisabledCSIForReadonlyCSIVolume(t *testing.T) { ObjectMeta: *objectMeta, Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{ - ClassicFullStack: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + ClassicFullStack: &oneagent.HostInjectSpec{}, }, }, }) diff --git a/pkg/api/validation/dynakube/dynakube_name.go b/pkg/api/validation/dynakube/dynakube_name.go index f87deea951..9f4d8d9963 100644 --- a/pkg/api/validation/dynakube/dynakube_name.go +++ b/pkg/api/validation/dynakube/dynakube_name.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "k8s.io/apimachinery/pkg/util/validation" ) diff --git a/pkg/api/validation/dynakube/dynakube_name_test.go b/pkg/api/validation/dynakube/dynakube_name_test.go index ee55d7410e..07cfe2a032 100644 --- a/pkg/api/validation/dynakube/dynakube_name_test.go +++ b/pkg/api/validation/dynakube/dynakube_name_test.go @@ -5,7 +5,7 @@ import ( "strings" "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) diff --git a/pkg/api/validation/dynakube/eec.go b/pkg/api/validation/dynakube/eec.go index ab84e0eba8..a38c558e7e 100644 --- a/pkg/api/validation/dynakube/eec.go +++ b/pkg/api/validation/dynakube/eec.go @@ -1,13 +1,17 @@ package validation import ( - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "fmt" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "golang.org/x/net/context" + "sigs.k8s.io/controller-runtime/pkg/client" ) const ( errorExtensionExecutionControllerImageNotSpecified = `DynaKube's specification enables the Prometheus feature, make sure you correctly specify the ExtensionExecutionController image.` errorExtensionExecutionControllerInvalidPVCConfiguration = `DynaKube specifies a PVC for the extension controller while ephemeral volume is also enabled. These settings are mutually exclusive, please choose only one.` + warningConflictingApiUrlForExtensions = `You are already using a Dynakube ('%s') that enables extensions. Having multiple Dynakubes with same '.spec.apiUrl' and '.spec.extensions' enabled can have severe side-effects on “sum” and “count” metrics and cause double-billing.` ) func extensionControllerImage(_ context.Context, _ *Validator, dk *dynakube.DynaKube) string { @@ -24,6 +28,31 @@ func extensionControllerImage(_ context.Context, _ *Validator, dk *dynakube.Dyna return "" } +func conflictingApiUrlForExtensions(ctx context.Context, dv *Validator, dk *dynakube.DynaKube) string { + if !dk.IsExtensionsEnabled() { + return "" + } + + validDynakubes := &dynakube.DynaKubeList{} + if err := dv.apiReader.List(ctx, validDynakubes, &client.ListOptions{Namespace: dk.Namespace}); err != nil { + log.Info("error occurred while listing dynakubes", "err", err.Error()) + + return "" + } + + for _, item := range validDynakubes.Items { + if item.Name == dk.Name { + continue + } + + if item.IsExtensionsEnabled() && (dk.ApiUrl() == item.ApiUrl()) { + return fmt.Sprintf(warningConflictingApiUrlForExtensions, item.Name) + } + } + + return "" +} + func extensionControllerPVCStorageDevice(_ context.Context, _ *Validator, dk *dynakube.DynaKube) string { if !dk.IsExtensionsEnabled() { return "" diff --git a/pkg/api/validation/dynakube/eec_test.go b/pkg/api/validation/dynakube/eec_test.go index 641b9d5be3..cc5d7a95c2 100644 --- a/pkg/api/validation/dynakube/eec_test.go +++ b/pkg/api/validation/dynakube/eec_test.go @@ -1,11 +1,17 @@ package validation import ( + "fmt" "testing" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/image" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestExtensionExecutionControllerImage(t *testing.T) { @@ -14,7 +20,12 @@ func TestExtensionExecutionControllerImage(t *testing.T) { &dynakube.DynaKube{ ObjectMeta: defaultDynakubeObjectMeta, Spec: dynakube.DynaKubeSpec{ - APIURL: testApiUrl, + APIURL: testApiUrl, + ActiveGate: activegate.Spec{ + Capabilities: []activegate.CapabilityDisplayName{ + activegate.KubeMonCapability.DisplayName, + }, + }, Extensions: &dynakube.ExtensionsSpec{}, Templates: dynakube.TemplatesSpec{ ExtensionExecutionController: dynakube.ExtensionExecutionControllerSpec{ @@ -36,6 +47,11 @@ func TestExtensionExecutionControllerImage(t *testing.T) { Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, Extensions: &dynakube.ExtensionsSpec{}, + ActiveGate: activegate.Spec{ + Capabilities: []activegate.CapabilityDisplayName{ + activegate.KubeMonCapability.DisplayName, + }, + }, Templates: dynakube.TemplatesSpec{ ExtensionExecutionController: dynakube.ExtensionExecutionControllerSpec{ ImageRef: image.Ref{ @@ -55,6 +71,11 @@ func TestExtensionExecutionControllerImage(t *testing.T) { Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, Extensions: &dynakube.ExtensionsSpec{}, + ActiveGate: activegate.Spec{ + Capabilities: []activegate.CapabilityDisplayName{ + activegate.KubeMonCapability.DisplayName, + }, + }, Templates: dynakube.TemplatesSpec{ ExtensionExecutionController: dynakube.ExtensionExecutionControllerSpec{ ImageRef: image.Ref{ @@ -74,6 +95,11 @@ func TestExtensionExecutionControllerImage(t *testing.T) { Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, Extensions: &dynakube.ExtensionsSpec{}, + ActiveGate: activegate.Spec{ + Capabilities: []activegate.CapabilityDisplayName{ + activegate.KubeMonCapability.DisplayName, + }, + }, }, }) }) @@ -87,6 +113,11 @@ func TestExtensionExecutionControllerPVCSettings(t *testing.T) { Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, Extensions: &dynakube.ExtensionsSpec{}, + ActiveGate: activegate.Spec{ + Capabilities: []activegate.CapabilityDisplayName{ + activegate.KubeMonCapability.DisplayName, + }, + }, Templates: dynakube.TemplatesSpec{ ExtensionExecutionController: dynakube.ExtensionExecutionControllerSpec{ ImageRef: image.Ref{ @@ -107,6 +138,11 @@ func TestExtensionExecutionControllerPVCSettings(t *testing.T) { Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, Extensions: &dynakube.ExtensionsSpec{}, + ActiveGate: activegate.Spec{ + Capabilities: []activegate.CapabilityDisplayName{ + activegate.KubeMonCapability.DisplayName, + }, + }, Templates: dynakube.TemplatesSpec{ ExtensionExecutionController: dynakube.ExtensionExecutionControllerSpec{ ImageRef: image.Ref{ @@ -127,6 +163,11 @@ func TestExtensionExecutionControllerPVCSettings(t *testing.T) { Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, Extensions: &dynakube.ExtensionsSpec{}, + ActiveGate: activegate.Spec{ + Capabilities: []activegate.CapabilityDisplayName{ + activegate.KubeMonCapability.DisplayName, + }, + }, Templates: dynakube.TemplatesSpec{ ExtensionExecutionController: dynakube.ExtensionExecutionControllerSpec{ ImageRef: image.Ref{ @@ -141,3 +182,100 @@ func TestExtensionExecutionControllerPVCSettings(t *testing.T) { }) }) } + +func TestWarnIfmultiplyDKwithExtensionsEnabled(t *testing.T) { + imgRef := image.Ref{ + Repository: "a", + Tag: "b", + } + // we want to exclude AG resources warning. + agSpec := activegate.Spec{ + Capabilities: []activegate.CapabilityDisplayName{ + activegate.KubeMonCapability.DisplayName, + }, + CapabilityProperties: activegate.CapabilityProperties{ + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("1"), + }, + }, + }, + } + dk1 := &dynakube.DynaKube{ + ObjectMeta: defaultDynakubeObjectMeta, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + Extensions: &dynakube.ExtensionsSpec{}, + Templates: dynakube.TemplatesSpec{ + ExtensionExecutionController: dynakube.ExtensionExecutionControllerSpec{ + ImageRef: imgRef, + }, + }, + ActiveGate: agSpec, + }, + } + + t.Run("no warning different ApiUrls", func(t *testing.T) { + dk2 := &dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Name: testName + "second", + Namespace: testNamespace, + }, + Spec: dynakube.DynaKubeSpec{ + APIURL: "https://f.q.d.n/123", + Extensions: &dynakube.ExtensionsSpec{}, + Templates: dynakube.TemplatesSpec{ + ExtensionExecutionController: dynakube.ExtensionExecutionControllerSpec{ + ImageRef: imgRef, + }, + }, + ActiveGate: agSpec, + }, + } + assertAllowedWithoutWarnings(t, dk1, dk2) + }) + t.Run("warning same ApiUrls", func(t *testing.T) { + dk2 := &dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Name: testName + "second", + Namespace: testNamespace, + }, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + Extensions: &dynakube.ExtensionsSpec{}, + Templates: dynakube.TemplatesSpec{ + ExtensionExecutionController: dynakube.ExtensionExecutionControllerSpec{ + ImageRef: imgRef, + }, + }, + ActiveGate: agSpec, + }, + } + warnings, err := assertAllowed(t, dk1, dk2) + require.NoError(t, err) + require.Len(t, warnings, 1) + + expected := fmt.Sprintf(warningConflictingApiUrlForExtensions, dk2.Name) + assert.Equal(t, expected, warnings[0]) + }) + + t.Run("no warning same ApiUrls and for second dk: extensions feature is disabled", func(t *testing.T) { + dk2 := &dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Name: testName + "second", + Namespace: testNamespace, + }, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + Extensions: nil, + Templates: dynakube.TemplatesSpec{ + ExtensionExecutionController: dynakube.ExtensionExecutionControllerSpec{ + ImageRef: imgRef, + }, + }, + ActiveGate: agSpec, + }, + } + assertAllowedWithoutWarnings(t, dk1, dk2) + }) +} diff --git a/pkg/api/validation/dynakube/extensions.go b/pkg/api/validation/dynakube/extensions.go new file mode 100644 index 0000000000..5853ead1e5 --- /dev/null +++ b/pkg/api/validation/dynakube/extensions.go @@ -0,0 +1,19 @@ +package validation + +import ( + "context" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" +) + +const ( + warningExtensionsWithoutK8SMonitoring = "The Dynakube is configured with extensions without an ActiveGate with `kubernetes-monitoring` enabled or the `automatic-kubernetes-monitoring` feature flag. You need to ensure that Kubernetes monitoring is setup for this cluster." +) + +func extensionsWithoutK8SMonitoring(ctx context.Context, dv *Validator, dk *dynakube.DynaKube) string { + if dk.IsExtensionsEnabled() && (!dk.ActiveGate().IsKubernetesMonitoringEnabled() || !dk.FeatureAutomaticKubernetesApiMonitoring()) { + return warningExtensionsWithoutK8SMonitoring + } + + return "" +} diff --git a/pkg/api/validation/dynakube/extensions_test.go b/pkg/api/validation/dynakube/extensions_test.go new file mode 100644 index 0000000000..9d047bce50 --- /dev/null +++ b/pkg/api/validation/dynakube/extensions_test.go @@ -0,0 +1,70 @@ +package validation + +import ( + "testing" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/image" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const testDynakubeName = "dynakube" + +func TestExtensionsWithoutK8SMonitoring(t *testing.T) { + t.Run("no error if extensions are enabled with activegate with k8s-monitoring", func(t *testing.T) { + dk := createStandaloneExtensionsDynakube(testDynakubeName, testApiUrl) + dk.Spec.ActiveGate = activegate.Spec{ + Capabilities: []activegate.CapabilityDisplayName{ + activegate.KubeMonCapability.DisplayName, + }, + } + assertAllowed(t, dk) + }) + t.Run("error if extensions are enabled without activegate with k8s-monitoring", func(t *testing.T) { + assertAllowedWithWarnings(t, 2, + createStandaloneExtensionsDynakube(testDynakubeName, testApiUrl)) + }) + t.Run("error if extensions are enabled with activegate with k8s-monitoring but automatic Kuberenetes API monitoring is disabled", func(t *testing.T) { + dk := createStandaloneExtensionsDynakube(testDynakubeName, testApiUrl) + dk.ObjectMeta.Annotations = map[string]string{ + dynakube.AnnotationFeatureAutomaticK8sApiMonitoring: "false", + } + dk.Spec.ActiveGate = activegate.Spec{ + Capabilities: []activegate.CapabilityDisplayName{ + activegate.KubeMonCapability.DisplayName, + }, + } + assertAllowedWithWarnings(t, 2, dk) + }) + t.Run("error if extensions are enabled but automatic Kuberenetes API monitoring is disabled and without activgate k8s-monitoring", func(t *testing.T) { + dk := createStandaloneExtensionsDynakube(testDynakubeName, testApiUrl) + dk.ObjectMeta.Annotations = map[string]string{ + dynakube.AnnotationFeatureAutomaticK8sApiMonitoring: "false", + } + assertAllowedWithWarnings(t, 2, dk) + }) +} + +func createStandaloneExtensionsDynakube(name, apiUrl string) *dynakube.DynaKube { + dk := &dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: testNamespace, + }, + Spec: dynakube.DynaKubeSpec{ + APIURL: apiUrl, + Extensions: &dynakube.ExtensionsSpec{}, + Templates: dynakube.TemplatesSpec{ + ExtensionExecutionController: dynakube.ExtensionExecutionControllerSpec{ + ImageRef: image.Ref{ + Repository: "repo/image", + Tag: "version", + }, + }, + }, + }, + } + + return dk +} diff --git a/pkg/api/validation/dynakube/featureflag.go b/pkg/api/validation/dynakube/featureflag.go index b40a5056bd..f36c56b2fc 100644 --- a/pkg/api/validation/dynakube/featureflag.go +++ b/pkg/api/validation/dynakube/featureflag.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" ) const ( diff --git a/pkg/api/validation/dynakube/featureflag_test.go b/pkg/api/validation/dynakube/featureflag_test.go index 6f88798179..f283775adc 100644 --- a/pkg/api/validation/dynakube/featureflag_test.go +++ b/pkg/api/validation/dynakube/featureflag_test.go @@ -5,7 +5,7 @@ import ( "fmt" "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) diff --git a/pkg/api/validation/dynakube/image.go b/pkg/api/validation/dynakube/image.go index 6ce9b55113..90840b8d1f 100644 --- a/pkg/api/validation/dynakube/image.go +++ b/pkg/api/validation/dynakube/image.go @@ -3,22 +3,17 @@ package validation import ( "context" "fmt" - "net/url" "strings" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/google/go-containerregistry/pkg/name" ) const ( - errorUsingTenantImageAsCustom = `Custom %s image must not reference the Dynatrace Environment directly.` - errorUnparsableImageRef = `Custom %s image can't be parsed, make sure it's a valid image reference.` ) func imageFieldHasTenantImage(_ context.Context, _ *Validator, dk *dynakube.DynaKube) string { - tenantHost := dk.ApiUrlHost() - type imageField struct { value string section string @@ -31,14 +26,14 @@ func imageFieldHasTenantImage(_ context.Context, _ *Validator, dk *dynakube.Dyna }, { section: "OneAgent", - value: dk.CustomOneAgentImage(), + value: dk.OneAgent().GetCustomImage(), }, } messages := []string{} for _, field := range imageFields { - message := checkImageField(field.value, field.section, tenantHost) + message := checkImageField(field.value, field.section) if message != "" { messages = append(messages, message) } @@ -47,18 +42,12 @@ func imageFieldHasTenantImage(_ context.Context, _ *Validator, dk *dynakube.Dyna return strings.Join(messages, ";") } -func checkImageField(image, section, disallowedHost string) (errorMsg string) { +func checkImageField(image, section string) (errorMsg string) { if image != "" { - ref, err := name.ParseReference(image) + _, err := name.ParseReference(image) if err != nil { return fmt.Sprintf(errorUnparsableImageRef, section) } - - refUrl, _ := url.Parse(ref.Name()) - - if refUrl.Host == disallowedHost { - return fmt.Sprintf(errorUsingTenantImageAsCustom, section) - } } return "" diff --git a/pkg/api/validation/dynakube/image_test.go b/pkg/api/validation/dynakube/image_test.go index a4b78a1429..2f2ed1dd80 100644 --- a/pkg/api/validation/dynakube/image_test.go +++ b/pkg/api/validation/dynakube/image_test.go @@ -5,8 +5,10 @@ import ( "strings" "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" + "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -25,8 +27,8 @@ func TestImageFieldHasTenantImage(t *testing.T) { }, Spec: dynakube.DynaKubeSpec{ APIURL: testTenantUrl + "/api", - OneAgent: dynakube.OneAgentSpec{ - ClassicFullStack: &dynakube.HostInjectSpec{ + OneAgent: oneagent.Spec{ + ClassicFullStack: &oneagent.HostInjectSpec{ Image: "BOOM", }, }, @@ -38,33 +40,30 @@ func TestImageFieldHasTenantImage(t *testing.T) { }, }) }) - t.Run("image fields are using tenant repos", func(t *testing.T) { - expectedMessage := strings.Join([]string{ - fmt.Sprintf(errorUsingTenantImageAsCustom, "ActiveGate"), - fmt.Sprintf(errorUsingTenantImageAsCustom, "OneAgent"), - }, ";") - assertDenied(t, []string{expectedMessage}, &dynakube.DynaKube{ + t.Run("valid image fields", func(t *testing.T) { + testRegistryUrl := "my.images.com" + assertAllowed(t, &dynakube.DynaKube{ ObjectMeta: metav1.ObjectMeta{ Name: "dynakube", }, Spec: dynakube.DynaKubeSpec{ APIURL: testTenantUrl + "/api", - OneAgent: dynakube.OneAgentSpec{ - ClassicFullStack: &dynakube.HostInjectSpec{ - Image: testTenantUrl + "/linux/oneagent:latest", + OneAgent: oneagent.Spec{ + HostMonitoring: &oneagent.HostInjectSpec{ + Image: testRegistryUrl + "/linux/oneagent:latest", }, }, ActiveGate: activegate.Spec{ CapabilityProperties: activegate.CapabilityProperties{ - Image: testTenantUrl + "/linux/activegate:latest", + Image: testRegistryUrl + "/linux/activegate:latest", }, }, }, }) }) - t.Run("valid image fields", func(t *testing.T) { + t.Run("valid image fields - only OA", func(t *testing.T) { testRegistryUrl := "my.images.com" assertAllowed(t, &dynakube.DynaKube{ ObjectMeta: metav1.ObjectMeta{ @@ -72,17 +71,110 @@ func TestImageFieldHasTenantImage(t *testing.T) { }, Spec: dynakube.DynaKubeSpec{ APIURL: testTenantUrl + "/api", - OneAgent: dynakube.OneAgentSpec{ - ClassicFullStack: &dynakube.HostInjectSpec{ + OneAgent: oneagent.Spec{ + HostMonitoring: &oneagent.HostInjectSpec{ Image: testRegistryUrl + "/linux/oneagent:latest", }, }, - ActiveGate: activegate.Spec{ - CapabilityProperties: activegate.CapabilityProperties{ - Image: testRegistryUrl + "/linux/activegate:latest", + }, + }) + }) + + t.Run("ip:port", func(t *testing.T) { + assertAllowed(t, &dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Name: "dynakube", + }, + Spec: dynakube.DynaKubeSpec{ + APIURL: testTenantUrl + "/api", + OneAgent: oneagent.Spec{ + HostMonitoring: &oneagent.HostInjectSpec{ + Image: "127.0.0.1:5000/test:tag", }, }, }, }) }) } + +func TestCheckImageField(t *testing.T) { + type testCase struct { + title string + imageURI string + isError bool + } + + testCases := []testCase{ + { + title: "uri without tag", + imageURI: "my.images.com/test", + isError: false, + }, + { + title: "uri with tag", + imageURI: "my.images.com/test:tag", + isError: false, + }, + { + title: "uri with protocol", + imageURI: "https://my.images.com/test:tag", + isError: false, + }, + { + title: "uri with port no protocol", + imageURI: "my.images.com:5000/test:tag", + isError: false, + }, + { + title: "uri with protocol:ip", + imageURI: "https://127.0.0.1/test:tag", + isError: false, + }, + { + title: "uri with ip:port no protocol", + imageURI: "127.0.0.1:5000/test:tag", + isError: false, + }, + { + title: "uri with ipv6:port", + imageURI: "[1080:0:0:0:8:800:200C:417A]:8888/test", + isError: false, + }, + { + title: "uri with ipv6:port:tag", + imageURI: "[1080:0:0:0:8:800:200C:417A]:8888/test:tag", + isError: false, + }, + { + title: "uri with protocol:ipv6:port", + imageURI: "https://[1080:0:0:0:8:800:200C:417A]:8888/test", + isError: true, // the image parsing library will error + }, + { + title: "uri with protocol:ip:port", + imageURI: "https://127.0.0.1:5000/test:tag", + isError: true, // the image parsing library will error + }, + { + title: "uri with protocol port", + imageURI: "https://my.images.com:5000/test:tag", + isError: true, // the image parsing library will error + }, + { + title: "some random URI", + imageURI: "BOOM", + isError: true, + }, + } + + for _, test := range testCases { + t.Run(test.title, func(t *testing.T) { + errMsg := checkImageField(test.imageURI, "test") + if test.isError { + require.NotEmpty(t, errMsg) + } else { + require.Empty(t, errMsg) + } + }) + } +} diff --git a/pkg/api/validation/dynakube/istio.go b/pkg/api/validation/dynakube/istio.go index 67b50237d5..050c0a7a2c 100644 --- a/pkg/api/validation/dynakube/istio.go +++ b/pkg/api/validation/dynakube/istio.go @@ -3,7 +3,7 @@ package validation import ( "context" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/istio" ) diff --git a/pkg/api/validation/dynakube/istio_test.go b/pkg/api/validation/dynakube/istio_test.go index 26ad6c1593..43253dd274 100644 --- a/pkg/api/validation/dynakube/istio_test.go +++ b/pkg/api/validation/dynakube/istio_test.go @@ -3,7 +3,7 @@ package validation import ( "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" ) func TestNoResourcesAvailable(t *testing.T) { diff --git a/pkg/api/validation/dynakube/kspm.go b/pkg/api/validation/dynakube/kspm.go index bcfc6cd55a..cca7d5d782 100644 --- a/pkg/api/validation/dynakube/kspm.go +++ b/pkg/api/validation/dynakube/kspm.go @@ -3,18 +3,26 @@ package validation import ( "context" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" ) const ( - errorKSPMMissingKubemon = `The Dynakube's specification specifies KSPM, but "kubernetes-monitoring" is not enabled on the Activegate.` - errorKSPMMissingImage = `The Dynakube's specification specifies KSPM, but no image repository/tag is configured.` + errorTooManyAGReplicas = `The Dynakube's specification specifies KSPM, but has more than one ActiveGate replica. Only one ActiveGate replica is allowed in combination with KSPM.` + warningKSPMMissingKubemon = "The Dynakube is configured with KSPM without an ActiveGate with `kubernetes-monitoring` enabled or the `automatic-kubernetes-monitoring` feature flag. You need to ensure that Kubernetes monitoring is setup for this cluster." + errorKSPMMissingImage = `The Dynakube's specification specifies KSPM, but no image repository/tag is configured.` ) -func missingKSPMDependency(_ context.Context, _ *Validator, dk *dynakube.DynaKube) string { - if dk.KSPM().IsEnabled() && - !dk.ActiveGate().IsKubernetesMonitoringEnabled() { - return errorKSPMMissingKubemon +func tooManyAGReplicas(_ context.Context, _ *Validator, dk *dynakube.DynaKube) string { + if dk.KSPM().IsEnabled() && dk.ActiveGate().GetReplicas() > 1 { + return errorTooManyAGReplicas + } + + return "" +} + +func kspmWithoutK8SMonitoring(_ context.Context, _ *Validator, dk *dynakube.DynaKube) string { + if dk.KSPM().IsEnabled() && (!dk.ActiveGate().IsKubernetesMonitoringEnabled() || !dk.FeatureAutomaticKubernetesApiMonitoring()) { + return warningKSPMMissingKubemon } return "" diff --git a/pkg/api/validation/dynakube/kspm_test.go b/pkg/api/validation/dynakube/kspm_test.go index 164fcb817a..52c51e4ab1 100644 --- a/pkg/api/validation/dynakube/kspm_test.go +++ b/pkg/api/validation/dynakube/kspm_test.go @@ -4,11 +4,67 @@ import ( "testing" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/image" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/kspm" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/kspm" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +func TestTooManyAGReplicas(t *testing.T) { + t.Run("activegate with 1 (per default) replica and kspm enabled", func(t *testing.T) { + assertAllowed(t, + &dynakube.DynaKube{ + ObjectMeta: defaultDynakubeObjectMeta, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + Kspm: &kspm.Spec{}, + ActiveGate: activegate.Spec{ + Capabilities: []activegate.CapabilityDisplayName{ + activegate.KubeMonCapability.DisplayName, + }, + }, + Templates: dynakube.TemplatesSpec{ + KspmNodeConfigurationCollector: kspm.NodeConfigurationCollectorSpec{ + ImageRef: image.Ref{ + Repository: "repo/image", + Tag: "version", + }, + }, + }, + }, + }) + }) + + t.Run("activegate with more than 1 replica and kspm enabled", func(t *testing.T) { + activeGate := activegate.Spec{ + Capabilities: []activegate.CapabilityDisplayName{ + activegate.KubeMonCapability.DisplayName, + }, + } + replicas := int32(3) + + activeGate.Replicas = &replicas + assertDenied(t, + []string{errorTooManyAGReplicas}, + &dynakube.DynaKube{ + ObjectMeta: defaultDynakubeObjectMeta, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + Kspm: &kspm.Spec{}, + ActiveGate: activeGate, + Templates: dynakube.TemplatesSpec{ + KspmNodeConfigurationCollector: kspm.NodeConfigurationCollectorSpec{ + ImageRef: image.Ref{ + Repository: "repo/image", + Tag: "version", + }, + }, + }, + }, + }) + }) +} + func TestMissingKSPMDependency(t *testing.T) { t.Run("both kspm and kubemon enabled", func(t *testing.T) { assertAllowed(t, @@ -35,14 +91,77 @@ func TestMissingKSPMDependency(t *testing.T) { }) t.Run("missing kubemon but kspm enabled", func(t *testing.T) { - assertDenied(t, - []string{errorKSPMMissingKubemon}, + assertAllowedWithWarnings(t, 1, &dynakube.DynaKube{ ObjectMeta: defaultDynakubeObjectMeta, Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, Kspm: &kspm.Spec{}, ActiveGate: activegate.Spec{}, + Templates: dynakube.TemplatesSpec{ + KspmNodeConfigurationCollector: kspm.NodeConfigurationCollectorSpec{ + ImageRef: image.Ref{ + Repository: "repo/image", + Tag: "version", + }, + }, + }, + }, + }) + }) + + t.Run("both kspm and kubemon enabled, automatic k8s monitoring disabled", func(t *testing.T) { + assertAllowedWithWarnings(t, 2, + &dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Name: testName, + Namespace: testNamespace, + Annotations: map[string]string{ + dynakube.AnnotationFeatureAutomaticK8sApiMonitoring: "false", + }, + }, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + Kspm: &kspm.Spec{}, + ActiveGate: activegate.Spec{ + Capabilities: []activegate.CapabilityDisplayName{ + activegate.KubeMonCapability.DisplayName, + }, + }, + Templates: dynakube.TemplatesSpec{ + KspmNodeConfigurationCollector: kspm.NodeConfigurationCollectorSpec{ + ImageRef: image.Ref{ + Repository: "repo/image", + Tag: "version", + }, + }, + }, + }, + }) + }) + + t.Run("missing kubemon, automatic k8s monitoring disabled, but kspm enabled", func(t *testing.T) { + assertAllowedWithWarnings(t, 1, + &dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Name: testName, + Namespace: testNamespace, + Annotations: map[string]string{ + dynakube.AnnotationFeatureAutomaticK8sApiMonitoring: "false", + }, + }, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + Kspm: &kspm.Spec{}, + ActiveGate: activegate.Spec{}, + Templates: dynakube.TemplatesSpec{ + KspmNodeConfigurationCollector: kspm.NodeConfigurationCollectorSpec{ + ImageRef: image.Ref{ + Repository: "repo/image", + Tag: "version", + }, + }, + }, }, }) }) diff --git a/pkg/api/validation/dynakube/logmonitoring.go b/pkg/api/validation/dynakube/logmonitoring.go index 636c576f02..c3912af4b5 100644 --- a/pkg/api/validation/dynakube/logmonitoring.go +++ b/pkg/api/validation/dynakube/logmonitoring.go @@ -3,20 +3,29 @@ package validation import ( "context" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" ) const ( - warningLogMonitoringIgnoredTemplate = "The Dynakube's `spec.templates.logMonitoring` section is skipped as the `spec.oneagent` section is also configured." - errorLogMonitoringMissingImage = `The Dynakube's specification specifies standalone Log monitoring, but no image repository/tag is configured.` + warningLogMonitoringIgnoredTemplate = "The Dynakube's `spec.templates.logMonitoring` section is skipped as the `spec.oneagent` section is also configured." + warningLogMonitoringWithoutK8SMonitoring = "The Dynakube is configured for Log monitoring without an ActiveGate with `kubernetes-monitoring` enabled or the `automatic-kubernetes-monitoring` feature flag. You need to ensure that Kubernetes monitoring is setup for this cluster." + errorLogMonitoringMissingImage = `The Dynakube's specification specifies standalone Log monitoring, but no image repository/tag is configured.` ) +func logMonitoringWithoutK8SMonitoring(ctx context.Context, dv *Validator, dk *dynakube.DynaKube) string { + if dk.LogMonitoring().IsEnabled() && (!dk.ActiveGate().IsKubernetesMonitoringEnabled() || !dk.FeatureAutomaticKubernetesApiMonitoring()) { + return warningLogMonitoringWithoutK8SMonitoring + } + + return "" +} + func ignoredLogMonitoringTemplate(ctx context.Context, dv *Validator, dk *dynakube.DynaKube) string { if dk.LogMonitoring().IsStandalone() { return "" } - if dk.NeedsOneAgent() && dk.LogMonitoring().TemplateSpec != nil { + if dk.OneAgent().IsDaemonsetRequired() && dk.LogMonitoring().TemplateSpec != nil { return warningLogMonitoringIgnoredTemplate } diff --git a/pkg/api/validation/dynakube/logmonitoring_test.go b/pkg/api/validation/dynakube/logmonitoring_test.go index 5a906ed598..82ce2c6654 100644 --- a/pkg/api/validation/dynakube/logmonitoring_test.go +++ b/pkg/api/validation/dynakube/logmonitoring_test.go @@ -4,40 +4,138 @@ import ( "testing" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/image" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/logmonitoring" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/logmonitoring" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +func TestLogMonitoringWithoutK8SMonitoring(t *testing.T) { + t.Run("no error if logMonitoring is enabled with activegate with k8s-monitoring", func(t *testing.T) { + dk := &dynakube.DynaKube{ + Spec: dynakube.DynaKubeSpec{ + OneAgent: oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{}, + }, + APIURL: testApiUrl, + LogMonitoring: &logmonitoring.Spec{}, + ActiveGate: activegate.Spec{ + Capabilities: []activegate.CapabilityDisplayName{ + activegate.KubeMonCapability.DisplayName, + }, + }, + }, + } + assertAllowed(t, dk) + }) + t.Run("error if logMonitoring is enabled with automatic k8s monitoring feature flag but no activegate with k8s-monitoring", func(t *testing.T) { + dk := &dynakube.DynaKube{ + Spec: dynakube.DynaKubeSpec{ + OneAgent: oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{}, + }, + APIURL: testApiUrl, + LogMonitoring: &logmonitoring.Spec{}, + }, + } + assertAllowedWithWarnings(t, 1, dk) + }) + t.Run("error if logMonitoring is enabled with activegate with k8s-monitoring but automatic-kubernetes-monitoring disables", func(t *testing.T) { + dk := &dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + dynakube.AnnotationFeatureAutomaticK8sApiMonitoring: "false", + }, + }, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + LogMonitoring: &logmonitoring.Spec{}, + ActiveGate: activegate.Spec{ + Capabilities: []activegate.CapabilityDisplayName{ + activegate.KubeMonCapability.DisplayName, + }, + }, + Templates: dynakube.TemplatesSpec{ + LogMonitoring: &logmonitoring.TemplateSpec{ + ImageRef: image.Ref{ + Repository: "repo/image", + Tag: "version", + }, + }, + }, + }, + } + assertAllowedWithWarnings(t, 2, dk) + }) + t.Run("error if logMonitoring is enabled without activegate with k8s-monitoring and automatic-kubernetes-monitoring disabled", func(t *testing.T) { + assertAllowedWithWarnings(t, 1, &dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + dynakube.AnnotationFeatureAutomaticK8sApiMonitoring: "false", + }, + }, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + LogMonitoring: &logmonitoring.Spec{}, + Templates: dynakube.TemplatesSpec{ + LogMonitoring: &logmonitoring.TemplateSpec{ + ImageRef: image.Ref{ + Repository: "repo/image", + Tag: "version", + }, + }, + }, + }, + }) + }) +} + func TestIgnoredLogMonitoringTemplate(t *testing.T) { t.Run("no warning if logMonitoring template section is empty", func(t *testing.T) { - dk := createStandaloneLogMonitoringDynakube(testName, "") - dk.Spec.OneAgent.CloudNativeFullStack = &dynakube.CloudNativeFullStackSpec{} - assertAllowedWithoutWarnings(t, dk) + dk := createStandaloneLogMonitoringDynakube(testName, testApiUrl, "") + dk.Spec.OneAgent.CloudNativeFullStack = &oneagent.CloudNativeFullStackSpec{} + dk.Spec.Templates.LogMonitoring = nil + assertAllowedWithWarnings(t, 1, dk) }) t.Run("warning if logMonitoring template section is not empty", func(t *testing.T) { - dk := createStandaloneLogMonitoringDynakube(testName, "something") - dk.Spec.OneAgent.CloudNativeFullStack = &dynakube.CloudNativeFullStackSpec{} - assertAllowedWithWarnings(t, 1, dk) + dk := createStandaloneLogMonitoringDynakube(testName, testApiUrl, "something") + dk.Spec.OneAgent.CloudNativeFullStack = &oneagent.CloudNativeFullStackSpec{} + assertAllowedWithWarnings(t, 2, dk) }) } -func createStandaloneLogMonitoringDynakube(name, nodeSelector string) *dynakube.DynaKube { +func createStandaloneLogMonitoringDynakube(name, apiUrl, nodeSelector string) *dynakube.DynaKube { dk := &dynakube.DynaKube{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: testNamespace, }, Spec: dynakube.DynaKubeSpec{ - APIURL: testApiUrl, + APIURL: apiUrl, LogMonitoring: &logmonitoring.Spec{}, + ActiveGate: activegate.Spec{ + Capabilities: []activegate.CapabilityDisplayName{ + activegate.KubeMonCapability.DisplayName, + }, + }, + Templates: dynakube.TemplatesSpec{ + LogMonitoring: &logmonitoring.TemplateSpec{ + ImageRef: image.Ref{ + Repository: "repo/image", + Tag: "version", + }, + }, + }, }, } if nodeSelector != "" { - dk.Spec.Templates.LogMonitoring = &logmonitoring.TemplateSpec{ - NodeSelector: map[string]string{"node": nodeSelector}, + if dk.Spec.Templates.LogMonitoring == nil { + dk.Spec.Templates.LogMonitoring = &logmonitoring.TemplateSpec{} } + + dk.Spec.Templates.LogMonitoring.NodeSelector = map[string]string{"node": nodeSelector} } return dk @@ -51,6 +149,11 @@ func TestMissingLogMonitoringImage(t *testing.T) { Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, LogMonitoring: &logmonitoring.Spec{}, + ActiveGate: activegate.Spec{ + Capabilities: []activegate.CapabilityDisplayName{ + activegate.KubeMonCapability.DisplayName, + }, + }, Templates: dynakube.TemplatesSpec{ LogMonitoring: &logmonitoring.TemplateSpec{ ImageRef: image.Ref{ diff --git a/pkg/api/validation/dynakube/module_test.go b/pkg/api/validation/dynakube/module_test.go index 9b90e471dd..4cef36bc9c 100644 --- a/pkg/api/validation/dynakube/module_test.go +++ b/pkg/api/validation/dynakube/module_test.go @@ -4,10 +4,11 @@ import ( "context" "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/kspm" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/logmonitoring" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/kspm" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/logmonitoring" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" "github.com/Dynatrace/dynatrace-operator/pkg/util/installconfig" "github.com/stretchr/testify/assert" ) @@ -24,58 +25,23 @@ func TestIsModuleDisabled(t *testing.T) { } testCases := []testCase{ - { - title: "csi module disabled but also configured in dk => error", - dk: dynakube.DynaKube{Spec: dynakube.DynaKubeSpec{OneAgent: dynakube.OneAgentSpec{CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{}}}}, - modules: installconfig.Modules{OneAgent: true, CSIDriver: false}, - moduleFunc: isCSIModuleDisabled, - expectedMessage: errorCSIModuleRequired, - }, - { - title: "csi module disabled but not configured => no error", - dk: dynakube.DynaKube{Spec: dynakube.DynaKubeSpec{OneAgent: dynakube.OneAgentSpec{CloudNativeFullStack: nil}}}, - modules: installconfig.Modules{OneAgent: true, CSIDriver: false}, - moduleFunc: isCSIModuleDisabled, - expectedMessage: "", - }, - { - title: "csi module enabled and also configured => no error", - dk: dynakube.DynaKube{Spec: dynakube.DynaKubeSpec{OneAgent: dynakube.OneAgentSpec{CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{}}}}, - modules: installconfig.Modules{OneAgent: true, CSIDriver: true}, - moduleFunc: isCSIModuleDisabled, - expectedMessage: "", - }, - { - title: "csi module disabled and app-monitoring configured => no error, as it's optional for app-monitoring", - dk: dynakube.DynaKube{Spec: dynakube.DynaKubeSpec{OneAgent: dynakube.OneAgentSpec{ApplicationMonitoring: &dynakube.ApplicationMonitoringSpec{}}}}, - modules: installconfig.Modules{OneAgent: true, CSIDriver: false}, - moduleFunc: isCSIModuleDisabled, - expectedMessage: "", - }, - { - title: "csi module disabled and host-monitoring configured => no error, as it's optional for host-monitoring", - dk: dynakube.DynaKube{Spec: dynakube.DynaKubeSpec{OneAgent: dynakube.OneAgentSpec{HostMonitoring: &dynakube.HostInjectSpec{}}}}, - modules: installconfig.Modules{OneAgent: true, CSIDriver: true}, - moduleFunc: isCSIModuleDisabled, - expectedMessage: "", - }, { title: "oa module disabled but also configured in dk => error", - dk: dynakube.DynaKube{Spec: dynakube.DynaKubeSpec{OneAgent: dynakube.OneAgentSpec{CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{}}}}, + dk: dynakube.DynaKube{Spec: dynakube.DynaKubeSpec{OneAgent: oneagent.Spec{CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{}}}}, modules: installconfig.Modules{OneAgent: false, CSIDriver: true}, moduleFunc: isOneAgentModuleDisabled, expectedMessage: errorOneAgentModuleDisabled, }, { title: "oa module disabled but not configured => no error", - dk: dynakube.DynaKube{Spec: dynakube.DynaKubeSpec{OneAgent: dynakube.OneAgentSpec{CloudNativeFullStack: nil}}}, + dk: dynakube.DynaKube{Spec: dynakube.DynaKubeSpec{OneAgent: oneagent.Spec{CloudNativeFullStack: nil}}}, modules: installconfig.Modules{OneAgent: false, CSIDriver: true}, moduleFunc: isOneAgentModuleDisabled, expectedMessage: "", }, { title: "oa module enabled and also configured => no error", - dk: dynakube.DynaKube{Spec: dynakube.DynaKubeSpec{OneAgent: dynakube.OneAgentSpec{CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{}}}}, + dk: dynakube.DynaKube{Spec: dynakube.DynaKubeSpec{OneAgent: oneagent.Spec{CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{}}}}, modules: installconfig.Modules{OneAgent: true, CSIDriver: true}, moduleFunc: isOneAgentModuleDisabled, expectedMessage: "", @@ -173,32 +139,3 @@ func TestIsModuleDisabled(t *testing.T) { }) } } - -func TestIsCSIDriverRequired(t *testing.T) { - t.Run("DynaKube with cloud native", func(t *testing.T) { - dk := dynakube.DynaKube{Spec: dynakube.DynaKubeSpec{OneAgent: dynakube.OneAgentSpec{CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{}}}} - assert.True(t, isCSIRequired(&dk)) - }) - - t.Run("DynaKube with host monitoring", func(t *testing.T) { - dk := dynakube.DynaKube{ - Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - HostMonitoring: &dynakube.HostInjectSpec{}, - }, - }, - } - assert.False(t, isCSIRequired(&dk)) - }) - - t.Run("DynaKube with application monitoring", func(t *testing.T) { - dk := dynakube.DynaKube{ - Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - ApplicationMonitoring: &dynakube.ApplicationMonitoringSpec{}, - }, - }, - } - assert.False(t, isCSIRequired(&dk)) - }) -} diff --git a/pkg/api/validation/dynakube/modules.go b/pkg/api/validation/dynakube/modules.go index 3117319bc0..7a16562273 100644 --- a/pkg/api/validation/dynakube/modules.go +++ b/pkg/api/validation/dynakube/modules.go @@ -3,12 +3,11 @@ package validation import ( "context" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/util/installconfig" ) var ( - errorCSIModuleRequired = "CSI Driver was disabled during Operator install. It is a necessary resource for Cloud Native Fullstack to work. Redeploy the Operator via Helm with the CSI Driver enabled." errorOneAgentModuleDisabled = installconfig.GetModuleValidationErrorMessage("OneAgent") errorActiveGateModuleDisabled = installconfig.GetModuleValidationErrorMessage("ActiveGate") errorExtensionsModuleDisabled = installconfig.GetModuleValidationErrorMessage("Extensions") @@ -17,7 +16,7 @@ var ( ) func isOneAgentModuleDisabled(_ context.Context, v *Validator, dk *dynakube.DynaKube) string { - if dk.NeedsOneAgent() && !v.modules.OneAgent { + if dk.OneAgent().IsDaemonsetRequired() && !v.modules.OneAgent { return errorOneAgentModuleDisabled } @@ -25,7 +24,7 @@ func isOneAgentModuleDisabled(_ context.Context, v *Validator, dk *dynakube.Dyna } func isActiveGateModuleDisabled(_ context.Context, v *Validator, dk *dynakube.DynaKube) string { - if dk.ActiveGate().IsEnabled() && !v.modules.ActiveGate { + if dk.ActiveGate().IsEnabled() && !v.modules.ActiveGate && !v.modules.KSPM { return errorActiveGateModuleDisabled } @@ -55,16 +54,3 @@ func isKSPMDisabled(_ context.Context, v *Validator, dk *dynakube.DynaKube) stri return "" } - -func isCSIModuleDisabled(_ context.Context, v *Validator, dk *dynakube.DynaKube) string { - if isCSIRequired(dk) && !v.modules.CSIDriver { - return errorCSIModuleRequired - } - - return "" -} - -// isCSIRequired checks if the provided a DynaKube strictly needs the csi-driver, and no fallbacks exist to provide the same functionality. -func isCSIRequired(dk *dynakube.DynaKube) bool { - return dk.CloudNativeFullstackMode() -} diff --git a/pkg/api/validation/dynakube/namespace_selector.go b/pkg/api/validation/dynakube/namespace_selector.go index 782bc04a77..1be8ac2476 100644 --- a/pkg/api/validation/dynakube/namespace_selector.go +++ b/pkg/api/validation/dynakube/namespace_selector.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/injection/namespace/mapper" "k8s.io/apimachinery/pkg/apis/meta/v1/validation" "k8s.io/apimachinery/pkg/util/validation/field" @@ -18,7 +18,7 @@ Make sure the namespaceSelector doesn't conflict with other Dynakubes namespaceS ) func conflictingNamespaceSelector(ctx context.Context, dv *Validator, dk *dynakube.DynaKube) string { - if !dk.NeedAppInjection() && !dk.MetadataEnrichmentEnabled() { + if !dk.OneAgent().IsAppInjectionNeeded() && !dk.MetadataEnrichmentEnabled() { return "" } @@ -35,7 +35,7 @@ func conflictingNamespaceSelector(ctx context.Context, dv *Validator, dk *dynaku } func namespaceSelectorViolateLabelSpec(_ context.Context, _ *Validator, dk *dynakube.DynaKube) string { - errs := validation.ValidateLabelSelector(dk.OneAgentNamespaceSelector(), validation.LabelSelectorValidationOptions{AllowInvalidLabelValueInSelector: false}, field.NewPath("spec", "namespaceSelector")) + errs := validation.ValidateLabelSelector(dk.OneAgent().GetNamespaceSelector(), validation.LabelSelectorValidationOptions{AllowInvalidLabelValueInSelector: false}, field.NewPath("spec", "namespaceSelector")) if len(errs) == 0 { return "" } diff --git a/pkg/api/validation/dynakube/namespace_selector_test.go b/pkg/api/validation/dynakube/namespace_selector_test.go index 4dc546d438..1a858b69f1 100644 --- a/pkg/api/validation/dynakube/namespace_selector_test.go +++ b/pkg/api/validation/dynakube/namespace_selector_test.go @@ -3,7 +3,8 @@ package validation import ( "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -13,9 +14,9 @@ func TestConflictingNamespaceSelector(t *testing.T) { ObjectMeta: defaultDynakubeObjectMeta, Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{ - ApplicationMonitoring: &dynakube.ApplicationMonitoringSpec{ - AppInjectionSpec: dynakube.AppInjectionSpec{ + OneAgent: oneagent.Spec{ + ApplicationMonitoring: &oneagent.ApplicationMonitoringSpec{ + AppInjectionSpec: oneagent.AppInjectionSpec{ NamespaceSelector: metav1.LabelSelector{ MatchLabels: dummyLabels, }, @@ -28,9 +29,9 @@ func TestConflictingNamespaceSelector(t *testing.T) { ObjectMeta: defaultDynakubeObjectMeta, Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{ - ApplicationMonitoring: &dynakube.ApplicationMonitoringSpec{ - AppInjectionSpec: dynakube.AppInjectionSpec{ + OneAgent: oneagent.Spec{ + ApplicationMonitoring: &oneagent.ApplicationMonitoringSpec{ + AppInjectionSpec: oneagent.AppInjectionSpec{ NamespaceSelector: metav1.LabelSelector{ MatchLabels: dummyLabels, }, @@ -47,9 +48,9 @@ func TestConflictingNamespaceSelector(t *testing.T) { ObjectMeta: defaultDynakubeObjectMeta, Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{ - ApplicationMonitoring: &dynakube.ApplicationMonitoringSpec{ - AppInjectionSpec: dynakube.AppInjectionSpec{ + OneAgent: oneagent.Spec{ + ApplicationMonitoring: &oneagent.ApplicationMonitoringSpec{ + AppInjectionSpec: oneagent.AppInjectionSpec{ NamespaceSelector: metav1.LabelSelector{ MatchLabels: dummyLabels, }, @@ -65,9 +66,9 @@ func TestConflictingNamespaceSelector(t *testing.T) { }, Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{ - ApplicationMonitoring: &dynakube.ApplicationMonitoringSpec{ - AppInjectionSpec: dynakube.AppInjectionSpec{ + OneAgent: oneagent.Spec{ + ApplicationMonitoring: &oneagent.ApplicationMonitoringSpec{ + AppInjectionSpec: oneagent.AppInjectionSpec{ NamespaceSelector: metav1.LabelSelector{ MatchLabels: dummyLabels, }, @@ -98,9 +99,9 @@ func TestConflictingNamespaceSelector(t *testing.T) { }, Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{ - ApplicationMonitoring: &dynakube.ApplicationMonitoringSpec{ - AppInjectionSpec: dynakube.AppInjectionSpec{ + OneAgent: oneagent.Spec{ + ApplicationMonitoring: &oneagent.ApplicationMonitoringSpec{ + AppInjectionSpec: oneagent.AppInjectionSpec{ NamespaceSelector: metav1.LabelSelector{ MatchLabels: map[string]string{ "dummy": label, @@ -120,9 +121,9 @@ func TestConflictingNamespaceSelector(t *testing.T) { }, Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{ - ApplicationMonitoring: &dynakube.ApplicationMonitoringSpec{ - AppInjectionSpec: dynakube.AppInjectionSpec{ + OneAgent: oneagent.Spec{ + ApplicationMonitoring: &oneagent.ApplicationMonitoringSpec{ + AppInjectionSpec: oneagent.AppInjectionSpec{ NamespaceSelector: metav1.LabelSelector{ MatchExpressions: []metav1.LabelSelectorRequirement{ { @@ -154,9 +155,9 @@ func TestConflictingNamespaceSelector(t *testing.T) { }, Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{ - ApplicationMonitoring: &dynakube.ApplicationMonitoringSpec{ - AppInjectionSpec: dynakube.AppInjectionSpec{ + OneAgent: oneagent.Spec{ + ApplicationMonitoring: &oneagent.ApplicationMonitoringSpec{ + AppInjectionSpec: oneagent.AppInjectionSpec{ NamespaceSelector: metav1.LabelSelector{ MatchLabels: map[string]string{ "dummy": label, @@ -178,9 +179,9 @@ func TestConflictingNamespaceSelector(t *testing.T) { }, Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{ - ApplicationMonitoring: &dynakube.ApplicationMonitoringSpec{ - AppInjectionSpec: dynakube.AppInjectionSpec{ + OneAgent: oneagent.Spec{ + ApplicationMonitoring: &oneagent.ApplicationMonitoringSpec{ + AppInjectionSpec: oneagent.AppInjectionSpec{ NamespaceSelector: metav1.LabelSelector{ MatchExpressions: []metav1.LabelSelectorRequirement{ { diff --git a/pkg/api/validation/dynakube/oneagent.go b/pkg/api/validation/dynakube/oneagent.go index 7eb441404a..8641a92ae6 100644 --- a/pkg/api/validation/dynakube/oneagent.go +++ b/pkg/api/validation/dynakube/oneagent.go @@ -4,9 +4,10 @@ import ( "context" "fmt" "regexp" + "slices" "strings" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/util/dtversion" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/env" "k8s.io/apimachinery/pkg/labels" @@ -16,7 +17,9 @@ import ( const ( errorConflictingOneagentMode = `The DynaKube specification attempts to use multiple OneAgent modes simultaneously, which is not supported.` - errorImageFieldSetWithoutCSIFlag = `The DynaKube specification attempts to enable ApplicationMonitoring mode and retrieve the respective image, but the CSI driver is not enabled.` + errorImageFieldSetWithoutCSIFlag = `The DynaKube specification attempts to enable ApplicationMonitoring mode and retrieve the respective image, but the CSI driver and/or node image pull is not enabled.` + + errorImagePullRequiresCodeModulesImage = `The DynaKube specification enables node image pull, but the code modules image is not set.` errorNodeSelectorConflict = `The Dynakube specification conflicts with another Dynakube's OneAgent or Standalone-LogMonitoring. Only one Agent per node is supported. Use a nodeSelector to avoid this conflict. Conflicting DynaKubes: %s` @@ -30,23 +33,29 @@ Use a nodeSelector to avoid this conflict. Conflicting DynaKubes: %s` versionRegex = `^\d+.\d+.\d+.\d{8}-\d{6}$` versionInvalidMessage = "The OneAgent's version is only valid in the format 'major.minor.patch.timestamp', e.g. 1.0.0.20240101-000000" + + errorDuplicateOneAgentArgument = "%s has been provided multiple times. Only --set-host-property and --set-host-tag arguments may be provided multiple times." + + errorHostIdSourceArgumentInCloudNative = "Setting --set-host-id-source in CloudNativFullstack mode is not allowed." + + errorSameHostTagMultipleTimes = "Providing the same tag(s) (%s) multiple times with --set-host-tag is not allowed." ) func conflictingOneAgentConfiguration(_ context.Context, _ *Validator, dk *dynakube.DynaKube) string { counter := 0 - if dk.ApplicationMonitoringMode() { + if dk.OneAgent().IsApplicationMonitoringMode() { counter += 1 } - if dk.CloudNativeFullstackMode() { + if dk.OneAgent().IsCloudNativeFullstackMode() { counter += 1 } - if dk.ClassicFullStackMode() { + if dk.OneAgent().IsClassicFullStackMode() { counter += 1 } - if dk.HostMonitoringMode() { + if dk.OneAgent().IsHostMonitoringMode() { counter += 1 } @@ -60,7 +69,7 @@ func conflictingOneAgentConfiguration(_ context.Context, _ *Validator, dk *dynak } func conflictingOneAgentNodeSelector(ctx context.Context, dv *Validator, dk *dynakube.DynaKube) string { - if !dk.LogMonitoring().IsStandalone() && !dk.NeedsOneAgent() { + if !dk.OneAgent().IsDaemonsetRequired() && !dk.LogMonitoring().IsStandalone() { return "" } @@ -71,7 +80,7 @@ func conflictingOneAgentNodeSelector(ctx context.Context, dv *Validator, dk *dyn return "" } - oneAgentNodeSelector := dk.OneAgentNodeSelector() + oneAgentNodeSelector := dk.OneAgent().GetNodeSelector(dk.LogMonitoring().GetNodeSelector()) conflictingDynakubes := make(map[string]bool) for _, item := range validDynakubes.Items { @@ -79,21 +88,13 @@ func conflictingOneAgentNodeSelector(ctx context.Context, dv *Validator, dk *dyn continue } - if item.NeedsOneAgent() { - if hasConflictingMatchLabels(oneAgentNodeSelector, item.OneAgentNodeSelector()) { + if hasLogMonitoringSelectorConflict(dk, &item) || hasOneAgentSelectorConflict(dk, &item) { + if hasConflictingMatchLabels(oneAgentNodeSelector, item.OneAgent().GetNodeSelector(dk.LogMonitoring().GetNodeSelector())) { log.Info("requested dynakube has conflicting OneAgent nodeSelector", "name", dk.Name, "namespace", dk.Namespace) conflictingDynakubes[item.Name] = true } } - - if item.LogMonitoring().IsStandalone() { - if hasConflictingMatchLabels(oneAgentNodeSelector, item.OneAgentNodeSelector()) { - log.Info("requested dynakube has conflicting LogMonitoring nodeSelector", "name", dk.Name, "namespace", dk.Namespace) - - conflictingDynakubes[item.Name] = true - } - } } if len(conflictingDynakubes) > 0 { @@ -103,6 +104,20 @@ func conflictingOneAgentNodeSelector(ctx context.Context, dv *Validator, dk *dyn return "" } +func hasLogMonitoringSelectorConflict(dk1, dk2 *dynakube.DynaKube) bool { + return dk1.LogMonitoring().IsStandalone() && dk1.ApiUrl() == dk2.ApiUrl() && + (dk2.OneAgent().IsDaemonsetRequired() || dk2.LogMonitoring().IsStandalone()) && + hasConflictingMatchLabels(dk1.OneAgent().GetNodeSelector(dk1.LogMonitoring().GetNodeSelector()), + dk2.OneAgent().GetNodeSelector(dk2.LogMonitoring().GetNodeSelector())) +} + +func hasOneAgentSelectorConflict(dk1, dk2 *dynakube.DynaKube) bool { + return dk1.OneAgent().IsDaemonsetRequired() && + (dk2.OneAgent().IsDaemonsetRequired() || dk2.LogMonitoring().IsStandalone() && dk1.ApiUrl() == dk2.ApiUrl()) && + hasConflictingMatchLabels(dk1.OneAgent().GetNodeSelector(dk1.LogMonitoring().GetNodeSelector()), + dk2.OneAgent().GetNodeSelector(dk2.LogMonitoring().GetNodeSelector())) +} + func mapKeysToString(m map[string]bool, sep string) string { keys := make([]string, 0, len(m)) for k := range m { @@ -113,8 +128,8 @@ func mapKeysToString(m map[string]bool, sep string) string { } func imageFieldSetWithoutCSIFlag(_ context.Context, v *Validator, dk *dynakube.DynaKube) string { - if dk.ApplicationMonitoringMode() { - if len(dk.Spec.OneAgent.ApplicationMonitoring.CodeModulesImage) > 0 && !v.modules.CSIDriver { + if dk.OneAgent().IsApplicationMonitoringMode() { + if len(dk.Spec.OneAgent.ApplicationMonitoring.CodeModulesImage) > 0 && !v.modules.CSIDriver && !dk.FeatureNodeImagePull() { return errorImageFieldSetWithoutCSIFlag } } @@ -122,6 +137,16 @@ func imageFieldSetWithoutCSIFlag(_ context.Context, v *Validator, dk *dynakube.D return "" } +func missingCodeModulesImage(_ context.Context, v *Validator, dk *dynakube.DynaKube) string { + if dk.OneAgent().IsAppInjectionNeeded() && + dk.FeatureNodeImagePull() && + len(dk.OneAgent().GetCustomCodeModulesImage()) == 0 { + return errorImagePullRequiresCodeModulesImage + } + + return "" +} + func hasConflictingMatchLabels(labelMap, otherLabelMap map[string]string) bool { if labelMap == nil || otherLabelMap == nil { return true @@ -136,7 +161,7 @@ func hasConflictingMatchLabels(labelMap, otherLabelMap map[string]string) bool { } func hasOneAgentVolumeStorageEnabled(dk *dynakube.DynaKube) (isEnabled bool, isSet bool) { - envVar := env.FindEnvVar(dk.GetOneAgentEnvironment(), oneagentEnableVolumeStorageEnvVarName) + envVar := env.FindEnvVar(dk.OneAgent().GetEnvironment(), oneagentEnableVolumeStorageEnvVarName) isSet = envVar != nil isEnabled = isSet && envVar.Value == "true" @@ -144,8 +169,8 @@ func hasOneAgentVolumeStorageEnabled(dk *dynakube.DynaKube) (isEnabled bool, isS } func unsupportedOneAgentImage(_ context.Context, _ *Validator, dk *dynakube.DynaKube) string { - if env.FindEnvVar(dk.GetOneAgentEnvironment(), oneagentInstallerScriptUrlEnvVarName) != nil || - env.FindEnvVar(dk.GetOneAgentEnvironment(), oneagentInstallerTokenEnvVarName) != nil { + if env.FindEnvVar(dk.OneAgent().GetEnvironment(), oneagentInstallerScriptUrlEnvVarName) != nil || + env.FindEnvVar(dk.OneAgent().GetEnvironment(), oneagentInstallerTokenEnvVarName) != nil { return warningOneAgentInstallerEnvVars } @@ -154,7 +179,7 @@ func unsupportedOneAgentImage(_ context.Context, _ *Validator, dk *dynakube.Dyna func conflictingOneAgentVolumeStorageSettings(_ context.Context, _ *Validator, dk *dynakube.DynaKube) string { volumeStorageEnabled, volumeStorageSet := hasOneAgentVolumeStorageEnabled(dk) - if dk.UseReadOnlyOneAgents() && volumeStorageSet && !volumeStorageEnabled { + if dk.OneAgent().IsReadOnlyFSSupported() && volumeStorageSet && !volumeStorageEnabled { return errorVolumeStorageReadOnlyModeConflict } @@ -162,7 +187,7 @@ func conflictingOneAgentVolumeStorageSettings(_ context.Context, _ *Validator, d } func conflictingHostGroupSettings(_ context.Context, _ *Validator, dk *dynakube.DynaKube) string { - if dk.HostGroupAsParam() != "" { + if dk.OneAgent().GetHostGroupAsParam() != "" { return warningHostGroupConflict } @@ -170,7 +195,7 @@ func conflictingHostGroupSettings(_ context.Context, _ *Validator, dk *dynakube. } func isOneAgentVersionValid(_ context.Context, _ *Validator, dk *dynakube.DynaKube) string { - agentVersion := dk.CustomOneAgentVersion() + agentVersion := dk.OneAgent().GetCustomVersion() if agentVersion == "" { return "" } @@ -187,3 +212,53 @@ func isOneAgentVersionValid(_ context.Context, _ *Validator, dk *dynakube.DynaKu return "" } + +func duplicateOneAgentArguments(_ context.Context, _ *Validator, dk *dynakube.DynaKube) string { + args := dk.OneAgent().GetArgumentsMap() + if args == nil { + return "" + } + + for key, values := range args { + if key != "--set-host-property" && key != "--set-host-tag" && len(values) > 1 { + return fmt.Sprintf(errorDuplicateOneAgentArgument, key) + } else if key == "--set-host-tag" { + if duplicatedTags := findDuplicates(values); len(duplicatedTags) > 0 { + return fmt.Sprintf(errorSameHostTagMultipleTimes, duplicatedTags) + } + } + } + + return "" +} + +func forbiddenHostIdSourceArgument(_ context.Context, _ *Validator, dk *dynakube.DynaKube) string { + args := dk.OneAgent().GetArgumentsMap() + if args == nil { + return "" + } + + for key := range args { + if dk.OneAgent().IsCloudNativeFullstackMode() && key == "--set-host-id-source" { + return errorHostIdSourceArgumentInCloudNative + } + } + + return "" +} + +func findDuplicates[S ~[]E, E comparable](s S) []E { + seen := make(map[E]bool) + + duplicates := make([]E, 0) + + for _, val := range s { + if _, ok := seen[val]; !ok { + seen[val] = true + } else if !slices.Contains(duplicates, val) { + duplicates = append(duplicates, val) + } + } + + return duplicates +} diff --git a/pkg/api/validation/dynakube/oneagent_test.go b/pkg/api/validation/dynakube/oneagent_test.go index 3461b07137..cbcc8a7795 100644 --- a/pkg/api/validation/dynakube/oneagent_test.go +++ b/pkg/api/validation/dynakube/oneagent_test.go @@ -4,8 +4,8 @@ import ( "fmt" "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/logmonitoring" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" "github.com/Dynatrace/dynatrace-operator/pkg/util/installconfig" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -17,7 +17,7 @@ func TestConflictingOneAgentConfiguration(t *testing.T) { ObjectMeta: defaultDynakubeObjectMeta, Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{ + OneAgent: oneagent.Spec{ ClassicFullStack: nil, HostMonitoring: nil, }, @@ -28,8 +28,8 @@ func TestConflictingOneAgentConfiguration(t *testing.T) { ObjectMeta: defaultDynakubeObjectMeta, Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{ - ClassicFullStack: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + ClassicFullStack: &oneagent.HostInjectSpec{}, HostMonitoring: nil, }, }, @@ -39,9 +39,9 @@ func TestConflictingOneAgentConfiguration(t *testing.T) { ObjectMeta: defaultDynakubeObjectMeta, Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{ + OneAgent: oneagent.Spec{ ClassicFullStack: nil, - HostMonitoring: &dynakube.HostInjectSpec{}, + HostMonitoring: &oneagent.HostInjectSpec{}, }, }, }) @@ -53,9 +53,9 @@ func TestConflictingOneAgentConfiguration(t *testing.T) { ObjectMeta: defaultDynakubeObjectMeta, Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{ - ClassicFullStack: &dynakube.HostInjectSpec{}, - HostMonitoring: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + ClassicFullStack: &oneagent.HostInjectSpec{}, + HostMonitoring: &oneagent.HostInjectSpec{}, }, }, }) @@ -66,9 +66,9 @@ func TestConflictingOneAgentConfiguration(t *testing.T) { ObjectMeta: defaultDynakubeObjectMeta, Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{ - ApplicationMonitoring: &dynakube.ApplicationMonitoringSpec{}, - HostMonitoring: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + ApplicationMonitoring: &oneagent.ApplicationMonitoringSpec{}, + HostMonitoring: &oneagent.HostInjectSpec{}, }, }, }) @@ -76,18 +76,17 @@ func TestConflictingOneAgentConfiguration(t *testing.T) { } func TestConflictingNodeSelector(t *testing.T) { - newCloudNativeDynakube := func(name string, annotations map[string]string, nodeSelectorValue string) *dynakube.DynaKube { + newCloudNativeDynakube := func(name, apiUrl, nodeSelectorValue string) *dynakube.DynaKube { return &dynakube.DynaKube{ ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: testNamespace, - Annotations: annotations, + Name: name, + Namespace: testNamespace, }, Spec: dynakube.DynaKubeSpec{ - APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{ - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{ - HostInjectSpec: dynakube.HostInjectSpec{ + APIURL: apiUrl, + OneAgent: oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{ + HostInjectSpec: oneagent.HostInjectSpec{ NodeSelector: map[string]string{ "node": nodeSelectorValue, }, @@ -98,14 +97,14 @@ func TestConflictingNodeSelector(t *testing.T) { } } - t.Run("valid dynakube specs", func(t *testing.T) { + t.Run("valid dynakube specs - 2 host-monitoring DK, different nodes", func(t *testing.T) { assertAllowedWithoutWarnings(t, &dynakube.DynaKube{ ObjectMeta: defaultDynakubeObjectMeta, Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{ - HostMonitoring: &dynakube.HostInjectSpec{ + OneAgent: oneagent.Spec{ + HostMonitoring: &oneagent.HostInjectSpec{ NodeSelector: map[string]string{ "node": "1", }, @@ -120,8 +119,8 @@ func TestConflictingNodeSelector(t *testing.T) { }, Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{ - HostMonitoring: &dynakube.HostInjectSpec{ + OneAgent: oneagent.Spec{ + HostMonitoring: &oneagent.HostInjectSpec{ NodeSelector: map[string]string{ "node": "2", }, @@ -129,7 +128,8 @@ func TestConflictingNodeSelector(t *testing.T) { }, }, }) - + }) + t.Run("valid dynakube specs - 1 cloud-native + 1 host-monitoring DK, different nodes", func(t *testing.T) { assertAllowedWithoutWarnings(t, &dynakube.DynaKube{ ObjectMeta: metav1.ObjectMeta{ @@ -138,9 +138,9 @@ func TestConflictingNodeSelector(t *testing.T) { }, Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{ - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{ - HostInjectSpec: dynakube.HostInjectSpec{ + OneAgent: oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{ + HostInjectSpec: oneagent.HostInjectSpec{ NodeSelector: map[string]string{ "node": "1", }, @@ -153,8 +153,8 @@ func TestConflictingNodeSelector(t *testing.T) { ObjectMeta: defaultDynakubeObjectMeta, Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{ - HostMonitoring: &dynakube.HostInjectSpec{ + OneAgent: oneagent.Spec{ + HostMonitoring: &oneagent.HostInjectSpec{ NodeSelector: map[string]string{ "node": "2", }, @@ -162,31 +162,41 @@ func TestConflictingNodeSelector(t *testing.T) { }, }, }) + }) - assertAllowedWithoutWarnings(t, newCloudNativeDynakube("dk1", map[string]string{}, "1"), - &dynakube.DynaKube{ - ObjectMeta: defaultDynakubeObjectMeta, - Spec: dynakube.DynaKubeSpec{ - APIURL: testApiUrl, - LogMonitoring: &logmonitoring.Spec{}, - Templates: dynakube.TemplatesSpec{ - LogMonitoring: &logmonitoring.TemplateSpec{ - NodeSelector: map[string]string{"node": "12"}, - }, - }, - }, - }) + t.Run("valid dynakube specs - 1 cloud-native + 1 log-monitoring DK, same tenant, different nodes", func(t *testing.T) { + api1 := "https://f1.q.d.n/api" + + assertAllowedWithoutWarnings(t, newCloudNativeDynakube("dk1", api1, "1"), + createStandaloneLogMonitoringDynakube("dk-lm", api1, "12")) + }) + + t.Run("valid dynakube specs - 1 cloud-native + 1 log-monitoring DK, different tenant, same nodes", func(t *testing.T) { + api1 := "https://f1.q.d.n/api" + api2 := "https://f2.q.d.n/api" + assertAllowedWithoutWarnings(t, newCloudNativeDynakube("dk1", api1, "1"), + createStandaloneLogMonitoringDynakube("dk-lm", api2, "1")) }) - t.Run(`invalid dynakube specs`, func(t *testing.T) { + + t.Run("valid dynakube specs - 2 log-monitoring DK, different tenant, same nodes", func(t *testing.T) { + api1 := "https://f1.q.d.n/api" + api2 := "https://f2.q.d.n/api" + assertAllowedWithWarnings(t, 1, createStandaloneLogMonitoringDynakube("dk1", api1, "1"), + createStandaloneLogMonitoringDynakube("dk-lm", api2, "1")) + }) + + t.Run("invalid dynakube specs - 1 cloud-native + 1 host-monitoring DK, SAME nodes, different tenant", func(t *testing.T) { + api1 := "https://f1.q.d.n/api" + api2 := "https://f2.q.d.n/api" assertDenied(t, []string{fmt.Sprintf(errorNodeSelectorConflict, "conflicting-dk")}, &dynakube.DynaKube{ ObjectMeta: defaultDynakubeObjectMeta, Spec: dynakube.DynaKubeSpec{ - APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{ - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{ - HostInjectSpec: dynakube.HostInjectSpec{ + APIURL: api1, + OneAgent: oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{ + HostInjectSpec: oneagent.HostInjectSpec{ NodeSelector: map[string]string{ "node": "1", }, @@ -201,9 +211,9 @@ func TestConflictingNodeSelector(t *testing.T) { Namespace: testNamespace, }, Spec: dynakube.DynaKubeSpec{ - APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{ - HostMonitoring: &dynakube.HostInjectSpec{ + APIURL: api2, + OneAgent: oneagent.Spec{ + HostMonitoring: &oneagent.HostInjectSpec{ NodeSelector: map[string]string{ "node": "1", }, @@ -211,26 +221,39 @@ func TestConflictingNodeSelector(t *testing.T) { }, }, }) - }) - t.Run(`invalid dynakube specs with existing log module`, func(t *testing.T) { - assertDenied(t, []string{fmt.Sprintf(errorNodeSelectorConflict, "dk-lm")}, - newCloudNativeDynakube("dk-cm", map[string]string{}, "1"), - createStandaloneLogMonitoringDynakube("dk-lm", "1")) - - assertDenied(t, []string{fmt.Sprintf(errorNodeSelectorConflict, ""), "dk-lm", "dk-cm2"}, - newCloudNativeDynakube("dk-cm1", map[string]string{}, "1"), - createStandaloneLogMonitoringDynakube("dk-lm", ""), - newCloudNativeDynakube("dk-cm2", map[string]string{}, "1")) - - assertDenied(t, []string{fmt.Sprintf(errorNodeSelectorConflict, "dk-lm")}, - newCloudNativeDynakube("dk-cn", map[string]string{}, "1"), - createStandaloneLogMonitoringDynakube("dk-lm", "1")) - assertDenied(t, []string{fmt.Sprintf(errorNodeSelectorConflict, "dk-cn")}, - createStandaloneLogMonitoringDynakube("dk-lm", "1"), - newCloudNativeDynakube("dk-cn", map[string]string{}, "1")) - assertDenied(t, []string{fmt.Sprintf(errorNodeSelectorConflict, "dk-lm2")}, - createStandaloneLogMonitoringDynakube("dk-lm1", "1"), - createStandaloneLogMonitoringDynakube("dk-lm2", "1")) + t.Run("invalid dynakube specs - 1 cloud-native + 1 log-monitoring DK, same tenant, same nodes", func(t *testing.T) { + api1 := "https://f1.q.d.n/api" + + assertDenied(t, []string{fmt.Sprintf(errorNodeSelectorConflict, "dk-lm")}, + newCloudNativeDynakube("dk-cm", api1, "1"), + createStandaloneLogMonitoringDynakube("dk-lm", api1, "1")) + }) + t.Run("multiple invalid dynakube specs - 2 cloud-native + 1 log-monitoring DK, same tenant, same nodes", func(t *testing.T) { + api1 := "https://f1.q.d.n/api" + + assertDenied(t, []string{fmt.Sprintf(errorNodeSelectorConflict, ""), "dk-lm", "dk-cm2"}, + newCloudNativeDynakube("dk-cm1", api1, "1"), + createStandaloneLogMonitoringDynakube("dk-lm", api1, ""), + newCloudNativeDynakube("dk-cm2", api1, "1")) + }) + + t.Run("invalid dynakube specs - 1 log-monitoring DK + 1 cloud-native, same tenant, same nodes", func(t *testing.T) { + api1 := "https://f1.q.d.n/api" + + assertDenied(t, []string{fmt.Sprintf(errorNodeSelectorConflict, "dk-cn")}, + createStandaloneLogMonitoringDynakube("dk-lm", api1, "1"), + newCloudNativeDynakube("dk-cn", api1, "1")) + }) + + t.Run("some invalid dynakube specs - 2 log-monitoring DK + 1 cloud-native, 2 tenants, same nodes", func(t *testing.T) { + api1 := "https://f1.q.d.n/api" + api2 := "https://f2.q.d.n/api" + + assertDenied(t, []string{fmt.Sprintf(errorNodeSelectorConflict, "dk-lm2")}, + createStandaloneLogMonitoringDynakube("dk-lm1", api1, "1"), + newCloudNativeDynakube("dk-cm1", api2, "1"), + createStandaloneLogMonitoringDynakube("dk-lm2", api1, "1")) + }) }) } @@ -254,9 +277,9 @@ func TestImageFieldSetWithoutCSIFlag(t *testing.T) { ObjectMeta: defaultDynakubeObjectMeta, Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{ - ApplicationMonitoring: &dynakube.ApplicationMonitoringSpec{ - AppInjectionSpec: dynakube.AppInjectionSpec{ + OneAgent: oneagent.Spec{ + ApplicationMonitoring: &oneagent.ApplicationMonitoringSpec{ + AppInjectionSpec: oneagent.AppInjectionSpec{ CodeModulesImage: testImage, }, }, @@ -265,7 +288,7 @@ func TestImageFieldSetWithoutCSIFlag(t *testing.T) { }) }) - t.Run("spec with appMon enabled, useCSIDriver not enabled but image set", func(t *testing.T) { + t.Run("spec with appMon enabled, csi driver not enabled but image set", func(t *testing.T) { setupDisabledCSIEnv(t) testImage := "testImage" @@ -273,9 +296,59 @@ func TestImageFieldSetWithoutCSIFlag(t *testing.T) { ObjectMeta: defaultDynakubeObjectMeta, Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{ - ApplicationMonitoring: &dynakube.ApplicationMonitoringSpec{ - AppInjectionSpec: dynakube.AppInjectionSpec{ + OneAgent: oneagent.Spec{ + ApplicationMonitoring: &oneagent.ApplicationMonitoringSpec{ + AppInjectionSpec: oneagent.AppInjectionSpec{ + CodeModulesImage: testImage, + }, + }, + }, + }, + }) + }) + + t.Run("spec with appMon enabled, csi driver not enabled but node image pull enabled and image set", func(t *testing.T) { + setupDisabledCSIEnv(t) + + testImage := "testImage" + assertAllowed(t, &dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Name: testName, + Namespace: testNamespace, + Annotations: map[string]string{ + dynakube.AnnotationFeatureNodeImagePull: "true", + }, + }, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + OneAgent: oneagent.Spec{ + ApplicationMonitoring: &oneagent.ApplicationMonitoringSpec{ + AppInjectionSpec: oneagent.AppInjectionSpec{ + CodeModulesImage: testImage, + }, + }, + }, + }, + }) + }) + + t.Run("spec with appMon enabled, csi driver and node image pull not enabled and image set", func(t *testing.T) { + setupDisabledCSIEnv(t) + + testImage := "testImage" + assertDenied(t, []string{errorImageFieldSetWithoutCSIFlag}, &dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Name: testName, + Namespace: testNamespace, + Annotations: map[string]string{ + dynakube.AnnotationFeatureNodeImagePull: "false", + }, + }, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + OneAgent: oneagent.Spec{ + ApplicationMonitoring: &oneagent.ApplicationMonitoringSpec{ + AppInjectionSpec: oneagent.AppInjectionSpec{ CodeModulesImage: testImage, }, }, @@ -283,6 +356,46 @@ func TestImageFieldSetWithoutCSIFlag(t *testing.T) { }, }) }) + + t.Run("spec with cloudnative enabled, csi driver and node image pull enabled and image not set", func(t *testing.T) { + assertDenied(t, []string{errorImagePullRequiresCodeModulesImage}, &dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Name: testName, + Namespace: testNamespace, + Annotations: map[string]string{ + dynakube.AnnotationFeatureNodeImagePull: "true", + }, + }, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + OneAgent: oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{ + AppInjectionSpec: oneagent.AppInjectionSpec{}, + }, + }, + }, + }) + }) + + t.Run("spec with appmon enabled, csi driver and node image pull enabled and image not set", func(t *testing.T) { + assertDenied(t, []string{errorImagePullRequiresCodeModulesImage}, &dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Name: testName, + Namespace: testNamespace, + Annotations: map[string]string{ + dynakube.AnnotationFeatureNodeImagePull: "true", + }, + }, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + OneAgent: oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{ + AppInjectionSpec: oneagent.AppInjectionSpec{}, + }, + }, + }, + }) + }) } func createDynakube(oaEnvVar ...string) *dynakube.DynaKube { @@ -301,9 +414,9 @@ func createDynakube(oaEnvVar ...string) *dynakube.DynaKube { }, Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{ - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{ - HostInjectSpec: dynakube.HostInjectSpec{ + OneAgent: oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{ + HostInjectSpec: oneagent.HostInjectSpec{ Env: envVars, }, }, @@ -378,8 +491,8 @@ func TestOneAgentHostGroup(t *testing.T) { ObjectMeta: defaultDynakubeObjectMeta, Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{ - ClassicFullStack: &dynakube.HostInjectSpec{ + OneAgent: oneagent.Spec{ + ClassicFullStack: &oneagent.HostInjectSpec{ Args: []string{"--set-host-group=arg"}, }, HostGroup: "", @@ -393,8 +506,8 @@ func TestOneAgentHostGroup(t *testing.T) { ObjectMeta: defaultDynakubeObjectMeta, Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{ - HostMonitoring: &dynakube.HostInjectSpec{ + OneAgent: oneagent.Spec{ + HostMonitoring: &oneagent.HostInjectSpec{ Args: []string{"--set-host-group=arg"}, }, HostGroup: "", @@ -409,9 +522,9 @@ func createDynakubeWithHostGroup(args []string, hostGroup string) *dynakube.Dyna ObjectMeta: defaultDynakubeObjectMeta, Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{ - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{ - HostInjectSpec: dynakube.HostInjectSpec{ + OneAgent: oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{ + HostInjectSpec: oneagent.HostInjectSpec{ Args: args, }, }, @@ -426,8 +539,8 @@ func TestIsOneAgentVersionValid(t *testing.T) { ObjectMeta: defaultDynakubeObjectMeta, Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{ - ClassicFullStack: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + ClassicFullStack: &oneagent.HostInjectSpec{}, }, }, } @@ -455,16 +568,290 @@ func TestIsOneAgentVersionValid(t *testing.T) { } for _, validVersion := range validVersions { - dk.Spec.OneAgent.ClassicFullStack.Version = validVersion + dk.OneAgent().ClassicFullStack.Version = validVersion t.Run(fmt.Sprintf("OneAgent custom version %s is allowed", validVersion), func(t *testing.T) { assertAllowed(t, &dk) }) } for _, invalidVersion := range invalidVersions { - dk.Spec.OneAgent.ClassicFullStack.Version = invalidVersion + dk.OneAgent().ClassicFullStack.Version = invalidVersion t.Run(fmt.Sprintf("OneAgent custom version %s is not allowed", invalidVersion), func(t *testing.T) { assertDenied(t, []string{versionInvalidMessage}, &dk) }) } } + +func TestPublicImageSetWithReadOnlyMode(t *testing.T) { + t.Run("reject dk with hostMon without csi and custom image", func(t *testing.T) { + setupDisabledCSIEnv(t) + assertAllowedWithoutWarnings(t, + &dynakube.DynaKube{ + ObjectMeta: defaultDynakubeObjectMeta, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + OneAgent: oneagent.Spec{ + HostMonitoring: &oneagent.HostInjectSpec{ + Image: "test/image/test-image:some-tag", + }, + }, + }, + }) + }) + t.Run("allow dk with hostMon without csi and no custom image", func(t *testing.T) { + setupDisabledCSIEnv(t) + assertAllowedWithoutWarnings(t, + &dynakube.DynaKube{ + ObjectMeta: defaultDynakubeObjectMeta, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + OneAgent: oneagent.Spec{ + HostMonitoring: &oneagent.HostInjectSpec{}, + }, + }, + }) + }) + t.Run("allow dk with hostMon with csi and custom image", func(t *testing.T) { + assertAllowedWithoutWarnings(t, &dynakube.DynaKube{ + ObjectMeta: defaultDynakubeObjectMeta, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + OneAgent: oneagent.Spec{ + HostMonitoring: &oneagent.HostInjectSpec{ + Image: "test/image/test-image:some-tag", + }, + }, + }, + }) + }) + t.Run("allow dk with classicFullStack without csi and custom image", func(t *testing.T) { + setupDisabledCSIEnv(t) + assertAllowedWithoutWarnings(t, &dynakube.DynaKube{ + ObjectMeta: defaultDynakubeObjectMeta, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + OneAgent: oneagent.Spec{ + ClassicFullStack: &oneagent.HostInjectSpec{ + Image: "test/image/test-image:some-tag", + }, + }, + }, + }) + }) + t.Run("allow dk with classicFullStack with csi and custom image", func(t *testing.T) { + assertAllowedWithoutWarnings(t, &dynakube.DynaKube{ + ObjectMeta: defaultDynakubeObjectMeta, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + OneAgent: oneagent.Spec{ + ClassicFullStack: &oneagent.HostInjectSpec{ + Image: "test/image/test-image:some-tag", + }, + }, + }, + }) + }) +} + +func TestOneAgentArguments(t *testing.T) { + type oneAgentArgumentTest struct { + testName string + arguments []string + expectedError string + } + + testcases := []oneAgentArgumentTest{ + { + testName: "duplicate arguments are rejected", + arguments: []string{ + "--set-server=foo", + "--set-server=bar", + }, + expectedError: fmt.Sprintf(errorDuplicateOneAgentArgument, "--set-server"), + }, + { + testName: "duplicate arguments with same value are rejected", + arguments: []string{ + "--set-server=foo", + "--set-server=foo", + }, + expectedError: fmt.Sprintf(errorDuplicateOneAgentArgument, "--set-server"), + }, + { + testName: "no duplicate arguments", + arguments: []string{ + "--set-server=foo", + "--set-host-source-id=bar", + }, + expectedError: "", + }, + { + testName: "duplicate host property", + arguments: []string{ + "--set-server=foo", + "--set-host-property=foo1=bar1", + "--set-host-property=foo2=bar2", + "--set-host-property=foo3=bar3", + "--set-host-property=foo3=bar3", + "--set-host-property=foo2=bar2", + "--set-host-property=foo1=bar1", + }, + expectedError: "", + }, + { + testName: "duplicate host tag", + arguments: []string{ + "--set-server=foo", + "--set-host-tag=foo=1", + "--set-host-tag=bar=1", + "--set-host-tag=dow=1", + }, + expectedError: "", + }, + { + testName: "duplicate host tag with same value", + arguments: []string{ + "--set-host-tag=foo=1", + "--set-host-tag=bar", + "--set-host-tag=foo=1", + "--set-host-tag=bar", + "--set-host-tag=doh", + "--set-host-tag=bar", + "--set-host-tag=foo=1", + }, + expectedError: fmt.Sprintf(errorSameHostTagMultipleTimes, "[foo=1 bar]"), + }, + { + testName: "arguments without value", + arguments: []string{ + "--enable-feature-a", + "--enable-feature-b", + "--enable-feature-c", + }, + expectedError: "", + }, + { + testName: "duplicate arguments without value", + arguments: []string{ + "--enable-feature-a", + "--enable-feature-b", + "--enable-feature-a", + "--enable-feature-c", + }, + expectedError: fmt.Sprintf(errorDuplicateOneAgentArgument, "--enable-feature-a"), + }, + } + + for _, tc := range testcases { + t.Run(tc.testName, func(t *testing.T) { + dk := &dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Name: "dynakube", + Namespace: testNamespace, + }, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + OneAgent: oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{ + HostInjectSpec: oneagent.HostInjectSpec{ + Args: tc.arguments, + }, + }, + }, + }, + } + if tc.expectedError == "" { + assertAllowedWithoutWarnings(t, dk) + } else { + assertDenied(t, []string{tc.expectedError}, dk) + } + }) + } +} + +func TestNoHostIdSourceArgument(t *testing.T) { + type oneAgentArgumentTest struct { + testName string + dk dynakube.DynaKube + expectedError string + } + + testcases := []oneAgentArgumentTest{ + { + testName: "host id source argument in cloud native full stack", + dk: dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Name: "dynakube", + Namespace: testNamespace, + }, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + OneAgent: oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{ + HostInjectSpec: oneagent.HostInjectSpec{ + Args: []string{ + "--set-server=foo", + "--set-host-id-source=foo", + }, + }, + }, + }, + }, + }, + expectedError: errorHostIdSourceArgumentInCloudNative, + }, + { + testName: "no host id source argument in cloud native full stack", + dk: dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Name: "dynakube", + Namespace: testNamespace, + }, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + OneAgent: oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{ + HostInjectSpec: oneagent.HostInjectSpec{ + Args: []string{ + "--set-server=foo", + }, + }, + }, + }, + }, + }, + expectedError: "", + }, + { + testName: "host id source argument in host monitoring stack", + dk: dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Name: "dynakube", + Namespace: testNamespace, + }, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + OneAgent: oneagent.Spec{ + HostMonitoring: &oneagent.HostInjectSpec{ + Args: []string{ + "--set-server=foo", + "--set-host-id-source=foo", + }, + }, + }, + }, + }, + expectedError: "", + }, + } + + for _, tc := range testcases { + t.Run(tc.testName, func(t *testing.T) { + if tc.expectedError == "" { + assertAllowedWithoutWarnings(t, &tc.dk) + } else { + assertDenied(t, []string{tc.expectedError}, &tc.dk) + } + }) + } +} diff --git a/pkg/api/validation/dynakube/preview_test.go b/pkg/api/validation/dynakube/preview_test.go index 8a36372634..89165a5551 100644 --- a/pkg/api/validation/dynakube/preview_test.go +++ b/pkg/api/validation/dynakube/preview_test.go @@ -3,7 +3,8 @@ package validation import ( "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" ) func TestPreviewWarning(t *testing.T) { @@ -12,8 +13,8 @@ func TestPreviewWarning(t *testing.T) { ObjectMeta: defaultDynakubeObjectMeta, Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{ - ApplicationMonitoring: &dynakube.ApplicationMonitoringSpec{}, + OneAgent: oneagent.Spec{ + ApplicationMonitoring: &oneagent.ApplicationMonitoringSpec{}, }, }, }) diff --git a/pkg/api/validation/dynakube/proxy_url.go b/pkg/api/validation/dynakube/proxy_url.go index db5ec10e9c..81d0f29967 100644 --- a/pkg/api/validation/dynakube/proxy_url.go +++ b/pkg/api/validation/dynakube/proxy_url.go @@ -5,7 +5,7 @@ import ( "net/url" "regexp" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/pkg/errors" ) diff --git a/pkg/api/validation/dynakube/proxy_url_test.go b/pkg/api/validation/dynakube/proxy_url_test.go index 7062fe4ffc..7fbc0b9006 100644 --- a/pkg/api/validation/dynakube/proxy_url_test.go +++ b/pkg/api/validation/dynakube/proxy_url_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/value" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/pkg/api/validation/dynakube/telemetryservice.go b/pkg/api/validation/dynakube/telemetryservice.go new file mode 100644 index 0000000000..03656a69aa --- /dev/null +++ b/pkg/api/validation/dynakube/telemetryservice.go @@ -0,0 +1,173 @@ +package validation + +import ( + "context" + "fmt" + "slices" + "strings" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/telemetryingest" + "github.com/Dynatrace/dynatrace-operator/pkg/consts" + agconsts "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/consts" + "github.com/Dynatrace/dynatrace-operator/pkg/otelcgen" + "k8s.io/apimachinery/pkg/util/validation" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + errorTelemetryIngestNotEnoughProtocols = `DynaKube's specification enables the TelemetryIngest feature, at least one Protocol has to be specified.` + errorTelemetryIngestUnknownProtocols = `DynaKube's specification enables the TelemetryIngest feature, unsupported protocols found on the Protocols list.` + errorTelemetryIngestDuplicatedProtocols = `DynaKube's specification enables the TelemetryIngest feature, duplicated protocols found on the Protocols list.` + errorTelemetryIngestNoDNS1053Label = `DynaKube's specification enables the TelemetryIngest feature, the telemetry service name violates DNS-1035. + [The length limit for the name is %d. Additionally a DNS-1035 name must consist of lower case alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character (e.g. 'my-name', or 'abc-123', regex used for validation is '[a-z]([-a-z0-9]*[a-z0-9])?')] + ` + errorTelemetryIngestServiceNameInUse = `The DynaKube's specification enables the TelemetryIngest feature, the telemetry service name is already used by other Dynakube.` + errorTelemetryIngestForbiddenServiceName = `The DynaKube's specification enables the TelemetryIngest feature, the telemetry service name is incorrect because of forbidden suffix.` +) + +func emptyTelemetryIngestProtocolsList(_ context.Context, _ *Validator, dk *dynakube.DynaKube) string { + if !dk.TelemetryIngest().IsEnabled() { + return "" + } + + if len(dk.TelemetryIngest().GetProtocols()) == 0 { + log.Info("requested dynakube specify empty list of Protocols") + + return errorTelemetryIngestNotEnoughProtocols + } + + return "" +} + +func unknownTelemetryIngestProtocols(_ context.Context, _ *Validator, dk *dynakube.DynaKube) string { + if !dk.TelemetryIngest().IsEnabled() { + return "" + } + + var unknownProtocols []string + + for _, protocol := range dk.TelemetryIngest().GetProtocols() { + if !slices.Contains(otelcgen.RegisteredProtocols, protocol) { + unknownProtocols = append(unknownProtocols, string(protocol)) + } + } + + if len(unknownProtocols) > 0 { + log.Info("requested dynakube specify unknown TelemetryIngest protocol(s)", "protocols", strings.Join(unknownProtocols, ",")) + + return errorTelemetryIngestUnknownProtocols + } + + return "" +} + +func duplicatedTelemetryIngestProtocols(_ context.Context, _ *Validator, dk *dynakube.DynaKube) string { + if !dk.TelemetryIngest().IsEnabled() { + return "" + } + + protocolsOccurrences := map[otelcgen.Protocol]int{} + + for _, protocol := range dk.TelemetryIngest().GetProtocols() { + if _, ok := protocolsOccurrences[protocol]; !ok { + protocolsOccurrences[protocol] = 1 + } else { + protocolsOccurrences[protocol] += 1 + } + } + + var duplicatedProtocols []string + + for protocol, count := range protocolsOccurrences { + if count > 1 { + duplicatedProtocols = append(duplicatedProtocols, string(protocol)) + } + } + + if len(duplicatedProtocols) > 0 { + log.Info("requested dynakube specify duplicated TelemetryIngest protocol(s)", "protocols", strings.Join(duplicatedProtocols, ",")) + + return errorTelemetryIngestDuplicatedProtocols + } + + return "" +} + +func invalidTelemetryIngestName(_ context.Context, _ *Validator, dk *dynakube.DynaKube) string { + if !dk.TelemetryIngest().IsEnabled() { + return "" + } + + var errs []string + + if dk.TelemetryIngest().ServiceName != "" { + errs = validation.IsDNS1035Label(dk.Spec.TelemetryIngest.ServiceName) + } + + if len(errs) == 0 { + return "" + } + + return invalidTelemetryIngestNameErrorMessage() +} + +func invalidTelemetryIngestNameErrorMessage() string { + return fmt.Sprintf(errorTelemetryIngestNoDNS1053Label, validation.DNS1035LabelMaxLength) +} + +func conflictingTelemetryIngestServiceNames(ctx context.Context, dv *Validator, dk *dynakube.DynaKube) string { + if !dk.TelemetryIngest().IsEnabled() { + return "" + } + + dkList := &dynakube.DynaKubeList{} + if err := dv.apiReader.List(ctx, dkList, &client.ListOptions{Namespace: dk.Namespace}); err != nil { + log.Info("error occurred while listing dynakubes", "err", err.Error()) + + return "" + } + + dkServiceName := dk.TelemetryIngest().GetServiceName() + + for _, otherDk := range dkList.Items { + if otherDk.Name == dk.Name { + continue + } + + if !otherDk.TelemetryIngest().IsEnabled() { + continue + } + + otherDkServiceName := otherDk.TelemetryIngest().GetServiceName() + + if otherDkServiceName == dkServiceName { + log.Info(errorTelemetryIngestServiceNameInUse, "other dynakube name", otherDk.Name, "other telemetry service name", otherDkServiceName, "namespace", otherDk.Namespace) + + return fmt.Sprintf("%s Conflicting Dynakube: %s. Conflicting telemetry service name: %s", errorTelemetryIngestServiceNameInUse, otherDk.Name, otherDkServiceName) + } + } + + return "" +} + +func forbiddenTelemetryIngestServiceNameSuffix(_ context.Context, _ *Validator, dk *dynakube.DynaKube) string { + if !dk.TelemetryIngest().IsEnabled() { + return "" + } + + if dk.TelemetryIngest().ServiceName == "" { + return "" + } + + if strings.HasSuffix(dk.TelemetryIngest().ServiceName, consts.ExtensionsControllerSuffix) || + strings.HasSuffix(dk.TelemetryIngest().ServiceName, telemetryingest.ServiceNameSuffix) || + strings.HasSuffix(dk.TelemetryIngest().ServiceName, "-"+agconsts.MultiActiveGateName) || + strings.HasSuffix(dk.TelemetryIngest().ServiceName, "-webhook") { + log.Info(errorTelemetryIngestForbiddenServiceName, "telemetry service name", dk.TelemetryIngest().ServiceName) + + return fmt.Sprintf("%s Telemetry service name: %s", errorTelemetryIngestForbiddenServiceName, dk.TelemetryIngest().ServiceName) + } + + return "" +} diff --git a/pkg/api/validation/dynakube/telemetryservice_test.go b/pkg/api/validation/dynakube/telemetryservice_test.go new file mode 100644 index 0000000000..db4a04009f --- /dev/null +++ b/pkg/api/validation/dynakube/telemetryservice_test.go @@ -0,0 +1,315 @@ +package validation + +import ( + "testing" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/telemetryingest" + "github.com/Dynatrace/dynatrace-operator/pkg/consts" + agconsts "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/consts" + "github.com/Dynatrace/dynatrace-operator/pkg/otelcgen" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + testOtherName = "test-other-name" + testServiceName = "test-service-name" + testOtherServiceName = "test-other-service-name" +) + +var otherDynakubeObjectMeta = metav1.ObjectMeta{ + Name: testOtherName, + Namespace: testNamespace, +} + +func TestTelemetryIngestProtocols(t *testing.T) { + t.Run(`no list of protocols`, func(t *testing.T) { + assertAllowed(t, + &dynakube.DynaKube{ + ObjectMeta: defaultDynakubeObjectMeta, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + TelemetryIngest: &telemetryingest.Spec{ + Protocols: nil, + }, + }, + }) + }) + + t.Run(`empty list of protocols`, func(t *testing.T) { + assertAllowed(t, + &dynakube.DynaKube{ + ObjectMeta: defaultDynakubeObjectMeta, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + TelemetryIngest: &telemetryingest.Spec{ + Protocols: []string{}, + }, + }, + }) + }) + + t.Run(`unknown protocol`, func(t *testing.T) { + assertDenied(t, + []string{errorTelemetryIngestUnknownProtocols}, + &dynakube.DynaKube{ + ObjectMeta: defaultDynakubeObjectMeta, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + TelemetryIngest: &telemetryingest.Spec{ + Protocols: []string{ + string(otelcgen.ZipkinProtocol), + string(otelcgen.OtlpProtocol), + "unknown", + }, + }, + }, + }) + }) + + t.Run(`unknown protocols`, func(t *testing.T) { + assertDenied(t, + []string{errorTelemetryIngestUnknownProtocols}, + &dynakube.DynaKube{ + ObjectMeta: defaultDynakubeObjectMeta, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + TelemetryIngest: &telemetryingest.Spec{ + Protocols: []string{ + string(otelcgen.ZipkinProtocol), + string(otelcgen.OtlpProtocol), + "unknown1", + "unknown2", + }, + }, + }, + }) + }) + + t.Run(`duplicated protocol`, func(t *testing.T) { + assertDenied(t, + []string{errorTelemetryIngestDuplicatedProtocols}, + &dynakube.DynaKube{ + ObjectMeta: defaultDynakubeObjectMeta, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + TelemetryIngest: &telemetryingest.Spec{ + Protocols: []string{ + string(otelcgen.ZipkinProtocol), + string(otelcgen.OtlpProtocol), + string(otelcgen.OtlpProtocol), + }, + }, + }, + }) + }) + + t.Run(`duplicated protocols`, func(t *testing.T) { + assertDenied(t, + []string{errorTelemetryIngestDuplicatedProtocols}, + &dynakube.DynaKube{ + ObjectMeta: defaultDynakubeObjectMeta, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + TelemetryIngest: &telemetryingest.Spec{ + Protocols: []string{ + string(otelcgen.ZipkinProtocol), + string(otelcgen.ZipkinProtocol), + string(otelcgen.OtlpProtocol), + string(otelcgen.OtlpProtocol), + string(otelcgen.JaegerProtocol), + }, + }, + }, + }) + }) + + t.Run(`default config`, func(t *testing.T) { + assertAllowed(t, + &dynakube.DynaKube{ + ObjectMeta: defaultDynakubeObjectMeta, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + TelemetryIngest: &telemetryingest.Spec{}, + }, + }) + }) + + t.Run(`no telemetry service`, func(t *testing.T) { + assertAllowed(t, + &dynakube.DynaKube{ + ObjectMeta: defaultDynakubeObjectMeta, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + }, + }) + }) + + t.Run(`service name too long`, func(t *testing.T) { + assertDenied(t, + []string{invalidTelemetryIngestNameErrorMessage()}, + &dynakube.DynaKube{ + ObjectMeta: defaultDynakubeObjectMeta, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + TelemetryIngest: &telemetryingest.Spec{ + ServiceName: "a123456789012345678901234567890123456789012345678901234567890123", + }, + }, + }) + }) + + t.Run(`service name violates DNS-1035`, func(t *testing.T) { + assertDenied(t, + []string{invalidTelemetryIngestNameErrorMessage()}, + &dynakube.DynaKube{ + ObjectMeta: defaultDynakubeObjectMeta, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + TelemetryIngest: &telemetryingest.Spec{ + ServiceName: "0123", + }, + }, + }) + }) +} + +func TestConflictingServiceNames(t *testing.T) { + t.Run(`no conflicts`, func(t *testing.T) { + assertAllowed(t, + &dynakube.DynaKube{ + ObjectMeta: defaultDynakubeObjectMeta, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + TelemetryIngest: &telemetryingest.Spec{}, + }, + }, + &dynakube.DynaKube{ + ObjectMeta: otherDynakubeObjectMeta, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + TelemetryIngest: &telemetryingest.Spec{}, + }, + }) + }) + + t.Run(`custom service name vs custom service name`, func(t *testing.T) { + assertDenied(t, + []string{errorTelemetryIngestServiceNameInUse}, + &dynakube.DynaKube{ + ObjectMeta: defaultDynakubeObjectMeta, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + TelemetryIngest: &telemetryingest.Spec{ + ServiceName: testServiceName, + }, + }, + }, + &dynakube.DynaKube{ + ObjectMeta: otherDynakubeObjectMeta, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + TelemetryIngest: &telemetryingest.Spec{ + ServiceName: testServiceName, + }, + }, + }) + }) + + t.Run(`custom service name vs default service name`, func(t *testing.T) { + assertDenied(t, + []string{errorTelemetryIngestServiceNameInUse}, + &dynakube.DynaKube{ + ObjectMeta: defaultDynakubeObjectMeta, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + TelemetryIngest: &telemetryingest.Spec{ + ServiceName: testOtherName + "-telemetry-ingest", + }, + }, + }, + &dynakube.DynaKube{ + ObjectMeta: otherDynakubeObjectMeta, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + TelemetryIngest: &telemetryingest.Spec{}, + }, + }) + }) + + t.Run(`default service name vs custom service name`, func(t *testing.T) { + assertDenied(t, + []string{errorTelemetryIngestServiceNameInUse}, + &dynakube.DynaKube{ + ObjectMeta: defaultDynakubeObjectMeta, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + TelemetryIngest: &telemetryingest.Spec{}, + }, + }, + &dynakube.DynaKube{ + ObjectMeta: otherDynakubeObjectMeta, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + TelemetryIngest: &telemetryingest.Spec{ + ServiceName: testName + "-telemetry-ingest", + }, + }, + }) + }) +} + +func TestForbiddenSuffix(t *testing.T) { + t.Run(`activegate`, func(t *testing.T) { + assertDenied(t, + []string{errorTelemetryIngestForbiddenServiceName}, + &dynakube.DynaKube{ + ObjectMeta: defaultDynakubeObjectMeta, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + TelemetryIngest: &telemetryingest.Spec{ + ServiceName: "test" + "-" + agconsts.MultiActiveGateName, + }, + }, + }) + }) + t.Run(`extensions`, func(t *testing.T) { + assertDenied(t, + []string{errorTelemetryIngestForbiddenServiceName}, + &dynakube.DynaKube{ + ObjectMeta: defaultDynakubeObjectMeta, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + TelemetryIngest: &telemetryingest.Spec{ + ServiceName: "test" + consts.ExtensionsControllerSuffix, + }, + }, + }) + }) + t.Run(`telemetry ingest`, func(t *testing.T) { + assertDenied(t, + []string{errorTelemetryIngestForbiddenServiceName}, + &dynakube.DynaKube{ + ObjectMeta: defaultDynakubeObjectMeta, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + TelemetryIngest: &telemetryingest.Spec{ + ServiceName: "test" + telemetryingest.ServiceNameSuffix, + }, + }, + }) + }) + t.Run(`webhook`, func(t *testing.T) { + assertDenied(t, + []string{errorTelemetryIngestForbiddenServiceName}, + &dynakube.DynaKube{ + ObjectMeta: defaultDynakubeObjectMeta, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + TelemetryIngest: &telemetryingest.Spec{ + ServiceName: "test-webhook", + }, + }, + }) + }) +} diff --git a/pkg/api/validation/dynakube/validation.go b/pkg/api/validation/dynakube/validation.go index 734802224e..8abed02462 100644 --- a/pkg/api/validation/dynakube/validation.go +++ b/pkg/api/validation/dynakube/validation.go @@ -6,7 +6,8 @@ import ( v1beta1 "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta1/dynakube" //nolint:staticcheck v1beta2 "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta2/dynakube" //nolint:staticcheck - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + v1beta3 "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" //nolint:staticcheck + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/util/installconfig" "github.com/Dynatrace/dynatrace-operator/pkg/webhook/validation" "k8s.io/apimachinery/pkg/runtime" @@ -23,25 +24,28 @@ type Validator struct { var ( validatorErrorFuncs = []validatorFunc{ - isCSIModuleDisabled, isActiveGateModuleDisabled, isExtensionsModuleDisabled, isLogMonitoringModuleDisabled, isKSPMDisabled, isOneAgentModuleDisabled, isOneAgentVersionValid, + duplicateOneAgentArguments, + forbiddenHostIdSourceArgument, NoApiUrl, IsInvalidApiUrl, IsThirdGenAPIUrl, disabledCSIForReadonlyCSIVolume, invalidActiveGateCapabilities, duplicateActiveGateCapabilities, + mutuallyExclusiveActiveGatePVsettings, invalidActiveGateProxyUrl, conflictingOneAgentConfiguration, conflictingOneAgentNodeSelector, conflictingNamespaceSelector, noResourcesAvailable, imageFieldSetWithoutCSIFlag, + missingCodeModulesImage, conflictingOneAgentVolumeStorageSettings, nameViolatesDNS1035, nameTooLong, @@ -49,9 +53,15 @@ var ( imageFieldHasTenantImage, extensionControllerImage, extensionControllerPVCStorageDevice, - missingKSPMDependency, + tooManyAGReplicas, missingKSPMImage, missingLogMonitoringImage, + emptyTelemetryIngestProtocolsList, + unknownTelemetryIngestProtocols, + duplicatedTelemetryIngestProtocols, + invalidTelemetryIngestName, + forbiddenTelemetryIngestServiceNameSuffix, + conflictingTelemetryIngestServiceNames, } validatorWarningFuncs = []validatorFunc{ missingActiveGateMemoryLimit, @@ -59,6 +69,10 @@ var ( conflictingHostGroupSettings, deprecatedFeatureFlag, ignoredLogMonitoringTemplate, + conflictingApiUrlForExtensions, + logMonitoringWithoutK8SMonitoring, + kspmWithoutK8SMonitoring, + extensionsWithoutK8SMonitoring, } updateValidatorErrorFuncs = []updateValidatorFunc{ IsMutatedApiUrl, @@ -149,6 +163,11 @@ func getDynakube(obj runtime.Object) (dk *dynakube.DynaKube, err error) { switch v := obj.(type) { case *dynakube.DynaKube: dk = v + case *v1beta3.DynaKube: + err = v.ConvertTo(dk) + if err != nil { + return + } case *v1beta2.DynaKube: err = v.ConvertTo(dk) if err != nil { diff --git a/pkg/api/validation/dynakube/validation_test.go b/pkg/api/validation/dynakube/validation_test.go index b5f536c2fa..d234eb10fe 100644 --- a/pkg/api/validation/dynakube/validation_test.go +++ b/pkg/api/validation/dynakube/validation_test.go @@ -6,8 +6,9 @@ import ( "testing" "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" "github.com/Dynatrace/dynatrace-operator/pkg/util/installconfig" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -57,14 +58,14 @@ func TestDynakubeValidator_Handle(t *testing.T) { ObjectMeta: defaultDynakubeObjectMeta, Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{ - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{ - HostInjectSpec: dynakube.HostInjectSpec{ + OneAgent: oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{ + HostInjectSpec: oneagent.HostInjectSpec{ NodeSelector: map[string]string{ "node": "1", }, }, - AppInjectionSpec: dynakube.AppInjectionSpec{ + AppInjectionSpec: oneagent.AppInjectionSpec{ NamespaceSelector: metav1.LabelSelector{ MatchLabels: dummyLabels, }, @@ -84,14 +85,14 @@ func TestDynakubeValidator_Handle(t *testing.T) { ObjectMeta: defaultDynakubeObjectMeta, Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{ - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{ - HostInjectSpec: dynakube.HostInjectSpec{ + OneAgent: oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{ + HostInjectSpec: oneagent.HostInjectSpec{ NodeSelector: map[string]string{ "node": "2", }, }, - AppInjectionSpec: dynakube.AppInjectionSpec{ + AppInjectionSpec: oneagent.AppInjectionSpec{ NamespaceSelector: metav1.LabelSelector{ MatchLabels: dummyLabels2, }, @@ -105,7 +106,6 @@ func TestDynakubeValidator_Handle(t *testing.T) { setupDisabledCSIEnv(t) assertDenied(t, []string{ - errorCSIModuleRequired, errorNoApiUrl, errorConflictingNamespaceSelector, fmt.Sprintf(errorDuplicateActiveGateCapability, activegate.KubeMonCapability.DisplayName), @@ -118,9 +118,9 @@ func TestDynakubeValidator_Handle(t *testing.T) { }, Spec: dynakube.DynaKubeSpec{ APIURL: "", - OneAgent: dynakube.OneAgentSpec{ - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{ - AppInjectionSpec: dynakube.AppInjectionSpec{ + OneAgent: oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{ + AppInjectionSpec: oneagent.AppInjectionSpec{ NamespaceSelector: metav1.LabelSelector{ MatchLabels: dummyLabels, }, @@ -143,9 +143,9 @@ func TestDynakubeValidator_Handle(t *testing.T) { }, Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{ - ApplicationMonitoring: &dynakube.ApplicationMonitoringSpec{ - AppInjectionSpec: dynakube.AppInjectionSpec{ + OneAgent: oneagent.Spec{ + ApplicationMonitoring: &oneagent.ApplicationMonitoringSpec{ + AppInjectionSpec: oneagent.AppInjectionSpec{ NamespaceSelector: metav1.LabelSelector{ MatchLabels: dummyLabels, }, @@ -161,8 +161,8 @@ func TestDynakubeValidator_Handle(t *testing.T) { }, Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{ - HostMonitoring: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + HostMonitoring: &oneagent.HostInjectSpec{}, }, }, }, &dummyNamespace, &dummyNamespace2) diff --git a/pkg/clients/dynatrace/activegate_version.go b/pkg/clients/dynatrace/activegate_version.go index 639ed3e4df..e7fb579e64 100644 --- a/pkg/clients/dynatrace/activegate_version.go +++ b/pkg/clients/dynatrace/activegate_version.go @@ -3,7 +3,6 @@ package dynatrace import ( "context" - "github.com/Dynatrace/dynatrace-operator/pkg/arch" "github.com/pkg/errors" ) @@ -13,7 +12,7 @@ func (dtc *dynatraceClient) GetLatestActiveGateVersion(ctx context.Context, os s LatestGatewayVersion string `json:"latestGatewayVersion"` }{} - url := dtc.getLatestActiveGateVersionUrl(os, arch.Arch) + url := dtc.getLatestActiveGateVersionUrl(os) err := dtc.makeRequestAndUnmarshal(ctx, url, dynatracePaaSToken, &response) return response.LatestGatewayVersion, errors.WithStack(err) diff --git a/pkg/clients/dynatrace/agent_version.go b/pkg/clients/dynatrace/agent_version.go index 8f5f05c2a7..78a973ab0d 100644 --- a/pkg/clients/dynatrace/agent_version.go +++ b/pkg/clients/dynatrace/agent_version.go @@ -53,23 +53,32 @@ func (dtc *dynatraceClient) GetLatestAgentVersion(ctx context.Context, os, insta return "", errors.New("os or installerType is empty") } - var flavor string - // Default installer type has no "multidistro" flavor - // so the default flavor is always needed in that case + url := dtc.getLatestAgentVersionUrl(os, installerType, determineFlavor(installerType), determineArch(installerType)) + err := dtc.makeRequestAndUnmarshal(ctx, url, dynatracePaaSToken, &response) + + return response.LatestAgentVersion, errors.WithStack(err) +} + +// determineArch gives you the proper arch value, because the OSAgent and ActiveGate images on the tenant-image-registry only have AMD images. +func determineArch(installerType string) string { if installerType == InstallerTypeDefault { - flavor = arch.FlavorDefault - } else { - flavor = arch.Flavor + return "" } - url := dtc.getLatestAgentVersionUrl(os, installerType, flavor, arch.Arch) - err := dtc.makeRequestAndUnmarshal(ctx, url, dynatracePaaSToken, &response) + return arch.Arch +} - return response.LatestAgentVersion, errors.WithStack(err) +// determineFlavor gives you the proper flavor value, because the default installer type has no "multidistro" flavor so the default flavor is always needed in that case. +func determineFlavor(installerType string) string { //nolint:nolintlint,unparam + if installerType == InstallerTypeDefault { + return arch.FlavorDefault + } + + return arch.Flavor } // GetAgentVersions gets available agent versions for the given OS and installer type. -func (dtc *dynatraceClient) GetAgentVersions(ctx context.Context, os, installerType, flavor, arch string) ([]string, error) { +func (dtc *dynatraceClient) GetAgentVersions(ctx context.Context, os, installerType, flavor string) ([]string, error) { response := struct { AvailableVersions []string `json:"availableVersions"` }{} @@ -78,7 +87,7 @@ func (dtc *dynatraceClient) GetAgentVersions(ctx context.Context, os, installerT return nil, errors.New("os or installerType is empty") } - url := dtc.getAgentVersionsUrl(os, installerType, flavor, arch) + url := dtc.getAgentVersionsUrl(os, installerType, flavor, determineArch(installerType)) err := dtc.makeRequestAndUnmarshal(ctx, url, dynatracePaaSToken, &response) return response.AvailableVersions, errors.WithStack(err) diff --git a/pkg/clients/dynatrace/agent_version_test.go b/pkg/clients/dynatrace/agent_version_test.go index c2c2019b30..94594d6795 100644 --- a/pkg/clients/dynatrace/agent_version_test.go +++ b/pkg/clients/dynatrace/agent_version_test.go @@ -214,7 +214,7 @@ func TestDynatraceClient_GetAgentVersions(t *testing.T) { dynatraceServer, dtc := createTestDynatraceClientWithFunc(t, versionsRequestHandler) defer dynatraceServer.Close() - availableVersions, err := dtc.GetAgentVersions(ctx, OsUnix, InstallerTypePaaS, "", "") + availableVersions, err := dtc.GetAgentVersions(ctx, OsUnix, InstallerTypePaaS, "") require.NoError(t, err) assert.Len(t, availableVersions, 4) @@ -227,7 +227,7 @@ func TestDynatraceClient_GetAgentVersions(t *testing.T) { dynatraceServer, dtc := createTestDynatraceClientWithFunc(t, errorHandler) defer dynatraceServer.Close() - availableVersions, err := dtc.GetAgentVersions(ctx, OsUnix, InstallerTypePaaS, "", "") + availableVersions, err := dtc.GetAgentVersions(ctx, OsUnix, InstallerTypePaaS, "") require.EqualError(t, err, "dynatrace server error 400: test-error") assert.Empty(t, availableVersions) diff --git a/pkg/clients/dynatrace/client.go b/pkg/clients/dynatrace/client.go index b4b3e36e8d..d02ec8f4e3 100644 --- a/pkg/clients/dynatrace/client.go +++ b/pkg/clients/dynatrace/client.go @@ -10,7 +10,7 @@ import ( "strings" "time" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/logmonitoring" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/logmonitoring" "github.com/pkg/errors" "golang.org/x/net/http/httpproxy" ) @@ -44,7 +44,7 @@ type Client interface { // GetAgentVersions on success returns an array of versions that can be used with GetAgent to // download a specific agent version - GetAgentVersions(ctx context.Context, os, installerType, flavor, arch string) ([]string, error) + GetAgentVersions(ctx context.Context, os, installerType, flavor string) ([]string, error) GetOneAgentConnectionInfo(ctx context.Context) (OneAgentConnectionInfo, error) diff --git a/pkg/clients/dynatrace/communication_hosts_test.go b/pkg/clients/dynatrace/communication_hosts_test.go index 7bcafa5ade..451f2193e5 100644 --- a/pkg/clients/dynatrace/communication_hosts_test.go +++ b/pkg/clients/dynatrace/communication_hosts_test.go @@ -146,7 +146,7 @@ func testCommunicationHostsGetCommunicationHosts(t *testing.T, dynatraceClient C res, err := dynatraceClient.GetOneAgentConnectionInfo(ctx) require.NoError(t, err) - assert.ObjectsAreEqualValues(res.CommunicationHosts, []CommunicationHost{ + assert.ObjectsAreEqual(res.CommunicationHosts, []CommunicationHost{ {Host: "host1.dynatracelabs.com", Port: 80, Protocol: "http"}, {Host: "host2.dynatracelabs.com", Port: 443, Protocol: "https"}, {Host: "12.0.9.1", Port: 80, Protocol: "http"}, diff --git a/pkg/clients/dynatrace/dynatrace_client_test.go b/pkg/clients/dynatrace/dynatrace_client_test.go index 6596d79c61..5b69771049 100644 --- a/pkg/clients/dynatrace/dynatrace_client_test.go +++ b/pkg/clients/dynatrace/dynatrace_client_test.go @@ -17,19 +17,19 @@ import ( var ( flavorUri = fmt.Sprintf("/v1/deployment/installer/agent/%s/%s/latest/metainfo?bitness=64&flavor=%s&arch=%s", - OsUnix, InstallerTypeDefault, arch.FlavorDefault+"a", arch.Arch) + OsUnix, InstallerTypePaaS, arch.FlavorMultidistro+"a", arch.Arch) flavourUriResponse = `{"error":{"code":400,"message":"Constraints violated.","constraintViolations":[{"path":"flavor","message":"'defaulta' must be any of [default, multidistro, musl]","parameterLocation":"QUERY","location":null}]}}` archUri = fmt.Sprintf("/v1/deployment/installer/agent/%s/%s/latest/metainfo?bitness=64&flavor=%s&arch=%s", - OsUnix, InstallerTypeDefault, arch.FlavorDefault, arch.Arch+"a") + OsUnix, InstallerTypePaaS, arch.FlavorMultidistro, arch.Arch+"a") archUriResponse = `{"error":{"code":400,"message":"Constraints violated.","constraintViolations":[{"path":"arch","message":"'x86a' must be any of [all, arm, ppc, ppcle, s390, sparc, x86, zos]","parameterLocation":"QUERY","location":null}]}}` flavorArchUri = fmt.Sprintf("/v1/deployment/installer/agent/%s/%s/latest/metainfo?bitness=64&flavor=%s&arch=%s", - OsUnix, InstallerTypeDefault, arch.FlavorDefault+"a", arch.Arch+"a") + OsUnix, InstallerTypePaaS, arch.FlavorMultidistro+"a", arch.Arch+"a") flavourArchUriResponse = `{"error":{"code":400,"message":"Constraints violated.","constraintViolations":[{"path":"flavor","message":"'defaulta' must be any of [default, multidistro, musl]","parameterLocation":"QUERY","location":null},{"path":"arch","message":"'x86a' must be any of [all, arm, ppc, ppcle, s390, sparc, x86, zos]","parameterLocation":"QUERY","location":null}]}}` oaLatestMetainfoUri = fmt.Sprintf("/v1/deployment/installer/agent/%s/%s/latest/metainfo?bitness=64&flavor=%s&arch=%s", - "aix", InstallerTypeDefault, arch.FlavorDefault, arch.Arch) + "aix", InstallerTypePaaS, arch.FlavorMultidistro, arch.Arch) oaLatestMetainfoUriResponse = `{"error":{"code":404,"message":"non supported architecture on OS "}}` ) @@ -120,7 +120,7 @@ func TestBuildHostCache(t *testing.T) { err := dc.buildHostCache(ctx) require.NoError(t, err) assert.NotEmpty(t, dc.hostCache) - assert.ObjectsAreEqualValues(dc.hostCache, map[string]hostInfo{ + assert.ObjectsAreEqual(dc.hostCache, map[string]hostInfo{ "10.11.12.13": {version: "1.142.0.20180313-173634", entityID: "dynatraceSampleEntityId"}, "192.168.0.1": {version: "1.142.0.20180313-173634", entityID: "dynatraceSampleEntityId"}, }) @@ -313,11 +313,6 @@ func testServerErrors(t *testing.T) { err = dtc.makeRequestAndUnmarshal(context.Background(), dtc.url+flavorArchUri, dynatracePaaSToken, &response) assert.Equal(t, "dynatrace server error 400: Constraints violated.\n\t- flavor: 'defaulta' must be any of [default, multidistro, musl]\n\t- arch: 'x86a' must be any of [all, arm, ppc, ppcle, s390, sparc, x86, zos]", err.Error()) }) - - t.Run("GetLatestAgentVersion - invalid architecture", func(t *testing.T) { - _, err = dtc.GetLatestAgentVersion(context.Background(), "aix", InstallerTypeDefault) - assert.Equal(t, "dynatrace server error 404: non supported architecture on OS ", err.Error()) - }) } func dynatraceServerErrorsHandler() http.HandlerFunc { diff --git a/pkg/clients/dynatrace/endpoints.go b/pkg/clients/dynatrace/endpoints.go index 7b56d8b5d9..387ad10dac 100644 --- a/pkg/clients/dynatrace/endpoints.go +++ b/pkg/clients/dynatrace/endpoints.go @@ -17,18 +17,28 @@ func (dtc *dynatraceClient) getLatestAgentUrl(os, installerType, flavor, arch st } func (dtc *dynatraceClient) getLatestAgentVersionUrl(os, installerType, flavor, arch string) string { - return fmt.Sprintf("%s/v1/deployment/installer/agent/%s/%s/latest/metainfo?bitness=64&flavor=%s&arch=%s", - dtc.url, os, installerType, flavor, arch) + if arch != "" { + return fmt.Sprintf("%s/v1/deployment/installer/agent/%s/%s/latest/metainfo?bitness=64&flavor=%s&arch=%s", + dtc.url, os, installerType, flavor, arch) + } + + return fmt.Sprintf("%s/v1/deployment/installer/agent/%s/%s/latest/metainfo?bitness=64&flavor=%s", + dtc.url, os, installerType, flavor) } -func (dtc *dynatraceClient) getLatestActiveGateVersionUrl(os, arch string) string { - return fmt.Sprintf("%s/v1/deployment/installer/gateway/%s/latest/metainfo?arch=%s", - dtc.url, os, arch) +func (dtc *dynatraceClient) getLatestActiveGateVersionUrl(os string) string { + return fmt.Sprintf("%s/v1/deployment/installer/gateway/%s/latest/metainfo", + dtc.url, os) } func (dtc *dynatraceClient) getAgentVersionsUrl(os, installerType, flavor, arch string) string { - return fmt.Sprintf("%s/v1/deployment/installer/agent/versions/%s/%s?flavor=%s&arch=%s", - dtc.url, os, installerType, flavor, arch) + if arch != "" { + return fmt.Sprintf("%s/v1/deployment/installer/agent/versions/%s/%s?flavor=%s&arch=%s", + dtc.url, os, installerType, flavor, arch) + } + + return fmt.Sprintf("%s/v1/deployment/installer/agent/versions/%s/%s?flavor=%s", + dtc.url, os, installerType, flavor) } func (dtc *dynatraceClient) getOneAgentConnectionInfoUrl() string { diff --git a/pkg/clients/dynatrace/oneagent_connection_info.go b/pkg/clients/dynatrace/oneagent_connection_info.go index 6281b0f141..d8d48826eb 100644 --- a/pkg/clients/dynatrace/oneagent_connection_info.go +++ b/pkg/clients/dynatrace/oneagent_connection_info.go @@ -82,7 +82,7 @@ func (dtc *dynatraceClient) readResponseForOneAgentConnectionInfo(response []byt hash := fnv.New32a() // Hash write implements Write interface, but never return err, so let's ignore it - _, _ = hash.Write([]byte(fmt.Sprintf("%s-%s-%d", e.Protocol, e.Host, e.Port))) + _, _ = fmt.Fprintf(hash, "%s-%s-%d", e.Protocol, e.Host, e.Port) communicationHosts[hash.Sum32()] = e } diff --git a/pkg/clients/dynatrace/processmoduleconfig.go b/pkg/clients/dynatrace/processmoduleconfig.go index de5821ee7a..f06f65bccf 100644 --- a/pkg/clients/dynatrace/processmoduleconfig.go +++ b/pkg/clients/dynatrace/processmoduleconfig.go @@ -3,11 +3,11 @@ package dynatrace import ( "context" "encoding/json" - "fmt" "net/http" + "sort" "strconv" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" "github.com/Dynatrace/dynatrace-operator/pkg/clients/utils" "github.com/pkg/errors" ) @@ -88,7 +88,7 @@ func (pmc *ProcessModuleConfig) removeProperty(index int) { pmc.Properties = append(pmc.Properties[0:index], pmc.Properties[index+1:]...) } -func (pmc *ProcessModuleConfig) AddConnectionInfo(oneAgentConnectionInfo dynakube.OneAgentConnectionInfoStatus, tenantToken string) *ProcessModuleConfig { +func (pmc *ProcessModuleConfig) AddConnectionInfo(oneAgentConnectionInfo oneagent.ConnectionInfoStatus, tenantToken string) *ProcessModuleConfig { tenant := ProcessModuleProperty{ Section: generalSectionName, Key: "tenant", @@ -146,6 +146,12 @@ func (pmc ProcessModuleConfig) ToMap() ConfMap { return sections } +func (pmc *ProcessModuleConfig) SortPropertiesByKey() { + sort.Slice(pmc.Properties, func(i, j int) bool { + return pmc.Properties[i].Key < pmc.Properties[j].Key + }) +} + func (pmc ProcessModuleConfig) IsEmpty() bool { return len(pmc.Properties) == 0 } @@ -163,7 +169,7 @@ func (dtc *dynatraceClient) GetProcessModuleConfig(ctx context.Context, prevRevi } if err != nil { - return nil, fmt.Errorf("error while requesting process module config: %w", err) + return nil, errors.WithMessage(err, "error while requesting process module config") } defer utils.CloseBodyAfterRequest(resp) @@ -179,7 +185,7 @@ func (dtc *dynatraceClient) GetProcessModuleConfig(ctx context.Context, prevRevi func (dtc *dynatraceClient) createProcessModuleConfigRequest(ctx context.Context, prevRevision uint) (*http.Request, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, dtc.getProcessModuleConfigUrl(), nil) if err != nil { - return nil, fmt.Errorf("error initializing http request: %w", err) + return nil, errors.WithMessage(err, "error initializing http request") } query := req.URL.Query() diff --git a/pkg/clients/dynatrace/processmoduleconfig_test.go b/pkg/clients/dynatrace/processmoduleconfig_test.go index 45080622fb..71794925c7 100644 --- a/pkg/clients/dynatrace/processmoduleconfig_test.go +++ b/pkg/clients/dynatrace/processmoduleconfig_test.go @@ -2,6 +2,7 @@ package dynatrace import ( "context" + "encoding/json" "fmt" "net/http" "testing" @@ -411,3 +412,74 @@ func TestProcessModuleConfig_AddNoProxy(t *testing.T) { }) } } + +func TestProcessModuleConfig_SortPropertiesByKey(t *testing.T) { + processModuleConfig := &ProcessModuleConfig{ + Revision: 0, + Properties: []ProcessModuleProperty{ + { + Section: "general", + Key: "baa", + Value: "random", + }, + { + Section: "general", + Key: "aaa", + Value: "random", + }, + { + Section: "general", + Key: "aba", + Value: "random", + }, + { + Section: "general", + Key: "bbb", + Value: "random", + }, + { + Section: "general", + Key: "aab", + Value: "random", + }, + }, + } + processModuleConfig.SortPropertiesByKey() + + expected := []ProcessModuleProperty{ + { + Section: "general", + Key: "aaa", + Value: "random", + }, + { + Section: "general", + Key: "aab", + Value: "random", + }, + { + Section: "general", + Key: "aba", + Value: "random", + }, + { + Section: "general", + Key: "baa", + Value: "random", + }, + { + Section: "general", + Key: "bbb", + Value: "random", + }, + } + assert.Equal(t, expected, processModuleConfig.Properties) + + expectedByteds, err := json.Marshal(expected) + require.NoError(t, err) + + actualBytes, err := json.Marshal(processModuleConfig.Properties) + require.NoError(t, err) + + assert.Equal(t, expectedByteds, actualBytes) +} diff --git a/pkg/clients/dynatrace/send_event.go b/pkg/clients/dynatrace/send_event.go index 79ffe6d1b9..2db0d54d10 100644 --- a/pkg/clients/dynatrace/send_event.go +++ b/pkg/clients/dynatrace/send_event.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "encoding/json" - "fmt" "net/http" "github.com/Dynatrace/dynatrace-operator/pkg/clients/utils" @@ -45,7 +44,7 @@ func (dtc *dynatraceClient) SendEvent(ctx context.Context, eventData *EventData) req, err := http.NewRequestWithContext(ctx, http.MethodPost, dtc.getEventsUrl(), bytes.NewBuffer(jsonStr)) if err != nil { - return fmt.Errorf("error initializing http request: %w", err) + return errors.WithMessage(err, "error initializing http request") } req.Header.Add("Content-Type", "application/json") @@ -53,7 +52,7 @@ func (dtc *dynatraceClient) SendEvent(ctx context.Context, eventData *EventData) response, err := dtc.httpClient.Do(req) if err != nil { - return fmt.Errorf("error making post request to dynatrace api: %w", err) + return errors.WithMessage(err, "error making post request to dynatrace api") } defer utils.CloseBodyAfterRequest(response) diff --git a/pkg/clients/dynatrace/settings.go b/pkg/clients/dynatrace/settings.go index 79242b1cbd..6c1b010dda 100644 --- a/pkg/clients/dynatrace/settings.go +++ b/pkg/clients/dynatrace/settings.go @@ -83,7 +83,7 @@ func (dtc *dynatraceClient) GetMonitoredEntitiesForKubeSystemUUID(ctx context.Co err = dtc.unmarshalToJson(res, &resDataJson) if err != nil { - return nil, fmt.Errorf("error parsing response body: %w", err) + return nil, errors.WithMessage(err, "error parsing response body") } return resDataJson.Entities, nil @@ -117,7 +117,7 @@ func (dtc *dynatraceClient) GetSettingsForMonitoredEntity(ctx context.Context, m err = dtc.unmarshalToJson(res, &resDataJson) if err != nil { - return GetSettingsResponse{}, fmt.Errorf("error parsing response body: %w", err) + return GetSettingsResponse{}, errors.WithMessage(err, "error parsing response body") } return resDataJson, nil @@ -151,7 +151,7 @@ func (dtc *dynatraceClient) GetSettingsForLogModule(ctx context.Context, monitor err = dtc.unmarshalToJson(res, &resDataJson) if err != nil { - return GetLogMonSettingsResponse{}, fmt.Errorf("error parsing response body: %w", err) + return GetLogMonSettingsResponse{}, errors.WithMessage(err, "error parsing response body") } return resDataJson, nil @@ -160,12 +160,12 @@ func (dtc *dynatraceClient) GetSettingsForLogModule(ctx context.Context, monitor func (dtc *dynatraceClient) unmarshalToJson(res *http.Response, resDataJson any) error { resData, err := dtc.getServerResponseData(res) if err != nil { - return fmt.Errorf("error reading response body: %w", err) + return errors.WithMessage(err, "error reading response body") } err = json.Unmarshal(resData, resDataJson) if err != nil { - return fmt.Errorf("error parsing response body: %w", err) + return errors.WithMessage(err, "error parsing response body") } return nil @@ -175,14 +175,14 @@ func handleErrorArrayResponseFromAPI(response []byte, statusCode int) error { if statusCode == http.StatusForbidden || statusCode == http.StatusUnauthorized { var se serverErrorResponse if err := json.Unmarshal(response, &se); err != nil { - return fmt.Errorf("response error: %d, can't unmarshal json response", statusCode) + return errors.Errorf("response error: %d, can't unmarshal json response", statusCode) } - return fmt.Errorf("response error: %d, %s", statusCode, se.ErrorMessage.Message) + return errors.Errorf("response error: %d, %s", statusCode, se.ErrorMessage.Message) } else { var se []serverErrorResponse if err := json.Unmarshal(response, &se); err != nil { - return fmt.Errorf("response error: %d, can't unmarshal json response", statusCode) + return errors.Errorf("response error: %d, can't unmarshal json response", statusCode) } var sb strings.Builder diff --git a/pkg/clients/dynatrace/settings_enrichment.go b/pkg/clients/dynatrace/settings_enrichment.go index 6b223aca0e..6ca0800ca4 100644 --- a/pkg/clients/dynatrace/settings_enrichment.go +++ b/pkg/clients/dynatrace/settings_enrichment.go @@ -2,13 +2,12 @@ package dynatrace import ( "context" - "errors" - "fmt" "net/http" "strings" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/clients/utils" + "github.com/pkg/errors" ) const ( @@ -72,7 +71,7 @@ func (dtc *dynatraceClient) GetRulesSettings(ctx context.Context, kubeSystemUUID return GetRulesSettingsResponse{}, nil } - return GetRulesSettingsResponse{}, errors.New(fmt.Errorf("error parsing response body: %w", err).Error()) + return GetRulesSettingsResponse{}, errors.WithMessage(err, "error parsing response body") } return resDataJson, nil diff --git a/pkg/clients/dynatrace/settings_enrichment_test.go b/pkg/clients/dynatrace/settings_enrichment_test.go index b6acd72d9a..95786697e9 100644 --- a/pkg/clients/dynatrace/settings_enrichment_test.go +++ b/pkg/clients/dynatrace/settings_enrichment_test.go @@ -7,7 +7,7 @@ import ( "net/http/httptest" "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/pkg/clients/dynatrace/settings_kubernetes.go b/pkg/clients/dynatrace/settings_kubernetes.go index f60745625e..59b6af2a25 100644 --- a/pkg/clients/dynatrace/settings_kubernetes.go +++ b/pkg/clients/dynatrace/settings_kubernetes.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "encoding/json" - "fmt" "io" "net/http" "strconv" @@ -67,12 +66,12 @@ func (dtc *dynatraceClient) performCreateOrUpdateKubernetesSetting(ctx context.C defer utils.CloseBodyAfterRequest(res) if err != nil { - return "", fmt.Errorf("error making post request to dynatrace api: %w", err) + return "", errors.WithMessage(err, "error making post request to dynatrace api") } resData, err := io.ReadAll(res.Body) if err != nil { - return "", fmt.Errorf("error reading response: %w", err) + return "", errors.WithMessage(err, "error reading response") } if res.StatusCode != http.StatusOK && @@ -88,7 +87,7 @@ func (dtc *dynatraceClient) performCreateOrUpdateKubernetesSetting(ctx context.C } if len(resDataJson) != 1 { - return "", fmt.Errorf("response is not containing exactly one entry %s", resData) + return "", errors.Errorf("response is not containing exactly one entry %s", resData) } return resDataJson[0].ObjectId, nil diff --git a/pkg/clients/dynatrace/settings_kubernetes_test.go b/pkg/clients/dynatrace/settings_kubernetes_test.go index a40493906d..b4881da087 100644 --- a/pkg/clients/dynatrace/settings_kubernetes_test.go +++ b/pkg/clients/dynatrace/settings_kubernetes_test.go @@ -34,7 +34,7 @@ func TestDynatraceClient_CreateOrUpdateKubernetesSetting(t *testing.T) { require.NotNil(t, actual) require.NoError(t, err) assert.Len(t, actual, len(testObjectID)) - assert.EqualValues(t, testObjectID, actual) + assert.Equal(t, testObjectID, actual) }) t.Run(`create settings for the given monitored entity id`, func(t *testing.T) { @@ -54,7 +54,7 @@ func TestDynatraceClient_CreateOrUpdateKubernetesSetting(t *testing.T) { require.NotNil(t, actual) require.NoError(t, err) assert.Len(t, actual, len(testObjectID)) - assert.EqualValues(t, testObjectID, actual) + assert.Equal(t, testObjectID, actual) }) t.Run(`don't create settings for the given monitored entity id because no kube-system uuid is provided`, func(t *testing.T) { @@ -134,7 +134,7 @@ func TestDynatraceClient_CreateOrUpdateAppKubernetesSetting(t *testing.T) { require.NotNil(t, actual) require.NoError(t, err) assert.Len(t, actual, len(testObjectID)) - assert.EqualValues(t, testObjectID, actual) + assert.Equal(t, testObjectID, actual) }) t.Run(`don't create app settings for the given monitored entity id because of api error`, func(t *testing.T) { @@ -174,7 +174,7 @@ func TestDynatraceClient_getKubernetesSettingBody(t *testing.T) { // assert require.NotNil(t, actual) assert.Len(t, actual, 1) - assert.EqualValues(t, hierarchicalMonitoringSettingsSchemaVersion, actual[0].SchemaVersion) + assert.Equal(t, hierarchicalMonitoringSettingsSchemaVersion, actual[0].SchemaVersion) assert.IsType(t, postKubernetesSettings{}, actual[0].Value) assert.True(t, actual[0].Value.(postKubernetesSettings).Enabled) bodyJson, err := json.Marshal(actual[0]) @@ -203,7 +203,7 @@ func TestDynatraceClient_getKubernetesSettingBody(t *testing.T) { // assert require.NotNil(t, actual) assert.Len(t, actual, 1) - assert.EqualValues(t, schemaVersionV1, actual[0].SchemaVersion) + assert.Equal(t, schemaVersionV1, actual[0].SchemaVersion) assert.IsType(t, postKubernetesSettings{}, actual[0].Value) assert.True(t, actual[0].Value.(postKubernetesSettings).Enabled) bodyJson, err := json.Marshal(actual[0]) diff --git a/pkg/clients/dynatrace/settings_logmonitoring.go b/pkg/clients/dynatrace/settings_logmonitoring.go index 5f7e0e53d2..38a6cbce7b 100644 --- a/pkg/clients/dynatrace/settings_logmonitoring.go +++ b/pkg/clients/dynatrace/settings_logmonitoring.go @@ -4,12 +4,12 @@ import ( "bytes" "context" "encoding/json" - "fmt" "io" "net/http" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/logmonitoring" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/logmonitoring" "github.com/Dynatrace/dynatrace-operator/pkg/clients/utils" + "github.com/pkg/errors" ) type IngestRuleMatchers struct { @@ -39,6 +39,7 @@ type posLogMonSettingsBody struct { const ( logMonitoringSettingsSchemaId = "builtin:logmonitoring.log-storage-settings" + schemaVersion = "1.0.16" ) func (dtc *dynatraceClient) performCreateLogMonSetting(ctx context.Context, body []posLogMonSettingsBody) (string, error) { //nolint:dupl @@ -56,12 +57,12 @@ func (dtc *dynatraceClient) performCreateLogMonSetting(ctx context.Context, body defer utils.CloseBodyAfterRequest(res) if err != nil { - return "", fmt.Errorf("error making post request to dynatrace api: %w", err) + return "", errors.WithMessage(err, "error making post request to dynatrace api") } resData, err := io.ReadAll(res.Body) if err != nil { - return "", fmt.Errorf("error reading response: %w", err) + return "", errors.WithMessage(err, "error reading response") } if res.StatusCode != http.StatusOK && @@ -77,7 +78,7 @@ func (dtc *dynatraceClient) performCreateLogMonSetting(ctx context.Context, body } if len(resDataJson) != 1 { - return "", fmt.Errorf("response is not containing exactly one entry %s", resData) + return "", errors.Errorf("response is not containing exactly one entry %s", resData) } return resDataJson[0].ObjectId, nil @@ -114,7 +115,7 @@ func createBaseLogMonSettings(clusterName, schemaId string, schemaVersion string } func (dtc *dynatraceClient) CreateLogMonitoringSetting(ctx context.Context, scope, clusterName string, matchers []logmonitoring.IngestRuleMatchers) (string, error) { - settings := createBaseLogMonSettings(clusterName, logMonitoringSettingsSchemaId, "1.0.0", scope, matchers) + settings := createBaseLogMonSettings(clusterName, logMonitoringSettingsSchemaId, schemaVersion, scope, matchers) objectId, err := dtc.performCreateLogMonSetting(ctx, []posLogMonSettingsBody{settings}) if err != nil { diff --git a/pkg/clients/dynatrace/settings_logmonitoring_test.go b/pkg/clients/dynatrace/settings_logmonitoring_test.go index 75ead5cbea..f28e57da06 100644 --- a/pkg/clients/dynatrace/settings_logmonitoring_test.go +++ b/pkg/clients/dynatrace/settings_logmonitoring_test.go @@ -6,8 +6,8 @@ import ( "net/http/httptest" "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/logmonitoring" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/logmonitoring" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -38,7 +38,7 @@ func TestDynatraceClient_CreateLogMonitoringSetting(t *testing.T) { require.NotNil(t, actual) require.NoError(t, err) assert.Len(t, actual, len(testObjectID)) - assert.EqualValues(t, testObjectID, actual) + assert.Equal(t, testObjectID, actual) }) t.Run("create settings logmonitoring settings with custom rule matchers", func(t *testing.T) { // arrange @@ -72,6 +72,6 @@ func TestDynatraceClient_CreateLogMonitoringSetting(t *testing.T) { require.NotNil(t, actual) require.NoError(t, err) assert.Len(t, actual, len(testObjectID)) - assert.EqualValues(t, testObjectID, actual) + assert.Equal(t, testObjectID, actual) }) } diff --git a/pkg/clients/dynatrace/settings_test.go b/pkg/clients/dynatrace/settings_test.go index 31731170d9..14a2d1f223 100644 --- a/pkg/clients/dynatrace/settings_test.go +++ b/pkg/clients/dynatrace/settings_test.go @@ -39,7 +39,7 @@ func TestDynatraceClient_GetMonitoredEntitiesForKubeSystemUUID(t *testing.T) { require.NotNil(t, actual) require.NoError(t, err) assert.Len(t, actual, 2) - assert.EqualValues(t, expected, actual) + assert.Equal(t, expected, actual) }) t.Run("no monitored entities for this uuid exist", func(t *testing.T) { @@ -61,7 +61,7 @@ func TestDynatraceClient_GetMonitoredEntitiesForKubeSystemUUID(t *testing.T) { require.NotNil(t, actual) require.NoError(t, err) assert.Empty(t, actual) - assert.EqualValues(t, expected, actual) + assert.Equal(t, expected, actual) }) t.Run("no monitored entities found because no kube-system uuid is provided", func(t *testing.T) { diff --git a/pkg/clients/dynatrace/token.go b/pkg/clients/dynatrace/token.go index 69a8202739..ace526a29a 100644 --- a/pkg/clients/dynatrace/token.go +++ b/pkg/clients/dynatrace/token.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "encoding/json" - "fmt" "net/http" "github.com/Dynatrace/dynatrace-operator/pkg/clients/utils" @@ -39,7 +38,7 @@ func (dtc *dynatraceClient) GetTokenScopes(ctx context.Context, token string) (T req, err := http.NewRequestWithContext(ctx, http.MethodPost, dtc.getTokensLookupUrl(), bytes.NewBuffer(jsonStr)) if err != nil { - return nil, fmt.Errorf("error initializing http request: %w", err) + return nil, errors.WithMessage(err, "error initializing http request") } req.Header.Add("Content-Type", "application/json") @@ -47,7 +46,7 @@ func (dtc *dynatraceClient) GetTokenScopes(ctx context.Context, token string) (T resp, err := dtc.httpClient.Do(req) if err != nil { - return nil, fmt.Errorf("error making post request to dynatrace api: %w", err) + return nil, errors.WithMessage(err, "error making post request to dynatrace api") } defer utils.CloseBodyAfterRequest(resp) diff --git a/pkg/clients/edgeconnect/client.go b/pkg/clients/edgeconnect/client.go index 700f7c3f59..811a305427 100644 --- a/pkg/clients/edgeconnect/client.go +++ b/pkg/clients/edgeconnect/client.go @@ -145,12 +145,12 @@ func (c *client) handleErrorResponseFromAPI(response []byte, statusCode int) err } func (c *client) handleErrorResponseFromSettingsApi(response []byte, statusCode int) error { - se := []SettingsApiResponse{} + se := SettingsApiResponse{} if err := json.Unmarshal(response, &se); err != nil { return errors.WithStack(errors.WithMessagef(err, "response error, can't unmarshal json response %d", statusCode)) } - return se[0].Error + return se.Error } func (c *client) getServerResponseData(response *http.Response) ([]byte, error) { diff --git a/pkg/clients/edgeconnect/environment_settings.go b/pkg/clients/edgeconnect/environment_settings.go index 25bb2bd019..6b9670bd7c 100644 --- a/pkg/clients/edgeconnect/environment_settings.go +++ b/pkg/clients/edgeconnect/environment_settings.go @@ -3,7 +3,6 @@ package edgeconnect import ( "bytes" "encoding/json" - "fmt" "net/http" "github.com/Dynatrace/dynatrace-operator/pkg/clients/utils" @@ -40,7 +39,7 @@ func (c *client) GetConnectionSettings() ([]EnvironmentSetting, error) { req, err := http.NewRequestWithContext(c.ctx, http.MethodGet, settingsObjectsUrl, nil) if err != nil { - return nil, fmt.Errorf("error initializing http request: %w", err) + return nil, errors.WithMessage(err, "error initializing http request") } q := req.URL.Query() @@ -53,19 +52,19 @@ func (c *client) GetConnectionSettings() ([]EnvironmentSetting, error) { defer utils.CloseBodyAfterRequest(response) if err != nil { - return nil, fmt.Errorf("error making post request to dynatrace api: %w", err) + return nil, errors.WithMessage(err, "error making post request to dynatrace api") } responseData, err := c.getSettingsApiResponseData(response) if err != nil { - return nil, fmt.Errorf("error getting server response data: %w", err) + return nil, errors.WithMessage(err, "error getting server response data") } var resDataJson EnvironmentSettingsResponse err = json.Unmarshal(responseData, &resDataJson) if err != nil { - return nil, fmt.Errorf("error parsing response body: %w", err) + return nil, errors.WithMessage(err, "error parsing response body") } return resDataJson.Items, nil @@ -79,7 +78,7 @@ func (c *client) CreateConnectionSetting(es EnvironmentSetting) error { req, err := http.NewRequestWithContext(c.ctx, http.MethodPost, c.getSettingsObjectsUrl(), bytes.NewBuffer(jsonStr)) if err != nil { - return fmt.Errorf("error initializing http request: %w", err) + return errors.WithMessage(err, "error initializing http request") } req.Header.Add("Content-Type", "application/json") @@ -89,13 +88,13 @@ func (c *client) CreateConnectionSetting(es EnvironmentSetting) error { defer utils.CloseBodyAfterRequest(response) if err != nil { - return fmt.Errorf("error making post request to dynatrace api: %w", err) + return errors.WithMessage(err, "error making post request to dynatrace api") } _, err = c.getSettingsApiResponseData(response) if err != nil { - return fmt.Errorf("error reading response data: %w", err) + return errors.WithMessage(err, "error reading response data") } return nil @@ -109,14 +108,14 @@ func (c *client) UpdateConnectionSetting(es EnvironmentSetting) error { req, err := http.NewRequestWithContext(c.ctx, http.MethodPut, c.getSettingsObjectsIdUrl(*es.ObjectId), bytes.NewBuffer(jsonStr)) if err != nil { - return fmt.Errorf("error initializing http request: %w", err) + return errors.WithMessage(err, "error initializing http request") } req.Header.Add("Content-Type", "application/json") response, err := c.httpClient.Do(req) if err != nil { - return fmt.Errorf("error making post request to dynatrace api: %w", err) + return errors.WithMessage(err, "error making post request to dynatrace api") } defer utils.CloseBodyAfterRequest(response) @@ -124,7 +123,7 @@ func (c *client) UpdateConnectionSetting(es EnvironmentSetting) error { _, err = c.getSettingsApiResponseData(response) if err != nil { - return fmt.Errorf("error reading response data: %w", err) + return errors.WithMessage(err, "error reading response data") } return nil @@ -133,7 +132,7 @@ func (c *client) UpdateConnectionSetting(es EnvironmentSetting) error { func (c *client) DeleteConnectionSetting(objectId string) error { req, err := http.NewRequestWithContext(c.ctx, http.MethodDelete, c.getSettingsObjectsIdUrl(objectId), nil) if err != nil { - return fmt.Errorf("error initializing http request: %w", err) + return errors.WithMessage(err, "error initializing http request") } response, err := c.httpClient.Do(req) @@ -141,13 +140,13 @@ func (c *client) DeleteConnectionSetting(objectId string) error { defer utils.CloseBodyAfterRequest(response) if err != nil { - return fmt.Errorf("error making post request to dynatrace api: %w", err) + return errors.WithMessage(err, "error making post request to dynatrace api") } _, err = c.getSettingsApiResponseData(response) if err != nil { - return fmt.Errorf("error reading response data: %w", err) + return errors.WithMessage(err, "error reading response data") } return nil diff --git a/pkg/clients/edgeconnect/environment_settings_test.go b/pkg/clients/edgeconnect/environment_settings_test.go index da9a12ce4c..a069483db9 100644 --- a/pkg/clients/edgeconnect/environment_settings_test.go +++ b/pkg/clients/edgeconnect/environment_settings_test.go @@ -164,8 +164,7 @@ func writeEnvironmentSettingsResponse(w http.ResponseWriter, status int) { } func writeSettingsApiResponse(w http.ResponseWriter, status int) { - errorResponse := []SettingsApiResponse{} - errorResponse = append(errorResponse, SettingsApiResponse{ + errorResponse := SettingsApiResponse{ Error: SettingsApiError{ Message: "test-message", ConstraintViolations: []ConstraintViolations{ @@ -178,8 +177,8 @@ func writeSettingsApiResponse(w http.ResponseWriter, status int) { Code: status, }, Code: status, - }, - ) + } + result, _ := json.Marshal(&errorResponse) w.WriteHeader(status) diff --git a/pkg/consts/agent_injection.go b/pkg/consts/agent_injection.go index 19bdc00311..8fb640d5bc 100644 --- a/pkg/consts/agent_injection.go +++ b/pkg/consts/agent_injection.go @@ -8,6 +8,8 @@ const ( AgentInitSecretName = "dynatrace-dynakube-config" AgentInitSecretConfigField = "config" + BootstrapperInitSecretName = "dynatrace-bootstrapper-config" + LdPreloadFilename = "ld.so.preload" LibAgentProcPath = "/agent/lib64/liboneagentproc.so" @@ -26,6 +28,7 @@ const ( AgentShareDirMount = "/mnt/share" AgentConfigDirMount = "/mnt/config" AgentConfInitDirMount = "/mnt/agent-conf" + AgentCodeModuleSource = "/opt/dynatrace/oneagent" TrustedCAsInitSecretField = "trustedcas" ActiveGateCAsInitSecretField = "agcerts" diff --git a/pkg/consts/extensions.go b/pkg/consts/extensions.go new file mode 100644 index 0000000000..78386dab39 --- /dev/null +++ b/pkg/consts/extensions.go @@ -0,0 +1,11 @@ +package consts + +const ( + ExtensionsSelfSignedTLSSecretSuffix = "-extensions-controller-tls" + + // shared volume name between eec and OtelC + ExtensionsTokensVolumeName = "tokens" + + ExtensionsControllerSuffix = "-extensions-controller" + ExtensionsCollectorTargetPortName = "collector-com" +) diff --git a/pkg/consts/otelc.go b/pkg/consts/otelc.go new file mode 100644 index 0000000000..4e655cc3d9 --- /dev/null +++ b/pkg/consts/otelc.go @@ -0,0 +1,7 @@ +package consts + +const ( + OtelcTokenSecretKey = "otelc.token" + OtelcTokenSecretValuePrefix = "dt0x01" + OtelCollectorComPort = 14599 +) diff --git a/pkg/consts/tls.go b/pkg/consts/tls.go new file mode 100644 index 0000000000..1efb27d753 --- /dev/null +++ b/pkg/consts/tls.go @@ -0,0 +1,10 @@ +package consts + +const ( + + // TLSKeyDataName is the key used to store a TLS private key in the secret's data field. + TLSKeyDataName = "tls.key" + + // TLSCrtDataName is the key used to store a TLS certificate in the secret's data field. + TLSCrtDataName = "tls.crt" +) diff --git a/pkg/controllers/certificates/certificate_secret_test.go b/pkg/controllers/certificates/certificate_secret_test.go index 14255822ba..976da7f1c5 100644 --- a/pkg/controllers/certificates/certificate_secret_test.go +++ b/pkg/controllers/certificates/certificate_secret_test.go @@ -150,7 +150,7 @@ func TestCreateOrUpdateIfNecessary(t *testing.T) { require.NoError(t, err) assert.NotNil(t, newSecret) - assert.EqualValues(t, certSecret.certificates.Data, newSecret.Data) + assert.Equal(t, certSecret.certificates.Data, newSecret.Data) }) t.Run(`update if secret exists`, func(t *testing.T) { fakeClient := fake.NewClient() @@ -174,7 +174,7 @@ func TestCreateOrUpdateIfNecessary(t *testing.T) { require.NoError(t, err) require.NotNil(t, newSecret) - require.EqualValues(t, certSecret.certificates.Data, newSecret.Data) + require.Equal(t, certSecret.certificates.Data, newSecret.Data) certSecret.secret = &newSecret certSecret.certificates.Data = map[string][]byte{testKey: testValue2} @@ -187,7 +187,7 @@ func TestCreateOrUpdateIfNecessary(t *testing.T) { require.NoError(t, err) assert.NotNil(t, newSecret) - assert.EqualValues(t, certSecret.certificates.Data, newSecret.Data) + assert.Equal(t, certSecret.certificates.Data, newSecret.Data) }) } diff --git a/pkg/controllers/certificates/certs.go b/pkg/controllers/certificates/certs.go index b641155c9b..2911e10a8a 100644 --- a/pkg/controllers/certificates/certs.go +++ b/pkg/controllers/certificates/certs.go @@ -7,11 +7,11 @@ import ( "crypto/x509" "crypto/x509/pkix" "encoding/pem" - "fmt" "math/big" "time" "github.com/Dynatrace/dynatrace-operator/pkg/util/certificates" + "github.com/pkg/errors" ) const intSerialNumberLimit = 128 @@ -136,7 +136,7 @@ func (cs *Certs) generateRootCerts(domain string, now time.Time) error { // Generate CA root certificate serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) if err != nil { - return fmt.Errorf("failed to generate serial number for root certificate: %w", err) + return errors.WithMessage(err, "failed to generate serial number for root certificate") } cs.rootPublicCert = &x509.Certificate{ @@ -166,7 +166,7 @@ func (cs *Certs) generateRootCerts(domain string, now time.Time) error { cs.rootPrivateKey.Public(), cs.rootPrivateKey) if err != nil { - return fmt.Errorf("failed to generate root certificate: %w", err) + return errors.WithMessage(err, "failed to generate root certificate") } cs.Data[RootCertOld] = cs.Data[RootCert] @@ -189,7 +189,7 @@ func (cs *Certs) generateServerCerts(domain string, now time.Time) error { // Generate server certificate serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) if err != nil { - return fmt.Errorf("failed to generate serial number for server certificate: %w", err) + return errors.WithMessage(err, "failed to generate serial number for server certificate") } tpl := &x509.Certificate{ @@ -215,7 +215,7 @@ func (cs *Certs) generateServerCerts(domain string, now time.Time) error { serverPublicCertDER, err := x509.CreateCertificate(rand.Reader, tpl, cs.rootPublicCert, privateKey.Public(), cs.rootPrivateKey) if err != nil { - return fmt.Errorf("failed to generate server certificate: %w", err) + return errors.WithMessage(err, "failed to generate server certificate") } cs.Data[ServerCert] = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: serverPublicCertDER}) @@ -228,7 +228,7 @@ func (cs *Certs) generateServerCerts(domain string, now time.Time) error { func (cs *Certs) generatePrivateKey(dataKey string) (*ecdsa.PrivateKey, error) { privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { - return nil, fmt.Errorf("failed to generate server private key: %w", err) + return nil, errors.WithMessage(err, "failed to generate server private key") } x509Encoded, err := x509.MarshalECPrivateKey(privateKey) diff --git a/pkg/controllers/certificates/certs_test.go b/pkg/controllers/certificates/certs_test.go index 5f50b9a512..916704c43f 100644 --- a/pkg/controllers/certificates/certs_test.go +++ b/pkg/controllers/certificates/certs_test.go @@ -31,7 +31,7 @@ func TestCertsValidation(t *testing.T) { // No changes should have been applied. assert.Equal(t, string(firstCerts.Data[RootCert]), string(newCerts.Data[RootCert])) - assert.Equal(t, "", string(firstCerts.Data[RootCertOld])) + assert.Empty(t, string(firstCerts.Data[RootCertOld])) assert.Equal(t, string(firstCerts.Data[RootKey]), string(newCerts.Data[RootKey])) assert.Equal(t, string(firstCerts.Data[ServerCert]), string(newCerts.Data[ServerCert])) assert.Equal(t, string(firstCerts.Data[ServerKey]), string(newCerts.Data[ServerKey])) @@ -46,7 +46,7 @@ func TestCertsValidation(t *testing.T) { // Server certificates should have been updated. assert.Equal(t, string(firstCerts.Data[RootCert]), string(newCerts.Data[RootCert])) - assert.Equal(t, "", string(firstCerts.Data[RootCertOld])) + assert.Empty(t, string(firstCerts.Data[RootCertOld])) assert.Equal(t, string(firstCerts.Data[RootKey]), string(newCerts.Data[RootKey])) assert.NotEqual(t, string(firstCerts.Data[ServerCert]), string(newCerts.Data[ServerCert])) assert.NotEqual(t, string(firstCerts.Data[ServerKey]), string(newCerts.Data[ServerKey])) diff --git a/pkg/controllers/certificates/webhook_cert_controller.go b/pkg/controllers/certificates/webhook_cert_controller.go index 39247c962f..5597ed81d1 100644 --- a/pkg/controllers/certificates/webhook_cert_controller.go +++ b/pkg/controllers/certificates/webhook_cert_controller.go @@ -36,7 +36,7 @@ func Add(mgr manager.Manager, ns string) error { Complete(newWebhookCertificateController(mgr, nil)) } -func AddBootstrap(mgr manager.Manager, ns string, cancelMgr context.CancelFunc) error { +func AddInit(mgr manager.Manager, ns string, cancelMgr context.CancelFunc) error { return ctrl.NewControllerManagedBy(mgr). For(&appsv1.Deployment{}). Named("webhook-boostrap-controller"). diff --git a/pkg/controllers/csi/config.go b/pkg/controllers/csi/config.go index 5f787bbead..4f27ba2756 100644 --- a/pkg/controllers/csi/config.go +++ b/pkg/controllers/csi/config.go @@ -16,10 +16,6 @@ limitations under the License. package dtcsi -import ( - "path/filepath" -) - const ( DataPath = "/data" DriverName = "csi.oneagent.dynatrace.com" @@ -30,6 +26,9 @@ const ( OverlayVarDirPath = "var" OverlayWorkDirPath = "work" SharedAgentBinDir = "codemodules" + SharedJobWorkDir = "work" + SharedAppMountsDir = "appmounts" + SharedDynaKubesDir = "_dynakubes" SharedAgentConfigDir = "config" DaemonSetName = "dynatrace-oneagent-csi-driver" @@ -37,8 +36,6 @@ const ( UnixUmask = 0000 ) -var MetadataAccessPath = filepath.Join(DataPath, "csi.db?_journal=WAL") - type CSIOptions struct { NodeId string Endpoint string diff --git a/pkg/controllers/csi/driver/server.go b/pkg/controllers/csi/driver/server.go index e3ee8fd931..59d437dc4a 100644 --- a/pkg/controllers/csi/driver/server.go +++ b/pkg/controllers/csi/driver/server.go @@ -22,6 +22,7 @@ import ( "net" "net/url" "os" + "path/filepath" "runtime" "strconv" "sync/atomic" @@ -34,6 +35,7 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi/metadata" "github.com/Dynatrace/dynatrace-operator/pkg/version" "github.com/container-storage-interface/spec/lib/go/csi" + "github.com/pkg/errors" "github.com/spf13/afero" "google.golang.org/grpc" "google.golang.org/grpc/codes" @@ -52,7 +54,6 @@ type Server struct { fs afero.Afero mounter mount.Interface - db metadata.Access publishers map[string]csivolumes.Publisher opts dtcsi.CSIOptions @@ -62,46 +63,44 @@ type Server struct { var _ csi.IdentityServer = &Server{} var _ csi.NodeServer = &Server{} -func NewServer(opts dtcsi.CSIOptions, db metadata.Access) *Server { +func NewServer(opts dtcsi.CSIOptions) *Server { return &Server{ opts: opts, fs: afero.Afero{Fs: afero.NewOsFs()}, mounter: mount.New(""), - db: db, path: metadata.PathResolver{RootDir: opts.RootDir}, } } -func (svr *Server) SetupWithManager(mgr ctrl.Manager) error { - return mgr.Add(svr) +func (srv *Server) SetupWithManager(mgr ctrl.Manager) error { + return mgr.Add(srv) } -func (svr *Server) Start(ctx context.Context) error { - defer metadata.LogAccessOverview(svr.db) - - endpoint, err := url.Parse(svr.opts.Endpoint) - addr := endpoint.Host + endpoint.Path +func (srv *Server) Start(ctx context.Context) error { + endpoint, err := url.Parse(srv.opts.Endpoint) if err != nil { - return fmt.Errorf("failed to parse endpoint '%s': %w", svr.opts.Endpoint, err) + return errors.WithMessage(err, fmt.Sprintf("failed to parse endpoint '%s'", srv.opts.Endpoint)) } + addr := endpoint.Host + endpoint.Path + if endpoint.Scheme == "unix" { - if err := svr.fs.Remove(addr); err != nil && !os.IsNotExist(err) { - return fmt.Errorf("failed to remove old endpoint on '%s': %w", addr, err) + if err := srv.fs.Remove(addr); err != nil && !os.IsNotExist(err) { + return errors.WithMessage(err, fmt.Sprintf("failed to remove old endpoint on '%s'", addr)) } } - svr.publishers = map[string]csivolumes.Publisher{ - appvolumes.Mode: appvolumes.NewAppVolumePublisher(svr.fs, svr.mounter, svr.db, svr.path), - hostvolumes.Mode: hostvolumes.NewHostVolumePublisher(svr.fs, svr.mounter, svr.db, svr.path), + srv.publishers = map[string]csivolumes.Publisher{ + appvolumes.Mode: appvolumes.NewPublisher(srv.fs, srv.mounter, srv.path), + hostvolumes.Mode: hostvolumes.NewPublisher(srv.fs, srv.mounter, srv.path), } log.Info("starting listener", "scheme", endpoint.Scheme, "address", addr) listener, err := net.Listen(endpoint.Scheme, addr) if err != nil { - return fmt.Errorf("failed to start server: %w", err) + return errors.WithMessage(err, "failed to start server") } maxGrpcRequests, err := strconv.Atoi(os.Getenv("GRPC_MAX_REQUESTS_LIMIT")) @@ -132,8 +131,8 @@ func (svr *Server) Start(ctx context.Context) error { } }() - csi.RegisterIdentityServer(server, svr) - csi.RegisterNodeServer(server, svr) + csi.RegisterIdentityServer(server, srv) + csi.RegisterNodeServer(server, srv) log.Info("listening for connections on address", "address", listener.Addr()) @@ -143,31 +142,31 @@ func (svr *Server) Start(ctx context.Context) error { return err } -func (svr *Server) GetPluginInfo(context.Context, *csi.GetPluginInfoRequest) (*csi.GetPluginInfoResponse, error) { +func (srv *Server) GetPluginInfo(context.Context, *csi.GetPluginInfoRequest) (*csi.GetPluginInfoResponse, error) { return &csi.GetPluginInfoResponse{Name: dtcsi.DriverName, VendorVersion: version.Version}, nil } -func (svr *Server) Probe(context.Context, *csi.ProbeRequest) (*csi.ProbeResponse, error) { +func (srv *Server) Probe(context.Context, *csi.ProbeRequest) (*csi.ProbeResponse, error) { return &csi.ProbeResponse{}, nil } -func (svr *Server) GetPluginCapabilities(context.Context, *csi.GetPluginCapabilitiesRequest) (*csi.GetPluginCapabilitiesResponse, error) { +func (srv *Server) GetPluginCapabilities(context.Context, *csi.GetPluginCapabilitiesRequest) (*csi.GetPluginCapabilitiesResponse, error) { return &csi.GetPluginCapabilitiesResponse{}, nil } -func (svr *Server) NodePublishVolume(ctx context.Context, req *csi.NodePublishVolumeRequest) (*csi.NodePublishVolumeResponse, error) { +func (srv *Server) NodePublishVolume(ctx context.Context, req *csi.NodePublishVolumeRequest) (*csi.NodePublishVolumeResponse, error) { volumeCfg, err := csivolumes.ParseNodePublishVolumeRequest(req) if err != nil { return nil, err } - if isMounted, err := svr.mounter.IsMountPoint(volumeCfg.TargetPath); err != nil && !os.IsNotExist(err) { + if isMounted, err := srv.mounter.IsMountPoint(volumeCfg.TargetPath); err != nil && !os.IsNotExist(err) { return nil, err } else if isMounted { return &csi.NodePublishVolumeResponse{}, nil } - publisher, ok := svr.publishers[volumeCfg.Mode] + publisher, ok := srv.publishers[volumeCfg.Mode] if !ok { return nil, status.Error(codes.Internal, "unknown csi mode provided, mode="+volumeCfg.Mode) } @@ -182,65 +181,107 @@ func (svr *Server) NodePublishVolume(ctx context.Context, req *csi.NodePublishVo "mountflags", req.GetVolumeCapability().GetMount().GetMountFlags(), ) - return publisher.PublishVolume(ctx, volumeCfg) + return publisher.PublishVolume(ctx, &volumeCfg) } -func (svr *Server) NodeUnpublishVolume(ctx context.Context, req *csi.NodeUnpublishVolumeRequest) (*csi.NodeUnpublishVolumeResponse, error) { +func (srv *Server) NodeUnpublishVolume(ctx context.Context, req *csi.NodeUnpublishVolumeRequest) (*csi.NodeUnpublishVolumeResponse, error) { volumeInfo, err := csivolumes.ParseNodeUnpublishVolumeRequest(req) if err != nil { return nil, err } - for _, publisher := range svr.publishers { - canUnpublish, err := publisher.CanUnpublishVolume(ctx, volumeInfo) - if err != nil { - log.Error(err, "couldn't determine if volume can be unpublished", "publisher", publisher) + srv.unmount(volumeInfo) + + return &csi.NodeUnpublishVolumeResponse{}, nil +} + +func (srv *Server) unmount(volumeInfo csivolumes.VolumeInfo) { + // targetPath always needs to be unmounted + if err := srv.mounter.Unmount(volumeInfo.TargetPath); err != nil { + log.Error(err, "Unmount failed", "path", volumeInfo.TargetPath) + } + + appMountDir := srv.path.AppMountForID(volumeInfo.VolumeID) + + mappedDir := srv.path.AppMountMappedDir(volumeInfo.VolumeID) // Unmount follows symlinks, so no need to check for them here + + _, err := srv.fs.Stat(mappedDir) + if os.IsNotExist(err) { // case for timed out mounts + _ = srv.fs.RemoveAll(appMountDir) + + return + } else if err != nil { + log.Error(err, "unexpected error when checking for app mount folder, trying to unmount just to be sure") + } + + if err := srv.mounter.Unmount(mappedDir); err != nil { + // Just try to unmount, nothing really can go wrong, just have to handle errors + log.Error(err, "Unmount failed", "path", mappedDir) + } else { + // special handling is needed, because after upgrade/restart the mappedDir will be still busy + needsCleanUp := []string{ + srv.path.AppMountVarDir(volumeInfo.VolumeID), + srv.path.AppMountWorkDir(volumeInfo.VolumeID), } - if canUnpublish { - response, err := publisher.UnpublishVolume(ctx, volumeInfo) - if err != nil { - return nil, err + for _, path := range needsCleanUp { + podInfoSymlinkPath := srv.findPodInfoSymlink(volumeInfo) // cleaning up the pod-info symlink here is far more efficient instead of having to walk the whole fs during cleanup + if podInfoSymlinkPath != "" { + _ = srv.fs.Remove(podInfoSymlinkPath) + + podInfoSymlinkDir := filepath.Dir(podInfoSymlinkPath) + + if isEmpty, _ := srv.fs.IsEmpty(podInfoSymlinkDir); isEmpty { + _ = srv.fs.Remove(podInfoSymlinkDir) + } } - return response, nil + err := srv.fs.RemoveAll(path) // you see correctly, we don't keep the logs of the app mounts, will keep them when they will have a use + if err != nil { + log.Error(err, "failed to clean up unmounted volume dir", "path", path) + } } - } - - svr.unmountUnknownVolume(*volumeInfo) - return &csi.NodeUnpublishVolumeResponse{}, nil + _ = srv.fs.RemoveAll(appMountDir) // try to cleanup fully, but lets not spam the logs with errors + } } -func (svr *Server) unmountUnknownVolume(volumeInfo csivolumes.VolumeInfo) { - log.Info("VolumeID not present in the database", "volumeID", volumeInfo.VolumeID, "targetPath", volumeInfo.TargetPath) +func (srv *Server) findPodInfoSymlink(volumeInfo csivolumes.VolumeInfo) string { + podInfoPath := srv.path.OverlayVarPodInfo(volumeInfo.VolumeID) + + podInfoBytes, err := srv.fs.ReadFile(srv.path.OverlayVarPodInfo(volumeInfo.VolumeID)) + if err != nil { + if os.IsNotExist(err) { + return "" + } - if err := svr.mounter.Unmount(volumeInfo.TargetPath); err != nil { - log.Error(err, "Tried to unmount unknown volume", "volumeID", volumeInfo.VolumeID) + log.Error(err, "failed to read pod-info file", "path", podInfoPath) } + + return string(podInfoBytes) } -func (svr *Server) NodeStageVolume(context.Context, *csi.NodeStageVolumeRequest) (*csi.NodeStageVolumeResponse, error) { +func (srv *Server) NodeStageVolume(context.Context, *csi.NodeStageVolumeRequest) (*csi.NodeStageVolumeResponse, error) { return nil, status.Error(codes.Unimplemented, "") } -func (svr *Server) NodeUnstageVolume(context.Context, *csi.NodeUnstageVolumeRequest) (*csi.NodeUnstageVolumeResponse, error) { +func (srv *Server) NodeUnstageVolume(context.Context, *csi.NodeUnstageVolumeRequest) (*csi.NodeUnstageVolumeResponse, error) { return nil, status.Error(codes.Unimplemented, "") } -func (svr *Server) NodeGetInfo(context.Context, *csi.NodeGetInfoRequest) (*csi.NodeGetInfoResponse, error) { - return &csi.NodeGetInfoResponse{NodeId: svr.opts.NodeId}, nil +func (srv *Server) NodeGetInfo(context.Context, *csi.NodeGetInfoRequest) (*csi.NodeGetInfoResponse, error) { + return &csi.NodeGetInfoResponse{NodeId: srv.opts.NodeId}, nil } -func (svr *Server) NodeGetCapabilities(context.Context, *csi.NodeGetCapabilitiesRequest) (*csi.NodeGetCapabilitiesResponse, error) { +func (srv *Server) NodeGetCapabilities(context.Context, *csi.NodeGetCapabilitiesRequest) (*csi.NodeGetCapabilitiesResponse, error) { return &csi.NodeGetCapabilitiesResponse{Capabilities: []*csi.NodeServiceCapability{}}, nil } -func (svr *Server) NodeGetVolumeStats(context.Context, *csi.NodeGetVolumeStatsRequest) (*csi.NodeGetVolumeStatsResponse, error) { +func (srv *Server) NodeGetVolumeStats(context.Context, *csi.NodeGetVolumeStatsRequest) (*csi.NodeGetVolumeStatsResponse, error) { return nil, status.Error(codes.Unimplemented, "") } -func (svr *Server) NodeExpandVolume(context.Context, *csi.NodeExpandVolumeRequest) (*csi.NodeExpandVolumeResponse, error) { +func (srv *Server) NodeExpandVolume(context.Context, *csi.NodeExpandVolumeRequest) (*csi.NodeExpandVolumeResponse, error) { return nil, status.Error(codes.Unimplemented, "") } @@ -248,19 +289,38 @@ func grpcLimiter(maxGrpcRequests int32) grpc.UnaryServerInterceptor { return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { var methodName string - switch { - case info.FullMethod == "/csi.v1.Node/NodePublishVolume": + var logValues []any + + switch info.FullMethod { + case "/csi.v1.Node/NodePublishVolume": req := req.(*csi.NodePublishVolumeRequest) methodName = "NodePublishVolume" - log.Info("GRPC call", "method", methodName, "volume-id", req.GetVolumeId()) - case info.FullMethod == "/csi.v1.Node/NodeUnpublishVolume": + vc, _ := csivolumes.ParseNodePublishVolumeRequest(req) + logValues = []any{ + "method", methodName, + "volume-id", vc.VolumeID, + "pod", vc.PodName, + "namespace", vc.PodNamespace, + "dynakube", vc.DynakubeName, + "mode", vc.Mode, + } + log.Info("GRPC call", logValues...) + case "/csi.v1.Node/NodeUnpublishVolume": req := req.(*csi.NodeUnpublishVolumeRequest) methodName = "NodeUnpublishVolume" - log.Info("GRPC call", "method", methodName, "volume-id", req.GetVolumeId()) + vi, _ := csivolumes.ParseNodeUnpublishVolumeRequest(req) + logValues = []any{ // this is all we get + "method", methodName, + "volume-id", vi.VolumeID, + "target-path", vi.TargetPath, + } + log.Info("GRPC call", logValues...) default: + log.Debug("GRPC call", "full_method", info.FullMethod) + resp, err := handler(ctx, req) if err != nil { - log.Error(err, "GRPC failed", "full_method", info.FullMethod) + log.Info("GRPC failed", "full_method", info.FullMethod, "err", err.Error()) } return resp, err @@ -270,13 +330,19 @@ func grpcLimiter(maxGrpcRequests int32) grpc.UnaryServerInterceptor { defer counter.Add(-1) if counter.Load() > maxGrpcRequests { - return nil, status.Error(codes.ResourceExhausted, fmt.Sprintf("rate limit exceeded, current value %d more than max %d", counter.Load(), MaxGrpcRequests)) + msg := fmt.Sprintf("rate limit exceeded, current value %d more than max %d", counter.Load(), MaxGrpcRequests) + + log.Info(msg, logValues...) + + return nil, status.Error(codes.ResourceExhausted, msg) } resp, err := handler(ctx, req) if err != nil { - log.Error(err, "GRPC call failed", "method", methodName, "full_method", info.FullMethod) + logValues = append(logValues, "error", err.Error()) + + log.Info("GRPC call failed", logValues...) } return resp, err diff --git a/pkg/controllers/csi/driver/volumes/app/config.go b/pkg/controllers/csi/driver/volumes/app/config.go index 3cc6bce1aa..27ed6c84b2 100644 --- a/pkg/controllers/csi/driver/volumes/app/config.go +++ b/pkg/controllers/csi/driver/volumes/app/config.go @@ -1,4 +1,4 @@ -package appvolumes +package app import ( "github.com/Dynatrace/dynatrace-operator/pkg/logd" diff --git a/pkg/controllers/csi/driver/volumes/app/publisher.go b/pkg/controllers/csi/driver/volumes/app/publisher.go index a26b872728..17285f4712 100644 --- a/pkg/controllers/csi/driver/volumes/app/publisher.go +++ b/pkg/controllers/csi/driver/volumes/app/publisher.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package appvolumes +package app import ( "context" @@ -22,293 +22,249 @@ import ( "io" "os" "path/filepath" - "strings" + "time" - dtcsi "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi" csivolumes "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi/driver/volumes" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi/metadata" + "github.com/Dynatrace/dynatrace-operator/pkg/injection/codemodule/installer/symlink" + "github.com/Dynatrace/dynatrace-operator/pkg/util/timeprovider" "github.com/container-storage-interface/spec/lib/go/csi" "github.com/pkg/errors" - dto "github.com/prometheus/client_model/go" "github.com/spf13/afero" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" mount "k8s.io/mount-utils" ) -func NewAppVolumePublisher(fs afero.Afero, mounter mount.Interface, db metadata.Access, path metadata.PathResolver) csivolumes.Publisher { - return &AppVolumePublisher{ +func NewPublisher(fs afero.Afero, mounter mount.Interface, path metadata.PathResolver) csivolumes.Publisher { + return &Publisher{ fs: fs, mounter: mounter, - db: db, path: path, + time: timeprovider.New(), } } -type AppVolumePublisher struct { +type Publisher struct { fs afero.Afero mounter mount.Interface - db metadata.Access + time *timeprovider.Provider path metadata.PathResolver } -func (publisher *AppVolumePublisher) PublishVolume(ctx context.Context, volumeCfg *csivolumes.VolumeConfig) (*csi.NodePublishVolumeResponse, error) { - bindCfg, err := csivolumes.NewBindConfig(ctx, publisher.db, volumeCfg) - if err != nil { - return nil, err - } - - hasTooManyAttempts, err := publisher.hasTooManyMountAttempts(ctx, bindCfg, volumeCfg) - if err != nil { - return nil, err - } - - if hasTooManyAttempts { +func (pub *Publisher) PublishVolume(ctx context.Context, volumeCfg *csivolumes.VolumeConfig) (*csi.NodePublishVolumeResponse, error) { + if pub.hasRetryLimitReached(volumeCfg) { log.Info("reached max mount attempts for pod, attaching dummy volume, monitoring disabled", "pod", volumeCfg.PodName) return &csi.NodePublishVolumeResponse{}, nil } - if !bindCfg.IsArchiveAvailable() { + if !pub.isCodeModuleAvailable(volumeCfg) { return nil, status.Error( codes.Unavailable, - "version or digest is not yet set, csi-provisioner hasn't finished setup yet for tenant: "+bindCfg.TenantUUID, + "version or digest is not yet set, csi-provisioner hasn't finished setup yet for DynaKube: "+volumeCfg.DynakubeName, ) } - if err := publisher.ensureMountSteps(ctx, bindCfg, volumeCfg); err != nil { - return nil, err + if err := pub.mountCodeModule(volumeCfg); err != nil { + return nil, status.Error(codes.Internal, fmt.Sprintf("failed to mount oneagent volume: %s", err)) } - agentsVersionsMetric.WithLabelValues(bindCfg.MetricVersionLabel()).Inc() - return &csi.NodePublishVolumeResponse{}, nil } -func (publisher *AppVolumePublisher) UnpublishVolume(ctx context.Context, volumeInfo *csivolumes.VolumeInfo) (*csi.NodeUnpublishVolumeResponse, error) { - volume, err := publisher.loadVolume(ctx, volumeInfo.VolumeID) - if err != nil { - log.Info("failed to load volume info", "error", err.Error()) - } - - if volume == nil { - return &csi.NodeUnpublishVolumeResponse{}, nil - } - - log.Info("loaded volume info", "id", volume.VolumeID, "pod name", volume.PodName, "version", volume.Version, "dynakube", volume.TenantUUID) - - if volume.Version == "" { - log.Info("requester has a dummy volume, no node-level unmount is needed") - - return &csi.NodeUnpublishVolumeResponse{}, publisher.db.DeleteVolume(ctx, volume.VolumeID) - } +// hasRetryLimitReached creates the base dir for a given app mount if it doesn't exist yet, checks the creation timestamp against the threshold +// if any of the FS calls fail in an unexpected way, then it is considered that the limit was reached. +func (pub *Publisher) hasRetryLimitReached(volumeCfg *csivolumes.VolumeConfig) bool { + appDir := pub.path.AppMountForID(volumeCfg.VolumeID) - overlayFSPath := publisher.path.AgentRunDirForVolume(volume.TenantUUID, volumeInfo.VolumeID) - publisher.unmountOneAgent(volumeInfo.TargetPath, overlayFSPath) + stat, err := pub.fs.Stat(appDir) + if errors.Is(err, os.ErrNotExist) { + // First run, create folder, to keep track of time + err := pub.fs.MkdirAll(appDir, os.ModePerm) + if err != nil { + log.Error(err, "failed to create base dir for app mount, skipping injection", "dir", appDir) - if err = publisher.db.DeleteVolume(ctx, volume.VolumeID); err != nil { - return nil, status.Error(codes.Internal, err.Error()) - } + return true + } - log.Info("deleted volume info", "ID", volume.VolumeID, "PodUID", volume.PodName, "Version", volume.Version, "TenantUUID", volume.TenantUUID) + return false + } else if err != nil { + log.Error(err, "unexpected failure in checking filesystem state, skipping injection", "dir", appDir) - if err = publisher.fs.RemoveAll(volumeInfo.TargetPath); err != nil { - return nil, status.Error(codes.Internal, err.Error()) + return true } - log.Info("volume has been unpublished", "targetPath", volumeInfo.TargetPath) - - publisher.fireVolumeUnpublishedMetric(*volume) - - return &csi.NodeUnpublishVolumeResponse{}, nil -} - -func (publisher *AppVolumePublisher) CanUnpublishVolume(ctx context.Context, volumeInfo *csivolumes.VolumeInfo) (bool, error) { - volume, err := publisher.loadVolume(ctx, volumeInfo.VolumeID) - if err != nil { - return false, status.Error(codes.Internal, "failed to get volume info from database: "+err.Error()) - } + limit := stat.ModTime().Add(volumeCfg.RetryTimeout) + log.Info("not first attempt, time remaining before skipping injection", "remaining time", time.Until(limit).String()) - return volume != nil, nil + return pub.time.Now().After(limit) } -func (publisher *AppVolumePublisher) fireVolumeUnpublishedMetric(volume metadata.Volume) { - if len(volume.Version) > 0 { - agentsVersionsMetric.WithLabelValues(volume.Version).Dec() +// isCodeModuleAvailable checks if the LatestAgentBinaryForDynaKube folder exists or not +func (pub *Publisher) isCodeModuleAvailable(volumeCfg *csivolumes.VolumeConfig) bool { + binDir := pub.path.LatestAgentBinaryForDynaKube(volumeCfg.DynakubeName) - var m = &dto.Metric{} - if err := agentsVersionsMetric.WithLabelValues(volume.Version).Write(m); err != nil { - log.Error(err, "failed to get the value of agent version metric") - } + stat, err := pub.fs.Stat(binDir) + if errors.Is(err, os.ErrNotExist) { + log.Info("no CodeModule is available to mount yet, will retry later", "dynakube", volumeCfg.DynakubeName) - if m.GetGauge().GetValue() <= float64(0) { - agentsVersionsMetric.DeleteLabelValues(volume.Version) - } - } -} + return false + } else if err != nil { + log.Error(err, "unexpected failure while checking for the latest available CodeModule", "dynakube", volumeCfg.DynakubeName) -func (publisher *AppVolumePublisher) buildLowerDir(bindCfg *csivolumes.BindConfig) string { - var binFolderName string - if bindCfg.ImageDigest == "" { - binFolderName = bindCfg.Version - } else { - binFolderName = bindCfg.ImageDigest + return false } - directories := []string{ - publisher.path.AgentSharedBinaryDirForAgent(binFolderName), - } - - return strings.Join(directories, ":") + return stat.IsDir() } -func (publisher *AppVolumePublisher) prepareUpperDir(bindCfg *csivolumes.BindConfig, volumeCfg *csivolumes.VolumeConfig) (string, error) { - upperDir := publisher.path.OverlayVarDir(bindCfg.TenantUUID, volumeCfg.VolumeID) - err := publisher.fs.MkdirAll(upperDir, os.ModePerm) +func (pub *Publisher) mountCodeModule(volumeCfg *csivolumes.VolumeConfig) error { + mappedDir := pub.path.AppMountMappedDir(volumeCfg.VolumeID) + err := pub.fs.MkdirAll(mappedDir, os.ModePerm) if err != nil { - return "", errors.WithMessagef(err, "failed create overlay upper directory structure, path: %s", upperDir) + return err } - destAgentConfPath := publisher.path.OverlayVarRuxitAgentProcConf(bindCfg.TenantUUID, volumeCfg.VolumeID) - - err = publisher.fs.MkdirAll(filepath.Dir(destAgentConfPath), os.ModePerm) + upperDir, err := pub.prepareUpperDir(volumeCfg) if err != nil { - return "", errors.WithMessagef(err, "failed create overlay upper directory agent config directory structure, path: %s", upperDir) + return err } - srcAgentConfPath := publisher.path.AgentSharedRuxitAgentProcConf(bindCfg.TenantUUID, volumeCfg.DynakubeName) - srcFile, err := publisher.fs.Open(srcAgentConfPath) + workDir := pub.path.AppMountWorkDir(volumeCfg.VolumeID) + err = pub.fs.MkdirAll(workDir, os.ModePerm) if err != nil { - return "", errors.WithMessagef(err, "failed to open ruxitagentproc.conf file, path: %s", srcAgentConfPath) + return err } - defer func() { _ = srcFile.Close() }() + lowerDir := pub.path.LatestAgentBinaryForDynaKube(volumeCfg.DynakubeName) - srcStat, err := srcFile.Stat() - if err != nil { - return "", errors.WithMessage(err, "failed to get source ruxitagentproc.conf file info") + linker, ok := pub.fs.Fs.(afero.LinkReader) + if ok { // will only be !ok during unit testing + lowerDir, err = linker.ReadlinkIfPossible(lowerDir) + if err != nil { + log.Info("failed to read symlink for latest CodeModule", "symlink", lowerDir) + + return err + } } - destFile, err := publisher.fs.OpenFile(destAgentConfPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, srcStat.Mode()) - if err != nil { - return "", errors.WithMessagef(err, "failed to open destination ruxitagentproc.conf file, path: %s", destAgentConfPath) + overlayOptions := []string{ + "lowerdir=" + lowerDir, + "upperdir=" + upperDir, + "workdir=" + workDir, } - defer func() { _ = destFile.Close() }() + if err := pub.fs.MkdirAll(volumeCfg.TargetPath, os.ModePerm); err != nil { + return err + } - _, err = io.Copy(destFile, srcFile) - if err != nil { - return "", errors.WithMessagef(err, "failed to copy ruxitagentproc.conf file to overlay, from->to: %s -> %s", srcAgentConfPath, destAgentConfPath) + if err := pub.mounter.Mount("overlay", mappedDir, "overlay", overlayOptions); err != nil { + return err } - return upperDir, nil -} + if err := pub.mounter.Mount(mappedDir, volumeCfg.TargetPath, "", []string{"bind"}); err != nil { + _ = pub.mounter.Unmount(mappedDir) -func (publisher *AppVolumePublisher) mountOneAgent(bindCfg *csivolumes.BindConfig, volumeCfg *csivolumes.VolumeConfig) error { - mappedDir := publisher.path.OverlayMappedDir(bindCfg.TenantUUID, volumeCfg.VolumeID) - _ = publisher.fs.MkdirAll(mappedDir, os.ModePerm) + return err + } - upperDir, err := publisher.prepareUpperDir(bindCfg, volumeCfg) + err = pub.addPodInfoSymlink(volumeCfg) if err != nil { return err } - workDir := publisher.path.OverlayWorkDir(bindCfg.TenantUUID, volumeCfg.VolumeID) - _ = publisher.fs.MkdirAll(workDir, os.ModePerm) - - overlayOptions := []string{ - "lowerdir=" + publisher.buildLowerDir(bindCfg), - "upperdir=" + upperDir, - "workdir=" + workDir, - } + return nil +} - if err := publisher.fs.MkdirAll(volumeCfg.TargetPath, os.ModePerm); err != nil { +func (pub *Publisher) addPodInfoSymlink(volumeCfg *csivolumes.VolumeConfig) error { + appMountPodInfoDir := pub.path.AppMountPodInfoDir(volumeCfg.DynakubeName, volumeCfg.PodNamespace, volumeCfg.PodName) + if err := pub.fs.MkdirAll(appMountPodInfoDir, os.ModePerm); err != nil { return err } - if err := publisher.mounter.Mount("overlay", mappedDir, "overlay", overlayOptions); err != nil { + targetDir := pub.path.AppMountForID(volumeCfg.VolumeID) + + if err := symlink.Remove(pub.fs.Fs, appMountPodInfoDir); err != nil { return err } - if err := publisher.mounter.Mount(mappedDir, volumeCfg.TargetPath, "", []string{"bind"}); err != nil { - _ = publisher.mounter.Unmount(mappedDir) - + err := symlink.Create(pub.fs.Fs, targetDir, appMountPodInfoDir) + if err != nil { return err } return nil } -func (publisher *AppVolumePublisher) unmountOneAgent(targetPath string, overlayFSPath string) { - if err := publisher.mounter.Unmount(targetPath); err != nil { - log.Error(err, "Unmount failed", "path", targetPath) +func (pub *Publisher) prepareUpperDir(volumeCfg *csivolumes.VolumeConfig) (string, error) { + upperDir := pub.path.AppMountVarDir(volumeCfg.VolumeID) + + err := pub.prepareAgentConfigInUpperDir(volumeCfg) + if err != nil { + return "", err } - if filepath.IsAbs(overlayFSPath) { - agentDirectoryForPod := filepath.Join(overlayFSPath, dtcsi.OverlayMappedDirPath) - if err := publisher.mounter.Unmount(agentDirectoryForPod); err != nil { - log.Error(err, "Unmount failed", "path", agentDirectoryForPod) - } + err = pub.preparePodInfoUpperDir(volumeCfg) + if err != nil { + return "", err } + + return upperDir, nil } -func (publisher *AppVolumePublisher) ensureMountSteps(ctx context.Context, bindCfg *csivolumes.BindConfig, volumeCfg *csivolumes.VolumeConfig) error { - if err := publisher.mountOneAgent(bindCfg, volumeCfg); err != nil { - return status.Error(codes.Internal, fmt.Sprintf("failed to mount oneagent volume: %s", err)) +func (pub *Publisher) prepareAgentConfigInUpperDir(volumeCfg *csivolumes.VolumeConfig) error { + destAgentConfPath := pub.path.OverlayVarRuxitAgentProcConf(volumeCfg.VolumeID) + + err := pub.fs.MkdirAll(filepath.Dir(destAgentConfPath), os.ModePerm) + if err != nil { + return errors.WithMessagef(err, "failed to create overlay upper directory agent config directory structure, path: %s", destAgentConfPath) } - if err := publisher.storeVolume(ctx, bindCfg, volumeCfg); err != nil { - overlayFSPath := publisher.path.AgentRunDirForVolume(bindCfg.TenantUUID, volumeCfg.VolumeID) - publisher.unmountOneAgent(volumeCfg.TargetPath, overlayFSPath) + srcAgentConfPath := pub.path.AgentSharedRuxitAgentProcConf(volumeCfg.DynakubeName) + srcFile, err := pub.fs.Open(srcAgentConfPath) - return status.Error(codes.Internal, fmt.Sprintf("Failed to store volume info: %s", err)) + if err != nil { + return errors.WithMessagef(err, "failed to open ruxitagentproc.conf file, path: %s", srcAgentConfPath) } - return nil -} + defer func() { _ = srcFile.Close() }() -func (publisher *AppVolumePublisher) hasTooManyMountAttempts(ctx context.Context, bindCfg *csivolumes.BindConfig, volumeCfg *csivolumes.VolumeConfig) (bool, error) { - volume, err := publisher.loadVolume(ctx, volumeCfg.VolumeID) + srcStat, err := srcFile.Stat() if err != nil { - return false, err + return errors.WithMessage(err, "failed to get source ruxitagentproc.conf file info") } - if volume == nil { - volume = createNewVolume(bindCfg, volumeCfg) + destFile, err := pub.fs.OpenFile(destAgentConfPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, srcStat.Mode()) + if err != nil { + return errors.WithMessagef(err, "failed to open destination ruxitagentproc.conf file, path: %s", destAgentConfPath) } - if volume.MountAttempts > bindCfg.MaxMountAttempts { - return true, nil - } + defer func() { _ = destFile.Close() }() - volume.MountAttempts += 1 + _, err = io.Copy(destFile, srcFile) + if err != nil { + return errors.WithMessagef(err, "failed to copy ruxitagentproc.conf file to overlay, from->to: %s -> %s", srcAgentConfPath, destAgentConfPath) + } - return false, publisher.db.InsertVolume(ctx, volume) + return err } -func (publisher *AppVolumePublisher) storeVolume(ctx context.Context, bindCfg *csivolumes.BindConfig, volumeCfg *csivolumes.VolumeConfig) error { - volume := createNewVolume(bindCfg, volumeCfg) - log.Info("inserting volume info", "ID", volume.VolumeID, "PodUID", volume.PodName, "Version", volume.Version, "TenantUUID", volume.TenantUUID) - - return publisher.db.InsertVolume(ctx, volume) -} +func (pub *Publisher) preparePodInfoUpperDir(volumeCfg *csivolumes.VolumeConfig) error { + content := pub.path.AppMountPodInfoDir(volumeCfg.DynakubeName, volumeCfg.PodNamespace, volumeCfg.PodName) + destPath := pub.path.OverlayVarPodInfo(volumeCfg.VolumeID) -func (publisher *AppVolumePublisher) loadVolume(ctx context.Context, volumeID string) (*metadata.Volume, error) { - volume, err := publisher.db.GetVolume(ctx, volumeID) + destFile, err := pub.fs.OpenFile(destPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.ModePerm) if err != nil { - return nil, err + return errors.WithMessagef(err, "failed to open destination pod-info file, path: %s", destPath) } - return volume, nil -} - -func createNewVolume(bindCfg *csivolumes.BindConfig, volumeCfg *csivolumes.VolumeConfig) *metadata.Volume { - version := bindCfg.Version - if bindCfg.ImageDigest != "" { - version = bindCfg.ImageDigest + _, err = destFile.WriteString(content) + if err != nil { + return errors.WithMessagef(err, "failed write into pod-info file, path: %s", destPath) } - return metadata.NewVolume(volumeCfg.VolumeID, volumeCfg.PodName, version, bindCfg.TenantUUID, 0) + return nil } diff --git a/pkg/controllers/csi/driver/volumes/app/publisher_test.go b/pkg/controllers/csi/driver/volumes/app/publisher_test.go index 2002e0cd11..6b3c07f393 100644 --- a/pkg/controllers/csi/driver/volumes/app/publisher_test.go +++ b/pkg/controllers/csi/driver/volumes/app/publisher_test.go @@ -1,409 +1,194 @@ -package appvolumes +package app import ( "context" - "fmt" + "os" "testing" + "time" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - dtcsi "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi" csivolumes "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi/driver/volumes" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi/metadata" - "github.com/prometheus/client_golang/prometheus/testutil" + "github.com/Dynatrace/dynatrace-operator/pkg/util/timeprovider" "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "k8s.io/mount-utils" ) -const ( - testPodUID = "a-pod" - testVolumeId = "a-volume" - testTargetPath = "/path/to/container/filesystem/opt/dynatrace/oneagent-paas" - testTenantUUID = "a-tenant-uuid" - testAgentVersion = "1.2-3" - testDynakubeName = "a-dynakube" - testImageDigest = "sha256:123456789" -) - func TestPublishVolume(t *testing.T) { - t.Run("using url", func(t *testing.T) { - mounter := mount.NewFakeMounter([]mount.MountPoint{}) - publisher := newPublisherForTesting(mounter) - mockUrlDynakubeMetadata(t, &publisher) - mockSharedRuxitAgentProcConf(t, &publisher) - - response, err := publisher.PublishVolume(context.Background(), createTestVolumeConfig()) - require.NoError(t, err) - assert.NotNil(t, response) - - require.NotEmpty(t, mounter.MountPoints) - - assert.Equal(t, "overlay", mounter.MountPoints[0].Device) - assert.Equal(t, "overlay", mounter.MountPoints[0].Type) - assert.Equal(t, []string{ - "lowerdir=/codemodules/1.2-3", - "upperdir=/a-tenant-uuid/run/a-volume/var", - "workdir=/a-tenant-uuid/run/a-volume/work"}, - mounter.MountPoints[0].Opts) - assert.Equal(t, "/a-tenant-uuid/run/a-volume/mapped", mounter.MountPoints[0].Path) - - assert.Equal(t, "overlay", mounter.MountPoints[1].Device) - assert.Equal(t, "", mounter.MountPoints[1].Type) - assert.Equal(t, []string{"bind"}, mounter.MountPoints[1].Opts) - assert.Equal(t, testTargetPath, mounter.MountPoints[1].Path) - - confCopied, err := publisher.fs.Exists(publisher.path.OverlayVarRuxitAgentProcConf(testTenantUUID, testVolumeId)) - require.NoError(t, err) - assert.True(t, confCopied) - - assertReferencesForPublishedVolume(t, &publisher, mounter) - }) + ctx := context.Background() + path := metadata.PathResolver{} - t.Run("using code modules image", func(t *testing.T) { + t.Run("early return - FS problem during timeout check == skip mounting", func(t *testing.T) { + fs := getFailFs(t) mounter := mount.NewFakeMounter([]mount.MountPoint{}) - publisher := newPublisherForTesting(mounter) - mockImageDynakubeMetadata(t, &publisher) - mockSharedRuxitAgentProcConf(t, &publisher) + volumeCfg := getTestVolumeConfig(t) - response, err := publisher.PublishVolume(context.Background(), createTestVolumeConfig()) - require.NoError(t, err) - assert.NotNil(t, response) - - require.NotEmpty(t, mounter.MountPoints) - - assert.Equal(t, "overlay", mounter.MountPoints[0].Device) - assert.Equal(t, "overlay", mounter.MountPoints[0].Type) - assert.Equal(t, []string{ - "lowerdir=/codemodules/" + testImageDigest, - "upperdir=/a-tenant-uuid/run/a-volume/var", - "workdir=/a-tenant-uuid/run/a-volume/work"}, - mounter.MountPoints[0].Opts) - assert.Equal(t, "/a-tenant-uuid/run/a-volume/mapped", mounter.MountPoints[0].Path) + pub := NewPublisher(fs, mounter, path) - assert.Equal(t, "overlay", mounter.MountPoints[1].Device) - assert.Equal(t, "", mounter.MountPoints[1].Type) - assert.Equal(t, []string{"bind"}, mounter.MountPoints[1].Opts) - assert.Equal(t, testTargetPath, mounter.MountPoints[1].Path) - - confCopied, err := publisher.fs.Exists(publisher.path.OverlayVarRuxitAgentProcConf(testTenantUUID, testVolumeId)) + resp, err := pub.PublishVolume(ctx, &volumeCfg) require.NoError(t, err) - assert.True(t, confCopied) - - assertReferencesForPublishedVolumeWithCodeModulesImage(t, &publisher, mounter) + require.NotNil(t, resp) }) - t.Run("too many mount attempts", func(t *testing.T) { + t.Run("early return - retry limit reached", func(t *testing.T) { + fs := getTestFs(t) mounter := mount.NewFakeMounter([]mount.MountPoint{}) - publisher := newPublisherForTesting(mounter) - mockFailedPublishedVolume(t, &publisher) + volumeCfg := getTestVolumeConfig(t) + require.NoError(t, fs.Mkdir(path.AppMountForID(volumeCfg.VolumeID), os.ModePerm)) + + pastTime := timeprovider.New() + pastTime.Set(time.Now().Add(2 * volumeCfg.RetryTimeout)) + pub := Publisher{ + fs: fs, + mounter: mounter, + path: path, + time: pastTime, + } - response, err := publisher.PublishVolume(context.Background(), createTestVolumeConfig()) + resp, err := pub.PublishVolume(ctx, &volumeCfg) require.NoError(t, err) - assert.NotNil(t, response) + require.NotNil(t, resp) - require.Empty(t, mounter.MountPoints) + assert.Empty(t, mounter.MountPoints) }) -} - -func TestPrepareUpperDir(t *testing.T) { - testFileContent := []byte{'t', 'e', 's', 't'} - testBindConfig := &csivolumes.BindConfig{ - TenantUUID: testTenantUUID, - } - t.Run("happy path -> file copied from shared dir to overlay dir", func(t *testing.T) { + t.Run("early return (with error) - no binary present", func(t *testing.T) { + fs := getTestFs(t) mounter := mount.NewFakeMounter([]mount.MountPoint{}) + volumeCfg := getTestVolumeConfig(t) - publisher := newPublisherForTesting(mounter) - mockSharedRuxitAgentProcConf(t, &publisher, testFileContent...) + pub := NewPublisher(fs, mounter, path) - upperDir, err := publisher.prepareUpperDir(testBindConfig, createTestVolumeConfig()) - require.NoError(t, err) - require.NotEmpty(t, upperDir) - assertUpperDirContent(t, &publisher, testFileContent) + resp, err := pub.PublishVolume(ctx, &volumeCfg) + require.Error(t, err) + require.Nil(t, resp) + + assert.Empty(t, mounter.MountPoints) }) - t.Run("sad path -> source file doesn't exist -> error", func(t *testing.T) { + t.Run("early return (with error) - binary is just a file", func(t *testing.T) { + fs := getTestFs(t) mounter := mount.NewFakeMounter([]mount.MountPoint{}) + volumeCfg := getTestVolumeConfig(t) + file, err := fs.Create(path.LatestAgentBinaryForDynaKube(volumeCfg.DynakubeName)) + require.NoError(t, err) + require.NoError(t, file.Close()) - publisher := newPublisherForTesting(mounter) + pub := NewPublisher(fs, mounter, path) - upperDir, err := publisher.prepareUpperDir(testBindConfig, createTestVolumeConfig()) + resp, err := pub.PublishVolume(ctx, &volumeCfg) require.Error(t, err) - require.Empty(t, upperDir) + require.Nil(t, resp) - confCopied, err := publisher.fs.Exists(publisher.path.OverlayVarRuxitAgentProcConf(testTenantUUID, testVolumeId)) - require.NoError(t, err) - assert.False(t, confCopied) + assert.Empty(t, mounter.MountPoints) }) -} -func assertUpperDirContent(t *testing.T, publisher *AppVolumePublisher, expected []byte) { - content, err := publisher.fs.ReadFile(publisher.path.OverlayVarRuxitAgentProcConf(testTenantUUID, testVolumeId)) - require.NoError(t, err) - assert.Equal(t, expected, content) -} + t.Run("early return (with error) - ruxit.conf not present", func(t *testing.T) { + fs := getTestFs(t) + mounter := mount.NewFakeMounter([]mount.MountPoint{}) + volumeCfg := getTestVolumeConfig(t) -func TestHasTooManyMountAttempts(t *testing.T) { - t.Run(`initial try`, func(t *testing.T) { - publisher := newPublisherForTesting(nil) - bindCfg := &csivolumes.BindConfig{ - TenantUUID: testTenantUUID, - MaxMountAttempts: dynakube.DefaultMaxFailedCsiMountAttempts, - } - volumeCfg := createTestVolumeConfig() + binaryDir := path.LatestAgentBinaryForDynaKube(volumeCfg.DynakubeName) + require.NoError(t, fs.MkdirAll(binaryDir, os.ModePerm)) - hasTooManyAttempts, err := publisher.hasTooManyMountAttempts(context.Background(), bindCfg, volumeCfg) + pub := NewPublisher(fs, mounter, path) - require.NoError(t, err) - assert.False(t, hasTooManyAttempts) + resp, err := pub.PublishVolume(ctx, &volumeCfg) + require.Error(t, err) + require.Nil(t, resp) - volume, err := publisher.db.GetVolume(context.Background(), volumeCfg.VolumeID) - require.NoError(t, err) - require.NotNil(t, volume) - assert.Equal(t, 1, volume.MountAttempts) + assert.Empty(t, mounter.MountPoints) }) - t.Run(`too many mount attempts`, func(t *testing.T) { - publisher := newPublisherForTesting(nil) - mockFailedPublishedVolume(t, &publisher) - - bindCfg := &csivolumes.BindConfig{ - MaxMountAttempts: dynakube.DefaultMaxFailedCsiMountAttempts, - } - hasTooManyAttempts, err := publisher.hasTooManyMountAttempts(context.Background(), bindCfg, createTestVolumeConfig()) - - require.NoError(t, err) - assert.True(t, hasTooManyAttempts) - }) -} + t.Run("happy path", func(t *testing.T) { + fs := getTestFs(t) + mounter := mount.NewFakeMounter([]mount.MountPoint{}) + volumeCfg := getTestVolumeConfig(t) -func TestUnpublishVolume(t *testing.T) { - t.Run(`valid metadata`, func(t *testing.T) { - resetMetrics() + // Binary present + binaryDir := path.LatestAgentBinaryForDynaKube(volumeCfg.DynakubeName) + require.NoError(t, fs.MkdirAll(binaryDir, os.ModePerm)) - mounter := mount.NewFakeMounter([]mount.MountPoint{ - {Path: testTargetPath}, - {Path: fmt.Sprintf("/%s/run/%s/mapped", testTenantUUID, testVolumeId)}, - }) - publisher := newPublisherForTesting(mounter) - mockPublishedVolume(t, &publisher) + // Config present + confFile := path.AgentSharedRuxitAgentProcConf(volumeCfg.DynakubeName) + conf := []byte("testing") + require.NoError(t, fs.WriteFile(confFile, conf, os.ModePerm)) - assert.Equal(t, 1, testutil.CollectAndCount(agentsVersionsMetric)) - assert.InEpsilon(t, 1, testutil.ToFloat64(agentsVersionsMetric.WithLabelValues(testAgentVersion)), 0.01) + pub := NewPublisher(fs, mounter, path) - response, err := publisher.UnpublishVolume(context.Background(), createTestVolumeInfo()) + resp, err := pub.PublishVolume(ctx, &volumeCfg) require.NoError(t, err) + require.NotNil(t, resp) - assert.Equal(t, 0, testutil.CollectAndCount(agentsVersionsMetric)) - assert.InDelta(t, 0, testutil.ToFloat64(agentsVersionsMetric.WithLabelValues(testAgentVersion)), 0.01) + // Directories created correctly + targetDirExists, _ := fs.IsDir(volumeCfg.TargetPath) + assert.True(t, targetDirExists) - require.NotNil(t, response) - require.Empty(t, mounter.MountPoints) - assertNoReferencesForUnpublishedVolume(t, &publisher) - }) + varDir := path.AppMountVarDir(volumeCfg.VolumeID) + varDirExits, _ := fs.IsDir(varDir) + assert.True(t, varDirExits) - t.Run(`invalid metadata`, func(t *testing.T) { - resetMetrics() + mappedDir := path.AppMountMappedDir(volumeCfg.VolumeID) + mappedDirExits, _ := fs.IsDir(mappedDir) + assert.True(t, mappedDirExits) - mounter := mount.NewFakeMounter([]mount.MountPoint{ - {Path: testTargetPath}, - {Path: fmt.Sprintf("/%s/run/%s/mapped", testTenantUUID, testVolumeId)}, - }) - publisher := newPublisherForTesting(mounter) - - response, err := publisher.UnpublishVolume(context.Background(), createTestVolumeInfo()) + workDir := path.AppMountWorkDir(volumeCfg.VolumeID) + workDirExits, _ := fs.IsDir(workDir) + assert.True(t, workDirExits) + // Config copied correctly, original untouched + copiedConfFile := path.OverlayVarRuxitAgentProcConf(volumeCfg.VolumeID) + copiedConf, err := fs.ReadFile(copiedConfFile) require.NoError(t, err) - require.NotNil(t, response) - require.NotEmpty(t, mounter.MountPoints) - assertNoReferencesForUnpublishedVolume(t, &publisher) - }) - - t.Run(`remove dummy volume created after too many failed attempts`, func(t *testing.T) { - resetMetrics() - - mounter := mount.NewFakeMounter([]mount.MountPoint{}) - publisher := newPublisherForTesting(mounter) - mockFailedPublishedVolume(t, &publisher) - - response, err := publisher.UnpublishVolume(context.Background(), createTestVolumeInfo()) + assert.Equal(t, conf, copiedConf) + originalConf, err := fs.ReadFile(confFile) require.NoError(t, err) - require.NotNil(t, response) - require.Empty(t, mounter.MountPoints) + assert.Equal(t, conf, originalConf) + + // Mount happened + // mounter.IsMountPoint can't be used as it uses os.Stat + require.Len(t, mounter.MountPoints, 2) + overlayMount := mounter.MountPoints[0] + assert.Equal(t, "overlay", overlayMount.Device) + assert.Equal(t, mappedDir, overlayMount.Path) + require.Len(t, overlayMount.Opts, 3) + assert.Contains(t, overlayMount.Opts[0], binaryDir) // lowerdir + assert.Contains(t, overlayMount.Opts[1], varDir) // upperdir + assert.Contains(t, overlayMount.Opts[2], workDir) // workdir + + bindMount := mounter.MountPoints[1] + assert.Equal(t, "overlay", bindMount.Device) // this is set to "overlay" by the FakeMounter to mimic a linux FS + assert.Equal(t, volumeCfg.TargetPath, bindMount.Path) }) } -func TestNodePublishAndUnpublishVolume(t *testing.T) { - resetMetrics() - - mounter := mount.NewFakeMounter([]mount.MountPoint{}) - publisher := newPublisherForTesting(mounter) - mockUrlDynakubeMetadata(t, &publisher) - mockSharedRuxitAgentProcConf(t, &publisher) - - publishResponse, err := publisher.PublishVolume(context.Background(), createTestVolumeConfig()) - - require.NoError(t, err) - assert.Equal(t, 1, testutil.CollectAndCount(agentsVersionsMetric)) - assert.InEpsilon(t, 1, testutil.ToFloat64(agentsVersionsMetric.WithLabelValues(testAgentVersion)), 0.01) - - require.NoError(t, err) - assert.NotNil(t, publishResponse) - assert.NotEmpty(t, mounter.MountPoints) - assertReferencesForPublishedVolume(t, &publisher, mounter) - - unpublishResponse, err := publisher.UnpublishVolume(context.Background(), createTestVolumeInfo()) - - assert.Equal(t, 0, testutil.CollectAndCount(agentsVersionsMetric)) - assert.InDelta(t, 0, testutil.ToFloat64(agentsVersionsMetric.WithLabelValues(testAgentVersion)), 0.01) - - require.NoError(t, err) - require.NotNil(t, unpublishResponse) - require.Empty(t, mounter.MountPoints) - assertNoReferencesForUnpublishedVolume(t, &publisher) -} - -func TestStoreAndLoadPodInfo(t *testing.T) { - mounter := mount.NewFakeMounter([]mount.MountPoint{}) - publisher := newPublisherForTesting(mounter) - - bindCfg := &csivolumes.BindConfig{ - Version: testAgentVersion, - TenantUUID: testTenantUUID, - } - - volumeCfg := createTestVolumeConfig() - - err := publisher.storeVolume(context.Background(), bindCfg, volumeCfg) - require.NoError(t, err) - volume, err := publisher.loadVolume(context.Background(), volumeCfg.VolumeID) - require.NoError(t, err) - require.NotNil(t, volume) - assert.Equal(t, testVolumeId, volume.VolumeID) - assert.Equal(t, testPodUID, volume.PodName) - assert.Equal(t, testAgentVersion, volume.Version) - assert.Equal(t, testTenantUUID, volume.TenantUUID) -} - -func TestLoadPodInfo_Empty(t *testing.T) { - mounter := mount.NewFakeMounter([]mount.MountPoint{}) - publisher := newPublisherForTesting(mounter) - - volume, err := publisher.loadVolume(context.Background(), testVolumeId) - require.NoError(t, err) - require.Nil(t, volume) -} - -func TestMountIfDBHasError(t *testing.T) { - mounter := mount.NewFakeMounter([]mount.MountPoint{}) - publisher := newPublisherForTesting(mounter) - publisher.db = &metadata.FakeFailDB{} - - bindCfg := &csivolumes.BindConfig{ - TenantUUID: testTenantUUID, - MaxMountAttempts: dynakube.DefaultMaxFailedCsiMountAttempts, - } - - err := publisher.ensureMountSteps(context.Background(), bindCfg, createTestVolumeConfig()) - require.Error(t, err) - require.Empty(t, mounter.MountPoints) -} - -func newPublisherForTesting(mounter *mount.FakeMounter) AppVolumePublisher { - csiOptions := dtcsi.CSIOptions{RootDir: "/"} +func getTestFs(t *testing.T) afero.Afero { + t.Helper() - tmpFs := afero.NewMemMapFs() - - return AppVolumePublisher{ - fs: afero.Afero{Fs: tmpFs}, - mounter: mounter, - db: metadata.FakeMemoryDB(), - path: metadata.PathResolver{RootDir: csiOptions.RootDir}, - } -} - -func mockPublishedVolume(t *testing.T, publisher *AppVolumePublisher) { - mockUrlDynakubeMetadata(t, publisher) - err := publisher.db.InsertVolume(context.Background(), metadata.NewVolume(testVolumeId, testPodUID, testAgentVersion, testTenantUUID, 0)) - require.NoError(t, err) - agentsVersionsMetric.WithLabelValues(testAgentVersion).Inc() -} - -func mockFailedPublishedVolume(t *testing.T, publisher *AppVolumePublisher) { - mockUrlDynakubeMetadata(t, publisher) - err := publisher.db.InsertVolume(context.Background(), metadata.NewVolume(testVolumeId, testPodUID, testAgentVersion, testTenantUUID, dynakube.DefaultMaxFailedCsiMountAttempts+1)) - require.NoError(t, err) + return afero.Afero{Fs: afero.NewMemMapFs()} } -func mockUrlDynakubeMetadata(t *testing.T, publisher *AppVolumePublisher) { - err := publisher.db.InsertDynakube(context.Background(), metadata.NewDynakube(testDynakubeName, testTenantUUID, testAgentVersion, "", 0)) - require.NoError(t, err) -} - -func mockSharedRuxitAgentProcConf(t *testing.T, publisher *AppVolumePublisher, content ...byte) { - file, err := publisher.fs.Create(publisher.path.AgentSharedRuxitAgentProcConf(testTenantUUID, testDynakubeName)) - defer func() { _ = file.Close() }() - require.NoError(t, err) +func getFailFs(t *testing.T) afero.Afero { + t.Helper() - if len(content) > 0 { - _, err = file.Write(content) - require.NoError(t, err) - } -} + afero.NewReadOnlyFs(getTestFs(t)) -func mockImageDynakubeMetadata(t *testing.T, publisher *AppVolumePublisher) { - err := publisher.db.InsertDynakube(context.Background(), metadata.NewDynakube(testDynakubeName, testTenantUUID, "", testImageDigest, dynakube.DefaultMaxFailedCsiMountAttempts)) - require.NoError(t, err) + return afero.Afero{Fs: afero.NewReadOnlyFs(getTestFs(t))} } -func assertReferencesForPublishedVolume(t *testing.T, publisher *AppVolumePublisher, mounter *mount.FakeMounter) { - assert.NotEmpty(t, mounter.MountPoints) +func getTestVolumeConfig(t *testing.T) csivolumes.VolumeConfig { + t.Helper() - volume, err := publisher.loadVolume(context.Background(), testVolumeId) - require.NoError(t, err) - assert.Equal(t, testVolumeId, volume.VolumeID) - assert.Equal(t, testPodUID, volume.PodName) - assert.Equal(t, testAgentVersion, volume.Version) - assert.Equal(t, testTenantUUID, volume.TenantUUID) -} - -func assertReferencesForPublishedVolumeWithCodeModulesImage(t *testing.T, publisher *AppVolumePublisher, mounter *mount.FakeMounter) { - assert.NotEmpty(t, mounter.MountPoints) - - volume, err := publisher.loadVolume(context.Background(), testVolumeId) - require.NoError(t, err) - assert.Equal(t, testVolumeId, volume.VolumeID) - assert.Equal(t, testPodUID, volume.PodName) - assert.Equal(t, testImageDigest, volume.Version) - assert.Equal(t, testTenantUUID, volume.TenantUUID) -} - -func assertNoReferencesForUnpublishedVolume(t *testing.T, publisher *AppVolumePublisher) { - volume, err := publisher.loadVolume(context.Background(), testVolumeId) - require.NoError(t, err) - require.Nil(t, volume) -} - -func resetMetrics() { - agentsVersionsMetric.DeleteLabelValues(testAgentVersion) - agentsVersionsMetric.DeleteLabelValues(testImageDigest) -} - -func createTestVolumeConfig() *csivolumes.VolumeConfig { - return &csivolumes.VolumeConfig{ - VolumeInfo: *createTestVolumeInfo(), - PodName: testPodUID, + return csivolumes.VolumeConfig{ + VolumeInfo: csivolumes.VolumeInfo{ + VolumeID: "test-id", + TargetPath: "test/path", + }, + PodName: "test-pod", Mode: Mode, - DynakubeName: testDynakubeName, - } -} - -func createTestVolumeInfo() *csivolumes.VolumeInfo { - return &csivolumes.VolumeInfo{ - VolumeID: testVolumeId, - TargetPath: testTargetPath, + DynakubeName: "test-dk", + RetryTimeout: time.Minute, } } diff --git a/pkg/controllers/csi/driver/volumes/bind_config.go b/pkg/controllers/csi/driver/volumes/bind_config.go deleted file mode 100644 index 50762eb65c..0000000000 --- a/pkg/controllers/csi/driver/volumes/bind_config.go +++ /dev/null @@ -1,50 +0,0 @@ -package csivolumes - -import ( - "context" - "fmt" - - "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi/metadata" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" -) - -type BindConfig struct { - TenantUUID string - Version string - ImageDigest string - DynakubeName string - MaxMountAttempts int -} - -func NewBindConfig(ctx context.Context, access metadata.Access, volumeCfg *VolumeConfig) (*BindConfig, error) { - dynakube, err := access.GetDynakube(ctx, volumeCfg.DynakubeName) - if err != nil { - return nil, status.Error(codes.Unavailable, fmt.Sprintf("failed to extract tenant for DynaKube %s: %s", volumeCfg.DynakubeName, err.Error())) - } - - if dynakube == nil { - return nil, status.Error(codes.Unavailable, fmt.Sprintf("dynakube (%s) is missing from metadata database", volumeCfg.DynakubeName)) - } - - return &BindConfig{ - TenantUUID: dynakube.TenantUUID, - Version: dynakube.LatestVersion, - ImageDigest: dynakube.ImageDigest, - DynakubeName: dynakube.Name, - MaxMountAttempts: dynakube.MaxFailedMountAttempts, - }, nil -} - -func (cfg BindConfig) IsArchiveAvailable() bool { - return cfg.Version != "" || cfg.ImageDigest != "" -} - -func (cfg BindConfig) MetricVersionLabel() string { - versionLabel := cfg.Version - if versionLabel == "" { - versionLabel = cfg.ImageDigest - } - - return versionLabel -} diff --git a/pkg/controllers/csi/driver/volumes/bind_config_test.go b/pkg/controllers/csi/driver/volumes/bind_config_test.go deleted file mode 100644 index 8debddb242..0000000000 --- a/pkg/controllers/csi/driver/volumes/bind_config_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package csivolumes - -import ( - "context" - "testing" - - "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi/metadata" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const ( - testDynakubeName = "a-dynakube" - testTenantUUID = "a-tenant-uuid" - testAgentVersion = "1.2-3" -) - -func TestNewBindConfig(t *testing.T) { - t.Run(`no dynakube in storage`, func(t *testing.T) { - volumeCfg := &VolumeConfig{ - DynakubeName: testDynakubeName, - } - - bindCfg, err := NewBindConfig(context.TODO(), metadata.FakeMemoryDB(), volumeCfg) - - require.Error(t, err) - assert.Nil(t, bindCfg) - }) - t.Run(`create correct bind config`, func(t *testing.T) { - volumeCfg := &VolumeConfig{ - DynakubeName: testDynakubeName, - } - - db := metadata.FakeMemoryDB() - - db.InsertDynakube(context.TODO(), metadata.NewDynakube(testDynakubeName, testTenantUUID, testAgentVersion, "", 0)) - - bindCfg, err := NewBindConfig(context.TODO(), db, volumeCfg) - - expected := BindConfig{ - TenantUUID: testTenantUUID, - Version: testAgentVersion, - DynakubeName: testDynakubeName, - } - - require.NoError(t, err) - assert.NotNil(t, bindCfg) - assert.Equal(t, expected, *bindCfg) - }) -} - -func TestIsArchiveAvailable(t *testing.T) { - t.Run(`no version, no digest`, func(t *testing.T) { - bindCfg := BindConfig{} - - assert.False(t, bindCfg.IsArchiveAvailable()) - }) - t.Run(`version set, no digest`, func(t *testing.T) { - bindCfg := BindConfig{ - Version: "1.2.3", - } - - assert.True(t, bindCfg.IsArchiveAvailable()) - }) - t.Run(`no version, digest set`, func(t *testing.T) { - bindCfg := BindConfig{ - ImageDigest: "sha256:123", - } - - assert.True(t, bindCfg.IsArchiveAvailable()) - }) -} - -func TestMetricVersionLabel(t *testing.T) { - t.Run(`no version, no digest`, func(t *testing.T) { - bindCfg := BindConfig{} - - assert.Empty(t, bindCfg.MetricVersionLabel()) - }) - t.Run(`version set, no digest`, func(t *testing.T) { - bindCfg := BindConfig{ - Version: "1.2.3", - } - - assert.Equal(t, bindCfg.Version, bindCfg.MetricVersionLabel()) - }) - t.Run(`no version, digest set`, func(t *testing.T) { - bindCfg := BindConfig{ - ImageDigest: "sha256:123", - } - - assert.Equal(t, bindCfg.ImageDigest, bindCfg.MetricVersionLabel()) - }) -} diff --git a/pkg/controllers/csi/driver/volumes/host/config.go b/pkg/controllers/csi/driver/volumes/host/config.go index 7a2ea252c7..f2d46ffd5f 100644 --- a/pkg/controllers/csi/driver/volumes/host/config.go +++ b/pkg/controllers/csi/driver/volumes/host/config.go @@ -1,4 +1,4 @@ -package hostvolumes +package host import ( "github.com/Dynatrace/dynatrace-operator/pkg/logd" diff --git a/pkg/controllers/csi/driver/volumes/host/publisher.go b/pkg/controllers/csi/driver/volumes/host/publisher.go index 7be4b9245d..d7e48901d8 100644 --- a/pkg/controllers/csi/driver/volumes/host/publisher.go +++ b/pkg/controllers/csi/driver/volumes/host/publisher.go @@ -14,13 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -package hostvolumes +package host import ( "context" - "fmt" "os" - "time" csivolumes "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi/driver/volumes" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi/metadata" @@ -31,120 +29,49 @@ import ( "k8s.io/mount-utils" ) -const failedToGetOsAgentVolumePrefix = "failed to get osagent volume info from database: " - -func NewHostVolumePublisher(fs afero.Afero, mounter mount.Interface, db metadata.Access, path metadata.PathResolver) csivolumes.Publisher { - return &HostVolumePublisher{ +func NewPublisher(fs afero.Afero, mounter mount.Interface, path metadata.PathResolver) csivolumes.Publisher { + return &Publisher{ fs: fs, mounter: mounter, - db: db, path: path, } } -// necessary for mocking, as the MounterMock will use the os package -type mountChecker func(mounter mount.Interface, file string) (bool, error) - -type HostVolumePublisher struct { - fs afero.Afero - mounter mount.Interface - db metadata.Access - isNotMounted mountChecker - path metadata.PathResolver +type Publisher struct { + fs afero.Afero + mounter mount.Interface + path metadata.PathResolver } -func (publisher *HostVolumePublisher) PublishVolume(ctx context.Context, volumeCfg *csivolumes.VolumeConfig) (*csi.NodePublishVolumeResponse, error) { - bindCfg, err := csivolumes.NewBindConfig(ctx, publisher.db, volumeCfg) - if err != nil { - return nil, err - } - - if err := publisher.mountOneAgent(bindCfg.TenantUUID, volumeCfg); err != nil { +func (pub *Publisher) PublishVolume(ctx context.Context, volumeCfg *csivolumes.VolumeConfig) (*csi.NodePublishVolumeResponse, error) { + if err := pub.mountStorageVolume(volumeCfg); err != nil { return nil, status.Error(codes.Internal, "failed to mount osagent volume: "+err.Error()) } - volume, err := publisher.db.GetOsAgentVolumeViaTenantUUID(ctx, bindCfg.TenantUUID) - if err != nil { - return nil, status.Error(codes.Internal, failedToGetOsAgentVolumePrefix+err.Error()) - } - - timestamp := time.Now() - if volume == nil { - storage := metadata.OsAgentVolume{ - VolumeID: volumeCfg.VolumeID, - TenantUUID: bindCfg.TenantUUID, - Mounted: true, - LastModified: ×tamp, - } - if err := publisher.db.InsertOsAgentVolume(ctx, &storage); err != nil { - return nil, status.Error(codes.Internal, fmt.Sprintf("failed to insert osagent volume info to database. info: %v err: %s", storage, err.Error())) - } - } else { - volume.VolumeID = volumeCfg.VolumeID - volume.Mounted = true - volume.LastModified = ×tamp - - if err := publisher.db.UpdateOsAgentVolume(ctx, volume); err != nil { - return nil, status.Error(codes.Internal, fmt.Sprintf("failed to update osagent volume info to database. info: %v err: %s", volume, err.Error())) - } - } - return &csi.NodePublishVolumeResponse{}, nil } -func (publisher *HostVolumePublisher) UnpublishVolume(ctx context.Context, volumeInfo *csivolumes.VolumeInfo) (*csi.NodeUnpublishVolumeResponse, error) { - volume, err := publisher.db.GetOsAgentVolumeViaVolumeID(ctx, volumeInfo.VolumeID) - if err != nil { - return nil, status.Error(codes.Internal, failedToGetOsAgentVolumePrefix+err.Error()) - } - - if volume == nil { - return &csi.NodeUnpublishVolumeResponse{}, nil - } - - publisher.unmountOneAgent(volumeInfo.TargetPath) - - timestamp := time.Now() - volume.Mounted = false - volume.LastModified = ×tamp +func (pub *Publisher) mountStorageVolume(volumeCfg *csivolumes.VolumeConfig) error { + oaStorageDir := pub.path.OsAgentDir(volumeCfg.DynakubeName) - if err := publisher.db.UpdateOsAgentVolume(ctx, volume); err != nil { - return nil, status.Error(codes.Internal, fmt.Sprintf("failed to update osagent volume info to database. info: %v err: %s", volume, err.Error())) - } - - log.Info("osagent volume has been unpublished", "targetPath", volumeInfo.TargetPath) - - return &csi.NodeUnpublishVolumeResponse{}, nil -} - -func (publisher *HostVolumePublisher) CanUnpublishVolume(ctx context.Context, volumeInfo *csivolumes.VolumeInfo) (bool, error) { - volume, err := publisher.db.GetOsAgentVolumeViaVolumeID(ctx, volumeInfo.VolumeID) - if err != nil { - return false, status.Error(codes.Internal, failedToGetOsAgentVolumePrefix+err.Error()) + err := pub.fs.MkdirAll(oaStorageDir, os.ModePerm) + if err != nil && !os.IsExist(err) { + return err } - return volume != nil, nil -} - -func (publisher *HostVolumePublisher) mountOneAgent(tenantUUID string, volumeCfg *csivolumes.VolumeConfig) error { - hostDir := publisher.path.OsAgentDir(tenantUUID) - _ = publisher.fs.MkdirAll(hostDir, os.ModePerm) + if err := pub.fs.MkdirAll(volumeCfg.TargetPath, os.ModePerm); err != nil { + log.Info("failed to create directory for osagent-storage mount", "directory", oaStorageDir) - if err := publisher.fs.MkdirAll(volumeCfg.TargetPath, os.ModePerm); err != nil { return err } - if err := publisher.mounter.Mount(hostDir, volumeCfg.TargetPath, "", []string{"bind"}); err != nil { - _ = publisher.mounter.Unmount(hostDir) + if err := pub.mounter.Mount(oaStorageDir, volumeCfg.TargetPath, "", []string{"bind"}); err != nil { + _ = pub.mounter.Unmount(oaStorageDir) + + log.Info("failed to mount directory for osagent-storage mount", "directory", oaStorageDir) return err } return nil } - -func (publisher *HostVolumePublisher) unmountOneAgent(targetPath string) { - if err := publisher.mounter.Unmount(targetPath); err != nil { - log.Error(err, "Unmount failed", "path", targetPath) - } -} diff --git a/pkg/controllers/csi/driver/volumes/host/publisher_test.go b/pkg/controllers/csi/driver/volumes/host/publisher_test.go index 042ca4ba22..ec23dc7b75 100644 --- a/pkg/controllers/csi/driver/volumes/host/publisher_test.go +++ b/pkg/controllers/csi/driver/volumes/host/publisher_test.go @@ -1,173 +1,80 @@ -package hostvolumes +package host import ( "context" "testing" "time" - dtcsi "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi" csivolumes "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi/driver/volumes" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi/metadata" "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - mount "k8s.io/mount-utils" -) - -const ( - testVolumeId = "a-volume" - testTargetPath = "/path/to/container/filesystem" - testTenantUUID = "a-tenant-uuid" - testDynakubeName = "a-dynakube" + "k8s.io/mount-utils" ) func TestPublishVolume(t *testing.T) { ctx := context.Background() + path := metadata.PathResolver{} - t.Run("ready dynakube", func(t *testing.T) { + t.Run("happy path", func(t *testing.T) { + fs := getTestFs(t) mounter := mount.NewFakeMounter([]mount.MountPoint{}) - publisher := newPublisherForTesting(mounter) - - mockDynakube(t, &publisher) - - response, err := publisher.PublishVolume(ctx, createTestVolumeConfig()) + volumeCfg := getTestVolumeConfig(t) + pub := NewPublisher(fs, mounter, path) + resp, err := pub.PublishVolume(ctx, &volumeCfg) require.NoError(t, err) - assert.NotNil(t, response) - assert.NotEmpty(t, mounter.MountPoints) - assertReferencesForPublishedVolume(t, &publisher, mounter) + require.NotNil(t, resp) + + expectedHostDir := path.OsAgentDir(volumeCfg.DynakubeName) + hostDirExists, _ := fs.IsDir(expectedHostDir) + assert.True(t, hostDirExists) + // mounter.IsMountPoint can't be used as it uses os.Stat + require.Len(t, mounter.MountPoints, 1) + hostMount := mounter.MountPoints[0] + assert.Equal(t, expectedHostDir, hostMount.Device) + assert.Equal(t, volumeCfg.TargetPath, hostMount.Path) }) - t.Run(`not ready dynakube`, func(t *testing.T) { - mounter := mount.NewFakeMounter([]mount.MountPoint{}) - publisher := newPublisherForTesting(mounter) - mockDynakubeWithoutVersion(t, &publisher) + t.Run("sad path", func(t *testing.T) { + fs := getFailFs(t) + mounter := mount.NewFakeMounter([]mount.MountPoint{}) + volumeCfg := getTestVolumeConfig(t) - response, err := publisher.PublishVolume(ctx, createTestVolumeConfig()) + pub := NewPublisher(fs, mounter, path) - require.NoError(t, err) - assert.NotNil(t, response) - assert.NotEmpty(t, mounter.MountPoints) - assertReferencesForPublishedVolume(t, &publisher, mounter) + resp, err := pub.PublishVolume(ctx, &volumeCfg) + require.Error(t, err) + require.Nil(t, resp) }) } -func TestUnpublishVolume(t *testing.T) { - t.Run("valid metadata", func(t *testing.T) { - mounter := mount.NewFakeMounter([]mount.MountPoint{ - {Path: testTargetPath}, - }) - publisher := newPublisherForTesting(mounter) - mockPublishedvolume(t, &publisher) - - response, err := publisher.UnpublishVolume(context.Background(), createTestVolumeInfo()) +func getTestFs(t *testing.T) afero.Afero { + t.Helper() - require.NoError(t, err) - assert.NotNil(t, response) - assert.Empty(t, mounter.MountPoints) - assertReferencesForUnpublishedVolume(t, &publisher) - }) - - t.Run("invalid metadata", func(t *testing.T) { - mounter := mount.NewFakeMounter([]mount.MountPoint{ - {Path: testTargetPath}, - }) - publisher := newPublisherForTesting(mounter) - - response, err := publisher.UnpublishVolume(context.Background(), createTestVolumeInfo()) - - require.NoError(t, err) - assert.NotNil(t, response) - assert.NotEmpty(t, mounter.MountPoints) - - volume, err := publisher.db.GetOsAgentVolumeViaVolumeID(context.Background(), testVolumeId) - require.NoError(t, err) - assert.Nil(t, volume) - }) + return afero.Afero{Fs: afero.NewMemMapFs()} } -func TestNodePublishAndUnpublishVolume(t *testing.T) { - mounter := mount.NewFakeMounter([]mount.MountPoint{}) - publisher := newPublisherForTesting(mounter) - mockDynakube(t, &publisher) - - publishResponse, err := publisher.PublishVolume(context.Background(), createTestVolumeConfig()) - require.NoError(t, err) - - assert.NotNil(t, publishResponse) - assert.NotEmpty(t, mounter.MountPoints) - assertReferencesForPublishedVolume(t, &publisher, mounter) +func getFailFs(t *testing.T) afero.Afero { + t.Helper() - unpublishResponse, err := publisher.UnpublishVolume(context.Background(), createTestVolumeInfo()) + afero.NewReadOnlyFs(getTestFs(t)) - require.NoError(t, err) - assert.NotNil(t, unpublishResponse) - assert.Empty(t, mounter.MountPoints) - assertReferencesForUnpublishedVolume(t, &publisher) + return afero.Afero{Fs: afero.NewReadOnlyFs(getTestFs(t))} } -func newPublisherForTesting(mounter *mount.FakeMounter) HostVolumePublisher { - csiOptions := dtcsi.CSIOptions{RootDir: "/"} +func getTestVolumeConfig(t *testing.T) csivolumes.VolumeConfig { + t.Helper() - tmpFs := afero.NewMemMapFs() - - return HostVolumePublisher{ - fs: afero.Afero{Fs: tmpFs}, - mounter: mounter, - db: metadata.FakeMemoryDB(), - path: metadata.PathResolver{RootDir: csiOptions.RootDir}, - isNotMounted: func(mounter mount.Interface, file string) (bool, error) { - return false, nil + return csivolumes.VolumeConfig{ + VolumeInfo: csivolumes.VolumeInfo{ + VolumeID: "test-id", + TargetPath: "test/path", }, - } -} - -func mockPublishedvolume(t *testing.T, publisher *HostVolumePublisher) { - mockDynakube(t, publisher) - - now := time.Now() - err := publisher.db.InsertOsAgentVolume(context.Background(), metadata.NewOsAgentVolume(testVolumeId, testTenantUUID, true, &now)) - require.NoError(t, err) -} - -func mockDynakube(t *testing.T, publisher *HostVolumePublisher) { - err := publisher.db.InsertDynakube(context.Background(), metadata.NewDynakube(testDynakubeName, testTenantUUID, "some-version", "", 0)) - require.NoError(t, err) -} - -func mockDynakubeWithoutVersion(t *testing.T, publisher *HostVolumePublisher) { - err := publisher.db.InsertDynakube(context.Background(), metadata.NewDynakube(testDynakubeName, testTenantUUID, "", "", 0)) - require.NoError(t, err) -} - -func assertReferencesForPublishedVolume(t *testing.T, publisher *HostVolumePublisher, mounter *mount.FakeMounter) { - assert.NotEmpty(t, mounter.MountPoints) - - volume, err := publisher.db.GetOsAgentVolumeViaVolumeID(context.Background(), testVolumeId) - require.NoError(t, err) - assert.Equal(t, testVolumeId, volume.VolumeID) - assert.Equal(t, testTenantUUID, volume.TenantUUID) - assert.True(t, volume.Mounted) -} - -func assertReferencesForUnpublishedVolume(t *testing.T, publisher *HostVolumePublisher) { - volume, err := publisher.db.GetOsAgentVolumeViaVolumeID(context.Background(), testVolumeId) - require.NoError(t, err) - assert.NotNil(t, volume) - assert.False(t, volume.Mounted) -} - -func createTestVolumeConfig() *csivolumes.VolumeConfig { - return &csivolumes.VolumeConfig{ - VolumeInfo: *createTestVolumeInfo(), + PodName: "test-pod", Mode: Mode, - DynakubeName: testDynakubeName, - } -} - -func createTestVolumeInfo() *csivolumes.VolumeInfo { - return &csivolumes.VolumeInfo{ - VolumeID: testVolumeId, - TargetPath: testTargetPath, + DynakubeName: "test-dk", + RetryTimeout: time.Microsecond, // doesn't matter } } diff --git a/pkg/controllers/csi/driver/volumes/publisher.go b/pkg/controllers/csi/driver/volumes/publisher.go index 4ca9547a09..cf168df3d9 100644 --- a/pkg/controllers/csi/driver/volumes/publisher.go +++ b/pkg/controllers/csi/driver/volumes/publisher.go @@ -8,6 +8,4 @@ import ( type Publisher interface { PublishVolume(ctx context.Context, volumeCfg *VolumeConfig) (*csi.NodePublishVolumeResponse, error) - UnpublishVolume(ctx context.Context, volumeInfo *VolumeInfo) (*csi.NodeUnpublishVolumeResponse, error) - CanUnpublishVolume(ctx context.Context, volumeInfo *VolumeInfo) (bool, error) } diff --git a/pkg/controllers/csi/driver/volumes/volume_config.go b/pkg/controllers/csi/driver/volumes/volume_config.go index 3426ec4b98..69d0aea4cc 100644 --- a/pkg/controllers/csi/driver/volumes/volume_config.go +++ b/pkg/controllers/csi/driver/volumes/volume_config.go @@ -3,13 +3,15 @@ package csivolumes import ( "time" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/container-storage-interface/spec/lib/go/csi" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) const ( - PodNameContextKey = "csi.storage.k8s.io/pod.name" + PodNameContextKey = "csi.storage.k8s.io/pod.name" + PodNamespaceContextKey = "csi.storage.k8s.io/pod.namespace" // CSIVolumeAttributeModeField used for identifying the origin of the NodePublishVolume request CSIVolumeAttributeModeField = "mode" @@ -27,88 +29,109 @@ type VolumeInfo struct { type VolumeConfig struct { VolumeInfo PodName string + PodNamespace string Mode string DynakubeName string RetryTimeout time.Duration } // Transforms the NodePublishVolumeRequest into a VolumeConfig -func ParseNodePublishVolumeRequest(req *csi.NodePublishVolumeRequest) (*VolumeConfig, error) { - if req.GetVolumeCapability() == nil { - return nil, status.Error(codes.InvalidArgument, "Volume capability missing in request") - } +func ParseNodePublishVolumeRequest(req *csi.NodePublishVolumeRequest) (VolumeConfig, error) { + volumeConfig := VolumeConfig{} + + volumeInfo, err := newVolumeInfo(req) + volumeConfig.VolumeInfo = volumeInfo - volID := req.GetVolumeId() - if volID == "" { - return nil, status.Error(codes.InvalidArgument, "Volume ID missing in request") + if err != nil { + return volumeConfig, err } - targetPath := req.GetTargetPath() - if targetPath == "" { - return nil, status.Error(codes.InvalidArgument, "Target path missing in request") + if req.GetVolumeCapability() == nil { + return volumeConfig, status.Error(codes.InvalidArgument, "Volume capability missing in request") } if req.GetVolumeCapability().GetBlock() != nil { - return nil, status.Error(codes.InvalidArgument, "cannot have block access type") + return volumeConfig, status.Error(codes.InvalidArgument, "cannot have block access type") } if req.GetVolumeCapability().GetMount() == nil { - return nil, status.Error(codes.InvalidArgument, "expecting to have mount access type") + return volumeConfig, status.Error(codes.InvalidArgument, "expecting to have mount access type") } volCtx := req.GetVolumeContext() if volCtx == nil { - return nil, status.Error(codes.InvalidArgument, "Publish context missing in request") + return volumeConfig, status.Error(codes.InvalidArgument, "Publish context missing in request") } podName := volCtx[PodNameContextKey] if podName == "" { - return nil, status.Error(codes.InvalidArgument, "No Pod Name included with request") + return volumeConfig, status.Error(codes.InvalidArgument, "No Pod Name included in request") } + volumeConfig.PodName = podName + + podNamespace := volCtx[PodNamespaceContextKey] + if podNamespace == "" { + return volumeConfig, status.Error(codes.InvalidArgument, "No Pod Namespace included in request") + } + + volumeConfig.PodNamespace = podNamespace + mode := volCtx[CSIVolumeAttributeModeField] if mode == "" { - return nil, status.Error(codes.InvalidArgument, "No mode attribute included with request") + return volumeConfig, status.Error(codes.InvalidArgument, "No mode attribute included in request") } + volumeConfig.Mode = mode + dynakubeName := volCtx[CSIVolumeAttributeDynakubeField] if dynakubeName == "" { - return nil, status.Error(codes.InvalidArgument, "No dynakube attribute included with request") + return volumeConfig, status.Error(codes.InvalidArgument, "No dynakube attribute included in request") } + volumeConfig.DynakubeName = dynakubeName + retryTimeoutValue := volCtx[CSIVolumeAttributeRetryTimeout] if retryTimeoutValue == "" { - return nil, status.Error(codes.InvalidArgument, "No retryTimeout attribute included with request") + retryTimeoutValue = dynakube.DefaultMaxCsiMountTimeout } retryTimeout, err := time.ParseDuration(retryTimeoutValue) if err != nil { - return nil, status.Error(codes.InvalidArgument, "The retryTimeout attribute has incorrect format") + return volumeConfig, status.Error(codes.InvalidArgument, "The retryTimeout attribute has incorrect format") } - return &VolumeConfig{ - VolumeInfo: VolumeInfo{ - VolumeID: volID, - TargetPath: targetPath, - }, - PodName: podName, - Mode: mode, - DynakubeName: dynakubeName, - RetryTimeout: retryTimeout, - }, nil + volumeConfig.RetryTimeout = retryTimeout + + return volumeConfig, nil } // Transforms the NodeUnpublishVolumeRequest into a VolumeInfo -func ParseNodeUnpublishVolumeRequest(req *csi.NodeUnpublishVolumeRequest) (*VolumeInfo, error) { +func ParseNodeUnpublishVolumeRequest(req *csi.NodeUnpublishVolumeRequest) (VolumeInfo, error) { + return newVolumeInfo(req) +} + +type baseRequest interface { + GetVolumeId() string + GetTargetPath() string +} + +func newVolumeInfo(req baseRequest) (VolumeInfo, error) { + info := VolumeInfo{} + volumeID := req.GetVolumeId() if volumeID == "" { - return nil, status.Error(codes.InvalidArgument, "Volume ID missing in request") + return info, status.Error(codes.InvalidArgument, "Volume ID missing in request") } + info.VolumeID = volumeID + targetPath := req.GetTargetPath() if targetPath == "" { - return nil, status.Error(codes.InvalidArgument, "Target path missing in request") + return info, status.Error(codes.InvalidArgument, "Target path missing in request") } - return &VolumeInfo{volumeID, targetPath}, nil + info.TargetPath = targetPath + + return info, nil } diff --git a/pkg/controllers/csi/driver/volumes/volume_config_test.go b/pkg/controllers/csi/driver/volumes/volume_config_test.go index 6c248d48d9..15b75cd4c9 100644 --- a/pkg/controllers/csi/driver/volumes/volume_config_test.go +++ b/pkg/controllers/csi/driver/volumes/volume_config_test.go @@ -2,7 +2,6 @@ package csivolumes import ( "testing" - "time" "github.com/container-storage-interface/spec/lib/go/csi" "github.com/stretchr/testify/assert" @@ -10,38 +9,44 @@ import ( ) const ( - testVolumeId = "a-volume-id" - testTargetPath = "a-target-path" - testPodUID = "a-pod-uid" + testVolumeId = "a-volume-id" + testTargetPath = "a-target-path" + testPodUID = "a-pod-uid" + testNs = "a-namespace" + testDynakubeName = "a-dynakube" ) func TestCSIDriverServer_ParsePublishVolumeRequest(t *testing.T) { - t.Run(`No volume capability`, func(t *testing.T) { - volumeCfg, err := ParseNodePublishVolumeRequest(&csi.NodePublishVolumeRequest{}) + t.Run("No volume id", func(t *testing.T) { + request := &csi.NodePublishVolumeRequest{} + volumeCfg, err := ParseNodePublishVolumeRequest(request) - require.EqualError(t, err, "rpc error: code = InvalidArgument desc = Volume capability missing in request") - assert.Nil(t, volumeCfg) + require.EqualError(t, err, "rpc error: code = InvalidArgument desc = Volume ID missing in request") + assert.NotNil(t, volumeCfg) }) - t.Run(`No volume id`, func(t *testing.T) { + t.Run("No target path", func(t *testing.T) { request := &csi.NodePublishVolumeRequest{ - VolumeCapability: &csi.VolumeCapability{}, + VolumeId: testVolumeId, } volumeCfg, err := ParseNodePublishVolumeRequest(request) - require.EqualError(t, err, "rpc error: code = InvalidArgument desc = Volume ID missing in request") - assert.Nil(t, volumeCfg) + require.EqualError(t, err, "rpc error: code = InvalidArgument desc = Target path missing in request") + require.NotNil(t, volumeCfg) + assert.Equal(t, request.GetVolumeId(), volumeCfg.VolumeID) }) - t.Run(`No target path`, func(t *testing.T) { + t.Run("No volume capability", func(t *testing.T) { request := &csi.NodePublishVolumeRequest{ - VolumeCapability: &csi.VolumeCapability{}, - VolumeId: testVolumeId, + VolumeId: testVolumeId, + TargetPath: testTargetPath, } volumeCfg, err := ParseNodePublishVolumeRequest(request) - require.EqualError(t, err, "rpc error: code = InvalidArgument desc = Target path missing in request") - assert.Nil(t, volumeCfg) + require.EqualError(t, err, "rpc error: code = InvalidArgument desc = Volume capability missing in request") + require.NotNil(t, volumeCfg) + assert.Equal(t, request.GetVolumeId(), volumeCfg.VolumeID) + assert.Equal(t, request.GetTargetPath(), volumeCfg.TargetPath) }) - t.Run(`Access type is of type block access`, func(t *testing.T) { + t.Run("Access type is of type block access", func(t *testing.T) { request := &csi.NodePublishVolumeRequest{ VolumeCapability: &csi.VolumeCapability{ AccessType: &csi.VolumeCapability_Block{ @@ -54,9 +59,11 @@ func TestCSIDriverServer_ParsePublishVolumeRequest(t *testing.T) { volumeCfg, err := ParseNodePublishVolumeRequest(request) require.EqualError(t, err, "rpc error: code = InvalidArgument desc = cannot have block access type") - assert.Nil(t, volumeCfg) + require.NotNil(t, volumeCfg) + assert.Equal(t, request.GetVolumeId(), volumeCfg.VolumeID) + assert.Equal(t, request.GetTargetPath(), volumeCfg.TargetPath) }) - t.Run(`Access type is not of type mount access`, func(t *testing.T) { + t.Run("Access type is not of type mount access", func(t *testing.T) { request := &csi.NodePublishVolumeRequest{ VolumeCapability: &csi.VolumeCapability{}, VolumeId: testVolumeId, @@ -65,9 +72,11 @@ func TestCSIDriverServer_ParsePublishVolumeRequest(t *testing.T) { volumeCfg, err := ParseNodePublishVolumeRequest(request) require.EqualError(t, err, "rpc error: code = InvalidArgument desc = expecting to have mount access type") - assert.Nil(t, volumeCfg) + require.NotNil(t, volumeCfg) + assert.Equal(t, request.GetVolumeId(), volumeCfg.VolumeID) + assert.Equal(t, request.GetTargetPath(), volumeCfg.TargetPath) }) - t.Run(`No volume context`, func(t *testing.T) { + t.Run("No volume context", func(t *testing.T) { request := &csi.NodePublishVolumeRequest{ VolumeCapability: &csi.VolumeCapability{ AccessType: &csi.VolumeCapability_Mount{ @@ -80,29 +89,29 @@ func TestCSIDriverServer_ParsePublishVolumeRequest(t *testing.T) { volumeCfg, err := ParseNodePublishVolumeRequest(request) require.EqualError(t, err, "rpc error: code = InvalidArgument desc = Publish context missing in request") - assert.Nil(t, volumeCfg) + require.NotNil(t, volumeCfg) + assert.Equal(t, request.GetVolumeId(), volumeCfg.VolumeID) + assert.Equal(t, request.GetTargetPath(), volumeCfg.TargetPath) }) - t.Run(`Pod name missing from requests volume context`, func(t *testing.T) { + t.Run("Pod name missing from requests volume context", func(t *testing.T) { request := &csi.NodePublishVolumeRequest{ VolumeCapability: &csi.VolumeCapability{ AccessType: &csi.VolumeCapability_Mount{ Mount: &csi.VolumeCapability_MountVolume{}, }, }, - VolumeId: testVolumeId, - TargetPath: testTargetPath, - VolumeContext: map[string]string{ - CSIVolumeAttributeDynakubeField: testDynakubeName, - CSIVolumeAttributeModeField: "test", - CSIVolumeAttributeRetryTimeout: "5m", - }, + VolumeId: testVolumeId, + TargetPath: testTargetPath, + VolumeContext: map[string]string{}, } volumeCfg, err := ParseNodePublishVolumeRequest(request) require.Error(t, err) - assert.Nil(t, volumeCfg) + require.NotNil(t, volumeCfg) + assert.Equal(t, request.GetVolumeId(), volumeCfg.VolumeID) + assert.Equal(t, request.GetTargetPath(), volumeCfg.TargetPath) }) - t.Run(`mode missing from requests volume context`, func(t *testing.T) { + t.Run("Pod namespace missing from requests volume context", func(t *testing.T) { request := &csi.NodePublishVolumeRequest{ VolumeCapability: &csi.VolumeCapability{ AccessType: &csi.VolumeCapability_Mount{ @@ -112,17 +121,18 @@ func TestCSIDriverServer_ParsePublishVolumeRequest(t *testing.T) { VolumeId: testVolumeId, TargetPath: testTargetPath, VolumeContext: map[string]string{ - PodNameContextKey: testPodUID, - CSIVolumeAttributeDynakubeField: testDynakubeName, - CSIVolumeAttributeRetryTimeout: "5m", + PodNameContextKey: testPodUID, }, } volumeCfg, err := ParseNodePublishVolumeRequest(request) require.Error(t, err) - assert.Nil(t, volumeCfg) + require.NotNil(t, volumeCfg) + assert.Equal(t, request.GetVolumeId(), volumeCfg.VolumeID) + assert.Equal(t, request.GetTargetPath(), volumeCfg.TargetPath) + assert.Equal(t, request.GetVolumeContext()[PodNameContextKey], volumeCfg.PodName) }) - t.Run(`dynakube missing from requests volume context`, func(t *testing.T) { + t.Run("mode missing from requests volume context", func(t *testing.T) { request := &csi.NodePublishVolumeRequest{ VolumeCapability: &csi.VolumeCapability{ AccessType: &csi.VolumeCapability_Mount{ @@ -132,17 +142,20 @@ func TestCSIDriverServer_ParsePublishVolumeRequest(t *testing.T) { VolumeId: testVolumeId, TargetPath: testTargetPath, VolumeContext: map[string]string{ - PodNameContextKey: testPodUID, - CSIVolumeAttributeModeField: "test", - CSIVolumeAttributeRetryTimeout: "5m", + PodNameContextKey: testPodUID, + PodNamespaceContextKey: testNs, }, } volumeCfg, err := ParseNodePublishVolumeRequest(request) require.Error(t, err) - assert.Nil(t, volumeCfg) + require.NotNil(t, volumeCfg) + assert.Equal(t, request.GetVolumeId(), volumeCfg.VolumeID) + assert.Equal(t, request.GetTargetPath(), volumeCfg.TargetPath) + assert.Equal(t, request.GetVolumeContext()[PodNameContextKey], volumeCfg.PodName) + assert.Equal(t, request.GetVolumeContext()[PodNamespaceContextKey], volumeCfg.PodNamespace) }) - t.Run(`retryTimeout missing from requests volume context`, func(t *testing.T) { + t.Run("dk name missing from requests volume context", func(t *testing.T) { request := &csi.NodePublishVolumeRequest{ VolumeCapability: &csi.VolumeCapability{ AccessType: &csi.VolumeCapability_Mount{ @@ -152,38 +165,23 @@ func TestCSIDriverServer_ParsePublishVolumeRequest(t *testing.T) { VolumeId: testVolumeId, TargetPath: testTargetPath, VolumeContext: map[string]string{ - PodNameContextKey: testPodUID, - CSIVolumeAttributeDynakubeField: testDynakubeName, - CSIVolumeAttributeModeField: "test", + PodNameContextKey: testPodUID, + PodNamespaceContextKey: testNs, + CSIVolumeAttributeModeField: "test", }, } volumeCfg, err := ParseNodePublishVolumeRequest(request) require.Error(t, err) - assert.Nil(t, volumeCfg) + require.NotNil(t, volumeCfg) + assert.Equal(t, request.GetVolumeId(), volumeCfg.VolumeID) + assert.Equal(t, request.GetTargetPath(), volumeCfg.TargetPath) + assert.Equal(t, request.GetVolumeContext()[PodNameContextKey], volumeCfg.PodName) + assert.Equal(t, request.GetVolumeContext()[PodNamespaceContextKey], volumeCfg.PodNamespace) + assert.Equal(t, request.GetVolumeContext()[CSIVolumeAttributeModeField], volumeCfg.Mode) }) - t.Run(`retryTimeout has incorrect format`, func(t *testing.T) { - request := &csi.NodePublishVolumeRequest{ - VolumeCapability: &csi.VolumeCapability{ - AccessType: &csi.VolumeCapability_Mount{ - Mount: &csi.VolumeCapability_MountVolume{}, - }, - }, - VolumeId: testVolumeId, - TargetPath: testTargetPath, - VolumeContext: map[string]string{ - PodNameContextKey: testPodUID, - CSIVolumeAttributeDynakubeField: testDynakubeName, - CSIVolumeAttributeModeField: "test", - CSIVolumeAttributeRetryTimeout: "5", - }, - } - volumeCfg, err := ParseNodePublishVolumeRequest(request) - require.Error(t, err) - assert.Nil(t, volumeCfg) - }) - t.Run(`request is parsed correctly`, func(t *testing.T) { + t.Run("happy path", func(t *testing.T) { request := &csi.NodePublishVolumeRequest{ VolumeCapability: &csi.VolumeCapability{ AccessType: &csi.VolumeCapability_Mount{ @@ -194,20 +192,21 @@ func TestCSIDriverServer_ParsePublishVolumeRequest(t *testing.T) { TargetPath: testTargetPath, VolumeContext: map[string]string{ PodNameContextKey: testPodUID, - CSIVolumeAttributeDynakubeField: testDynakubeName, + PodNamespaceContextKey: testNs, CSIVolumeAttributeModeField: "test", - CSIVolumeAttributeRetryTimeout: "5m", + CSIVolumeAttributeDynakubeField: "dk", }, } volumeCfg, err := ParseNodePublishVolumeRequest(request) require.NoError(t, err) assert.NotNil(t, volumeCfg) - assert.Equal(t, testVolumeId, volumeCfg.VolumeID) - assert.Equal(t, testTargetPath, volumeCfg.TargetPath) - assert.Equal(t, testPodUID, volumeCfg.PodName) - assert.Equal(t, "test", volumeCfg.Mode) - assert.Equal(t, testDynakubeName, volumeCfg.DynakubeName) - assert.Equal(t, time.Minute*5, volumeCfg.RetryTimeout) + assert.Equal(t, request.GetVolumeId(), volumeCfg.VolumeID) + assert.Equal(t, request.GetTargetPath(), volumeCfg.TargetPath) + assert.Equal(t, request.GetVolumeContext()[PodNameContextKey], volumeCfg.PodName) + assert.Equal(t, request.GetVolumeContext()[PodNamespaceContextKey], volumeCfg.PodNamespace) + assert.Equal(t, request.GetVolumeContext()[CSIVolumeAttributeModeField], volumeCfg.Mode) + assert.Equal(t, request.GetVolumeContext()[CSIVolumeAttributeDynakubeField], volumeCfg.DynakubeName) + assert.NotNil(t, volumeCfg.RetryTimeout) }) } diff --git a/pkg/controllers/csi/gc/binaries.go b/pkg/controllers/csi/gc/binaries.go deleted file mode 100644 index 76f1d6fbe0..0000000000 --- a/pkg/controllers/csi/gc/binaries.go +++ /dev/null @@ -1,165 +0,0 @@ -package csigc - -import ( - "context" - "os" - "strings" - - "github.com/pkg/errors" - "github.com/spf13/afero" - mount "k8s.io/mount-utils" -) - -func (gc *CSIGarbageCollector) runBinaryGarbageCollection(ctx context.Context, tenantUUID string) error { - binDirs, err := gc.getSharedBinDirs() - if err != nil { - return err - } - - oldBinDirs, err := gc.getTenantBinDirs(tenantUUID) - if err != nil { - return err - } - - binDirs = append(binDirs, oldBinDirs...) - - binsToDelete, err := gc.collectUnusedAgentBins(ctx, binDirs, tenantUUID) - if err != nil { - return err - } - - if len(binsToDelete) == 0 { - log.Info("no shared binary dirs to delete on the node") - - return nil - } - - return gc.deleteBinDirs(binsToDelete) -} - -func (gc *CSIGarbageCollector) collectUnusedAgentBins(ctx context.Context, imageDirs []os.FileInfo, tenantUUID string) ([]string, error) { - var toDelete []string - - usedAgentVersions, err := gc.db.GetLatestVersions(ctx) - if err != nil { - log.Info("failed to get the used image versions") - - return nil, err - } - - usedAgentDigest, err := gc.db.GetUsedImageDigests(ctx) - if err != nil { - log.Info("failed to get the used image digests") - - return nil, err - } - - mountedAgentBins, err := getRelevantOverlayMounts(gc.mounter, []string{gc.path.AgentBinaryDir(tenantUUID), gc.path.AgentSharedBinaryDirBase()}) - if err != nil { - log.Info("failed to get all mounted versions") - - return nil, err - } - - for _, imageDir := range imageDirs { - agentBin := imageDir.Name() - sharedPath := gc.path.AgentSharedBinaryDirForAgent(agentBin) - tenantPath := gc.path.AgentBinaryDirForVersion(tenantUUID, agentBin) - - switch { - case usedAgentVersions[agentBin]: // versions that may not be used, but a dynakube references it - continue - case usedAgentDigest[agentBin]: // images that may not be used, but a dynakube references it - continue - } - - if !mountedAgentBins[sharedPath] { // based on mount, active shared codemodule mounts - toDelete = append(toDelete, sharedPath) - } - - if !mountedAgentBins[tenantPath] { // based on mount, active tenant codemodule mounts - toDelete = append(toDelete, tenantPath) - } - } - - return toDelete, nil -} - -func (gc *CSIGarbageCollector) deleteBinDirs(imageDirs []string) error { - for _, dir := range imageDirs { - err := gc.fs.RemoveAll(dir) - if err != nil { - log.Info("failed to delete codemodule bin dir", "dir", dir) - - return errors.WithStack(err) - } - - log.Info("deleted codemodule bin dir", "dir", dir) - } - - return nil -} - -func (gc *CSIGarbageCollector) getTenantBinDirs(tenantUUID string) ([]os.FileInfo, error) { - binPath := gc.path.AgentBinaryDir(tenantUUID) - - binDirs, err := afero.Afero{Fs: gc.fs}.ReadDir(binPath) - if os.IsNotExist(err) { - log.Info("no codemodule versions stored in deprecated path", "path", binPath) - - return nil, nil - } else if err != nil { - log.Info("failed to read codemodule versions stored in deprecated path", "path", binPath) - - return nil, errors.WithStack(err) - } - - return binDirs, nil -} - -func (gc *CSIGarbageCollector) getSharedBinDirs() ([]os.FileInfo, error) { - sharedPath := gc.path.AgentSharedBinaryDirBase() - - imageDirs, err := afero.Afero{Fs: gc.fs}.ReadDir(sharedPath) - if os.IsNotExist(err) { - log.Info("no shared codemodules stored ", "path", sharedPath) - - return nil, nil - } - - if err != nil { - log.Info("failed to read shared image directory", "path", sharedPath) - - return nil, errors.WithStack(err) - } - - return imageDirs, nil -} - -func getRelevantOverlayMounts(mounter mount.Interface, baseFolders []string) (map[string]bool, error) { - mountPoints, err := mounter.List() - if err != nil { - log.Error(err, "failed to list all mount points") - - return nil, err - } - - relevantMounts := map[string]bool{} - - for _, mountPoint := range mountPoints { - if mountPoint.Device == "overlay" { - for _, opt := range mountPoint.Opts { - for _, baseFolder := range baseFolders { - if strings.HasPrefix(opt, "lowerdir="+baseFolder) { - split := strings.Split(opt, "=") - relevantMounts[split[1]] = true - - break - } - } - } - } - } - - return relevantMounts, nil -} diff --git a/pkg/controllers/csi/gc/binaries_test.go b/pkg/controllers/csi/gc/binaries_test.go deleted file mode 100644 index dedfb527b6..0000000000 --- a/pkg/controllers/csi/gc/binaries_test.go +++ /dev/null @@ -1,171 +0,0 @@ -package csigc - -import ( - "context" - "os" - "testing" - - "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi/metadata" - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - mount "k8s.io/mount-utils" -) - -const ( - testVersion = "some-version" -) - -var ( - testPathResolver = metadata.PathResolver{ - RootDir: "test", - } -) - -func TestRunBinaryGarbageCollection(t *testing.T) { - ctx := context.Background() - - t.Run("bad database", func(t *testing.T) { - testDir := testPathResolver.AgentSharedBinaryDirForAgent(testVersion) - fs := createTestDirs(t, testDir) - gc := CSIGarbageCollector{ - fs: fs, - db: &metadata.FakeFailDB{}, - path: testPathResolver, - } - err := gc.runBinaryGarbageCollection(ctx, testTenantUUID) - require.Error(t, err) - }) - t.Run("no error on empty fs", func(t *testing.T) { - gc := CSIGarbageCollector{ - fs: afero.NewMemMapFs(), - mounter: mount.NewFakeMounter(nil), - db: metadata.FakeMemoryDB(), - } - err := gc.runBinaryGarbageCollection(ctx, testTenantUUID) - require.NoError(t, err) - }) - t.Run("deletes unused", func(t *testing.T) { - testSharedDir := testPathResolver.AgentSharedBinaryDirForAgent(testVersion) - testTenantBinDir := testPathResolver.AgentBinaryDirForVersion(testTenantUUID, testVersion) - fs := createTestDirs(t, testSharedDir, testTenantBinDir) - gc := CSIGarbageCollector{ - fs: fs, - db: metadata.FakeMemoryDB(), - mounter: mount.NewFakeMounter(nil), - path: testPathResolver, - } - err := gc.runBinaryGarbageCollection(ctx, testTenantUUID) - require.NoError(t, err) - _, err = fs.Stat(testSharedDir) - require.Error(t, err) - assert.True(t, os.IsNotExist(err)) - - _, err = fs.Stat(testTenantBinDir) - require.Error(t, err) - assert.True(t, os.IsNotExist(err)) - }) - t.Run("deletes nothing, because of dynakube metadata present", func(t *testing.T) { - testDir := testPathResolver.AgentSharedBinaryDirForAgent(testVersion) - fs := createTestDirs(t, testDir) - gc := CSIGarbageCollector{ - fs: fs, - db: metadata.FakeMemoryDB(), - mounter: mount.NewFakeMounter(nil), - } - gc.db.InsertDynakube(ctx, &metadata.Dynakube{ - Name: "test", - TenantUUID: "test", - LatestVersion: "test", - ImageDigest: testVersion, - }) - - err := gc.runBinaryGarbageCollection(ctx, testTenantUUID) - require.NoError(t, err) - - _, err = fs.Stat(testDir) - require.NoError(t, err) - }) - t.Run("deletes nothing, because of volume metadata present", func(t *testing.T) { - testDir := testPathResolver.AgentSharedBinaryDirForAgent(testVersion) - fs := createTestDirs(t, testDir) - gc := CSIGarbageCollector{ - fs: fs, - db: metadata.FakeMemoryDB(), - mounter: mount.NewFakeMounter(nil), - } - gc.db.InsertVolume(ctx, &metadata.Volume{ - VolumeID: "test", - TenantUUID: "test", - Version: testVersion, - PodName: "test", - }) - - err := gc.runBinaryGarbageCollection(ctx, testTenantUUID) - require.NoError(t, err) - - _, err = fs.Stat(testDir) - require.NoError(t, err) - }) - t.Run("deletes nothing, because directory is mounted", func(t *testing.T) { - testSharedDir := testPathResolver.AgentSharedBinaryDirForAgent(testVersion) - testTenantBinDir := testPathResolver.AgentBinaryDirForVersion(testTenantUUID, testVersion) - fs := createTestDirs(t, testSharedDir, testTenantBinDir) - gc := CSIGarbageCollector{ - fs: fs, - db: metadata.FakeMemoryDB(), - mounter: mount.NewFakeMounter([]mount.MountPoint{ - { - Type: "overlay", - Opts: []string{"upperdir=beep", "lowerdir=" + testSharedDir, "workdir=boop"}, - }, - { - Type: "overlay", - Opts: []string{"lowerdir=" + testTenantBinDir, "upperdir=beep", "workdir=boop"}, - }, - }), - } - - err := gc.runBinaryGarbageCollection(ctx, testTenantUUID) - require.NoError(t, err) - - _, err = fs.Stat(testSharedDir) - require.NoError(t, err) - - _, err = fs.Stat(testTenantBinDir) - require.NoError(t, err) - }) -} - -func TestGetSharedImageDirs(t *testing.T) { - t.Run("no error on empty fs", func(t *testing.T) { - fs := afero.NewMemMapFs() - gc := CSIGarbageCollector{ - fs: fs, - path: testPathResolver, - } - dirs, err := gc.getSharedBinDirs() - require.NoError(t, err) - assert.Nil(t, dirs) - }) - t.Run("get image cache dirs", func(t *testing.T) { - testDir := testPathResolver.AgentSharedBinaryDirForAgent(testVersion) - fs := createTestDirs(t, testDir) - gc := CSIGarbageCollector{ - fs: fs, - path: testPathResolver, - } - dirs, err := gc.getSharedBinDirs() - require.NoError(t, err) - assert.Len(t, dirs, 1) - }) -} - -func createTestDirs(t *testing.T, paths ...string) afero.Fs { - fs := afero.NewMemMapFs() - for _, path := range paths { - require.NoError(t, fs.MkdirAll(path, 0755)) - } - - return fs -} diff --git a/pkg/controllers/csi/gc/config.go b/pkg/controllers/csi/gc/config.go deleted file mode 100644 index e530420989..0000000000 --- a/pkg/controllers/csi/gc/config.go +++ /dev/null @@ -1,38 +0,0 @@ -package csigc - -import ( - "github.com/Dynatrace/dynatrace-operator/pkg/logd" - "github.com/prometheus/client_golang/prometheus" - "sigs.k8s.io/controller-runtime/pkg/metrics" -) - -var ( - log = logd.Get().WithName("csi-gc") - - reclaimedMemoryMetric = prometheus.NewCounter(prometheus.CounterOpts{ - Namespace: "dynatrace", - Subsystem: "csi_driver", - Name: "gc_reclaimed", - Help: "Amount of memory reclaimed by the GC", - }) - - foldersRemovedMetric = prometheus.NewCounter(prometheus.CounterOpts{ - Namespace: "dynatrace", - Subsystem: "csi_driver", - Name: "gc_folder_rmv", - Help: "Number of folders deleted by the GC", - }) - - gcRunsMetric = prometheus.NewCounter(prometheus.CounterOpts{ - Namespace: "dynatrace", - Subsystem: "csi_driver", - Name: "gc_runs", - Help: "Number of GC runs", - }) -) - -func init() { - metrics.Registry.MustRegister(reclaimedMemoryMetric) - metrics.Registry.MustRegister(foldersRemovedMetric) - metrics.Registry.MustRegister(gcRunsMetric) -} diff --git a/pkg/controllers/csi/gc/reconciler.go b/pkg/controllers/csi/gc/reconciler.go deleted file mode 100644 index c86f9c8aa9..0000000000 --- a/pkg/controllers/csi/gc/reconciler.go +++ /dev/null @@ -1,107 +0,0 @@ -package csigc - -import ( - "context" - "os" - "time" - - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - dtcsi "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi" - "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi/metadata" - "github.com/pkg/errors" - "github.com/spf13/afero" - k8serrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/mount-utils" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/reconcile" -) - -// CSIGarbageCollector removes unused and outdated agent versions -type CSIGarbageCollector struct { - apiReader client.Reader - fs afero.Fs - db metadata.Access - mounter mount.Interface - - path metadata.PathResolver - - maxUnmountedVolumeAge time.Duration -} - -var _ reconcile.Reconciler = (*CSIGarbageCollector)(nil) - -// NewCSIGarbageCollector returns a new CSIGarbageCollector -func NewCSIGarbageCollector(apiReader client.Reader, opts dtcsi.CSIOptions, db metadata.Access) *CSIGarbageCollector { - mounter := mount.New("") - - return &CSIGarbageCollector{ - apiReader: apiReader, - fs: afero.NewOsFs(), - db: db, - path: metadata.PathResolver{RootDir: opts.RootDir}, - mounter: mounter, - maxUnmountedVolumeAge: determineMaxUnmountedVolumeAge(os.Getenv(maxUnmountedCsiVolumeAgeEnv)), - } -} - -func (gc *CSIGarbageCollector) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) { - log.Info("running OneAgent garbage collection", "namespace", request.Namespace, "name", request.Name) - - defaultReconcileResult := reconcile.Result{} - - dynakube, err := getDynakubeFromRequest(ctx, gc.apiReader, request) - if err != nil { - return defaultReconcileResult, err - } - - if dynakube == nil { - return defaultReconcileResult, nil - } - - if !dynakube.NeedAppInjection() { - log.Info("app injection not enabled, skip garbage collection", "dynakube", dynakube.Name) - - return defaultReconcileResult, nil - } - - tenantUUID, err := dynakube.TenantUUIDFromApiUrl() - if err != nil { - log.Info("failed to get tenantUUID of DynaKube, checking later") - - return defaultReconcileResult, err - } - - log.Info("running log garbage collection") - gc.runUnmountedVolumeGarbageCollection(tenantUUID) - - if err := ctx.Err(); err != nil { - return defaultReconcileResult, err - } - - log.Info("running binary garbage collection") - - if err := gc.runBinaryGarbageCollection(ctx, tenantUUID); err != nil { - log.Info("failed to garbage collect the shared images") - - return defaultReconcileResult, err - } - - return defaultReconcileResult, nil -} - -func getDynakubeFromRequest(ctx context.Context, apiReader client.Reader, request reconcile.Request) (*dynakube.DynaKube, error) { - var dk dynakube.DynaKube - if err := apiReader.Get(ctx, request.NamespacedName, &dk); err != nil { - if k8serrors.IsNotFound(err) { - log.Info("given DynaKube object not found") - - return nil, nil //nolint:nilnil - } - - log.Info("failed to get DynaKube object") - - return nil, errors.WithStack(err) - } - - return &dk, nil -} diff --git a/pkg/controllers/csi/gc/reconciler_test.go b/pkg/controllers/csi/gc/reconciler_test.go deleted file mode 100644 index ee90537589..0000000000 --- a/pkg/controllers/csi/gc/reconciler_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package csigc - -import ( - "context" - "fmt" - "testing" - - "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi/metadata" - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/reconcile" -) - -func TestReconcile(t *testing.T) { - tenantUUID := "testTenant" - apiUrl := fmt.Sprintf("https://%s.dev.dynatracelabs.com/api", tenantUUID) - namespace := "test-namespace" - - t.Run(`no latest version in status`, func(t *testing.T) { - dk := dynakube.DynaKube{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: namespace, - }, - Spec: dynakube.DynaKubeSpec{ - APIURL: apiUrl, - }, - } - gc := CSIGarbageCollector{ - apiReader: fake.NewClient(&dk), - fs: afero.NewMemMapFs(), - db: metadata.FakeMemoryDB(), - } - result, err := gc.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: dk.Name}}) - - require.NoError(t, err) - assert.Equal(t, reconcile.Result{}, result) - }) -} diff --git a/pkg/controllers/csi/gc/unmounted.go b/pkg/controllers/csi/gc/unmounted.go deleted file mode 100644 index 9696add7eb..0000000000 --- a/pkg/controllers/csi/gc/unmounted.go +++ /dev/null @@ -1,97 +0,0 @@ -package csigc - -import ( - "os" - "strconv" - "time" - - "github.com/spf13/afero" -) - -const ( - defaultMaxUnmountedCsiVolumeAge = 7 * 24 * time.Hour - maxUnmountedCsiVolumeAgeEnv = "MAX_UNMOUNTED_VOLUME_AGE" -) - -func (gc *CSIGarbageCollector) runUnmountedVolumeGarbageCollection(tenantUUID string) { - unmountedVolumes, err := gc.getUnmountedVolumes(tenantUUID) - if err != nil { - log.Info("failed to get unmounted volume information", "error", err) - - return - } - - gc.removeUnmountedVolumesIfNecessary(unmountedVolumes, tenantUUID) -} - -func (gc *CSIGarbageCollector) getUnmountedVolumes(tenantUUID string) ([]os.FileInfo, error) { - var unusedVolumeIDs []os.FileInfo - - mountsDirectoryPath := gc.path.AgentRunDir(tenantUUID) - - volumeIDs, err := afero.ReadDir(gc.fs, mountsDirectoryPath) - if err != nil { - if os.IsNotExist(err) { - log.Info("no mount directories found for this tenant, moving on", "tenantUUID", tenantUUID, "path", mountsDirectoryPath) - - return nil, nil - } - - return nil, err - } - - for _, volumeID := range volumeIDs { - mappedDir := gc.path.OverlayMappedDir(tenantUUID, volumeID.Name()) - isUnused, err := afero.IsEmpty(gc.fs, mappedDir) - - if err != nil { - log.Info("failed to check if directory is empty, skipping", "folder", mappedDir, "error", err) - - continue - } - - if isUnused { - unusedVolumeIDs = append(unusedVolumeIDs, volumeID) - } - } - - return unusedVolumeIDs, nil -} - -func (gc *CSIGarbageCollector) removeUnmountedVolumesIfNecessary(unusedVolumeIDs []os.FileInfo, tenantUUID string) { - for _, unusedVolumeID := range unusedVolumeIDs { - if gc.isUnmountedVolumeTooOld(unusedVolumeID.ModTime()) { - err := gc.fs.RemoveAll(gc.path.AgentRunDirForVolume(tenantUUID, unusedVolumeID.Name())) - if err != nil { - log.Info("failed to remove logs for pod", "podUID", unusedVolumeID.Name(), "error", err) - } - } - } -} - -func (gc *CSIGarbageCollector) isUnmountedVolumeTooOld(t time.Time) bool { - return gc.maxUnmountedVolumeAge == 0 || time.Since(t) > gc.maxUnmountedVolumeAge -} - -func determineMaxUnmountedVolumeAge(maxAgeEnvValue string) time.Duration { - if maxAgeEnvValue == "" { - return defaultMaxUnmountedCsiVolumeAge - } - - maxAge, err := strconv.Atoi(maxAgeEnvValue) - if err != nil { - log.Error(err, "failed to parse MaxUnmountedCsiVolumeAge from", "env", maxUnmountedCsiVolumeAgeEnv, "value", maxAgeEnvValue) - - return defaultMaxUnmountedCsiVolumeAge - } - - if maxAge <= 0 { - log.Info("max unmounted csi volume age is set to 0, files will be deleted immediately") - - return 0 - } - - log.Info("max unmounted csi volume age used", "age in days", maxAge) - - return 24 * time.Duration(maxAge) * time.Hour -} diff --git a/pkg/controllers/csi/gc/unmounted_test.go b/pkg/controllers/csi/gc/unmounted_test.go deleted file mode 100644 index 4cef241376..0000000000 --- a/pkg/controllers/csi/gc/unmounted_test.go +++ /dev/null @@ -1,184 +0,0 @@ -package csigc - -import ( - "os" - "path/filepath" - "testing" - "time" - - "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi/metadata" - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - mount "k8s.io/mount-utils" -) - -const ( - testRootDir = "root-dir" - testTenantUUID = "tenant-12" - - testVersion1 = "v1" - testVersion2 = "v2" - testVersion3 = "v3" -) - -var ( - testVolumeFolderPath = filepath.Join(testRootDir, testTenantUUID, "run") -) - -func TestGetUnmountedVolumes(t *testing.T) { - t.Run("no error if no volumes are present", func(t *testing.T) { - gc := NewMockGarbageCollector() - _ = gc.fs.MkdirAll(testVolumeFolderPath, 0770) - - unmountedVolumes, err := gc.getUnmountedVolumes(testTenantUUID) - - require.NoError(t, err) - assert.Equal(t, []os.FileInfo(nil), unmountedVolumes) - }) - t.Run("mounted volumes are not collected", func(t *testing.T) { - gc := NewMockGarbageCollector() - gc.mockMountedVolumeIDPath(testVersion1) - - unmountedVolumes, err := gc.getUnmountedVolumes(testTenantUUID) - - require.NoError(t, err) - assert.Equal(t, []os.FileInfo(nil), unmountedVolumes) - }) - t.Run("unmounted volumes are collected", func(t *testing.T) { - gc := NewMockGarbageCollector() - gc.mockUnmountedVolumeIDPath(testVersion1) - - unmountedVolumes, err := gc.getUnmountedVolumes(testTenantUUID) - - require.NoError(t, err) - assert.Equal(t, testVersion1, unmountedVolumes[0].Name()) - }) - t.Run("multiple unmounted volumes are collected", func(t *testing.T) { - gc := NewMockGarbageCollector() - gc.mockUnmountedVolumeIDPath(testVersion1, testVersion2, testVersion3) - - unmountedVolumes, err := gc.getUnmountedVolumes(testTenantUUID) - - require.NoError(t, err) - require.Len(t, unmountedVolumes, 3) - assert.Equal(t, testVersion1, unmountedVolumes[0].Name()) - assert.Equal(t, testVersion2, unmountedVolumes[1].Name()) - assert.Equal(t, testVersion3, unmountedVolumes[2].Name()) - }) - t.Run("multiple unmounted volumes are collected while mounted volume is present", func(t *testing.T) { - gc := NewMockGarbageCollector() - gc.mockMountedVolumeIDPath(testVersion3) - gc.mockUnmountedVolumeIDPath(testVersion1, testVersion2) - - unmountedVolumes, err := gc.getUnmountedVolumes(testTenantUUID) - - require.NoError(t, err) - require.Len(t, unmountedVolumes, 2) - assert.Equal(t, testVersion1, unmountedVolumes[0].Name()) - assert.Equal(t, testVersion2, unmountedVolumes[1].Name()) - }) -} - -func TestIsUnmountedVolumeTooOld(t *testing.T) { - t.Run("default is false for current timestamp", func(t *testing.T) { - gc := CSIGarbageCollector{ - maxUnmountedVolumeAge: defaultMaxUnmountedCsiVolumeAge, - } - - isOlder := gc.isUnmountedVolumeTooOld(time.Now()) - - assert.False(t, isOlder) - }) - - t.Run("default is true for timestamp 14 days in past", func(t *testing.T) { - gc := CSIGarbageCollector{ - maxUnmountedVolumeAge: defaultMaxUnmountedCsiVolumeAge, - } - - isOlder := gc.isUnmountedVolumeTooOld(time.Now().AddDate(0, 0, -15)) - - assert.True(t, isOlder) - }) - - t.Run("is true if maxUnmountedCsiVolumeAge == 0", func(t *testing.T) { - gc := CSIGarbageCollector{ - maxUnmountedVolumeAge: 0, - } - - isOlder := gc.isUnmountedVolumeTooOld(time.Now().AddDate(0, 0, 15)) - - assert.True(t, isOlder) - }) -} - -func TestRemoveUnmountedVolumesIfNecessary(t *testing.T) { - t.Run("remove only too old unmounted volume", func(t *testing.T) { - gc := NewMockGarbageCollector() - gc.mockUnmountedVolumeIDPath(testVersion1, testVersion2, testVersion3) - - unmountedVolumes, err := gc.getUnmountedVolumes(testTenantUUID) - require.NoError(t, err) - require.NotNil(t, unmountedVolumes) - - oldVolume := unmountedVolumes[0] - err = gc.fs.Chtimes(filepath.Join(testVolumeFolderPath, oldVolume.Name()), oldVolume.ModTime(), oldVolume.ModTime().AddDate(0, 0, -15)) - require.NoError(t, err) - - older := gc.isUnmountedVolumeTooOld(oldVolume.ModTime()) - require.True(t, older) - - gc.removeUnmountedVolumesIfNecessary(unmountedVolumes, testTenantUUID) - oldVolumeExists, err := afero.DirExists(gc.fs, filepath.Join(testVolumeFolderPath, oldVolume.Name())) - require.NoError(t, err) - assert.False(t, oldVolumeExists) - - for _, remainingVolume := range unmountedVolumes[1:] { - volumeExists, err := afero.DirExists(gc.fs, filepath.Join(testVolumeFolderPath, remainingVolume.Name())) - require.NoError(t, err) - assert.True(t, volumeExists) - } - }) -} - -func TestDetermineMaxUnmountedVolumeAge(t *testing.T) { - t.Run("no env set ==> use default", func(t *testing.T) { - maxVolumeAge := determineMaxUnmountedVolumeAge("") - - assert.Equal(t, defaultMaxUnmountedCsiVolumeAge, maxVolumeAge) - }) - - t.Run("use (short) duration from env", func(t *testing.T) { - maxVolumeAge := determineMaxUnmountedVolumeAge("1") - - assert.Equal(t, time.Hour*24, maxVolumeAge) - }) - - t.Run("negative duration in env => use 0", func(t *testing.T) { - maxVolumeAge := determineMaxUnmountedVolumeAge("-1") - - assert.Equal(t, time.Duration(0), maxVolumeAge) - }) -} - -func (gc *CSIGarbageCollector) mockMountedVolumeIDPath(volumeIDs ...string) { - for _, volumeID := range volumeIDs { - _ = gc.fs.MkdirAll(filepath.Join(testVolumeFolderPath, volumeID, "mapped", "something"), os.ModePerm) - } -} - -func (gc *CSIGarbageCollector) mockUnmountedVolumeIDPath(volumeIDs ...string) { - for _, volumeID := range volumeIDs { - _ = gc.fs.MkdirAll(filepath.Join(testVolumeFolderPath, volumeID, "mapped"), os.ModePerm) - } -} - -func NewMockGarbageCollector(mountPoints ...mount.MountPoint) *CSIGarbageCollector { - return &CSIGarbageCollector{ - fs: afero.NewMemMapFs(), - db: metadata.FakeMemoryDB(), - path: metadata.PathResolver{RootDir: testRootDir}, - mounter: mount.NewFakeMounter(mountPoints), - maxUnmountedVolumeAge: defaultMaxUnmountedCsiVolumeAge, - } -} diff --git a/pkg/controllers/csi/metadata/correctness.go b/pkg/controllers/csi/metadata/correctness.go index 19dd50e547..a542663841 100644 --- a/pkg/controllers/csi/metadata/correctness.go +++ b/pkg/controllers/csi/metadata/correctness.go @@ -4,29 +4,37 @@ import ( "context" "os" "path/filepath" + "strings" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" dtcsi "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi" - "github.com/pkg/errors" + "github.com/Dynatrace/dynatrace-operator/pkg/injection/codemodule/installer/symlink" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/env" "github.com/spf13/afero" - corev1 "k8s.io/api/core/v1" - k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/mount-utils" "sigs.k8s.io/controller-runtime/pkg/client" ) type CorrectnessChecker struct { + fs afero.Afero apiReader client.Reader - fs afero.Fs - access Access + mounter mount.Interface path PathResolver } -func NewCorrectnessChecker(cl client.Reader, access Access, opts dtcsi.CSIOptions) *CorrectnessChecker { +type OverlayMount struct { + Path string + LowerDir string + UpperDir string + WorkDir string +} + +func NewCorrectnessChecker(apiReader client.Reader, opts dtcsi.CSIOptions) *CorrectnessChecker { return &CorrectnessChecker{ - apiReader: cl, - fs: afero.NewOsFs(), + apiReader: apiReader, + fs: afero.Afero{Fs: afero.NewOsFs()}, + mounter: mount.New(""), path: PathResolver{RootDir: opts.RootDir}, - access: access, } } @@ -34,180 +42,154 @@ func NewCorrectnessChecker(cl client.Reader, access Access, opts dtcsi.CSIOption // Removes not valid entries // "Moves" agent bins from deprecated location. (just creates a symlink) func (checker *CorrectnessChecker) CorrectCSI(ctx context.Context) error { - defer LogAccessOverview(checker.access) - - if err := checker.removeVolumesForMissingPods(ctx); err != nil { - return err - } - - if err := checker.removeMissingDynakubes(ctx); err != nil { - return err - } - - if err := checker.copyCodeModulesFromDeprecatedBin(ctx); err != nil { - return err - } - - checker.migrateAppMounts(ctx) + checker.migrateAppMounts() + checker.migrateHostMounts(ctx) return nil } -// Removes volume entries if their pod is no longer exists -func (checker *CorrectnessChecker) removeVolumesForMissingPods(ctx context.Context) error { - if checker.apiReader == nil { - log.Info("no kubernetes client configured, skipping orphaned volume metadata cleanup") - - return nil - } +func (checker *CorrectnessChecker) migrateAppMounts() { + baseDir := checker.path.RootDir - podNames, err := checker.access.GetPodNames(ctx) + appMounts, err := GetRelevantOverlayMounts(checker.mounter, baseDir) if err != nil { - return err + log.Error(err, "failed to get relevant overlay mounts") } - pruned := []string{} + oldAppMounts := []OverlayMount{} - for podName := range podNames { - var pod corev1.Pod - if err := checker.apiReader.Get(ctx, client.ObjectKey{Name: podName}, &pod); !k8serrors.IsNotFound(err) { - continue + for _, appMount := range appMounts { + if !strings.HasPrefix(appMount.Path, checker.path.AppMountsBaseDir()) { + oldAppMounts = append(oldAppMounts, appMount) } - - volumeID := podNames[podName] - if err := checker.access.DeleteVolume(ctx, volumeID); err != nil { - return err - } - - pruned = append(pruned, volumeID+"|"+podName) } - log.Info("CSI volumes database is corrected for missing pods (volume|pod)", "prunedRows", pruned) + checker.fs.MkdirAll(checker.path.AppMountsBaseDir(), os.ModePerm) - return nil -} - -// Removes dynakube entries if their Dynakube instance no longer exists in the cluster -func (checker *CorrectnessChecker) removeMissingDynakubes(ctx context.Context) error { - if checker.apiReader == nil { - log.Info("no kubernetes client configured, skipping orphaned dynakube metadata cleanup") - - return nil - } - - dynakubes, err := checker.access.GetTenantsToDynakubes(ctx) - if err != nil { - return err - } + for _, appMount := range oldAppMounts { + oldPath := filepath.Dir(appMount.Path) + volumeID := filepath.Base(oldPath) + newPath := checker.path.AppMountForID(volumeID) - pruned := []string{} - - for dynakubeName := range dynakubes { - var dk dynakube.DynaKube - if err := checker.apiReader.Get(ctx, client.ObjectKey{Name: dynakubeName}, &dk); !k8serrors.IsNotFound(err) { + exists, _ := checker.fs.DirExists(newPath) + if exists { continue } - if err := checker.access.DeleteDynakube(ctx, dynakubeName); err != nil { - return err + err := symlink.Create(checker.fs.Fs, oldPath, newPath) + if err != nil { + log.Error(err, "failed to symlink old app mount to new location", "old-path", oldPath, "new-path", newPath) + } else { + log.Info("migrated old app mount to new location", "old-path", oldPath, "new-path", newPath) } - - tenantUUID := dynakubes[dynakubeName] - pruned = append(pruned, tenantUUID+"|"+dynakubeName) } - - log.Info("CSI tenants database is corrected for missing dynakubes (tenant|dynakube)", "prunedRows", pruned) - - return nil } -func (checker *CorrectnessChecker) copyCodeModulesFromDeprecatedBin(ctx context.Context) error { - dynakubes, err := checker.access.GetAllDynakubes(ctx) +func (checker *CorrectnessChecker) migrateHostMounts(ctx context.Context) { + dks, err := GetRelevantDynaKubes(ctx, checker.apiReader) if err != nil { - return err - } + log.Error(err, "failed to list the available dynakubes, skipping host mount migration") - moved := []string{} + return + } - for _, dynakube := range dynakubes { - if dynakube.TenantUUID == "" || dynakube.LatestVersion == "" { + for _, dk := range dks { + if !dk.OneAgent().IsReadOnlyFSSupported() { continue } - deprecatedBin := checker.path.AgentBinaryDirForVersion(dynakube.TenantUUID, dynakube.LatestVersion) - currentBin := checker.path.AgentSharedBinaryDirForAgent(dynakube.LatestVersion) + checker.fs.MkdirAll(checker.path.DynaKubeDir(dk.Name), os.ModePerm) - linked, err := checker.safelyLinkCodeModule(deprecatedBin, currentBin) - if err != nil { - return err - } + newPath := checker.path.OsAgentDir(dk.Name) - if linked { - moved = append(moved, dynakube.TenantUUID+"|"+dynakube.LatestVersion) + newExists, _ := checker.fs.DirExists(newPath) + if newExists { + continue } - } - - log.Info("CSI filesystem corrected, linked deprecated agent binary to current location (tenant|version-bin)", "movedBins", moved) - return nil -} - -func (checker *CorrectnessChecker) safelyLinkCodeModule(deprecatedBin, currentBin string) (bool, error) { - if folderExists(checker.fs, deprecatedBin) && !folderExists(checker.fs, currentBin) { - log.Info("linking codemodule from deprecated location", "path", deprecatedBin) - // MemMapFs (used for testing) doesn't comply with the Linker interface - linker, ok := checker.fs.(afero.Linker) - if !ok { - log.Info("symlinking not possible", "path", deprecatedBin) + tenantUUID, err := TenantUUIDFromApiUrl(dk.ApiUrl()) + if err != nil { + log.Error(err, "malformed ApiUrl for dynakube, skipping host dir migration for it", "dk", dk.Name, "apiUrl", dk.ApiUrl()) - return false, nil + continue } - err := checker.fs.MkdirAll(filepath.Dir(currentBin), 0755) + oldPath := checker.path.OldOsAgentDir(tenantUUID) + + oldExists, err := checker.fs.DirExists(oldPath) if err != nil { - log.Info("failed to create parent dir for new path", "path", currentBin) + log.Error(err, "failed to check deprecated host dir existence, skipping host dir migration for it", "dk", dk.Name, "apiUrl", dk.ApiUrl()) - return false, errors.WithStack(err) + continue } - log.Info("creating symlink", "from", deprecatedBin, "to", currentBin) - - if err := linker.SymlinkIfPossible(deprecatedBin, currentBin); err != nil { - log.Info("symlinking failed", "path", deprecatedBin) - - return false, errors.WithStack(err) + if !oldExists { + continue } - return true, nil + err = symlink.Create(checker.fs.Fs, oldPath, newPath) + if err != nil { + log.Error(err, "failed to symlink old host mount to new location", "old-path", oldPath, "new-path", newPath) + } else { + log.Info("migrated old host mount to new location", "old-path", oldPath, "new-path", newPath) + } } - - return false, nil } -func (checker *CorrectnessChecker) migrateAppMounts(ctx context.Context) { - volumes := checker.access.GetAllAppMounts(ctx) +func GetRelevantDynaKubes(ctx context.Context, apiReader client.Reader) ([]dynakube.DynaKube, error) { + var dkList dynakube.DynaKubeList - for _, volume := range volumes { - err := checker.access.InsertVolume(ctx, volume) - if err != nil { - log.Info("failed to insert volume", "id", volume.VolumeID, "error", err) + err := apiReader.List(ctx, &dkList, client.InNamespace(env.DefaultNamespace())) + if err != nil { + return nil, err + } - continue - } + var relevantDks []dynakube.DynaKube - // we need to prevent filling the DB with entries if the CSI Pod is restarted - err = checker.access.DeleteAppMount(ctx, volume.VolumeID) - if err != nil { - log.Info("failed to delete app_mount entry", "id", volume.VolumeID, "error", err) + for _, dk := range dkList.Items { + if dk.OneAgent().IsAppInjectionNeeded() || dk.OneAgent().IsReadOnlyFSSupported() { + relevantDks = append(relevantDks, dk) } } + + return relevantDks, nil } -func folderExists(fs afero.Fs, filename string) bool { - info, err := fs.Stat(filename) - if os.IsNotExist(err) { - return false +func GetRelevantOverlayMounts(mounter mount.Interface, baseFolder string) ([]OverlayMount, error) { + mountPoints, err := mounter.List() + if err != nil { + return nil, err + } + + relevantMounts := []OverlayMount{} + + for _, mountPoint := range mountPoints { + if mountPoint.Device == "overlay" { + if !strings.HasPrefix(mountPoint.Path, baseFolder) { + continue + } + + overlayMount := OverlayMount{ + Path: mountPoint.Path, + } + + for _, opt := range mountPoint.Opts { + switch { + case strings.HasPrefix(opt, "lowerdir="): + split := strings.SplitN(opt, "=", 2) + overlayMount.LowerDir = split[1] + case strings.HasPrefix(opt, "upperdir="): + split := strings.SplitN(opt, "=", 2) + overlayMount.UpperDir = split[1] + case strings.HasPrefix(opt, "workdir="): + split := strings.SplitN(opt, "=", 2) + overlayMount.WorkDir = split[1] + } + } + + relevantMounts = append(relevantMounts, overlayMount) + } } - return info.IsDir() + return relevantMounts, nil } diff --git a/pkg/controllers/csi/metadata/correctness_test.go b/pkg/controllers/csi/metadata/correctness_test.go index a833469daf..e7c5521c39 100644 --- a/pkg/controllers/csi/metadata/correctness_test.go +++ b/pkg/controllers/csi/metadata/correctness_test.go @@ -1,163 +1,90 @@ package metadata import ( - "context" - "fmt" - "strconv" "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - dtcsi "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/mount-utils" ) -func createTestDynakube(index int) Dynakube { - return Dynakube{ - TenantUUID: fmt.Sprintf("asc%d", index), - LatestVersion: strconv.Itoa(123 * index), - Name: fmt.Sprintf("dk%d", index), - ImageDigest: fmt.Sprintf("sha256:%d", 123*index), - MaxFailedMountAttempts: index, - } -} - -func createTestVolume(index int) Volume { - return Volume{ - VolumeID: fmt.Sprintf("vol-%d", index), - PodName: fmt.Sprintf("pod%d", index), - Version: createTestDynakube(index).LatestVersion, - TenantUUID: createTestDynakube(index).TenantUUID, - MountAttempts: index, - } -} - -func TestCorrectCSI(t *testing.T) { - t.Run("error on no db or missing tables", func(t *testing.T) { - db := emptyMemoryDB() - - checker := NewCorrectnessChecker(nil, db, dtcsi.CSIOptions{}) - - err := checker.CorrectCSI(context.TODO()) - - require.Error(t, err) +func TestGetRelevantOverlayMounts(t *testing.T) { + t.Run("get only relevant mounts", func(t *testing.T) { + baseFolder := "/test/folder" + expectedPath := baseFolder + "/some/sub/folder" + expectedLowerDir := "/data/codemodules/cXVheS5pby9keW5hdHJhY2UvZHluYXRyYWNlLWJvb3RzdHJhcHBlcjpzbmFwc2hvdA==" + expectedUpperDir := "/data/appmounts/csi-a3dd8a9ab6e64e92efca99a0d180da60ab807f0e31a04e11edb451311130211c/var" + expectedWorkDir := "/data/appmounts/csi-a3dd8a9ab6e64e92efca99a0d180da60ab807f0e31a04e11edb451311130211c/work" + + relevantMountPoint := mount.MountPoint{ + Device: "overlay", + Path: expectedPath, + Type: "overlay", + Opts: []string{ + "lowerdir=" + expectedLowerDir, + "upperdir=" + expectedUpperDir, + "workdir=" + expectedWorkDir, + }, + } + + mounter := mount.NewFakeMounter([]mount.MountPoint{ + relevantMountPoint, + { + Device: "not-relevant-mount-type", + }, + { + Device: "overlay", + Path: "not-relevant-overlay-mount", + Type: "overlay", + }, + }) + + mounts, err := GetRelevantOverlayMounts(mounter, baseFolder) + require.NoError(t, err) + require.NotNil(t, mounts) + require.Len(t, mounts, 1) + assert.Equal(t, expectedPath, mounts[0].Path) + assert.Equal(t, expectedLowerDir, mounts[0].LowerDir) + assert.Equal(t, expectedUpperDir, mounts[0].UpperDir) + assert.Equal(t, expectedWorkDir, mounts[0].WorkDir) }) - t.Run("no error on empty db", func(t *testing.T) { - db := FakeMemoryDB() - - checker := NewCorrectnessChecker(nil, db, dtcsi.CSIOptions{}) - - err := checker.CorrectCSI(context.TODO()) + t.Run("works with no mount points", func(t *testing.T) { + mounter := mount.NewFakeMounter([]mount.MountPoint{}) + mounts, err := GetRelevantOverlayMounts(mounter, "") require.NoError(t, err) + require.NotNil(t, mounts) + require.Empty(t, mounts) }) - t.Run("no error on nil apiReader, database is not cleaned", func(t *testing.T) { - ctx := context.TODO() - testVolume1 := createTestVolume(1) - testDynakube1 := createTestDynakube(1) - db := FakeMemoryDB() - db.InsertVolume(ctx, &testVolume1) - db.InsertDynakube(ctx, &testDynakube1) - - checker := NewCorrectnessChecker(nil, db, dtcsi.CSIOptions{}) - - err := checker.CorrectCSI(context.TODO()) - - require.NoError(t, err) - vol, err := db.GetVolume(ctx, testVolume1.VolumeID) - require.NoError(t, err) - assert.Equal(t, &testVolume1, vol) - - require.NoError(t, err) - dk, err := db.GetDynakube(ctx, testDynakube1.Name) - require.NoError(t, err) - assert.Equal(t, &testDynakube1, dk) - }) - - t.Run("nothing to remove, everything is still correct", func(t *testing.T) { - ctx := context.TODO() - testVolume1 := createTestVolume(1) - testDynakube1 := createTestDynakube(1) - db := FakeMemoryDB() - db.InsertVolume(ctx, &testVolume1) - db.InsertDynakube(ctx, &testDynakube1) - client := fake.NewClient( - &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: testVolume1.PodName}}, - &dynakube.DynaKube{ObjectMeta: metav1.ObjectMeta{Name: testDynakube1.Name}}, - ) - - checker := NewCorrectnessChecker(client, db, dtcsi.CSIOptions{}) - - err := checker.CorrectCSI(ctx) - - require.NoError(t, err) - vol, err := db.GetVolume(ctx, testVolume1.VolumeID) - require.NoError(t, err) - assert.Equal(t, &testVolume1, vol) - - require.NoError(t, err) - dk, err := db.GetDynakube(ctx, testDynakube1.Name) - require.NoError(t, err) - assert.Equal(t, &testDynakube1, dk) + t.Run("ignores irrelevant mounts", func(t *testing.T) { + mounter := mount.NewFakeMounter([]mount.MountPoint{ + { + Device: "not-relevant-mount-type", + }, + { + Device: "overlay", + Path: "not-relevant-overlay-mount", + Type: "overlay", + }, + }) + mounts, err := GetRelevantOverlayMounts(mounter, "/test") + require.NoError(t, err) + require.NotNil(t, mounts) + require.Empty(t, mounts) }) - t.Run("remove unnecessary entries in the filesystem", func(t *testing.T) { - ctx := context.TODO() - testVolume1 := createTestVolume(1) - testVolume2 := createTestVolume(2) - testVolume3 := createTestVolume(3) - - testDynakube1 := createTestDynakube(1) - testDynakube2 := createTestDynakube(2) - testDynakube3 := createTestDynakube(3) - - db := FakeMemoryDB() - db.InsertVolume(ctx, &testVolume1) - db.InsertVolume(ctx, &testVolume2) - db.InsertVolume(ctx, &testVolume3) - db.InsertDynakube(ctx, &testDynakube1) - db.InsertDynakube(ctx, &testDynakube2) - db.InsertDynakube(ctx, &testDynakube3) - - client := fake.NewClient( - &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: testVolume1.PodName}}, - &dynakube.DynaKube{ObjectMeta: metav1.ObjectMeta{Name: testDynakube1.Name}}, - ) - - checker := NewCorrectnessChecker(client, db, dtcsi.CSIOptions{}) - - err := checker.CorrectCSI(ctx) - require.NoError(t, err) - - vol, err := db.GetVolume(ctx, testVolume1.VolumeID) - require.NoError(t, err) - assert.Equal(t, &testVolume1, vol) - - ten, err := db.GetDynakube(ctx, testDynakube1.Name) - require.NoError(t, err) - assert.Equal(t, &testDynakube1, ten) - - // PURGED - vol, err = db.GetVolume(ctx, testVolume2.VolumeID) - require.NoError(t, err) - assert.Nil(t, vol) - - // PURGED - vol, err = db.GetVolume(ctx, testVolume3.VolumeID) - require.NoError(t, err) - assert.Nil(t, vol) +} - // PURGED - ten, err = db.GetDynakube(ctx, testDynakube2.TenantUUID) - require.NoError(t, err) - assert.Nil(t, ten) +func TestMigrateAppMounts(t *testing.T) { + // Unfortunately, this is not unit-testable. + // Its output would be a bunch of symlinks, + // however the afero.MemMapFs does not support symlinking. + t.SkipNow() +} - // PURGED - ten, err = db.GetDynakube(ctx, testDynakube3.TenantUUID) - require.NoError(t, err) - assert.Nil(t, ten) - }) +func TestMigrateHostMounts(t *testing.T) { + // Unfortunately, this is not unit-testable. + // Its output would be a bunch of symlinks, + // however the afero.MemMapFs does not support symlinking. + t.SkipNow() } diff --git a/pkg/controllers/csi/metadata/deprecared_test.go b/pkg/controllers/csi/metadata/deprecared_test.go new file mode 100644 index 0000000000..45868e0216 --- /dev/null +++ b/pkg/controllers/csi/metadata/deprecared_test.go @@ -0,0 +1,76 @@ +package metadata + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTenantUUIDFromApiUrl(t *testing.T) { + t.Run("happy path", func(t *testing.T) { + apiUrl := "https://demo.dev.dynatracelabs.com/api" + expectedTenantId := "demo" + + actualTenantId, err := TenantUUIDFromApiUrl(apiUrl) + + require.NoErrorf(t, err, "Expected that getting tenant id from '%s' will be successful", apiUrl) + assert.Equalf(t, expectedTenantId, actualTenantId, "Expected that tenant id of %s is %s, but found %s", + apiUrl, expectedTenantId, actualTenantId, + ) + }) + + t.Run("happy path (alternative)", func(t *testing.T) { + apiUrl := "https://dynakube-activegate.dynatrace/e/tenant/api/v2/metrics/ingest" + expectedTenantId := "tenant" + + actualTenantId, err := TenantUUIDFromApiUrl(apiUrl) + + require.NoErrorf(t, err, "Expected that getting tenant id from '%s' will be successful", apiUrl) + assert.Equalf(t, expectedTenantId, actualTenantId, "Expected that tenant id of %s is %s, but found %s", + apiUrl, expectedTenantId, actualTenantId, + ) + }) + + t.Run("happy path (alternative, no domain)", func(t *testing.T) { + apiUrl := "https://dynakube-activegate/e/tenant/api/v2/metrics/ingest" + expectedTenantId := "tenant" + + actualTenantId, err := TenantUUIDFromApiUrl(apiUrl) + + require.NoErrorf(t, err, "Expected that getting tenant id from '%s' will be successful", apiUrl) + assert.Equalf(t, expectedTenantId, actualTenantId, "Expected that tenant id of %s is %s, but found %s", + apiUrl, expectedTenantId, actualTenantId, + ) + }) + + t.Run("missing API URL protocol", func(t *testing.T) { + apiUrl := "demo.dev.dynatracelabs.com/api" + expectedTenantId := "" + expectedError := "problem getting tenant id from API URL 'demo.dev.dynatracelabs.com/api'" + + actualTenantId, err := TenantUUIDFromApiUrl(apiUrl) + + require.EqualErrorf(t, err, expectedError, "Expected that getting tenant id from '%s' will result in: '%v'", + apiUrl, expectedError, + ) + assert.Equalf(t, expectedTenantId, actualTenantId, "Expected that tenant id of %s is %s, but found %s", + apiUrl, expectedTenantId, actualTenantId, + ) + }) + + t.Run("suffix-only, relative API URL", func(t *testing.T) { + apiUrl := "/api" + expectedTenantId := "" + expectedError := "problem getting tenant id from API URL '/api'" + + actualTenantId, err := TenantUUIDFromApiUrl(apiUrl) + + require.EqualErrorf(t, err, expectedError, "Expected that getting tenant id from '%s' will result in: '%v'", + apiUrl, expectedError, + ) + assert.Equalf(t, expectedTenantId, actualTenantId, "Expected that tenant id of %s is %s, but found %s", + apiUrl, expectedTenantId, actualTenantId, + ) + }) +} diff --git a/pkg/controllers/csi/metadata/deprecated.go b/pkg/controllers/csi/metadata/deprecated.go new file mode 100644 index 0000000000..f3bddb740c --- /dev/null +++ b/pkg/controllers/csi/metadata/deprecated.go @@ -0,0 +1,35 @@ +package metadata + +import ( + "net/url" + "strings" + + "github.com/pkg/errors" +) + +// only kept for migration +func TenantUUIDFromApiUrl(apiUrl string) (string, error) { + parsedUrl, err := url.Parse(apiUrl) + if err != nil { + return "", errors.WithMessagef(err, "problem parsing tenant id from url %s", apiUrl) + } + + // Path = "/e//api" -> ["e", "", "api"] + subPaths := strings.FieldsFunc(parsedUrl.Path, runeIs('/')) + if len(subPaths) >= 3 && subPaths[0] == "e" && subPaths[2] == "api" { + return subPaths[1], nil + } + + hostnameWithDomains := strings.FieldsFunc(parsedUrl.Hostname(), runeIs('.')) + if len(hostnameWithDomains) >= 1 { + return hostnameWithDomains[0], nil + } + + return "", errors.Errorf("problem getting tenant id from API URL '%s'", apiUrl) +} + +func runeIs(wanted rune) func(rune) bool { + return func(actual rune) bool { + return actual == wanted + } +} diff --git a/pkg/controllers/csi/metadata/fakes.go b/pkg/controllers/csi/metadata/fakes.go deleted file mode 100644 index da034b8ff2..0000000000 --- a/pkg/controllers/csi/metadata/fakes.go +++ /dev/null @@ -1,117 +0,0 @@ -package metadata - -import ( - "context" - "database/sql" -) - -func emptyMemoryDB() *SqliteAccess { - db := SqliteAccess{} - _ = db.connect(sqliteDriverName, ":memory:") - - return &db -} - -func FakeMemoryDB() *SqliteAccess { - db := SqliteAccess{} - ctx := context.Background() - _ = db.Setup(ctx, ":memory:") - _ = db.createTables(ctx) - - return &db -} - -func checkIfTablesExist(db *SqliteAccess) bool { - var volumesTable string - - row := db.conn.QueryRow("SELECT name FROM sqlite_master WHERE type='table' AND name=?;", volumesTableName) - - err := row.Scan(&volumesTable) - if err != nil { - return false - } - - var tenantsTable string - - row = db.conn.QueryRow("SELECT name FROM sqlite_master WHERE type='table' AND name=?;", dynakubesTableName) - - err = row.Scan(&tenantsTable) - if err != nil { - return false - } - - if tenantsTable != dynakubesTableName || volumesTable != volumesTableName { - return false - } - - return true -} - -type FakeFailDB struct{} - -func (f *FakeFailDB) Setup(_ context.Context, _ string) error { return sql.ErrTxDone } -func (f *FakeFailDB) InsertDynakube(_ context.Context, _ *Dynakube) error { - return sql.ErrTxDone -} -func (f *FakeFailDB) UpdateDynakube(_ context.Context, _ *Dynakube) error { - return sql.ErrTxDone -} -func (f *FakeFailDB) DeleteDynakube(_ context.Context, _ string) error { - return sql.ErrTxDone -} -func (f *FakeFailDB) GetDynakube(_ context.Context, _ string) (*Dynakube, error) { - return nil, sql.ErrTxDone -} -func (f *FakeFailDB) GetTenantsToDynakubes(_ context.Context) (map[string]string, error) { - return nil, sql.ErrTxDone -} -func (f *FakeFailDB) GetAllDynakubes(_ context.Context) ([]*Dynakube, error) { - return nil, sql.ErrTxDone -} - -func (f *FakeFailDB) InsertOsAgentVolume(_ context.Context, _ *OsAgentVolume) error { - return sql.ErrTxDone -} -func (f *FakeFailDB) GetOsAgentVolumeViaVolumeID(_ context.Context, _ string) (*OsAgentVolume, error) { - return nil, sql.ErrTxDone -} -func (f *FakeFailDB) GetOsAgentVolumeViaTenantUUID(_ context.Context, _ string) (*OsAgentVolume, error) { - return nil, sql.ErrTxDone -} -func (f *FakeFailDB) UpdateOsAgentVolume(_ context.Context, _ *OsAgentVolume) error { - return sql.ErrTxDone -} -func (f *FakeFailDB) GetAllOsAgentVolumes(_ context.Context) ([]*OsAgentVolume, error) { - return nil, sql.ErrTxDone -} - -func (f *FakeFailDB) InsertVolume(_ context.Context, _ *Volume) error { return sql.ErrTxDone } -func (f *FakeFailDB) DeleteVolume(_ context.Context, _ string) error { return sql.ErrTxDone } -func (f *FakeFailDB) GetVolume(_ context.Context, _ string) (*Volume, error) { - return nil, sql.ErrTxDone -} -func (f *FakeFailDB) GetAllVolumes(_ context.Context) ([]*Volume, error) { return nil, sql.ErrTxDone } -func (f *FakeFailDB) GetPodNames(_ context.Context) (map[string]string, error) { - return nil, sql.ErrTxDone -} -func (f *FakeFailDB) GetUsedVersions(_ context.Context, _ string) (map[string]bool, error) { - return nil, sql.ErrTxDone -} -func (f *FakeFailDB) GetAllUsedVersions(_ context.Context) (map[string]bool, error) { - return nil, sql.ErrTxDone -} -func (f *FakeFailDB) GetLatestVersions(_ context.Context) (map[string]bool, error) { - return nil, sql.ErrTxDone -} -func (f *FakeFailDB) GetAllAppMounts(_ context.Context) []*Volume { - return nil -} -func (f *FakeFailDB) DeleteAppMount(_ context.Context, _ string) error { return nil } - -func (f *FakeFailDB) GetUsedImageDigests(_ context.Context) (map[string]bool, error) { - return nil, sql.ErrTxDone -} - -func (f *FakeFailDB) IsImageDigestUsed(_ context.Context, _ string) (bool, error) { - return false, sql.ErrTxDone -} diff --git a/pkg/controllers/csi/metadata/metadata.go b/pkg/controllers/csi/metadata/metadata.go deleted file mode 100644 index f86f2602b0..0000000000 --- a/pkg/controllers/csi/metadata/metadata.go +++ /dev/null @@ -1,143 +0,0 @@ -package metadata - -import ( - "context" - "time" -) - -// Dynakube stores the necessary info from the Dynakube that is needed to be used during volume mount/unmount. -type Dynakube struct { - Name string `json:"name"` - TenantUUID string `json:"tenantUUID"` - LatestVersion string `json:"latestVersion"` - ImageDigest string `json:"imageDigest"` - MaxFailedMountAttempts int `json:"maxFailedMountAttempts"` -} - -// NewDynakube returns a new metadata.Dynakube if all fields are set. -func NewDynakube(dynakubeName, tenantUUID, latestVersion, imageDigest string, maxFailedMountAttempts int) *Dynakube { - if tenantUUID == "" || dynakubeName == "" { - return nil - } - - return &Dynakube{ - Name: dynakubeName, - TenantUUID: tenantUUID, - LatestVersion: latestVersion, - ImageDigest: imageDigest, - MaxFailedMountAttempts: maxFailedMountAttempts, - } -} - -type Volume struct { - VolumeID string `json:"volumeID"` - PodName string `json:"podName"` - Version string `json:"version"` - TenantUUID string `json:"tenantUUID"` - MountAttempts int `json:"mountAttempts"` -} - -// NewVolume returns a new Volume if all fields (except version) are set. -func NewVolume(id, podName, version, tenantUUID string, mountAttempts int) *Volume { - if id == "" || podName == "" || tenantUUID == "" { - return nil - } - - if mountAttempts < 0 { - mountAttempts = 0 - } - - return &Volume{ - VolumeID: id, - PodName: podName, - Version: version, - TenantUUID: tenantUUID, - MountAttempts: mountAttempts, - } -} - -type OsAgentVolume struct { - LastModified *time.Time `json:"lastModified"` - VolumeID string `json:"volumeID"` - TenantUUID string `json:"tenantUUID"` - Mounted bool `json:"mounted"` -} - -// NewOsAgentVolume returns a new volume if all fields are set. -func NewOsAgentVolume(volumeID, tenantUUID string, mounted bool, timeStamp *time.Time) *OsAgentVolume { - if volumeID == "" || tenantUUID == "" || timeStamp == nil { - return nil - } - - return &OsAgentVolume{VolumeID: volumeID, TenantUUID: tenantUUID, Mounted: mounted, LastModified: timeStamp} -} - -type Access interface { - Setup(ctx context.Context, path string) error - - InsertDynakube(ctx context.Context, dynakube *Dynakube) error - UpdateDynakube(ctx context.Context, dynakube *Dynakube) error - DeleteDynakube(ctx context.Context, dynakubeName string) error - GetDynakube(ctx context.Context, dynakubeName string) (*Dynakube, error) - GetTenantsToDynakubes(ctx context.Context) (map[string]string, error) - GetAllDynakubes(ctx context.Context) ([]*Dynakube, error) - - InsertOsAgentVolume(ctx context.Context, volume *OsAgentVolume) error - GetOsAgentVolumeViaVolumeID(ctx context.Context, volumeID string) (*OsAgentVolume, error) - GetOsAgentVolumeViaTenantUUID(ctx context.Context, volumeID string) (*OsAgentVolume, error) - UpdateOsAgentVolume(ctx context.Context, volume *OsAgentVolume) error - GetAllOsAgentVolumes(ctx context.Context) ([]*OsAgentVolume, error) - - InsertVolume(ctx context.Context, volume *Volume) error - DeleteVolume(ctx context.Context, volumeID string) error - GetVolume(ctx context.Context, volumeID string) (*Volume, error) - GetAllVolumes(ctx context.Context) ([]*Volume, error) - GetPodNames(ctx context.Context) (map[string]string, error) - GetUsedVersions(ctx context.Context, tenantUUID string) (map[string]bool, error) - GetAllUsedVersions(ctx context.Context) (map[string]bool, error) - GetLatestVersions(ctx context.Context) (map[string]bool, error) - GetUsedImageDigests(ctx context.Context) (map[string]bool, error) - IsImageDigestUsed(ctx context.Context, imageDigest string) (bool, error) - GetAllAppMounts(ctx context.Context) []*Volume - DeleteAppMount(ctx context.Context, appMountName string) error -} - -type AccessOverview struct { - Volumes []*Volume `json:"volumes"` - Dynakubes []*Dynakube `json:"dynakubes"` - OsAgentVolumes []*OsAgentVolume `json:"osAgentVolumes"` -} - -func NewAccessOverview(access Access) (*AccessOverview, error) { - ctx := context.Background() - - volumes, err := access.GetAllVolumes(ctx) - if err != nil { - return nil, err - } - - dynakubes, err := access.GetAllDynakubes(ctx) - if err != nil { - return nil, err - } - - osVolumes, err := access.GetAllOsAgentVolumes(ctx) - if err != nil { - return nil, err - } - - return &AccessOverview{ - Volumes: volumes, - Dynakubes: dynakubes, - OsAgentVolumes: osVolumes, - }, nil -} - -func LogAccessOverview(access Access) { - overview, err := NewAccessOverview(access) - if err != nil { - log.Error(err, "Failed to get an overview of the stored csi metadata") - } - - log.Info("The current overview of the csi metadata", "overview", overview) -} diff --git a/pkg/controllers/csi/metadata/metadata_test.go b/pkg/controllers/csi/metadata/metadata_test.go deleted file mode 100644 index 50add01477..0000000000 --- a/pkg/controllers/csi/metadata/metadata_test.go +++ /dev/null @@ -1,79 +0,0 @@ -package metadata - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -const ( - testName = "test-name" - testID = "test-id" - testUUID = "test-uuid" - testVersion = "test-version" - testDigest = "test-digest" - testMaxFailedMountAttempts = 3 - testMountAttempts = 1 -) - -func TestNewDynakube(t *testing.T) { - t.Run("initializes correctly", func(t *testing.T) { - dynakube := NewDynakube(testName, testUUID, testVersion, testDigest, testMaxFailedMountAttempts) - - assert.Equal(t, testName, dynakube.Name) - assert.Equal(t, testUUID, dynakube.TenantUUID) - assert.Equal(t, testVersion, dynakube.LatestVersion) - assert.Equal(t, testDigest, dynakube.ImageDigest) - assert.Equal(t, testMaxFailedMountAttempts, dynakube.MaxFailedMountAttempts) - }) - t.Run("returns nil if name or uuid is empty", func(t *testing.T) { - dynakube := NewDynakube("", testUUID, testVersion, testDigest, testMaxFailedMountAttempts) - - assert.Nil(t, dynakube) - - dynakube = NewDynakube(testName, "", testVersion, testDigest, testMaxFailedMountAttempts) - - assert.Nil(t, dynakube) - }) -} - -func TestNewVolume(t *testing.T) { - t.Run("initializes correctly", func(t *testing.T) { - volume := NewVolume(testID, testName, testVersion, testUUID, testMountAttempts) - - assert.Equal(t, testID, volume.VolumeID) - assert.Equal(t, testName, volume.PodName) - assert.Equal(t, testVersion, volume.Version) - assert.Equal(t, testUUID, volume.TenantUUID) - assert.Equal(t, testMountAttempts, volume.MountAttempts) - }) - t.Run("returns nil if id, name, or uuid is unset", func(t *testing.T) { - volume := NewVolume("", testName, testVersion, testUUID, testMountAttempts) - - assert.Nil(t, volume) - - volume = NewVolume(testID, "", testVersion, testUUID, testMountAttempts) - - assert.Nil(t, volume) - - volume = NewVolume(testID, testName, testVersion, "", testMountAttempts) - - assert.Nil(t, volume) - - volume = NewVolume(testID, testName, testVersion, testUUID, 0) - - assert.NotNil(t, volume) - assert.Equal(t, 0, volume.MountAttempts) - }) - t.Run("sets default value for mount attempts if less than 0", func(t *testing.T) { - volume := NewVolume(testID, testName, testVersion, testUUID, -1) - - assert.NotNil(t, volume) - assert.Equal(t, 0, volume.MountAttempts) - - volume = NewVolume(testID, testName, testVersion, testUUID, -2) - - assert.NotNil(t, volume) - assert.Equal(t, 0, volume.MountAttempts) - }) -} diff --git a/pkg/controllers/csi/metadata/path_resolver.go b/pkg/controllers/csi/metadata/path_resolver.go index da9d9e9488..93ef2abd51 100644 --- a/pkg/controllers/csi/metadata/path_resolver.go +++ b/pkg/controllers/csi/metadata/path_resolver.go @@ -11,67 +11,141 @@ type PathResolver struct { RootDir string } -func (pr PathResolver) TenantDir(tenantUUID string) string { - return filepath.Join(pr.RootDir, tenantUUID) +func (pr PathResolver) Base(name string) string { + return filepath.Join(pr.RootDir, name) } -func (pr PathResolver) OsAgentDir(tenantUUID string) string { - return filepath.Join(pr.TenantDir(tenantUUID), "osagent") +func (pr PathResolver) DynaKubesBaseDir() string { + return pr.Base(dtcsi.SharedDynaKubesDir) } -func (pr PathResolver) AgentBinaryDir(tenantUUID string) string { - return filepath.Join(pr.TenantDir(tenantUUID), dtcsi.AgentBinaryDir) +func (pr PathResolver) DynaKubeDir(dynakubeName string) string { + return filepath.Join(pr.DynaKubesBaseDir(), dynakubeName) } -// Deprecated -func (pr PathResolver) AgentBinaryDirForVersion(tenantUUID string, version string) string { - return filepath.Join(pr.AgentBinaryDir(tenantUUID), version) +func (pr PathResolver) OsAgentDir(dynakubeName string) string { + return filepath.Join(pr.DynaKubeDir(dynakubeName), "osagent") } func (pr PathResolver) AgentSharedBinaryDirBase() string { - return filepath.Join(pr.RootDir, dtcsi.SharedAgentBinDir) + return pr.Base(dtcsi.SharedAgentBinDir) +} + +func (pr PathResolver) AgentJobWorkDirBase() string { + return filepath.Join(pr.RootDir, dtcsi.SharedJobWorkDir) +} + +func (pr PathResolver) AgentJobWorkDirForJob(jobName string) string { + return filepath.Join(pr.AgentJobWorkDirBase(), jobName) +} + +func (pr PathResolver) AgentSharedBinaryDirForAgent(versionOrDigest string) string { + return filepath.Join(pr.AgentSharedBinaryDirBase(), versionOrDigest) +} + +func (pr PathResolver) LatestAgentBinaryForDynaKube(dynakubeName string) string { + return filepath.Join(pr.DynaKubeDir(dynakubeName), "latest-codemodule") } func (pr PathResolver) AgentTempUnzipRootDir() string { - return filepath.Join(pr.RootDir, "tmp_zip") + return pr.Base("tmp_zip") } func (pr PathResolver) AgentTempUnzipDir() string { return filepath.Join(pr.AgentTempUnzipRootDir(), "opt", "dynatrace", "oneagent") } -func (pr PathResolver) AgentSharedBinaryDirForAgent(versionOrDigest string) string { - return filepath.Join(pr.AgentSharedBinaryDirBase(), versionOrDigest) +func (pr PathResolver) AgentConfigDir(dynakubeName string) string { + return filepath.Join(pr.DynaKubeDir(dynakubeName), dtcsi.SharedAgentConfigDir) } -func (pr PathResolver) AgentConfigDir(tenantUUID string, dynakubeName string) string { - return filepath.Join(pr.TenantDir(tenantUUID), dynakubeName, dtcsi.SharedAgentConfigDir) +func (pr PathResolver) AgentSharedRuxitAgentProcConf(dynakubeName string) string { + return filepath.Join(pr.AgentConfigDir(dynakubeName), processmoduleconfig.RuxitAgentProcPath) +} + +func (pr PathResolver) OverlayVarRuxitAgentProcConf(volumeID string) string { + return filepath.Join(pr.AppMountVarDir(volumeID), processmoduleconfig.RuxitAgentProcPath) +} + +func (pr PathResolver) OverlayVarPodInfo(volumeID string) string { + return filepath.Join(pr.AppMountVarDir(volumeID), "pod-info") +} + +// AppMountsBaseDir replaces the AgentRunDir, the base directory where all the volumes for the app-mounts are stored +func (pr PathResolver) AppMountsBaseDir() string { + return pr.Base(dtcsi.SharedAppMountsDir) +} + +// AppMountForID replaces AgentRunDirForVolume, the directory where a given app-mount volume is stored +func (pr PathResolver) AppMountForID(volumeID string) string { + return filepath.Join(pr.AppMountsBaseDir(), volumeID) +} + +// AppMountForDK is a directory where a given app-mount volume is stored under a certain dynakube +func (pr PathResolver) AppMountForDK(dkName string) string { + return filepath.Join(pr.DynaKubeDir(dkName), dtcsi.SharedAppMountsDir) +} + +// AppMountMappedDir replaces OverlayMappedDir, the directory where the overlay layers combine into +func (pr PathResolver) AppMountMappedDir(volumeID string) string { + return filepath.Join(pr.AppMountForID(volumeID), dtcsi.OverlayMappedDirPath) +} + +// AppMountVarDir replaces OverlayVarDir, the directory where the container using the volume writes +func (pr PathResolver) AppMountVarDir(volumeID string) string { + return filepath.Join(pr.AppMountForID(volumeID), dtcsi.OverlayVarDirPath) +} + +// AppMountWorkDir replaces OverlayWorkDir, the directory that is necessary for overlayFS to work +func (pr PathResolver) AppMountWorkDir(volumeID string) string { + return filepath.Join(pr.AppMountForID(volumeID), dtcsi.OverlayWorkDirPath) +} + +func (pr PathResolver) AppMountPodInfoDir(dkName, podNamespace, podName string) string { + return filepath.Join(pr.AppMountForDK(dkName), podNamespace, podName) +} + +// Deprecated kept for future migration/cleanup +func (pr PathResolver) TenantDir(tenantUUID string) string { + return pr.Base(tenantUUID) } -func (pr PathResolver) AgentSharedRuxitAgentProcConf(tenantUUID, dynakubeName string) string { - return filepath.Join(pr.AgentConfigDir(tenantUUID, dynakubeName), processmoduleconfig.RuxitAgentProcPath) +// Deprecated kept for future migration/cleanup +func (pr PathResolver) AgentRunDir(dynakubeName string) string { + return filepath.Join(pr.TenantDir(dynakubeName), dtcsi.AgentRunDir) } -func (pr PathResolver) OverlayVarRuxitAgentProcConf(tenantUUID, volumeId string) string { - return filepath.Join(pr.OverlayVarDir(tenantUUID, volumeId), processmoduleconfig.RuxitAgentProcPath) +// Deprecated kept for future migration/cleanup +func (pr PathResolver) AgentRunDirForVolume(dynakubeName string, volumeId string) string { + return filepath.Join(pr.AgentRunDir(dynakubeName), volumeId) } -func (pr PathResolver) AgentRunDir(tenantUUID string) string { - return filepath.Join(pr.TenantDir(tenantUUID), dtcsi.AgentRunDir) +// Deprecated kept for future migration/cleanup +func (pr PathResolver) OverlayMappedDir(dynakubeName string, volumeId string) string { + return filepath.Join(pr.AgentRunDirForVolume(dynakubeName, volumeId), dtcsi.OverlayMappedDirPath) } -func (pr PathResolver) AgentRunDirForVolume(tenantUUID string, volumeId string) string { - return filepath.Join(pr.AgentRunDir(tenantUUID), volumeId) +// Deprecated kept for future migration/cleanup +func (pr PathResolver) OverlayVarDir(dynakubeName string, volumeId string) string { + return filepath.Join(pr.AgentRunDirForVolume(dynakubeName, volumeId), dtcsi.OverlayVarDirPath) } -func (pr PathResolver) OverlayMappedDir(tenantUUID string, volumeId string) string { - return filepath.Join(pr.AgentRunDirForVolume(tenantUUID, volumeId), dtcsi.OverlayMappedDirPath) +// Deprecated kept for future migration/cleanup +func (pr PathResolver) OverlayWorkDir(dynakubeName string, volumeId string) string { + return filepath.Join(pr.AgentRunDirForVolume(dynakubeName, volumeId), dtcsi.OverlayWorkDirPath) } -func (pr PathResolver) OverlayVarDir(tenantUUID string, volumeId string) string { - return filepath.Join(pr.AgentRunDirForVolume(tenantUUID, volumeId), dtcsi.OverlayVarDirPath) +// Deprecated kept for future migration/cleanup +func (pr PathResolver) OldOsAgentDir(tenantUUID string) string { + return filepath.Join(pr.TenantDir(tenantUUID), "osagent") +} + +// Deprecated kept for future migration/cleanup +func (pr PathResolver) OldAgentConfigDir(tenantUUID string, dynakubeName string) string { + return filepath.Join(pr.TenantDir(tenantUUID), dynakubeName, dtcsi.SharedAgentConfigDir) } -func (pr PathResolver) OverlayWorkDir(tenantUUID string, volumeId string) string { - return filepath.Join(pr.AgentRunDirForVolume(tenantUUID, volumeId), dtcsi.OverlayWorkDirPath) +// Deprecated kept for future migration/cleanup +func (pr PathResolver) OldAgentSharedRuxitAgentProcConf(tenantUUID, dynakubeName string) string { + return filepath.Join(pr.OldAgentConfigDir(tenantUUID, dynakubeName), processmoduleconfig.RuxitAgentProcPath) } diff --git a/pkg/controllers/csi/metadata/path_resolver_test.go b/pkg/controllers/csi/metadata/path_resolver_test.go deleted file mode 100644 index e93b3e2223..0000000000 --- a/pkg/controllers/csi/metadata/path_resolver_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package metadata - -import ( - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestPathResolver(t *testing.T) { - rootDir := "/testroot/tmp" - tenantUUID := "asj23443" - - pathResolver := PathResolver{RootDir: rootDir} - fakeEnv := filepath.Join(rootDir, tenantUUID) - fakeVolume := "csi-sdf3ijiji3jldisomeid" - agentRunDirForVolume := filepath.Join(fakeEnv, "run", fakeVolume) - - assert.Equal(t, fakeEnv, pathResolver.TenantDir(tenantUUID)) - assert.Equal(t, filepath.Join(fakeEnv, "bin"), pathResolver.AgentBinaryDir(tenantUUID)) - assert.Equal(t, filepath.Join(fakeEnv, "bin", "v1"), pathResolver.AgentBinaryDirForVersion(tenantUUID, "v1")) - assert.Equal(t, filepath.Join(fakeEnv, "run"), pathResolver.AgentRunDir(tenantUUID)) - assert.Equal(t, agentRunDirForVolume, pathResolver.AgentRunDirForVolume(tenantUUID, fakeVolume)) - assert.Equal(t, filepath.Join(agentRunDirForVolume, "mapped"), pathResolver.OverlayMappedDir(tenantUUID, fakeVolume)) - assert.Equal(t, filepath.Join(agentRunDirForVolume, "var"), pathResolver.OverlayVarDir(tenantUUID, fakeVolume)) - assert.Equal(t, filepath.Join(agentRunDirForVolume, "work"), pathResolver.OverlayWorkDir(tenantUUID, fakeVolume)) -} diff --git a/pkg/controllers/csi/metadata/sqlite.go b/pkg/controllers/csi/metadata/sqlite.go deleted file mode 100644 index c6f1ba91da..0000000000 --- a/pkg/controllers/csi/metadata/sqlite.go +++ /dev/null @@ -1,814 +0,0 @@ -package metadata - -import ( - "context" - "database/sql" - "strconv" - "strings" - "time" - - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/mattn/go-sqlite3" - "github.com/pkg/errors" -) - -var ( - dynakubesAlterStatementMaxFailedMountAttempts = ` - ALTER TABLE dynakubes - ADD COLUMN MaxFailedMountAttempts INT NOT NULL DEFAULT ` + strconv.FormatInt(dynakube.DefaultMaxFailedCsiMountAttempts, 10) + ";" - // "Not null"-columns need a default value set -) - -const ( - sqliteDriverName = "sqlite3" - - // CREATE - dynakubesTableName = "dynakubes" - dynakubesCreateStatement = ` - CREATE TABLE IF NOT EXISTS dynakubes ( - Name VARCHAR NOT NULL, - TenantUUID VARCHAR NOT NULL, - LatestVersion VARCHAR NOT NULL, - PRIMARY KEY (Name) - ); ` - - volumesTableName = "volumes" - volumesCreateStatement = ` - CREATE TABLE IF NOT EXISTS volumes ( - ID VARCHAR NOT NULL, - PodName VARCHAR NOT NULL, - Version VARCHAR NOT NULL, - TenantUUID VARCHAR NOT NULL, - PRIMARY KEY (ID) - );` - - osAgentVolumesTableName = "osagent_volumes" - osAgentVolumesCreateStatement = ` - CREATE TABLE IF NOT EXISTS osagent_volumes ( - TenantUUID VARCHAR NOT NULL, - VolumeID VARCHAR NOT NULL, - Mounted BOOLEAN NOT NULL, - LastModified DATETIME NOT NULL, - PRIMARY KEY (TenantUUID) - );` - - // ALTER - dynakubesAlterStatementImageDigestColumn = ` - ALTER TABLE dynakubes - ADD COLUMN ImageDigest VARCHAR NOT NULL DEFAULT ''; - ` - - volumesAlterStatementMountAttempts = ` - ALTER TABLE volumes - ADD COLUMN MountAttempts INT NOT NULL DEFAULT 0;` - - // INSERT - insertDynakubeStatement = ` - INSERT INTO dynakubes (Name, TenantUUID, LatestVersion, ImageDigest, MaxFailedMountAttempts) - VALUES (?,?,?,?, ?); - ` - - insertVolumeStatement = ` - INSERT INTO volumes (ID, PodName, Version, TenantUUID, MountAttempts) - VALUES (?,?,?,?,?) - ON CONFLICT(ID) DO UPDATE SET - PodName=excluded.PodName, - Version=excluded.Version, - TenantUUID=excluded.TenantUUID, - MountAttempts=excluded.MountAttempts; - ` - - insertOsAgentVolumeStatement = ` - INSERT INTO osagent_volumes (TenantUUID, VolumeID, Mounted, LastModified) - VALUES (?,?,?,?); - ` - - // UPDATE - updateDynakubeStatement = ` - UPDATE dynakubes - SET LatestVersion = ?, TenantUUID = ?, ImageDigest = ?, MaxFailedMountAttempts = ? - WHERE Name = ?; - ` - - updateOsAgentVolumeStatement = ` - UPDATE osagent_volumes - SET VolumeID = ?, Mounted = ?, LastModified = ? - WHERE TenantUUID = ?; - ` - - // GET - getDynakubeStatement = ` - SELECT TenantUUID, LatestVersion, ImageDigest, MaxFailedMountAttempts - FROM dynakubes - WHERE Name = ?; - ` - - getVolumeStatement = ` - SELECT PodName, Version, TenantUUID, MountAttempts - FROM volumes - WHERE ID = ?; - ` - - getOsAgentVolumeViaVolumeIDStatement = ` - SELECT TenantUUID, Mounted, LastModified - FROM osagent_volumes - WHERE VolumeID = ?; - ` - - getOsAgentVolumeViaTenantUUIDStatement = ` - SELECT VolumeID, Mounted, LastModified - FROM osagent_volumes - WHERE TenantUUID = ?; - ` - - // GET ALL - getAllDynakubesStatement = ` - SELECT Name, TenantUUID, LatestVersion, ImageDigest, MaxFailedMountAttempts - FROM dynakubes; - ` - - getAllVolumesStatement = ` - SELECT ID, PodName, Version, TenantUUID, MountAttempts - FROM volumes; - ` - - getAllOsAgentVolumes = ` - SELECT TenantUUID, VolumeID, Mounted, LastModified - FROM osagent_volumes; - ` - - // DELETE - deleteVolumeStatement = "DELETE FROM volumes WHERE ID = ?;" - - deleteDynakubeStatement = "DELETE FROM dynakubes WHERE Name = ?;" - - deleteAppMountStatement = "DELETE FROM app_mounts WHERE volume_meta_id = ?;" - - // SPECIAL - getUsedVersionsStatement = ` - SELECT DISTINCT Version - FROM volumes - WHERE TenantUUID = ?; - ` - - getAllUsedVersionsStatement = ` - SELECT DISTINCT Version - FROM volumes; - ` - - getUsedImageDigestStatement = ` - SELECT DISTINCT ImageDigest - FROM dynakubes - WHERE ImageDigest != ""; - ` - - getLatestVersionsStatement = ` - SELECT DISTINCT LatestVersion - FROM dynakubes; - ` - - getPodNamesStatement = ` - SELECT ID, PodName - FROM volumes; - ` - - getTenantsToDynakubesStatement = ` - SELECT tenantUUID, Name - FROM dynakubes; - ` - - countImageDigestStatement = ` - SELECT COUNT(*) - FROM dynakubes - WHERE ImageDigest = ?; - ` - - getAllAppMountsStatement = ` - SELECT volume_meta_id, code_module_version, location, mount_attempts, pod_name - FROM app_mounts - INNER JOIN volume_meta - WHERE volume_meta.id = app_mounts.volume_meta_id AND app_mounts.deleted_at IS NULL; - ` -) - -type SqliteAccess struct { - conn *sql.DB -} - -// NewAccess creates a new SqliteAccess, connects to the database. -func NewAccess(ctx context.Context, path string) (Access, error) { - access := SqliteAccess{} - - err := access.Setup(ctx, path) - if err != nil { - log.Error(err, "failed to connect to the database") - - return nil, err - } - - return &access, nil -} - -func (access *SqliteAccess) connect(driver, path string) error { - db, err := sql.Open(driver, path) - if err != nil { - err := errors.WithStack(errors.WithMessagef(err, "couldn't connect to db %s", path)) - access.conn = nil - - return err - } - - access.conn = db - - return nil -} - -func (access *SqliteAccess) createTables(ctx context.Context) error { - err := access.setupDynakubeTable(ctx) - if err != nil { - return err - } - - err = access.setupVolumeTable(ctx) - if err != nil { - return err - } - - if _, err := access.conn.ExecContext(ctx, osAgentVolumesCreateStatement); err != nil { - return errors.WithStack(errors.WithMessagef(err, "couldn't create the table %s", osAgentVolumesTableName)) - } - - return nil -} - -func (access *SqliteAccess) setupVolumeTable(ctx context.Context) error { - _, err := access.conn.Exec(volumesCreateStatement) - if err != nil { - return errors.WithMessagef(err, "couldn't create the table %s", volumesTableName) - } - - err = access.executeAlterStatement(ctx, volumesAlterStatementMountAttempts) - if err != nil { - return err - } - - return nil -} - -// setupDynakubeTable creates the dynakubes table if it doesn't exist and tries to add additional columns -func (access *SqliteAccess) setupDynakubeTable(ctx context.Context) error { - if _, err := access.conn.Exec(dynakubesCreateStatement); err != nil { - return errors.WithStack(errors.WithMessagef(err, "couldn't create the table %s", dynakubesTableName)) - } - - err := access.executeAlterStatement(ctx, dynakubesAlterStatementImageDigestColumn) - if err != nil { - return err - } - - err = access.executeAlterStatement(ctx, dynakubesAlterStatementMaxFailedMountAttempts) - if err != nil { - return err - } - - return nil -} - -func (access *SqliteAccess) executeAlterStatement(ctx context.Context, statement string) error { - if _, err := access.conn.ExecContext(ctx, statement); err != nil { - sqliteErr := sqlite3.Error{} - isSqliteErr := errors.As(err, &sqliteErr) - - if isSqliteErr && sqliteErr.Code != sqlite3.ErrError { - return errors.WithStack(err) - } - } - - return nil -} - -// Setup connects to the database and creates the necessary tables if they don't exist -func (access *SqliteAccess) Setup(ctx context.Context, path string) error { - if err := access.connect(sqliteDriverName, path); err != nil { - return err - } - - if err := access.createTables(ctx); err != nil { - return err - } - - return nil -} - -// InsertDynakube inserts a new Dynakube -func (access *SqliteAccess) InsertDynakube(ctx context.Context, dynakube *Dynakube) error { - err := access.executeStatement(ctx, insertDynakubeStatement, dynakube.Name, dynakube.TenantUUID, dynakube.LatestVersion, dynakube.ImageDigest, dynakube.MaxFailedMountAttempts) - if err != nil { - err = errors.WithMessagef(err, "couldn't insert dynakube entry, tenantUUID '%s', latest version '%s', name '%s', image digest '%s'", - dynakube.TenantUUID, - dynakube.LatestVersion, - dynakube.Name, - dynakube.ImageDigest) - } - - return err -} - -// UpdateDynakube updates an existing Dynakube by matching the name -func (access *SqliteAccess) UpdateDynakube(ctx context.Context, dynakube *Dynakube) error { - err := access.executeStatement(ctx, updateDynakubeStatement, dynakube.LatestVersion, dynakube.TenantUUID, dynakube.ImageDigest, dynakube.MaxFailedMountAttempts, dynakube.Name) - if err != nil { - err = errors.WithMessagef(err, "couldn't update dynakube, tenantUUID '%s', latest version '%s', name '%s', image digest '%s'", - dynakube.TenantUUID, - dynakube.LatestVersion, - dynakube.Name, - dynakube.ImageDigest) - } - - return err -} - -// DeleteDynakube deletes an existing Dynakube using its name -func (access *SqliteAccess) DeleteDynakube(ctx context.Context, dynakubeName string) error { - err := access.executeStatement(ctx, deleteDynakubeStatement, dynakubeName) - if err != nil { - err = errors.WithMessagef(err, "couldn't delete dynakube, name '%s'", dynakubeName) - } - - return err -} - -// GetDynakube gets Dynakube using its name -func (access *SqliteAccess) GetDynakube(ctx context.Context, dynakubeName string) (*Dynakube, error) { - var tenantUUID string - - var latestVersion string - - var imageDigest string - - var maxFailedMountAttempts int - - err := access.querySimpleStatement(ctx, getDynakubeStatement, dynakubeName, &tenantUUID, &latestVersion, &imageDigest, &maxFailedMountAttempts) - if err != nil { - err = errors.WithMessagef(err, "couldn't get dynakube, name '%s'", dynakubeName) - } - - return NewDynakube(dynakubeName, tenantUUID, latestVersion, imageDigest, maxFailedMountAttempts), err -} - -// InsertVolume inserts a new Volume -func (access *SqliteAccess) InsertVolume(ctx context.Context, volume *Volume) error { - err := access.executeStatement(ctx, insertVolumeStatement, volume.VolumeID, volume.PodName, volume.Version, volume.TenantUUID, volume.MountAttempts) - if err != nil { - err = errors.WithMessagef(err, "couldn't insert volume info, volume id '%s', pod '%s', version '%s', dynakube '%s'", - volume.VolumeID, - volume.PodName, - volume.Version, - volume.TenantUUID) - } - - return err -} - -// GetVolume gets Volume by its ID -func (access *SqliteAccess) GetVolume(ctx context.Context, volumeID string) (*Volume, error) { - var podName string - - var version string - - var tenantUUID string - - var mountAttempts int - - err := access.querySimpleStatement(ctx, getVolumeStatement, volumeID, &podName, &version, &tenantUUID, &mountAttempts) - if err != nil { - err = errors.WithMessagef(err, "couldn't get volume field for volume id '%s'", volumeID) - } - - return NewVolume(volumeID, podName, version, tenantUUID, mountAttempts), err -} - -// DeleteVolume deletes a Volume by its ID -func (access *SqliteAccess) DeleteVolume(ctx context.Context, volumeID string) error { - err := access.executeStatement(ctx, deleteVolumeStatement, volumeID) - if err != nil { - err = errors.WithMessagef(err, "couldn't delete volume for volume id '%s'", volumeID) - } - - return err -} - -// InsertOsAgentVolume inserts a new OsAgentVolume -func (access *SqliteAccess) InsertOsAgentVolume(ctx context.Context, volume *OsAgentVolume) error { - err := access.executeStatement(ctx, insertOsAgentVolumeStatement, volume.TenantUUID, volume.VolumeID, volume.Mounted, volume.LastModified) - if err != nil { - err = errors.WithMessagef(err, "couldn't insert osAgentVolume info, volume id '%s', tenant UUID '%s', mounted '%t', last modified '%s'", - volume.VolumeID, - volume.TenantUUID, - volume.Mounted, - volume.LastModified) - } - - return err -} - -// UpdateOsAgentVolume updates an existing OsAgentVolume by matching the tenantUUID -func (access *SqliteAccess) UpdateOsAgentVolume(ctx context.Context, volume *OsAgentVolume) error { - err := access.executeStatement(ctx, updateOsAgentVolumeStatement, volume.VolumeID, volume.Mounted, volume.LastModified, volume.TenantUUID) - if err != nil { - err = errors.WithMessagef(err, "couldn't update osAgentVolume info, tenantUUID '%s', mounted '%t', last modified '%s', volume id '%s'", - volume.TenantUUID, - volume.Mounted, - volume.LastModified, - volume.VolumeID) - } - - return err -} - -// GetOsAgentVolumeViaVolumeID gets an OsAgentVolume by its VolumeID -func (access *SqliteAccess) GetOsAgentVolumeViaVolumeID(ctx context.Context, volumeID string) (*OsAgentVolume, error) { - var tenantUUID string - - var mounted bool - - var lastModified time.Time - - err := access.querySimpleStatement(ctx, getOsAgentVolumeViaVolumeIDStatement, volumeID, &tenantUUID, &mounted, &lastModified) - if err != nil { - err = errors.WithMessagef(err, "couldn't get osAgentVolume info for volume id '%s'", volumeID) - } - - return NewOsAgentVolume(volumeID, tenantUUID, mounted, &lastModified), err -} - -// GetOsAgentVolumeViaTenantUUID gets an OsAgentVolume by its tenantUUID -func (access *SqliteAccess) GetOsAgentVolumeViaTenantUUID(ctx context.Context, tenantUUID string) (*OsAgentVolume, error) { - var volumeID string - - var mounted bool - - var lastModified time.Time - - err := access.querySimpleStatement(ctx, getOsAgentVolumeViaTenantUUIDStatement, tenantUUID, &volumeID, &mounted, &lastModified) - if err != nil { - err = errors.WithMessagef(err, "couldn't get osAgentVolume info for tenant uuid '%s'", tenantUUID) - } - - return NewOsAgentVolume(volumeID, tenantUUID, mounted, &lastModified), err -} - -// GetAllVolumes gets all the Volumes from the database -func (access *SqliteAccess) GetAllVolumes(ctx context.Context) ([]*Volume, error) { - rows, err := access.conn.QueryContext(ctx, getAllVolumesStatement) - if err != nil { - return nil, errors.WithStack(errors.WithMessage(err, "couldn't get all the volumes")) - } - - volumes := []*Volume{} - - defer func() { _ = rows.Close() }() - - for rows.Next() { - var id string - - var podName string - - var version string - - var tenantUUID string - - var mountAttempts int - - err := rows.Scan(&id, &podName, &version, &tenantUUID, &mountAttempts) - if err != nil { - return nil, errors.WithStack(errors.WithMessage(err, "couldn't scan volume from database")) - } - - volumes = append(volumes, NewVolume(id, podName, version, tenantUUID, mountAttempts)) - } - - return volumes, nil -} - -// GetAllDynakubes gets all the Dynakubes from the database -func (access *SqliteAccess) GetAllDynakubes(ctx context.Context) ([]*Dynakube, error) { - rows, err := access.conn.QueryContext(ctx, getAllDynakubesStatement) - if err != nil { - return nil, errors.WithStack(errors.WithMessage(err, "couldn't get all the dynakubes")) - } - - dynakubes := []*Dynakube{} - - defer func() { _ = rows.Close() }() - - for rows.Next() { - var name string - - var version string - - var tenantUUID string - - var imageDigest string - - var maxFailedMountAttempts int - - err := rows.Scan(&name, &tenantUUID, &version, &imageDigest, &maxFailedMountAttempts) - if err != nil { - return nil, errors.WithStack(errors.WithMessage(err, "couldn't scan dynakube from database")) - } - - dynakubes = append(dynakubes, NewDynakube(name, tenantUUID, version, imageDigest, maxFailedMountAttempts)) - } - - return dynakubes, nil -} - -// GetAllOsAgentVolumes gets all the OsAgentVolume from the database -func (access *SqliteAccess) GetAllOsAgentVolumes(ctx context.Context) ([]*OsAgentVolume, error) { - rows, err := access.conn.QueryContext(ctx, getAllOsAgentVolumes) - if err != nil { - return nil, errors.WithStack(errors.WithMessage(err, "couldn't get all the osagent volumes")) - } - - osVolumes := []*OsAgentVolume{} - - defer func() { _ = rows.Close() }() - - for rows.Next() { - var volumeID string - - var tenantUUID string - - var mounted bool - - var timeStamp time.Time - - err := rows.Scan(&tenantUUID, &volumeID, &mounted, &timeStamp) - if err != nil { - return nil, errors.WithStack(errors.WithMessage(err, "couldn't scan osagent volume from database")) - } - - osVolumes = append(osVolumes, NewOsAgentVolume(volumeID, tenantUUID, mounted, &timeStamp)) - } - - return osVolumes, nil -} - -// GetUsedVersions gets all UNIQUE versions present in the `volumes` for a given tenantUUID database in map. -// Map is used to make sure we don't return the same version multiple time, -// it's also easier to check if a version is in it or not. (a Set in style of Golang) -func (access *SqliteAccess) GetUsedVersions(ctx context.Context, tenantUUID string) (map[string]bool, error) { - rows, err := access.conn.QueryContext(ctx, getUsedVersionsStatement, tenantUUID) - if err != nil { - return nil, errors.WithStack(errors.WithMessagef(err, "couldn't get used version info for tenant uuid '%s'", tenantUUID)) - } - - versions := map[string]bool{} - - defer func() { _ = rows.Close() }() - - for rows.Next() { - var version string - - err := rows.Scan(&version) - if err != nil { - return nil, errors.WithStack(errors.WithMessagef(err, "couldn't scan used version info for tenant uuid '%s'", tenantUUID)) - } - - versions[version] = true - } - - return versions, nil -} - -// GetUsedVersions gets all UNIQUE versions present in the `volumes` database in map. -// Map is used to make sure we don't return the same version multiple time, -// it's also easier to check if a version is in it or not. (a Set in style of Golang) -func (access *SqliteAccess) GetAllUsedVersions(ctx context.Context) (map[string]bool, error) { - rows, err := access.conn.QueryContext(ctx, getAllUsedVersionsStatement) - if err != nil { - return nil, errors.WithStack(errors.WithMessagef(err, "couldn't get all used version info")) - } - - versions := map[string]bool{} - - defer func() { _ = rows.Close() }() - - for rows.Next() { - var version string - - err := rows.Scan(&version) - if err != nil { - return nil, errors.WithStack(errors.WithMessagef(err, "couldn't scan used version info")) - } - - if _, ok := versions[version]; !ok { - versions[version] = true - } - } - - return versions, nil -} - -// GetLatestVersions gets all UNIQUE latestVersions present in the `dynakubes` database in map. -// Map is used to make sure we don't return the same version multiple time, -// it's also easier to check if a version is in it or not. (a Set in style of Golang) -func (access *SqliteAccess) GetLatestVersions(ctx context.Context) (map[string]bool, error) { - rows, err := access.conn.QueryContext(ctx, getLatestVersionsStatement) - if err != nil { - return nil, errors.WithStack(errors.WithMessage(err, "couldn't get all the latests version info for tenant uuid")) - } - - versions := map[string]bool{} - - defer func() { _ = rows.Close() }() - - for rows.Next() { - var version string - - err := rows.Scan(&version) - if err != nil { - return nil, errors.WithStack(errors.WithMessage(err, "couldn't scan latest version info ")) - } - - versions[version] = true - } - - return versions, nil -} - -// GetUsedImageDigests gets all UNIQUE image digests present in the `dynakubes` database in a map. -// Map is used to make sure we don't return the same digest multiple time, -// it's also easier to check if a digest is in it or not. (a Set in style of Golang) -func (access *SqliteAccess) GetUsedImageDigests(ctx context.Context) (map[string]bool, error) { - rows, err := access.conn.QueryContext(ctx, getUsedImageDigestStatement) - if err != nil { - return nil, errors.WithStack(errors.WithMessage(err, "couldn't get used image digests from database")) - } - - imageDigests := map[string]bool{} - - defer func() { _ = rows.Close() }() - - for rows.Next() { - var digest string - - err := rows.Scan(&digest) - if err != nil { - return nil, errors.WithStack(errors.WithMessage(err, "failed to scan from image digests database")) - } - - if _, ok := imageDigests[digest]; !ok { - imageDigests[digest] = true - } - } - - return imageDigests, nil -} - -// IsImageDigestUsed checks if the specified image digest is present in the database. -func (access *SqliteAccess) IsImageDigestUsed(ctx context.Context, imageDigest string) (bool, error) { - var count int - - err := access.querySimpleStatement(ctx, countImageDigestStatement, imageDigest, &count) - if err != nil { - return false, errors.WithMessagef(err, "couldn't count usage of image digest: %s", imageDigest) - } - - return count > 0, nil -} - -// GetPodNames gets all PodNames present in the `volumes` database in map with their corresponding volumeIDs. -func (access *SqliteAccess) GetPodNames(ctx context.Context) (map[string]string, error) { - rows, err := access.conn.QueryContext(ctx, getPodNamesStatement) - if err != nil { - return nil, errors.WithStack(errors.WithMessage(err, "couldn't get all pod names")) - } - - podNames := map[string]string{} - - defer func() { _ = rows.Close() }() - - for rows.Next() { - var podName string - - var volumeID string - - err := rows.Scan(&volumeID, &podName) - if err != nil { - return nil, errors.WithStack(errors.WithMessage(err, "couldn't scan pod name from database")) - } - - podNames[podName] = volumeID - } - - return podNames, nil -} - -// GetTenantsToDynakubes gets all Dynakubes and maps their name to the corresponding TenantUUID. -func (access *SqliteAccess) GetTenantsToDynakubes(ctx context.Context) (map[string]string, error) { - rows, err := access.conn.QueryContext(ctx, getTenantsToDynakubesStatement) - if err != nil { - return nil, errors.WithStack(errors.WithMessage(err, "couldn't get all tenants to dynakube metadata")) - } - - dynakubes := map[string]string{} - - defer func() { _ = rows.Close() }() - - for rows.Next() { - var uuid string - - var dynakube string - - err := rows.Scan(&uuid, &dynakube) - if err != nil { - return nil, errors.WithStack(errors.WithMessage(err, "couldn't scan tenant to dynakube metadata from database")) - } - - dynakubes[dynakube] = uuid - } - - return dynakubes, nil -} - -func (access *SqliteAccess) GetAllAppMounts(ctx context.Context) []*Volume { - rows, err := access.conn.QueryContext(ctx, getAllAppMountsStatement) - if err != nil { - log.Info("skipping migration due to error getting all app mounts", "error", err) - - return nil - } - - defer func() { _ = rows.Close() }() - - var volumes = make([]*Volume, 0) - - for rows.Next() { - var code_module_version, volume_meta_id, location, pod_name string - - var mount_attempts int - - err := rows.Scan(&volume_meta_id, &code_module_version, &location, &mount_attempts, &pod_name) - if err != nil { - log.Info("couldn't scan app_mount from database", "error", err) - - continue - } - - tenantUUID := getTenantUUIDFromLocation(location) - if tenantUUID == "" { - log.Info("could not parse tenantUUID from location", "location", location) - - continue - } - - volumes = append(volumes, NewVolume(volume_meta_id, pod_name, code_module_version, tenantUUID, mount_attempts)) - } - - return volumes -} - -func (access *SqliteAccess) DeleteAppMount(ctx context.Context, appMountID string) error { - err := access.executeStatement(ctx, deleteAppMountStatement, appMountID) - if err != nil { - return err - } - - return nil -} - -// Executes the provided SQL statement on the database. -// The `vars` are passed to the SQL statement (in-order), to fill in the SQL wildcards. -func (access *SqliteAccess) executeStatement(ctx context.Context, statement string, vars ...any) error { - _, err := access.conn.ExecContext(ctx, statement, vars...) - - return errors.WithStack(err) -} - -// Executes the provided SQL SELECT statement on the database. -// The SQL statement should always return a single row. -// The `id` is passed to the SQL query to fill in an SQL wildcard -// The `vars` are filled with the values of the return of the SELECT statement, so the `vars` need to be pointers. -func (access *SqliteAccess) querySimpleStatement(ctx context.Context, statement, id string, vars ...any) error { - row := access.conn.QueryRowContext(ctx, statement, id) - - err := row.Scan(vars...) - if err != nil && err != sql.ErrNoRows { - return errors.WithStack(err) - } - - return nil -} - -func getTenantUUIDFromLocation(location string) string { - var tennantUUIDIndex = 2 - - result := strings.Split(location, "/") - if len(result) > tennantUUIDIndex { - return result[tennantUUIDIndex] - } - - return "" -} diff --git a/pkg/controllers/csi/metadata/sqlite_test.go b/pkg/controllers/csi/metadata/sqlite_test.go deleted file mode 100644 index e764db7fca..0000000000 --- a/pkg/controllers/csi/metadata/sqlite_test.go +++ /dev/null @@ -1,645 +0,0 @@ -package metadata - -import ( - "context" - "fmt" - "strconv" - "testing" - "time" - - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestNewAccess(t *testing.T) { - db, err := NewAccess(context.TODO(), ":memory:") - require.NoError(t, err) - assert.NotNil(t, db.(*SqliteAccess).conn) -} - -func TestSetup(t *testing.T) { - db := SqliteAccess{} - err := db.Setup(context.TODO(), ":memory:") - - require.NoError(t, err) - assert.True(t, checkIfTablesExist(&db)) -} - -func TestSetup_badPath(t *testing.T) { - db := SqliteAccess{} - err := db.Setup(context.TODO(), "/asd") - require.Error(t, err) - - assert.False(t, checkIfTablesExist(&db)) -} - -func TestConnect(t *testing.T) { - path := ":memory:" - db := SqliteAccess{} - err := db.connect(sqliteDriverName, path) - require.NoError(t, err) - assert.NotNil(t, db.conn) -} - -func TestConnect_badDriver(t *testing.T) { - db := SqliteAccess{} - err := db.connect("die", "") - require.Error(t, err) - assert.Nil(t, db.conn) -} - -func TestCreateTables(t *testing.T) { - ctx := context.TODO() - - t.Run("volume table is created correctly", func(t *testing.T) { - db := emptyMemoryDB() - - err := db.createTables(ctx) - require.NoError(t, err) - - var volumeTableName string - - row := db.conn.QueryRow("SELECT name FROM sqlite_master WHERE type='table' AND name=?;", volumesTableName) - err = row.Scan(&volumeTableName) - require.NoError(t, err) - assert.Equal(t, volumesTableName, volumeTableName) - - rows, err := db.conn.Query("PRAGMA table_info(" + volumesTableName + ")") - require.NoError(t, err) - assert.NotNil(t, rows) - - columns := []string{ - "ID", - "PodName", - "Version", - "TenantUUID", - "MountAttempts", - } - - for _, column := range columns { - assert.True(t, rows.Next()) - - var id, name, columnType, notNull, primaryKey string - - var defaultValue = new(string) - - err = rows.Scan(&id, &name, &columnType, ¬Null, &defaultValue, &primaryKey) - - require.NoError(t, err) - assert.Equal(t, column, name) - - if column == "MountAttempts" { - assert.Equal(t, "0", *defaultValue) - assert.Equal(t, "1", notNull) - } - } - }) - t.Run("dynakube table is created correctly", func(t *testing.T) { - db := emptyMemoryDB() - - err := db.createTables(ctx) - require.NoError(t, err) - - var dkTable string - - row := db.conn.QueryRow("SELECT name FROM sqlite_master WHERE type='table' AND name=?;", dynakubesTableName) - err = row.Scan(&dkTable) - require.NoError(t, err) - assert.Equal(t, dynakubesTableName, dkTable) - - rows, err := db.conn.Query("PRAGMA table_info(" + dynakubesTableName + ")") - require.NoError(t, err) - assert.NotNil(t, rows) - - columns := []string{ - "Name", - "TenantUUID", - "LatestVersion", - "ImageDigest", - "MaxFailedMountAttempts", - } - - for _, column := range columns { - assert.True(t, rows.Next()) - - var id, name, columnType, notNull, primaryKey string - - var defaultValue = new(string) - - err = rows.Scan(&id, &name, &columnType, ¬Null, &defaultValue, &primaryKey) - - require.NoError(t, err) - assert.Equal(t, column, name) - - if column == "MaxFailedMountAttempts" { - maxFailedMountAttempts, err := strconv.Atoi(*defaultValue) - require.NoError(t, err) - assert.Equal(t, strconv.Itoa(dynakube.DefaultMaxFailedCsiMountAttempts), *defaultValue) - assert.Equal(t, dynakube.DefaultMaxFailedCsiMountAttempts, maxFailedMountAttempts) - assert.Equal(t, "1", notNull) - } - } - }) -} - -func TestInsertDynakube(t *testing.T) { - testDynakube1 := createTestDynakube(1) - - db := FakeMemoryDB() - - err := db.InsertDynakube(context.TODO(), &testDynakube1) - require.NoError(t, err) - - var uuid, lv, name string - - var imageDigest string - - var maxMountAttempts int - - row := db.conn.QueryRow(fmt.Sprintf("SELECT * FROM %s WHERE TenantUUID = ?;", dynakubesTableName), testDynakube1.TenantUUID) - err = row.Scan(&name, &uuid, &lv, &imageDigest, &maxMountAttempts) - require.NoError(t, err) - assert.Equal(t, testDynakube1.TenantUUID, uuid) - assert.Equal(t, testDynakube1.LatestVersion, lv) - assert.Equal(t, testDynakube1.Name, name) - assert.Equal(t, testDynakube1.ImageDigest, imageDigest) - assert.Equal(t, testDynakube1.MaxFailedMountAttempts, maxMountAttempts) -} - -func TestGetDynakube_Empty(t *testing.T) { - testDynakube1 := createTestDynakube(1) - db := FakeMemoryDB() - - gt, err := db.GetDynakube(context.TODO(), testDynakube1.TenantUUID) - require.NoError(t, err) - assert.Nil(t, gt) -} - -func TestGetDynakube(t *testing.T) { - ctx := context.TODO() - - t.Run("get dynakube", func(t *testing.T) { - testDynakube1 := createTestDynakube(1) - db := FakeMemoryDB() - err := db.InsertDynakube(ctx, &testDynakube1) - require.NoError(t, err) - - dynakube, err := db.GetDynakube(ctx, testDynakube1.Name) - require.NoError(t, err) - assert.Equal(t, testDynakube1, *dynakube) - }) -} - -func TestUpdateDynakube(t *testing.T) { - ctx := context.TODO() - testDynakube1 := createTestDynakube(1) - db := FakeMemoryDB() - err := db.InsertDynakube(ctx, &testDynakube1) - require.NoError(t, err) - - copyDynakube := testDynakube1 - copyDynakube.LatestVersion = "132.546" - copyDynakube.ImageDigest = "" - copyDynakube.MaxFailedMountAttempts = 10 - err = db.UpdateDynakube(ctx, ©Dynakube) - require.NoError(t, err) - - var uuid, lv, name string - - var imageDigest string - - var maxFailedMountAttempts int - - row := db.conn.QueryRow(fmt.Sprintf("SELECT Name, TenantUUID, LatestVersion, ImageDigest, MaxFailedMountAttempts FROM %s WHERE Name = ?;", dynakubesTableName), copyDynakube.Name) - err = row.Scan(&name, &uuid, &lv, &imageDigest, &maxFailedMountAttempts) - - require.NoError(t, err) - assert.Equal(t, copyDynakube.TenantUUID, uuid) - assert.Equal(t, copyDynakube.LatestVersion, lv) - assert.Equal(t, copyDynakube.Name, name) - assert.Equal(t, copyDynakube.MaxFailedMountAttempts, maxFailedMountAttempts) - assert.Empty(t, imageDigest) -} - -func TestGetTenantsToDynakubes(t *testing.T) { - ctx := context.TODO() - testDynakube1 := createTestDynakube(1) - testDynakube2 := createTestDynakube(2) - - db := FakeMemoryDB() - err := db.InsertDynakube(ctx, &testDynakube1) - require.NoError(t, err) - err = db.InsertDynakube(ctx, &testDynakube2) - require.NoError(t, err) - - dynakubes, err := db.GetTenantsToDynakubes(ctx) - require.NoError(t, err) - assert.Len(t, dynakubes, 2) - assert.Equal(t, testDynakube1.TenantUUID, dynakubes[testDynakube1.Name]) - assert.Equal(t, testDynakube2.TenantUUID, dynakubes[testDynakube2.Name]) -} - -func TestGetAllDynakubes(t *testing.T) { - ctx := context.TODO() - - t.Run("get multiple dynakubes", func(t *testing.T) { - testDynakube1 := createTestDynakube(1) - testDynakube2 := createTestDynakube(2) - - db := FakeMemoryDB() - err := db.InsertDynakube(ctx, &testDynakube1) - require.NoError(t, err) - err = db.InsertDynakube(ctx, &testDynakube2) - require.NoError(t, err) - - dynakubes, err := db.GetAllDynakubes(ctx) - require.NoError(t, err) - assert.Len(t, dynakubes, 2) - }) -} - -func TestGetAllVolumes(t *testing.T) { - ctx := context.TODO() - testVolume1 := createTestVolume(1) - testVolume2 := createTestVolume(2) - - db := FakeMemoryDB() - err := db.InsertVolume(ctx, &testVolume1) - require.NoError(t, err) - err = db.InsertVolume(ctx, &testVolume2) - require.NoError(t, err) - - volumes, err := db.GetAllVolumes(ctx) - require.NoError(t, err) - assert.Len(t, volumes, 2) - assert.Equal(t, testVolume1, *volumes[0]) - assert.Equal(t, testVolume2, *volumes[1]) -} - -func TestGetAllOsAgentVolumes(t *testing.T) { - ctx := context.TODO() - testDynakube1 := createTestDynakube(1) - testDynakube2 := createTestDynakube(2) - - now := time.Now() - osVolume1 := OsAgentVolume{ - VolumeID: "vol-1", - TenantUUID: testDynakube1.TenantUUID, - Mounted: true, - LastModified: &now, - } - osVolume2 := OsAgentVolume{ - VolumeID: "vol-2", - TenantUUID: testDynakube2.TenantUUID, - Mounted: true, - LastModified: &now, - } - db := FakeMemoryDB() - err := db.InsertOsAgentVolume(ctx, &osVolume1) - require.NoError(t, err) - err = db.InsertOsAgentVolume(ctx, &osVolume2) - require.NoError(t, err) - - osVolumes, err := db.GetAllOsAgentVolumes(ctx) - require.NoError(t, err) - assert.Len(t, osVolumes, 2) -} - -func TestDeleteDynakube(t *testing.T) { - ctx := context.TODO() - testDynakube1 := createTestDynakube(1) - testDynakube2 := createTestDynakube(2) - - db := FakeMemoryDB() - err := db.InsertDynakube(ctx, &testDynakube1) - require.NoError(t, err) - err = db.InsertDynakube(ctx, &testDynakube2) - require.NoError(t, err) - - err = db.DeleteDynakube(ctx, testDynakube1.Name) - require.NoError(t, err) - dynakubes, err := db.GetTenantsToDynakubes(ctx) - require.NoError(t, err) - assert.Len(t, dynakubes, 1) - assert.Equal(t, testDynakube2.TenantUUID, dynakubes[testDynakube2.Name]) -} - -func TestGetVolume_Empty(t *testing.T) { - ctx := context.TODO() - testVolume1 := createTestVolume(1) - db := FakeMemoryDB() - - vo, err := db.GetVolume(ctx, testVolume1.PodName) - require.NoError(t, err) - assert.Nil(t, vo) -} - -func TestInsertVolume(t *testing.T) { - ctx := context.TODO() - testVolume1 := createTestVolume(1) - db := FakeMemoryDB() - - err := db.InsertVolume(ctx, &testVolume1) - require.NoError(t, err) - - row := db.conn.QueryRow(fmt.Sprintf("SELECT * FROM %s WHERE ID = ?;", volumesTableName), testVolume1.VolumeID) - - var id string - - var puid string - - var ver string - - var tuid string - - var mountAttempts int - err = row.Scan(&id, &puid, &ver, &tuid, &mountAttempts) - - require.NoError(t, err) - assert.Equal(t, testVolume1.VolumeID, id) - assert.Equal(t, testVolume1.PodName, puid) - assert.Equal(t, testVolume1.Version, ver) - assert.Equal(t, testVolume1.TenantUUID, tuid) - assert.Equal(t, testVolume1.MountAttempts, mountAttempts) - - newPodName := "something-else" - testVolume1.PodName = newPodName - err = db.InsertVolume(ctx, &testVolume1) - require.NoError(t, err) - - row = db.conn.QueryRow(fmt.Sprintf("SELECT * FROM %s WHERE ID = ?;", volumesTableName), testVolume1.VolumeID) - err = row.Scan(&id, &puid, &ver, &tuid, &mountAttempts) - - require.NoError(t, err) - assert.Equal(t, testVolume1.VolumeID, id) - assert.Equal(t, testVolume1.PodName, puid) - assert.Equal(t, testVolume1.Version, ver) - assert.Equal(t, testVolume1.TenantUUID, tuid) - assert.Equal(t, testVolume1.MountAttempts, mountAttempts) -} - -func TestInsertOsAgentVolume(t *testing.T) { - testDynakube1 := createTestDynakube(1) - db := FakeMemoryDB() - - now := time.Now() - volume := OsAgentVolume{ - VolumeID: "vol-4", - TenantUUID: testDynakube1.TenantUUID, - Mounted: true, - LastModified: &now, - } - - err := db.InsertOsAgentVolume(context.TODO(), &volume) - require.NoError(t, err) - - row := db.conn.QueryRow(fmt.Sprintf("SELECT * FROM %s WHERE TenantUUID = ?;", osAgentVolumesTableName), volume.TenantUUID) - - var volumeID string - - var tenantUUID string - - var mounted bool - - var lastModified time.Time - err = row.Scan(&tenantUUID, &volumeID, &mounted, &lastModified) - require.NoError(t, err) - assert.Equal(t, volumeID, volume.VolumeID) - assert.Equal(t, tenantUUID, volume.TenantUUID) - assert.Equal(t, mounted, volume.Mounted) - assert.True(t, volume.LastModified.Equal(lastModified)) -} - -func TestGetOsAgentVolumeViaVolumeID(t *testing.T) { - ctx := context.TODO() - testDynakube1 := createTestDynakube(1) - db := FakeMemoryDB() - - now := time.Now() - expected := OsAgentVolume{ - VolumeID: "vol-4", - TenantUUID: testDynakube1.TenantUUID, - Mounted: true, - LastModified: &now, - } - - err := db.InsertOsAgentVolume(ctx, &expected) - require.NoError(t, err) - actual, err := db.GetOsAgentVolumeViaVolumeID(ctx, expected.VolumeID) - require.NoError(t, err) - require.NoError(t, err) - assert.Equal(t, expected.VolumeID, actual.VolumeID) - assert.Equal(t, expected.TenantUUID, actual.TenantUUID) - assert.Equal(t, expected.Mounted, actual.Mounted) - assert.True(t, expected.LastModified.Equal(*actual.LastModified)) -} - -func TestGetOsAgentVolumeViaTennatUUID(t *testing.T) { - ctx := context.TODO() - testDynakube1 := createTestDynakube(1) - db := FakeMemoryDB() - - now := time.Now() - expected := OsAgentVolume{ - VolumeID: "vol-4", - TenantUUID: testDynakube1.TenantUUID, - Mounted: true, - LastModified: &now, - } - - err := db.InsertOsAgentVolume(ctx, &expected) - require.NoError(t, err) - actual, err := db.GetOsAgentVolumeViaTenantUUID(ctx, expected.TenantUUID) - require.NoError(t, err) - assert.Equal(t, expected.VolumeID, actual.VolumeID) - assert.Equal(t, expected.TenantUUID, actual.TenantUUID) - assert.Equal(t, expected.Mounted, actual.Mounted) - assert.True(t, expected.LastModified.Equal(*actual.LastModified)) -} - -func TestUpdateOsAgentVolume(t *testing.T) { - ctx := context.TODO() - testDynakube1 := createTestDynakube(1) - db := FakeMemoryDB() - - now := time.Now() - - oldEntry := OsAgentVolume{ - VolumeID: "vol-4", - TenantUUID: testDynakube1.TenantUUID, - Mounted: true, - LastModified: &now, - } - - err := db.InsertOsAgentVolume(ctx, &oldEntry) - require.NoError(t, err) - - newEntry := oldEntry - newEntry.Mounted = false - err = db.UpdateOsAgentVolume(ctx, &newEntry) - require.NoError(t, err) - - actual, err := db.GetOsAgentVolumeViaVolumeID(ctx, oldEntry.VolumeID) - require.NoError(t, err) - assert.Equal(t, oldEntry.VolumeID, actual.VolumeID) - assert.Equal(t, oldEntry.TenantUUID, actual.TenantUUID) - assert.NotEqual(t, oldEntry.Mounted, actual.Mounted) - assert.True(t, oldEntry.LastModified.Equal(*actual.LastModified)) -} - -func TestGetVolume(t *testing.T) { - ctx := context.TODO() - testVolume1 := createTestVolume(1) - db := FakeMemoryDB() - err := db.InsertVolume(ctx, &testVolume1) - require.NoError(t, err) - - volume, err := db.GetVolume(ctx, testVolume1.VolumeID) - require.NoError(t, err) - assert.Equal(t, testVolume1, *volume) -} - -func TestUpdateVolume(t *testing.T) { - ctx := context.TODO() - testVolume1 := createTestVolume(1) - db := FakeMemoryDB() - err := db.InsertVolume(ctx, &testVolume1) - - require.NoError(t, err) - - testVolume1.PodName = "different pod name" - testVolume1.Version = "new version" - testVolume1.TenantUUID = "asdf-1234" - testVolume1.MountAttempts = 10 - err = db.InsertVolume(ctx, &testVolume1) - - require.NoError(t, err) - - insertedVolume, err := db.GetVolume(ctx, testVolume1.VolumeID) - - require.NoError(t, err) - assert.Equal(t, testVolume1.VolumeID, insertedVolume.VolumeID) - assert.Equal(t, testVolume1.PodName, insertedVolume.PodName) - assert.Equal(t, testVolume1.Version, insertedVolume.Version) - assert.Equal(t, testVolume1.TenantUUID, insertedVolume.TenantUUID) - assert.Equal(t, testVolume1.MountAttempts, insertedVolume.MountAttempts) -} - -func TestGetUsedVersions(t *testing.T) { - ctx := context.TODO() - testVolume1 := createTestVolume(1) - db := FakeMemoryDB() - err := db.InsertVolume(ctx, &testVolume1) - testVolume11 := testVolume1 - testVolume11.VolumeID = "vol-11" - testVolume11.Version = "321" - - require.NoError(t, err) - err = db.InsertVolume(ctx, &testVolume11) - require.NoError(t, err) - - versions, err := db.GetUsedVersions(ctx, testVolume1.TenantUUID) - require.NoError(t, err) - assert.Len(t, versions, 2) - assert.True(t, versions[testVolume1.Version]) - assert.True(t, versions[testVolume11.Version]) -} - -func TestGetAllUsedVersions(t *testing.T) { - ctx := context.TODO() - db := FakeMemoryDB() - testVolume1 := createTestVolume(1) - err := db.InsertVolume(ctx, &testVolume1) - testVolume11 := testVolume1 - testVolume11.VolumeID = "vol-11" - testVolume11.Version = "321" - - require.NoError(t, err) - err = db.InsertVolume(ctx, &testVolume11) - require.NoError(t, err) - - versions, err := db.GetAllUsedVersions(ctx) - require.NoError(t, err) - assert.Len(t, versions, 2) - assert.True(t, versions[testVolume1.Version]) - assert.True(t, versions[testVolume11.Version]) -} - -func TestGetUsedImageDigests(t *testing.T) { - ctx := context.TODO() - db := FakeMemoryDB() - testDynakube1 := createTestDynakube(1) - err := db.InsertDynakube(ctx, &testDynakube1) - require.NoError(t, err) - - copyDynakube := testDynakube1 - copyDynakube.Name = "copy" - err = db.InsertDynakube(ctx, ©Dynakube) - require.NoError(t, err) - - testDynakube2 := createTestDynakube(2) - err = db.InsertDynakube(ctx, &testDynakube2) - require.NoError(t, err) - - digests, err := db.GetUsedImageDigests(ctx) - require.NoError(t, err) - assert.Len(t, digests, 2) - assert.True(t, digests[testDynakube1.ImageDigest]) - assert.True(t, digests[copyDynakube.ImageDigest]) - assert.True(t, digests[testDynakube2.ImageDigest]) -} - -func TestIsImageDigestUsed(t *testing.T) { - ctx := context.TODO() - db := FakeMemoryDB() - - isUsed, err := db.IsImageDigestUsed(ctx, "test") - require.NoError(t, err) - require.False(t, isUsed) - - testDynakube1 := createTestDynakube(1) - err = db.InsertDynakube(ctx, &testDynakube1) - require.NoError(t, err) - - isUsed, err = db.IsImageDigestUsed(ctx, testDynakube1.ImageDigest) - require.NoError(t, err) - require.True(t, isUsed) -} - -func TestGetPodNames(t *testing.T) { - ctx := context.TODO() - testVolume1 := createTestVolume(1) - testVolume2 := createTestVolume(2) - - db := FakeMemoryDB() - err := db.InsertVolume(ctx, &testVolume1) - require.NoError(t, err) - err = db.InsertVolume(ctx, &testVolume2) - require.NoError(t, err) - - podNames, err := db.GetPodNames(ctx) - require.NoError(t, err) - assert.Len(t, podNames, 2) - assert.Equal(t, testVolume1.VolumeID, podNames[testVolume1.PodName]) - assert.Equal(t, testVolume2.VolumeID, podNames[testVolume2.PodName]) -} - -func TestDeleteVolume(t *testing.T) { - ctx := context.TODO() - testVolume1 := createTestVolume(1) - testVolume2 := createTestVolume(2) - - db := FakeMemoryDB() - err := db.InsertVolume(ctx, &testVolume1) - require.NoError(t, err) - err = db.InsertVolume(ctx, &testVolume2) - require.NoError(t, err) - - err = db.DeleteVolume(ctx, testVolume2.VolumeID) - require.NoError(t, err) - podNames, err := db.GetPodNames(ctx) - require.NoError(t, err) - assert.Len(t, podNames, 1) - assert.Equal(t, testVolume1.VolumeID, podNames[testVolume1.PodName]) -} diff --git a/pkg/controllers/csi/provisioner/cleanup/appmounts.go b/pkg/controllers/csi/provisioner/cleanup/appmounts.go new file mode 100644 index 0000000000..402fd5be6f --- /dev/null +++ b/pkg/controllers/csi/provisioner/cleanup/appmounts.go @@ -0,0 +1,57 @@ +package cleanup + +func (c *Cleaner) removeDeprecatedMounts(fsState fsState) { + stillMountedCounter := 0 + + for _, depDir := range fsState.deprecatedDks { + runDir := c.path.AgentRunDir(depDir) + + volumeDirs, err := c.fs.ReadDir(runDir) + if err != nil { + log.Info("couldn't list volume dirs", "path", runDir) + + continue + } + + for _, volumeDir := range volumeDirs { + mappedDir := c.path.OverlayMappedDir(depDir, volumeDir.Name()) + + isEmpty, _ := c.fs.IsEmpty(mappedDir) + if isEmpty { + volumeDirPath := c.path.AgentRunDirForVolume(depDir, volumeDir.Name()) + + err := c.fs.RemoveAll(volumeDirPath) + if err == nil { + log.Info("removed unused volume", "path", volumeDirPath) + + continue + } + } + + stillMountedCounter++ + } + + isEmpty, _ := c.fs.IsEmpty(runDir) + if !isEmpty { + continue + } + + err = c.fs.RemoveAll(runDir) + if err == nil { + log.Info("removed empty deprecated run dir", "path", runDir) + } else { + continue + } + + tenantDir := c.path.TenantDir(depDir) + + err = c.fs.RemoveAll(c.path.DynaKubeDir(tenantDir)) + if err == nil { + log.Info("removed empty deprecated dir", "path", tenantDir) + } + } + + if stillMountedCounter > 0 { + log.Info("there are a still mounted deprecated app mounts", "count", stillMountedCounter) + } +} diff --git a/pkg/controllers/csi/provisioner/cleanup/appmounts_test.go b/pkg/controllers/csi/provisioner/cleanup/appmounts_test.go new file mode 100644 index 0000000000..22c626d744 --- /dev/null +++ b/pkg/controllers/csi/provisioner/cleanup/appmounts_test.go @@ -0,0 +1,83 @@ +package cleanup + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRemoveDeprecatedMounts(t *testing.T) { + t.Run("empty fsState -> no panic", func(t *testing.T) { + cleaner := createCleaner(t) + + cleaner.removeDeprecatedMounts(fsState{}) + }) + + t.Run("remove unmounted deprecated dirs (empty mapped subdir)", func(t *testing.T) { + cleaner := createCleaner(t) + + deprecateDir := []string{ + "t0", + "t1", + "t2", + } + + for i, folder := range deprecateDir { + cleaner.createDeprecatedDirs(t, folder, i, i*2) + + expectedDir := cleaner.path.AgentRunDir(folder) + exists, _ := cleaner.fs.Exists(expectedDir) + require.True(t, exists) + + subdirs, err := cleaner.fs.ReadDir(expectedDir) + require.NoError(t, err) + require.Len(t, subdirs, i+i*2) + } + + cleaner.removeDeprecatedMounts(fsState{ + deprecatedDks: deprecateDir, + }) + + for i, folder := range deprecateDir { + expectedDir := cleaner.path.AgentRunDir(folder) + + exists, _ := cleaner.fs.Exists(expectedDir) + if i == 0 { + require.False(t, exists) + } else { + require.True(t, exists) + + subdirs, err := cleaner.fs.ReadDir(expectedDir) + require.NoError(t, err) + require.Len(t, subdirs, i) + } + } + }) +} + +func (c *Cleaner) createDeprecatedDirs(t *testing.T, name string, subDirAmount, emptySubDirAmount int) { + t.Helper() + + runDir := c.path.AgentRunDir(name) + err := c.fs.MkdirAll(runDir, os.ModePerm) + require.NoError(t, err) + + for i := range subDirAmount { + mappedDir := c.path.OverlayMappedDir(name, fmt.Sprintf("volume-%d", i)) + err := c.fs.MkdirAll(mappedDir, os.ModePerm) + require.NoError(t, err) + file, err := c.fs.Create(filepath.Join(mappedDir, "something")) + require.NoError(t, err) + _, err = file.WriteString("something") + require.NoError(t, err) + } + + for i := range emptySubDirAmount { + mappedDir := c.path.OverlayMappedDir(name, fmt.Sprintf("volume-%d", i+subDirAmount)) + err := c.fs.MkdirAll(mappedDir, os.ModePerm) + require.NoError(t, err) + } +} diff --git a/pkg/controllers/csi/provisioner/cleanup/binaries.go b/pkg/controllers/csi/provisioner/cleanup/binaries.go new file mode 100644 index 0000000000..9c2c3206fe --- /dev/null +++ b/pkg/controllers/csi/provisioner/cleanup/binaries.go @@ -0,0 +1,117 @@ +package cleanup + +import ( + "strings" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi/metadata" + "golang.org/x/exp/maps" +) + +func (c *Cleaner) removeUnusedBinaries(dks []dynakube.DynaKube, fsState fsState) { + c.removeOldBinarySymlinks(dks, fsState) + + keptBins, err := c.collectStillMountedBins() + if err != nil { + return + } + + relevantLatestBins := c.collectRelevantLatestBins(dks) + + for k, v := range relevantLatestBins { + keptBins[k] = v + } + + c.removeOldSharedBinaries(keptBins) +} + +func (c *Cleaner) removeOldSharedBinaries(keptBins map[string]bool) { + sharedBins, err := c.fs.ReadDir(c.path.AgentSharedBinaryDirBase()) + if err != nil { + log.Info("failed to list the shared binaries directory, skipping unused binaries cleanup") + + return + } + + for _, dir := range sharedBins { + sharedBinPath := c.path.AgentSharedBinaryDirForAgent(dir.Name()) + + _, ok := keptBins[sharedBinPath] + if !ok { + err := c.fs.RemoveAll(sharedBinPath) + if err != nil { + log.Error(err, "failed to remove shared binary", "path", sharedBinPath) + + continue + } + + log.Info("removed old shared binary", "path", sharedBinPath) + } + } +} + +func (c *Cleaner) removeOldBinarySymlinks(dks []dynakube.DynaKube, fsState fsState) { + shouldBePresent := map[string]bool{} + for _, dk := range dks { + shouldBePresent[dk.Name] = true + } + + for _, dkDir := range fsState.binDks { + if _, ok := shouldBePresent[dkDir]; !ok { + latest := c.path.LatestAgentBinaryForDynaKube(dkDir) + if err := c.fs.Remove(latest); err == nil { + log.Info("removed old latest bin symlink", "path", latest) + } + } + } + + for _, depDir := range fsState.deprecatedDks { + if _, ok := shouldBePresent[depDir]; !ok { // for the rare case where dk.Name == tenantUUID + latest := c.path.LatestAgentBinaryForDynaKube(depDir) + if err := c.fs.Remove(latest); err == nil { + log.Info("removed old deprecated latest bin symlink", "path", latest) + } + } + } +} + +func (c *Cleaner) collectStillMountedBins() (map[string]bool, error) { + mountedBins := map[string]bool{} + + overlays, err := metadata.GetRelevantOverlayMounts(c.mounter, c.path.RootDir) + if err != nil { + log.Info("failed to list active overlay mounts, skipping unused binaries cleanup") + + return nil, err + } + + for _, overlay := range overlays { + mountedBins[overlay.LowerDir] = true + } + + if len(mountedBins) > 0 { + log.Info("binaries to keep because they are still mounted", "paths", strings.Join(maps.Keys(mountedBins), ",")) + } + + return mountedBins, nil +} + +func (c *Cleaner) collectRelevantLatestBins(dks []dynakube.DynaKube) map[string]bool { + latestBins := map[string]bool{} + + for _, dk := range dks { + if !dk.OneAgent().IsAppInjectionNeeded() { + continue + } + + latestLink := c.path.LatestAgentBinaryForDynaKube(dk.Name) + + c.addRelevantPath(latestLink, latestBins) + } + + if len(latestBins) > 0 { + log.Info("binaries to keep because they are the latest for existing dynakubes", "paths", strings.Join(maps.Keys(latestBins), ",")) + } + + return latestBins +} diff --git a/pkg/controllers/csi/provisioner/cleanup/binaries_test.go b/pkg/controllers/csi/provisioner/cleanup/binaries_test.go new file mode 100644 index 0000000000..2b7003e09b --- /dev/null +++ b/pkg/controllers/csi/provisioner/cleanup/binaries_test.go @@ -0,0 +1,286 @@ +package cleanup + +import ( + "os" + "testing" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/mount-utils" +) + +func TestRemoveUnusedBinaries(t *testing.T) { + // Not possible to test, as parts of it rely on turning symlinks into actual paths, and clearing up according to these paths. + // each individual part is testable + t.SkipNow() +} + +func TestRemoveOldSharedBinaries(t *testing.T) { + t.Run("empty fs -> no panic", func(t *testing.T) { + cleaner := createCleaner(t) + + keptBins := map[string]bool{} + + cleaner.removeOldSharedBinaries(keptBins) + }) + t.Run("empty shared dir -> no panic", func(t *testing.T) { + cleaner := createCleaner(t) + cleaner.fs.MkdirAll(cleaner.path.AgentSharedBinaryDirBase(), os.ModePerm) + + keptBins := map[string]bool{} + + cleaner.removeOldSharedBinaries(keptBins) + }) + + t.Run("empty keptBins -> remove all", func(t *testing.T) { + cleaner := createCleaner(t) + cleaner.fs.MkdirAll(cleaner.path.AgentSharedBinaryDirBase(), os.ModePerm) + + keptBins := map[string]bool{} + agentVersions := []string{"test1", "test2"} + + for _, folder := range agentVersions { + cleaner.createSharedBinDir(t, folder) + + expectedDir := cleaner.path.AgentSharedBinaryDirForAgent(folder) + exists, _ := cleaner.fs.Exists(expectedDir) + require.True(t, exists) + } + + cleaner.removeOldSharedBinaries(keptBins) + + for _, folder := range agentVersions { + expectedDir := cleaner.path.AgentSharedBinaryDirForAgent(folder) + exists, _ := cleaner.fs.Exists(expectedDir) + require.False(t, exists) + } + }) + + t.Run("keptBins set -> only remove orphans", func(t *testing.T) { + cleaner := createCleaner(t) + cleaner.fs.MkdirAll(cleaner.path.AgentSharedBinaryDirBase(), os.ModePerm) + + keptBins := map[string]bool{ + cleaner.path.AgentSharedBinaryDirForAgent("test1"): true, + cleaner.path.AgentSharedBinaryDirForAgent("test2"): true, + } + agentVersions := []string{"test1", "test2"} + orphans := []string{"o1", "o2"} + + for _, version := range append(agentVersions, orphans...) { + cleaner.createSharedBinDir(t, version) + + expectedDir := cleaner.path.AgentSharedBinaryDirForAgent(version) + exists, _ := cleaner.fs.Exists(expectedDir) + require.True(t, exists) + } + + cleaner.removeOldSharedBinaries(keptBins) + + for _, folder := range agentVersions { + expectedDir := cleaner.path.AgentSharedBinaryDirForAgent(folder) + exists, _ := cleaner.fs.Exists(expectedDir) + require.True(t, exists) + } + + for _, folder := range orphans { + expectedDir := cleaner.path.AgentSharedBinaryDirForAgent(folder) + exists, _ := cleaner.fs.Exists(expectedDir) + require.False(t, exists) + } + }) +} + +func TestCollectStillMountedBins(t *testing.T) { + t.Run("0 mounts -> empty", func(t *testing.T) { + cleaner := createCleaner(t) + + relevantBins, err := cleaner.collectStillMountedBins() + + require.NoError(t, err) + require.Empty(t, relevantBins) + }) + t.Run("get mounted bins", func(t *testing.T) { + cleaner := createCleaner(t) + cleaner.path.RootDir = "special" + expectedPath := cleaner.path.RootDir + "/something" + expectedLowerDir := expectedPath + "/else" + + relevantMountPoint := mount.MountPoint{ + Device: "overlay", + Path: expectedPath, + Type: "overlay", + Opts: []string{ + "lowerdir=" + expectedLowerDir, + "upperdir=...", + "workdir=...", + }, + } + + mockMountPoints(t, cleaner, relevantMountPoint) + + relevantBins, err := cleaner.collectStillMountedBins() + + require.NoError(t, err) + require.Len(t, relevantBins, 1) + assert.True(t, relevantBins[expectedLowerDir]) + }) +} + +func TestCollectRelevantLatestBins(t *testing.T) { + t.Run("no dk -> do nothing", func(t *testing.T) { + cleaner := createCleaner(t) + + relevantBins := cleaner.collectRelevantLatestBins([]dynakube.DynaKube{}) + + require.Empty(t, relevantBins) + }) + t.Run("no relevant dk -> do nothing", func(t *testing.T) { + cleaner := createCleaner(t) + + relevantBins := cleaner.collectRelevantLatestBins([]dynakube.DynaKube{ + createHostMonDk(t, "hostmon", "url"), + }) + + require.Empty(t, relevantBins) + }) + + t.Run("relevant dk -> try to resolve symlink", func(t *testing.T) { + cleaner := createCleaner(t) + + relevantBins := cleaner.collectRelevantLatestBins([]dynakube.DynaKube{ + createAppMonDk(t, "appmon", "url"), + }) + + require.NotEmpty(t, relevantBins) + }) +} + +func TestRemoveOldBinarySymlinks(t *testing.T) { + t.Run("no dk -> remove everything", func(t *testing.T) { + cleaner := createCleaner(t) + dks := []dynakube.DynaKube{} + + binDirs := []string{"test1", "test2"} + + for _, folder := range binDirs { + cleaner.createBinDirs(t, folder) + + expectedDir := cleaner.path.LatestAgentBinaryForDynaKube(folder) + exists, _ := cleaner.fs.Exists(expectedDir) + require.True(t, exists) + } + + cleaner.removeOldBinarySymlinks(dks, fsState{ + binDks: binDirs, + }) + + for _, folder := range binDirs { + exists, _ := cleaner.fs.Exists(cleaner.path.LatestAgentBinaryForDynaKube(folder)) + require.False(t, exists) + } + }) + + t.Run("dk -> don't remove", func(t *testing.T) { + cleaner := createCleaner(t) + dks := []dynakube.DynaKube{ + createCloudNativeDk(t, "cloudnative", "-"), + createAppMonDk(t, "appmon", "-"), + } + + binDirs := []string{dks[0].Name, dks[1].Name, "test1", "test2"} + + for _, folder := range binDirs { + cleaner.createBinDirs(t, folder) + + expectedDir := cleaner.path.LatestAgentBinaryForDynaKube(folder) + exists, _ := cleaner.fs.Exists(expectedDir) + require.True(t, exists) + } + + cleaner.removeOldBinarySymlinks(dks, fsState{ + binDks: binDirs, + }) + + for _, folder := range binDirs[:2] { + exists, _ := cleaner.fs.Exists(cleaner.path.LatestAgentBinaryForDynaKube(folder)) + require.True(t, exists) + } + + for _, folder := range binDirs[2:] { + exists, _ := cleaner.fs.Exists(cleaner.path.LatestAgentBinaryForDynaKube(folder)) + require.False(t, exists) + } + }) + + t.Run("dk.Name == tenantUUID -> don't remove", func(t *testing.T) { + cleaner := createCleaner(t) + dks := []dynakube.DynaKube{ + createCloudNativeDk(t, "cloudnative", "-"), + createAppMonDk(t, "appmon", "-"), + } + + binDirs := []string{dks[0].Name, dks[1].Name, "test1", "test2"} + + for _, folder := range binDirs { + cleaner.createBinDirs(t, folder) + + expectedDir := cleaner.path.LatestAgentBinaryForDynaKube(folder) + exists, _ := cleaner.fs.Exists(expectedDir) + require.True(t, exists) + } + + cleaner.removeOldBinarySymlinks(dks, fsState{ + deprecatedDks: binDirs, + }) + + for _, folder := range binDirs[:2] { + exists, _ := cleaner.fs.Exists(cleaner.path.LatestAgentBinaryForDynaKube(folder)) + require.True(t, exists) + } + + for _, folder := range binDirs[2:] { + exists, _ := cleaner.fs.Exists(cleaner.path.LatestAgentBinaryForDynaKube(folder)) + require.False(t, exists) + } + }) +} + +func mockMountPoints(t *testing.T, cleaner *Cleaner, mountPoints ...mount.MountPoint) { + t.Helper() + + cleaner.mounter = mount.NewFakeMounter(mountPoints) +} + +func createAppMonDk(t *testing.T, name, apiUrl string) dynakube.DynaKube { + t.Helper() + + dk := createBaseDk(t, name, apiUrl) + dk.Spec.OneAgent.ApplicationMonitoring = &oneagent.ApplicationMonitoringSpec{} + + return dk +} + +func createBaseDk(t *testing.T, name, apiUrl string) dynakube.DynaKube { + t.Helper() + + return dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Spec: dynakube.DynaKubeSpec{ + APIURL: apiUrl, + }, + } +} + +func (c *Cleaner) createSharedBinDir(t *testing.T, version string) { + t.Helper() + + binDir := c.path.AgentSharedBinaryDirForAgent(version) + err := c.fs.MkdirAll(binDir, os.ModePerm) + require.NoError(t, err) +} diff --git a/pkg/controllers/dynakube/extension/otel/config.go b/pkg/controllers/csi/provisioner/cleanup/config.go similarity index 55% rename from pkg/controllers/dynakube/extension/otel/config.go rename to pkg/controllers/csi/provisioner/cleanup/config.go index 43f9727302..df646bf98a 100644 --- a/pkg/controllers/dynakube/extension/otel/config.go +++ b/pkg/controllers/csi/provisioner/cleanup/config.go @@ -1,9 +1,9 @@ -package otel +package cleanup import ( "github.com/Dynatrace/dynatrace-operator/pkg/logd" ) var ( - log = logd.Get().WithName("extension-otel") + log = logd.Get().WithName("csi-cleanup") ) diff --git a/pkg/controllers/csi/provisioner/cleanup/hostmounts.go b/pkg/controllers/csi/provisioner/cleanup/hostmounts.go new file mode 100644 index 0000000000..d3f64c185f --- /dev/null +++ b/pkg/controllers/csi/provisioner/cleanup/hostmounts.go @@ -0,0 +1,82 @@ +package cleanup + +import ( + "strings" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi/metadata" + "golang.org/x/exp/maps" + "k8s.io/mount-utils" +) + +func (c *Cleaner) isMountPoint(file string) (bool, error) { + fakeMounter, ok := c.mounter.(*mount.FakeMounter) + if ok { + // you can't use the fake mounter IsLikelyNotMountPoint, as it will still use the os package + err, ok := fakeMounter.MountCheckErrors[file] + if ok { + if err == nil { + return true, nil + } + + return false, err + } else { + return false, nil + } + } + + return c.mounter.IsMountPoint(file) +} + +func (c *Cleaner) removeHostMounts(dks []dynakube.DynaKube, fsState fsState) { + relevantHostDirs := c.collectRelevantHostDirs(dks) + + for _, hostDk := range fsState.hostDks { + possibleHostDirs := []string{ + c.path.OsAgentDir(hostDk), + c.path.OldOsAgentDir(hostDk), + } + + for _, hostDir := range possibleHostDirs { + isMountPoint, err := c.isMountPoint(hostDir) + if err == nil && !isMountPoint && !relevantHostDirs[hostDir] { + err := c.fs.RemoveAll(hostDir) + if err == nil { + log.Info("removed old host mount directory", "path", hostDir) + } + } + } + } +} + +func (c *Cleaner) collectRelevantHostDirs(dks []dynakube.DynaKube) map[string]bool { + hostDirs := map[string]bool{} + + for _, dk := range dks { + if !dk.OneAgent().IsReadOnlyFSSupported() { + continue + } + + hostDir := c.path.OsAgentDir(dk.Name) + + hostDirs[hostDir] = true + + c.safeAddRelevantPath(hostDir, hostDirs) + + tenantUUID, err := metadata.TenantUUIDFromApiUrl(dk.ApiUrl()) + if err != nil { + log.Error(err, "malformed ApiUrl for dynakube during host mount directory cleanup", "dk", dk.Name, "apiUrl", dk.ApiUrl()) + + continue + } + + deprecatedHostDirLink := c.path.OldOsAgentDir(tenantUUID) + c.safeAddRelevantPath(deprecatedHostDirLink, hostDirs) + } + + if len(hostDirs) > 0 { + log.Info("host directories to keep because they have a related dynakube", "paths", strings.Join(maps.Keys(hostDirs), ",")) + } + + return hostDirs +} diff --git a/pkg/controllers/csi/provisioner/cleanup/hostmounts_test.go b/pkg/controllers/csi/provisioner/cleanup/hostmounts_test.go new file mode 100644 index 0000000000..e0f0094b23 --- /dev/null +++ b/pkg/controllers/csi/provisioner/cleanup/hostmounts_test.go @@ -0,0 +1,193 @@ +package cleanup + +import ( + "fmt" + "os" + "testing" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/mount-utils" +) + +func TestRemoveHostMounts(t *testing.T) { + tenantUUID1 := "tenant1" + apiUrl1 := fmt.Sprintf("https://%s.dev.dynatracelabs.com/api", tenantUUID1) + + tenantUUID2 := "tenant2" + apiUrl2 := fmt.Sprintf("https://%s.dev.dynatracelabs.com/api", tenantUUID2) + + t.Run("no dk -> no relevant dirs -> remove all", func(t *testing.T) { + cleaner := createCleaner(t) + dks := []dynakube.DynaKube{} + hostFolders := []string{tenantUUID1, tenantUUID2, "random-name1", "random-name2"} + + for _, folder := range hostFolders { + cleaner.createHostDirs(t, folder) + + expectedDir := cleaner.path.OsAgentDir(folder) + exists, _ := cleaner.fs.Exists(expectedDir) + require.True(t, exists) + } + + cleaner.removeHostMounts(dks, fsState{ + hostDks: hostFolders, + }) + + for _, folder := range hostFolders { + exists, _ := cleaner.fs.Exists(cleaner.path.OsAgentDir(folder)) + require.False(t, exists) + } + }) + + t.Run("relevant dk -> remove only orphans", func(t *testing.T) { + cleaner := createCleaner(t) + dks := []dynakube.DynaKube{ + createHostMonDk(t, "hostmon", apiUrl1), + createCloudNativeDk(t, "cloudnative", apiUrl2), + } + folders := []string{ + cleaner.path.OldOsAgentDir(tenantUUID1), + cleaner.path.OsAgentDir(dks[0].Name), + cleaner.path.OsAgentDir(dks[1].Name), + cleaner.path.OldOsAgentDir("random-name1"), + cleaner.path.OsAgentDir("random-name2"), + } + + for _, folder := range folders { + err := cleaner.fs.MkdirAll(folder, os.ModePerm) + require.NoError(t, err) + } + + cleaner.removeHostMounts(dks, fsState{ + hostDks: []string{dks[0].Name, dks[1].Name, tenantUUID1, "random-name1", "random-name2"}, + }) + + for _, folder := range folders[:3] { + exists, _ := cleaner.fs.Exists(folder) + require.True(t, exists) + } + + for _, folder := range folders[3:] { + exists, _ := cleaner.fs.Exists(folder) + require.False(t, exists) + } + }) + + t.Run("don't remove mounted orphans", func(t *testing.T) { + cleaner := createCleaner(t) + dks := []dynakube.DynaKube{} + hostFolders := []string{tenantUUID1, tenantUUID2} + fakeMounter := mount.NewFakeMounter(nil) + fakeMounter.MountCheckErrors = map[string]error{} + + for _, folder := range hostFolders { + cleaner.createHostDirs(t, folder) + + expectedDir := cleaner.path.OsAgentDir(folder) + exists, _ := cleaner.fs.Exists(expectedDir) + require.True(t, exists) + + fakeMounter.MountCheckErrors[expectedDir] = nil + } + + cleaner.mounter = fakeMounter + + cleaner.removeHostMounts(dks, fsState{ + hostDks: hostFolders, + }) + + for _, folder := range hostFolders { + exists, _ := cleaner.fs.Exists(cleaner.path.OsAgentDir(folder)) + require.True(t, exists) + } + }) +} + +func TestCollectRelevantHostDirs(t *testing.T) { + tenantUUID1 := "tenant1" + apiUrl1 := fmt.Sprintf("https://%s.dev.dynatracelabs.com/api", tenantUUID1) + + tenantUUID2 := "tenant2" + apiUrl2 := fmt.Sprintf("https://%s.dev.dynatracelabs.com/api", tenantUUID2) + + t.Run("no dk -> no relevant dirs", func(t *testing.T) { + cleaner := createCleaner(t) + dks := []dynakube.DynaKube{} + + relevantDirs := cleaner.collectRelevantHostDirs(dks) + + require.Empty(t, relevantDirs) + }) + + t.Run("not-relevant dk -> no relevant dirs", func(t *testing.T) { + cleaner := createCleaner(t) + dks := []dynakube.DynaKube{ + createAppMonDk(t, "appmon1", apiUrl1), + createAppMonDk(t, "appmon2", apiUrl2), + } + + relevantDirs := cleaner.collectRelevantHostDirs(dks) + + require.Empty(t, relevantDirs) + }) + + t.Run("relevant dks, but not existing -> current path always added", func(t *testing.T) { + cleaner := createCleaner(t) + dks := []dynakube.DynaKube{ + createHostMonDk(t, "hostmon", apiUrl1), + createCloudNativeDk(t, "cloudnative", apiUrl2), + } + + relevantDirs := cleaner.collectRelevantHostDirs(dks) + + require.NotEmpty(t, relevantDirs) + require.Len(t, relevantDirs, 2) + assert.Contains(t, relevantDirs, cleaner.path.OsAgentDir(dks[0].Name)) + assert.Contains(t, relevantDirs, cleaner.path.OsAgentDir(dks[1].Name)) + assert.NotContains(t, relevantDirs, cleaner.path.OsAgentDir(tenantUUID1)) + assert.NotContains(t, relevantDirs, cleaner.path.OsAgentDir(tenantUUID2)) + }) + + t.Run("relevant dk -> relevant dirs, deprecated(tenantUUID) location dir included if exists", func(t *testing.T) { + cleaner := createCleaner(t) + dks := []dynakube.DynaKube{ + createHostMonDk(t, "hostmon", apiUrl1), + createCloudNativeDk(t, "cloudnative", apiUrl2), + createAppMonDk(t, "appmon", apiUrl1), + } + + cleaner.createDeprecatedHostDirs(t, tenantUUID1) + cleaner.createHostDirs(t, dks[0].Name) + + relevantDirs := cleaner.collectRelevantHostDirs(dks) + + require.NotEmpty(t, relevantDirs) + require.Len(t, relevantDirs, 3) + assert.Contains(t, relevantDirs, cleaner.path.OsAgentDir(dks[0].Name)) + assert.Contains(t, relevantDirs, cleaner.path.OsAgentDir(dks[1].Name)) + assert.NotContains(t, relevantDirs, cleaner.path.OsAgentDir(dks[2].Name)) + assert.Contains(t, relevantDirs, cleaner.path.OldOsAgentDir(tenantUUID1)) + assert.NotContains(t, relevantDirs, cleaner.path.OsAgentDir(tenantUUID2)) + }) +} + +func createHostMonDk(t *testing.T, name, apiUrl string) dynakube.DynaKube { + t.Helper() + + dk := createBaseDk(t, name, apiUrl) + dk.Spec.OneAgent.HostMonitoring = &oneagent.HostInjectSpec{} + + return dk +} + +func createCloudNativeDk(t *testing.T, name, apiUrl string) dynakube.DynaKube { + t.Helper() + + dk := createBaseDk(t, name, apiUrl) + dk.Spec.OneAgent.CloudNativeFullStack = &oneagent.CloudNativeFullStackSpec{} + + return dk +} diff --git a/pkg/controllers/csi/provisioner/cleanup/run.go b/pkg/controllers/csi/provisioner/cleanup/run.go new file mode 100644 index 0000000000..1840690903 --- /dev/null +++ b/pkg/controllers/csi/provisioner/cleanup/run.go @@ -0,0 +1,194 @@ +package cleanup + +import ( + "context" + "os" + + dtcsi "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi" + "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi/metadata" + "github.com/spf13/afero" + "k8s.io/mount-utils" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type Cleaner struct { + fs afero.Afero + apiReader client.Reader + mounter mount.Interface + path metadata.PathResolver +} + +// fsState collects all the "top-level" folders we care about and categorizes them +type fsState struct { + // deprecatedDks are the dynakube-dirs under /data that have a /run directory, which used to contain the app-mounts, these directories are named after the tenantUUID + deprecatedDks []string + // binDks are dynakube-dirs that have a "latest-symlink" pointing at a codemodule binary + binDks []string + // hostDks are dynakube-dirs that contain the folder that was mounted to Host OneAgents + hostDks []string +} + +func New(fs afero.Afero, apiReader client.Reader, path metadata.PathResolver, mounter mount.Interface) *Cleaner { + return &Cleaner{ + fs: fs, + apiReader: apiReader, + path: path, + mounter: mounter, + } +} + +// Run will only execute the cleanup logic if enough time has passed from the previous run, to not overload the IO of the node +func (c *Cleaner) Run(ctx context.Context) error { + tickerResetFunc := checkTicker() + if tickerResetFunc == nil { + return nil + } + defer tickerResetFunc() + + return c.run(ctx) +} + +// InstantRun will always execute the cleanup logic ignoring the time passed from previous run +func (c *Cleaner) InstantRun(ctx context.Context) error { + defer resetTickerAfterDelete() + + return c.run(ctx) +} + +func (c *Cleaner) run(ctx context.Context) error { + fsState, err := c.getFilesystemState() + if err != nil { + return err + } + + c.removeDeprecatedMounts(fsState) + + dks, err := metadata.GetRelevantDynaKubes(ctx, c.apiReader) + if err != nil { + log.Info("failed to list available dynakubes, skipping cleanup") + + return err + } + + c.removeHostMounts(dks, fsState) + c.removeUnusedBinaries(dks, fsState) + + return nil +} + +func (c *Cleaner) getFilesystemState() (fsState fsState, err error) { //nolint:revive + rootSubDirs, err := c.fs.ReadDir(c.path.RootDir) + if err != nil { + log.Info("failed to list the contents of the root directory of the csi-provisioner", "rootDir", c.path.RootDir) + + return fsState, err + } + + var unknownDirs []string + + defer func() { + for _, unknown := range unknownDirs { + log.Info("removing unknown path", "path", unknown) + _ = c.fs.RemoveAll(unknown) + } + }() + + for _, fileInfo := range rootSubDirs { + if !fileInfo.IsDir() || + fileInfo.Name() == dtcsi.SharedAppMountsDir || + fileInfo.Name() == dtcsi.SharedJobWorkDir || + fileInfo.Name() == dtcsi.SharedDynaKubesDir || + fileInfo.Name() == dtcsi.SharedAgentBinDir { + continue + } + + deprecatedExists, _ := c.fs.Exists(c.path.AgentRunDir(fileInfo.Name())) + if deprecatedExists { + fsState.deprecatedDks = append(fsState.deprecatedDks, fileInfo.Name()) + } + + hostExists, _ := c.fs.Exists(c.path.OldOsAgentDir(fileInfo.Name())) + if hostExists { + fsState.hostDks = append(fsState.hostDks, fileInfo.Name()) + } + + if !deprecatedExists && !hostExists { + unknownDirs = append(unknownDirs, c.path.Base(fileInfo.Name())) + } + } + + dkDirs, err := c.fs.ReadDir(c.path.DynaKubesBaseDir()) + if os.IsNotExist(err) { + return fsState, nil + } else if err != nil { + log.Info("failed to list the contents of the root directory of the csi-provisioner", "rootDir", c.path.RootDir) + + return fsState, err + } + + for _, fileInfo := range dkDirs { + if !fileInfo.IsDir() { + continue + } + + binExists, _ := c.fs.Exists(c.path.LatestAgentBinaryForDynaKube(fileInfo.Name())) + if binExists { + fsState.binDks = append(fsState.binDks, fileInfo.Name()) + } + + hostExists, _ := c.fs.Exists(c.path.OsAgentDir(fileInfo.Name())) + if hostExists { + fsState.hostDks = append(fsState.hostDks, fileInfo.Name()) + } + + if !binExists && !hostExists { + unknownDirs = append(unknownDirs, c.path.DynaKubeDir(fileInfo.Name())) + } + } + + return fsState, nil +} + +// safeAddRelevantPath follows the symlink that is provided in the `path` param and adds the actual path to the provided map +// It checks for the existence of the path and verifies if it is a symlink. +// Trying to follow a path that is not a symlink will case an error. +// Should be used for paths that are "maybe" symlinks, more expensive then its addRelevantPath. +func (c *Cleaner) safeAddRelevantPath(path string, relevantPaths map[string]bool) { + fInfo, err := c.fs.Stat(path) + if err != nil { + if !os.IsNotExist(err) { + log.Error(err, "failed to check if host mount directory is a symlink") + } + + return + } + + if fInfo.Mode() != os.ModeSymlink { + relevantPaths[path] = true + + return + } + + c.addRelevantPath(path, relevantPaths) +} + +// addRelevantPath follows the symlink that is provided in the `path` param and adds the actual path to the provided map +// does no checking for the existence of the path and does not verify if it is a symlink. +// Should be used for paths that are 100% to be symlinks to save on IO. +func (c *Cleaner) addRelevantPath(path string, relevantPaths map[string]bool) { + linker, ok := c.fs.Fs.(afero.LinkReader) + if ok { + actualPath, err := linker.ReadlinkIfPossible(path) + if err != nil { + log.Error(err, "failed to follow symlink", "path", path) + + return + } + + relevantPaths[actualPath] = true + } else { // only should happen during tests + log.Info("following symlinks not possible, unexpected behavior") + + relevantPaths[path] = true + } +} diff --git a/pkg/controllers/csi/provisioner/cleanup/run_test.go b/pkg/controllers/csi/provisioner/cleanup/run_test.go new file mode 100644 index 0000000000..c2c0e12239 --- /dev/null +++ b/pkg/controllers/csi/provisioner/cleanup/run_test.go @@ -0,0 +1,178 @@ +package cleanup + +import ( + "os" + "testing" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" + "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi/metadata" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/mount-utils" +) + +func TestGetFilesystemState(t *testing.T) { + t.Run("no error on empty FS", func(t *testing.T) { + cleaner := createCleaner(t) + + fsState, err := cleaner.getFilesystemState() + + require.NoError(t, err) + assert.Empty(t, fsState) + }) + t.Run("remove unknown dirs", func(t *testing.T) { + cleaner := createCleaner(t) + + cleaner.fs.Mkdir(cleaner.path.Base("test1"), os.ModePerm) + cleaner.fs.Mkdir(cleaner.path.Base("test2"), os.ModePerm) + + files, err := cleaner.fs.ReadDir(cleaner.path.RootDir) + require.NoError(t, err) + assert.Len(t, files, 2) + + fsState, err := cleaner.getFilesystemState() + + require.NoError(t, err) + assert.Empty(t, fsState) + + files, err = cleaner.fs.ReadDir(cleaner.path.RootDir) + require.NoError(t, err) + assert.Empty(t, files) + }) + t.Run("don't touch unknown files, to keep the db intact, just in case", func(t *testing.T) { + cleaner := createCleaner(t) + + cleaner.fs.Create(cleaner.path.Base("test1")) + cleaner.fs.Create(cleaner.path.Base("test2")) + + files, err := cleaner.fs.ReadDir(cleaner.path.RootDir) + require.NoError(t, err) + assert.Len(t, files, 2) + + fsState, err := cleaner.getFilesystemState() + + require.NoError(t, err) + assert.Empty(t, fsState) + + files, err = cleaner.fs.ReadDir(cleaner.path.RootDir) + require.NoError(t, err) + assert.Len(t, files, 2) + }) + t.Run("don't touch well-known dirs", func(t *testing.T) { + cleaner := createCleaner(t) + + cleaner.fs.Mkdir(cleaner.path.AgentSharedBinaryDirBase(), os.ModePerm) + cleaner.fs.Mkdir(cleaner.path.AppMountsBaseDir(), os.ModePerm) + + files, err := cleaner.fs.ReadDir(cleaner.path.RootDir) + require.NoError(t, err) + assert.Len(t, files, 2) + + fsState, err := cleaner.getFilesystemState() + + require.NoError(t, err) + assert.Empty(t, fsState) + + files, err = cleaner.fs.ReadDir(cleaner.path.RootDir) + require.NoError(t, err) + assert.Len(t, files, 2) + }) + + t.Run("get fsState", func(t *testing.T) { + cleaner := createCleaner(t) + + dkName1 := "dk1" + dkName2 := "dk2" + dkName3 := "dk3" + + cleaner.createDeprecatedDirs(t, dkName1, 0, 2) + + cleaner.createBinDirs(t, dkName1) + cleaner.createBinDirs(t, dkName2) + + cleaner.createHostDirs(t, dkName2) + cleaner.createHostDirs(t, dkName3) + + fsState, err := cleaner.getFilesystemState() + require.NoError(t, err) + + assert.Len(t, fsState.deprecatedDks, 1) + assert.Contains(t, fsState.deprecatedDks, dkName1) + + assert.Len(t, fsState.binDks, 2) + assert.Contains(t, fsState.binDks, dkName1) + assert.Contains(t, fsState.binDks, dkName2) + + assert.Len(t, fsState.hostDks, 2) + assert.Contains(t, fsState.hostDks, dkName2) + assert.Contains(t, fsState.hostDks, dkName3) + }) +} + +func TestSafeAddRelevantPath(t *testing.T) { + t.Run("no error if path doesn't exist and no addition", func(t *testing.T) { + cleaner := createCleaner(t) + + relevantPaths := map[string]bool{} + + cleaner.safeAddRelevantPath("something", relevantPaths) + assert.Empty(t, relevantPaths) + }) + + t.Run("not symlink => added without change", func(t *testing.T) { + cleaner := createCleaner(t) + path := "something" + cleaner.fs.Mkdir(path, os.ModePerm) + + relevantPaths := map[string]bool{} + + cleaner.safeAddRelevantPath(path, relevantPaths) + assert.Contains(t, relevantPaths, path) + }) + + t.Run("symlink => would be added after following the link", func(t *testing.T) { + // can't be tested, as it relies on following symlinks + t.SkipNow() + }) +} + +func TestAddRelevantPath(t *testing.T) { + // can't be tested, as it relies on following symlinks + t.SkipNow() +} + +func createCleaner(t *testing.T) *Cleaner { + t.Helper() + + return &Cleaner{ + fs: afero.Afero{Fs: afero.NewMemMapFs()}, + mounter: mount.NewFakeMounter(nil), + apiReader: fake.NewClient(), + path: metadata.PathResolver{}, + } +} + +func (c *Cleaner) createBinDirs(t *testing.T, name string) { + t.Helper() + + binDir := c.path.LatestAgentBinaryForDynaKube(name) + err := c.fs.MkdirAll(binDir, os.ModePerm) + require.NoError(t, err) +} + +func (c *Cleaner) createHostDirs(t *testing.T, name string) { + t.Helper() + + hostDir := c.path.OsAgentDir(name) + err := c.fs.MkdirAll(hostDir, os.ModePerm) + require.NoError(t, err) +} + +func (c *Cleaner) createDeprecatedHostDirs(t *testing.T, tenantUUID string) { + t.Helper() + + hostDir := c.path.OldOsAgentDir(tenantUUID) + err := c.fs.MkdirAll(hostDir, os.ModePerm) + require.NoError(t, err) +} diff --git a/pkg/controllers/csi/provisioner/cleanup/ticker.go b/pkg/controllers/csi/provisioner/cleanup/ticker.go new file mode 100644 index 0000000000..a90f898921 --- /dev/null +++ b/pkg/controllers/csi/provisioner/cleanup/ticker.go @@ -0,0 +1,75 @@ +package cleanup + +import ( + "os" + "sync" + "time" +) + +const ( + defaultCleanupPeriod = "30m" + cleanupEnv = "CLEANUP_PERIOD" +) + +var ( + cleanupPeriod time.Duration + ticker *time.Ticker +) + +// checkTicker will initialize (if needed) and check the a ticker if enough time has passed since the last cleanup +func checkTicker() func() { + setupCleanUpPeriod() + + if ticker == nil { + log.Info("initial run of CSI filesystem cleanup") + + return func() { + ticker = time.NewTicker(cleanupPeriod) + } + } + + select { + case <-ticker.C: + log.Info("running CSI filesystem cleanup") + + return func() { + ticker.Reset(cleanupPeriod) + } + default: + log.Info("skipping CSI filesystem cleanup, it only runs every given period", "period", cleanupPeriod.String()) + + return nil + } +} + +func setupCleanUpPeriod() { + sync.OnceFunc(func() { + rawDuration := os.Getenv(cleanupEnv) + + duration, err := time.ParseDuration(rawDuration) + if err != nil { + if rawDuration != "" { + log.Info("custom cleanup period could be parsed, falling back to default", "env", cleanupEnv, "value", rawDuration, "default", defaultCleanupPeriod) + } + + duration, _ = time.ParseDuration(defaultCleanupPeriod) + } + + cleanupPeriod = duration + })() +} + +// resetTickerAfterDelete is for the specific scenario of dynakube deletion +// its purpose is to reset the ticker safely, but not check it, so the cleanup will always run after a DynaKube deletion +// meant to be called via defer +func resetTickerAfterDelete() { + setupCleanUpPeriod() + + if ticker == nil { + log.Info("initial run of CSI filesystem cleanup") + + ticker = time.NewTicker(cleanupPeriod) + } else { + ticker.Reset(cleanupPeriod) + } +} diff --git a/pkg/controllers/csi/provisioner/cleanup/ticker_test.go b/pkg/controllers/csi/provisioner/cleanup/ticker_test.go new file mode 100644 index 0000000000..b5c7529606 --- /dev/null +++ b/pkg/controllers/csi/provisioner/cleanup/ticker_test.go @@ -0,0 +1,49 @@ +package cleanup + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestTicker(t *testing.T) { + customCleanUpPeriod := "22m" + t.Setenv(cleanupEnv, customCleanUpPeriod) + + parsedDuration, _ := time.ParseDuration(customCleanUpPeriod) + + // Initially cleanup period is not set + require.Empty(t, cleanupPeriod) + + // Initially ticker is not set + require.Nil(t, ticker) + + // Works with nil ticker + resetTickerAfterDelete() + + // ticker is now set + require.NotNil(t, ticker) + // cleanup period is now set and respects env + require.Equal(t, parsedDuration, cleanupPeriod) + + // Works with not-nil ticker + resetTickerAfterDelete() + + ticker.Stop() + ticker = nil + + // Works with nil ticker + resetFunc := checkTicker() + require.NotNil(t, resetFunc) + require.Nil(t, ticker) + + resetFunc() + require.NotNil(t, ticker) + + // Works with not-nil ticker + resetFunc = checkTicker() + require.Nil(t, resetFunc) + + ticker.Stop() +} diff --git a/pkg/controllers/csi/provisioner/config.go b/pkg/controllers/csi/provisioner/config.go index 608fb0617a..a550d85ce5 100644 --- a/pkg/controllers/csi/provisioner/config.go +++ b/pkg/controllers/csi/provisioner/config.go @@ -4,11 +4,6 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/logd" ) -const ( - failedInstallAgentVersionEvent = "FailedInstallAgentVersion" - installAgentVersionEvent = "InstallAgentVersion" -) - var ( log = logd.Get().WithName("csi-provisioner") ) diff --git a/pkg/controllers/csi/provisioner/controller.go b/pkg/controllers/csi/provisioner/controller.go index a0ae0fc70d..4442945089 100644 --- a/pkg/controllers/csi/provisioner/controller.go +++ b/pkg/controllers/csi/provisioner/controller.go @@ -18,25 +18,24 @@ package csiprovisioner import ( "context" - "fmt" "time" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" dtcsi "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi" - csigc "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi/gc" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi/metadata" + "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi/provisioner/cleanup" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/dynatraceclient" - "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/processmoduleconfigsecret" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/token" "github.com/Dynatrace/dynatrace-operator/pkg/injection/codemodule/installer" "github.com/Dynatrace/dynatrace-operator/pkg/injection/codemodule/installer/image" + "github.com/Dynatrace/dynatrace-operator/pkg/injection/codemodule/installer/job" "github.com/Dynatrace/dynatrace-operator/pkg/injection/codemodule/installer/url" "github.com/pkg/errors" "github.com/spf13/afero" + batchv1 "k8s.io/api/batch/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/tools/record" + "k8s.io/mount-utils" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/manager" @@ -51,43 +50,44 @@ const ( type urlInstallerBuilder func(afero.Fs, dtclient.Client, *url.Properties) installer.Installer type imageInstallerBuilder func(context.Context, afero.Fs, *image.Properties) (installer.Installer, error) +type jobInstallerBuilder func(context.Context, afero.Fs, *job.Properties) installer.Installer // OneAgentProvisioner reconciles a DynaKube object type OneAgentProvisioner struct { - client client.Client - apiReader client.Reader - fs afero.Fs - recorder record.EventRecorder - db metadata.Access - gc reconcile.Reconciler + apiReader client.Reader + kubeClient client.Client + fs afero.Fs dynatraceClientBuilder dynatraceclient.Builder urlInstallerBuilder urlInstallerBuilder imageInstallerBuilder imageInstallerBuilder - opts dtcsi.CSIOptions + jobInstallerBuilder jobInstallerBuilder + cleaner *cleanup.Cleaner path metadata.PathResolver } // NewOneAgentProvisioner returns a new OneAgentProvisioner -func NewOneAgentProvisioner(mgr manager.Manager, opts dtcsi.CSIOptions, db metadata.Access) *OneAgentProvisioner { +func NewOneAgentProvisioner(mgr manager.Manager, opts dtcsi.CSIOptions) *OneAgentProvisioner { + fs := afero.NewOsFs() + path := metadata.PathResolver{RootDir: opts.RootDir} + return &OneAgentProvisioner{ - client: mgr.GetClient(), apiReader: mgr.GetAPIReader(), - opts: opts, - fs: afero.NewOsFs(), - recorder: mgr.GetEventRecorderFor("OneAgentProvisioner"), - db: db, - path: metadata.PathResolver{RootDir: opts.RootDir}, - gc: csigc.NewCSIGarbageCollector(mgr.GetAPIReader(), opts, db), + kubeClient: mgr.GetClient(), + fs: fs, + path: path, dynatraceClientBuilder: dynatraceclient.NewBuilder(mgr.GetAPIReader()), urlInstallerBuilder: url.NewUrlInstaller, imageInstallerBuilder: image.NewImageInstaller, + jobInstallerBuilder: job.NewInstaller, + cleaner: cleanup.New(afero.Afero{Fs: fs}, mgr.GetAPIReader(), path, mount.New("")), } } func (provisioner *OneAgentProvisioner) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&dynakube.DynaKube{}). + Owns(&batchv1.Job{}). Named("provisioner-controller"). Complete(provisioner) } @@ -95,19 +95,26 @@ func (provisioner *OneAgentProvisioner) SetupWithManager(mgr ctrl.Manager) error func (provisioner *OneAgentProvisioner) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) { log.Info("reconciling DynaKube", "namespace", request.Namespace, "dynakube", request.Name) - dk, err := provisioner.getDynaKube(ctx, request.NamespacedName) + var dk dynakube.DynaKube + + err := provisioner.apiReader.Get(ctx, request.NamespacedName, &dk) if err != nil { if k8serrors.IsNotFound(err) { - return reconcile.Result{}, provisioner.db.DeleteDynakube(ctx, request.Name) + err = provisioner.cleaner.InstantRun(ctx) + if err != nil { + log.Error(err, "failed to run clean-up after dynakube deletion") + } + + return reconcile.Result{}, nil } return reconcile.Result{}, err } - if !isProvisionerNeeded(dk) { + if !isProvisionerNeeded(&dk) { log.Info("CSI driver provisioner not needed") - return reconcile.Result{RequeueAfter: longRequeueDuration}, provisioner.db.DeleteDynakube(ctx, request.Name) + return reconcile.Result{RequeueAfter: longRequeueDuration}, provisioner.cleaner.Run(ctx) } err = provisioner.setupFileSystem(dk) @@ -115,189 +122,54 @@ func (provisioner *OneAgentProvisioner) Reconcile(ctx context.Context, request r return reconcile.Result{}, err } - dynakubeMetadata, err := provisioner.setupDynakubeMetadata(ctx, dk) // needed for the CSI-resilience feature - if err != nil { - return reconcile.Result{}, err - } - - if !dk.NeedAppInjection() { + if !dk.OneAgent().IsAppInjectionNeeded() { log.Info("app injection not necessary, skip agent codemodule download", "dynakube", dk.Name) + _ = provisioner.cleaner.Run(ctx) + return reconcile.Result{RequeueAfter: longRequeueDuration}, nil } - if dk.CodeModulesImage() == "" && dk.CodeModulesVersion() == "" { + if dk.OneAgent().GetCodeModulesImage() == "" && dk.OneAgent().GetCodeModulesVersion() == "" { log.Info("dynakube status is not yet ready, requeuing", "dynakube", dk.Name) - return reconcile.Result{RequeueAfter: shortRequeueDuration}, err + return reconcile.Result{RequeueAfter: shortRequeueDuration}, nil } - err = provisioner.provisionCodeModules(ctx, dk, dynakubeMetadata) - if err != nil { - return reconcile.Result{}, err - } + err = provisioner.installAgent(ctx, dk) + if err != nil && errors.Is(err, errNotReady) { + log.Info(err.Error(), "dynakube", dk.Name) - err = provisioner.collectGarbage(ctx, request) - if err != nil { + return reconcile.Result{RequeueAfter: notReadyRequeueDuration}, nil + } else if err != nil { return reconcile.Result{}, err } + _ = provisioner.cleaner.Run(ctx) + return reconcile.Result{RequeueAfter: defaultRequeueDuration}, nil } func isProvisionerNeeded(dk *dynakube.DynaKube) bool { - return dk.CloudNativeFullstackMode() || - dk.ApplicationMonitoringMode() || - dk.HostMonitoringMode() -} - -func (provisioner *OneAgentProvisioner) setupFileSystem(dk *dynakube.DynaKube) error { - tenantUUID, err := dk.TenantUUIDFromApiUrl() - if err != nil { - return err - } - - if err := provisioner.createCSIDirectories(tenantUUID); err != nil { - log.Error(err, "error when creating csi directories", "path", provisioner.path.TenantDir(tenantUUID)) - - return errors.WithStack(err) - } - - log.Info("csi directories exist", "path", provisioner.path.TenantDir(tenantUUID)) - - return nil -} - -func (provisioner *OneAgentProvisioner) setupDynakubeMetadata(ctx context.Context, dk *dynakube.DynaKube) (*metadata.Dynakube, error) { - dynakubeMetadata, oldDynakubeMetadata, err := provisioner.handleMetadata(ctx, dk) - if err != nil { - return nil, err - } - - // Create/update the dynakubeMetadata entry while `LatestVersion` is not necessarily set - // so the host oneagent-storages can be mounted before the standalone agent binaries are ready to be mounted - return dynakubeMetadata, provisioner.createOrUpdateDynakubeMetadata(ctx, oldDynakubeMetadata, dynakubeMetadata) + return dk.OneAgent().IsAppInjectionNeeded() || dk.OneAgent().IsReadOnlyFSSupported() } -func (provisioner *OneAgentProvisioner) collectGarbage(ctx context.Context, request reconcile.Request) error { - _, err := provisioner.gc.Reconcile(ctx, request) - - return err -} - -func (provisioner *OneAgentProvisioner) provisionCodeModules(ctx context.Context, dk *dynakube.DynaKube, dynakubeMetadata *metadata.Dynakube) error { - oldDynakubeMetadata := *dynakubeMetadata - // creates a dt client and checks tokens exist for the given dynakube - dtc, err := buildDtc(provisioner, ctx, dk) - if err != nil { - return err - } - - requeue, err := provisioner.updateAgentInstallation(ctx, dtc, dynakubeMetadata, dk) - if requeue || err != nil { - return err +func (provisioner *OneAgentProvisioner) setupFileSystem(dk dynakube.DynaKube) error { + dynakubeDir := provisioner.path.DynaKubeDir(dk.GetName()) + if err := provisioner.fs.MkdirAll(dynakubeDir, 0755); err != nil { + return errors.WithMessagef(err, "failed to create directory %s", dynakubeDir) } - // Set/Update the `LatestVersion` field in the database entry - err = provisioner.createOrUpdateDynakubeMetadata(ctx, oldDynakubeMetadata, dynakubeMetadata) - if err != nil { - return err - } - - return nil -} - -func (provisioner *OneAgentProvisioner) updateAgentInstallation( - ctx context.Context, dtc dtclient.Client, - dynakubeMetadata *metadata.Dynakube, - dk *dynakube.DynaKube, -) ( - requeue bool, - err error, -) { - latestProcessModuleConfig, err := processmoduleconfigsecret.GetSecretData(ctx, provisioner.apiReader, dk.Name, dk.Namespace) - if err != nil { - return false, err - } - - if dk.CodeModulesImage() != "" { - updatedDigest, err := provisioner.installAgentImage(ctx, *dk, latestProcessModuleConfig) - if err != nil { - log.Info("error when updating agent from image", "error", err.Error()) - // reporting error but not returning it to avoid immediate requeue and subsequently calling the API every few seconds - return true, nil - } else if updatedDigest != "" { - dynakubeMetadata.LatestVersion = "" - dynakubeMetadata.ImageDigest = updatedDigest - } - } else { - updateVersion, err := provisioner.installAgentZip(ctx, *dk, dtc, latestProcessModuleConfig) - if err != nil { - log.Info("error when updating agent from zip", "error", err.Error()) - // reporting error but not returning it to avoid immediate requeue and subsequently calling the API every few seconds - return true, nil - } else if updateVersion != "" { - dynakubeMetadata.LatestVersion = updateVersion - dynakubeMetadata.ImageDigest = "" - } - } - - return false, nil -} - -func (provisioner *OneAgentProvisioner) handleMetadata(ctx context.Context, dk *dynakube.DynaKube) (*metadata.Dynakube, metadata.Dynakube, error) { - dynakubeMetadata, err := provisioner.db.GetDynakube(ctx, dk.Name) - if err != nil { - return nil, metadata.Dynakube{}, errors.WithStack(err) - } - - // In case of a new dynakubeMetadata - var oldDynakubeMetadata metadata.Dynakube - if dynakubeMetadata != nil { - oldDynakubeMetadata = *dynakubeMetadata - } - - tenantUUID, err := dk.TenantUUIDFromApiUrl() - if err != nil { - return nil, metadata.Dynakube{}, err - } - - dynakubeMetadata = metadata.NewDynakube( - dk.Name, - tenantUUID, - oldDynakubeMetadata.LatestVersion, - oldDynakubeMetadata.ImageDigest, - dk.FeatureMaxFailedCsiMountAttempts()) - - return dynakubeMetadata, oldDynakubeMetadata, nil -} - -func (provisioner *OneAgentProvisioner) createOrUpdateDynakubeMetadata(ctx context.Context, oldDynakube metadata.Dynakube, dynakube *metadata.Dynakube) error { - if oldDynakube != *dynakube { - log.Info("dynakube has changed", - "name", dynakube.Name, - "tenantUUID", dynakube.TenantUUID, - "version", dynakube.LatestVersion, - "max mount attempts", dynakube.MaxFailedMountAttempts) - - if oldDynakube == (metadata.Dynakube{}) { - log.Info("adding dynakube to db", "tenantUUID", dynakube.TenantUUID, "version", dynakube.LatestVersion) - - return provisioner.db.InsertDynakube(ctx, dynakube) - } else { - log.Info("updating dynakube in db", - "old version", oldDynakube.LatestVersion, "new version", dynakube.LatestVersion, - "old tenantUUID", oldDynakube.TenantUUID, "new tenantUUID", dynakube.TenantUUID) - - return provisioner.db.UpdateDynakube(ctx, dynakube) - } + agentBinaryDir := provisioner.path.AgentSharedBinaryDirBase() + if err := provisioner.fs.MkdirAll(agentBinaryDir, 0755); err != nil { + return errors.WithMessagef(err, "failed to create directory %s", agentBinaryDir) } return nil } -func buildDtc(provisioner *OneAgentProvisioner, ctx context.Context, dk *dynakube.DynaKube) (dtclient.Client, error) { - tokenReader := token.NewReader(provisioner.apiReader, dk) +func buildDtc(provisioner *OneAgentProvisioner, ctx context.Context, dk dynakube.DynaKube) (dtclient.Client, error) { + tokenReader := token.NewReader(provisioner.apiReader, &dk) tokens, err := tokenReader.ReadTokens(ctx) if err != nil { @@ -306,34 +178,13 @@ func buildDtc(provisioner *OneAgentProvisioner, ctx context.Context, dk *dynakub dynatraceClient, err := provisioner.dynatraceClientBuilder. SetContext(ctx). - SetDynakube(*dk). + SetDynakube(dk). SetTokens(tokens). Build() if err != nil { - return nil, fmt.Errorf("failed to create Dynatrace client: %w", err) + return nil, errors.WithMessage(err, "failed to create Dynatrace client") } return dynatraceClient, nil } - -func (provisioner *OneAgentProvisioner) getDynaKube(ctx context.Context, name types.NamespacedName) (*dynakube.DynaKube, error) { - var dk dynakube.DynaKube - err := provisioner.apiReader.Get(ctx, name, &dk) - - return &dk, err -} - -func (provisioner *OneAgentProvisioner) createCSIDirectories(tenantUUID string) error { - tenantDir := provisioner.path.TenantDir(tenantUUID) - if err := provisioner.fs.MkdirAll(tenantDir, 0755); err != nil { - return fmt.Errorf("failed to create directory %s: %w", tenantDir, err) - } - - agentBinaryDir := provisioner.path.AgentBinaryDir(tenantUUID) - if err := provisioner.fs.MkdirAll(agentBinaryDir, 0755); err != nil { - return fmt.Errorf("failed to create directory %s: %w", agentBinaryDir, err) - } - - return nil -} diff --git a/pkg/controllers/csi/provisioner/controller_test.go b/pkg/controllers/csi/provisioner/controller_test.go index 97bf7951fa..6c6b9a015e 100644 --- a/pkg/controllers/csi/provisioner/controller_test.go +++ b/pkg/controllers/csi/provisioner/controller_test.go @@ -2,711 +2,427 @@ package csiprovisioner import ( "context" - "encoding/base64" + "encoding/json" "errors" "os" "path/filepath" - "strings" "testing" "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" - "github.com/Dynatrace/dynatrace-operator/pkg/api/status" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi/metadata" - "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/connectioninfo" + "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi/provisioner/cleanup" + "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/dynatraceclient" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/processmoduleconfigsecret" + "github.com/Dynatrace/dynatrace-operator/pkg/injection/codemodule/installer" + "github.com/Dynatrace/dynatrace-operator/pkg/injection/codemodule/installer/image" + "github.com/Dynatrace/dynatrace-operator/pkg/injection/codemodule/installer/job" + "github.com/Dynatrace/dynatrace-operator/pkg/injection/codemodule/installer/url" + "github.com/Dynatrace/dynatrace-operator/pkg/injection/codemodule/processmoduleconfig" + dtclientmock "github.com/Dynatrace/dynatrace-operator/test/mocks/pkg/clients/dynatrace" dtbuildermock "github.com/Dynatrace/dynatrace-operator/test/mocks/pkg/controllers/dynakube/dynatraceclient" installermock "github.com/Dynatrace/dynatrace-operator/test/mocks/pkg/injection/codemodule/installer" - reconcilermock "github.com/Dynatrace/dynatrace-operator/test/mocks/sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/tools/record" + "k8s.io/mount-utils" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) -const ( - testAPIURL = "http://test-uid/api" - tenantUUID = "test-uid" - dkName = "dynakube-test" - testNamespace = "test-namespace" - testVersion = "1.2.3" - testImageID = "test-image-id" - otherDkName = "other-dk" - errorMsg = "test-error" - agentVersion = "12345" - testRuxitConf = ` -[general] -key value -` -) - -type mkDirAllErrorFs struct { - afero.Fs -} - -func (fs *mkDirAllErrorFs) MkdirAll(_ string, _ os.FileMode) error { - return errors.New(errorMsg) -} - -func TestOneAgentProvisioner_Reconcile(t *testing.T) { +func TestReconcile(t *testing.T) { ctx := context.Background() - dynakubeName := "test-dk" - - t.Run("no dynakube instance", func(t *testing.T) { - gc := reconcilermock.NewReconciler(t) - provisioner := &OneAgentProvisioner{ - apiReader: fake.NewClient(), - db: metadata.FakeMemoryDB(), - gc: gc, - } - result, err := provisioner.Reconcile(context.TODO(), reconcile.Request{}) + t.Run("no dynakube(ie.: delete case) => do nothing, no error", func(t *testing.T) { // TODO: Replace "do nothing" with "run GC" + prov := createProvisioner(t) + dk := createDynaKubeBase(t) + + result, err := prov.Reconcile(ctx, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(dk)}) require.NoError(t, err) require.NotNil(t, result) - require.Equal(t, reconcile.Result{}, result) + + assert.False(t, areFsDirsCreated(t, prov, dk)) }) - t.Run("dynakube deleted", func(t *testing.T) { - gc := reconcilermock.NewReconciler(t) - db := metadata.FakeMemoryDB() - metadataDk := metadata.Dynakube{TenantUUID: tenantUUID, LatestVersion: agentVersion, Name: dkName} - _ = db.InsertDynakube(ctx, &metadataDk) - provisioner := &OneAgentProvisioner{ - apiReader: fake.NewClient(), - db: db, - gc: gc, - } - result, err := provisioner.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: metadataDk.Name}}) + t.Run("dynakube doesn't need app-injection => no error, long requeue", func(t *testing.T) { + dk := createDynaKubeNoCSI(t) + prov := createProvisioner(t, dk) + + result, err := prov.Reconcile(ctx, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(dk)}) require.NoError(t, err) require.NotNil(t, result) - require.Equal(t, reconcile.Result{}, result) + assert.Equal(t, longRequeueDuration, result.RequeueAfter) - ten, err := db.GetDynakube(ctx, metadataDk.TenantUUID) - require.NoError(t, err) - require.Nil(t, ten) + assert.False(t, areFsDirsCreated(t, prov, dk)) }) - t.Run("csi driver not used (classicFullstack)", func(t *testing.T) { - gc := reconcilermock.NewReconciler(t) - provisioner := &OneAgentProvisioner{ - apiReader: fake.NewClient( - &dynakube.DynaKube{ - ObjectMeta: metav1.ObjectMeta{ - Name: dynakubeName, - }, - Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - ClassicFullStack: &dynakube.HostInjectSpec{}, - }, - }, - }, - ), - db: metadata.FakeMemoryDB(), - gc: gc, - } - result, err := provisioner.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: dynakubeName}}) + t.Run("dynakube status not ready => only setup base fs, no error, short requeue", func(t *testing.T) { + dk := createNotReadyDynaKube(t) + prov := createProvisioner(t, dk) + + result, err := prov.Reconcile(ctx, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(dk)}) require.NoError(t, err) require.NotNil(t, result) - require.Equal(t, reconcile.Result{RequeueAfter: longRequeueDuration}, result) + assert.Equal(t, shortRequeueDuration, result.RequeueAfter) + + assert.True(t, areFsDirsCreated(t, prov, dk)) }) - t.Run("host monitoring used", func(t *testing.T) { - fakeClient := fake.NewClient( - addFakeTenantUUID( - &dynakube.DynaKube{ - ObjectMeta: metav1.ObjectMeta{ - Name: dkName, - }, - Spec: dynakube.DynaKubeSpec{ - APIURL: testAPIURL, - OneAgent: dynakube.OneAgentSpec{ - HostMonitoring: &dynakube.HostInjectSpec{}, - }, - }, - }, - ), - &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: dkName, - }, - Data: map[string][]byte{ - dtclient.ApiToken: []byte("api-token"), - }, - }, - ) - mockDtcBuilder := dtbuildermock.NewBuilder(t) - - gc := reconcilermock.NewReconciler(t) - db := metadata.FakeMemoryDB() - - provisioner := &OneAgentProvisioner{ - apiReader: fakeClient, - client: fakeClient, - fs: afero.NewMemMapFs(), - db: db, - gc: gc, - path: metadata.PathResolver{}, - dynatraceClientBuilder: mockDtcBuilder, - } - result, err := provisioner.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: dkName}}) + t.Run("dynakube with version => url installer used, no error", func(t *testing.T) { + dk := createDynaKubeWithVersion(t) + prov := createProvisioner(t, dk, createToken(t, dk), createPMCSecret(t, dk)) + installer := createSuccessfulInstaller(t) + prov.urlInstallerBuilder = mockUrlInstallerBuilder(t, installer) + prov.dynatraceClientBuilder = mockSuccessfulDtClientBuilder(t) + createPMCSourceFile(t, prov, dk) + + result, err := prov.Reconcile(ctx, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(dk)}) require.NoError(t, err) require.NotNil(t, result) - require.Equal(t, reconcile.Result{RequeueAfter: longRequeueDuration}, result) + assert.Equal(t, defaultRequeueDuration, result.RequeueAfter) - dynakubeMetadatas, err := db.GetAllDynakubes(ctx) - require.NoError(t, err) - require.Len(t, dynakubeMetadatas, 1) + assert.True(t, areFsDirsCreated(t, prov, dk)) + installer.AssertCalled(t, "InstallAgent", mock.Anything, mock.Anything) }) - t.Run("no tokens", func(t *testing.T) { - gc := reconcilermock.NewReconciler(t) - provisioner := &OneAgentProvisioner{ - apiReader: fake.NewClient( - addFakeTenantUUID( - &dynakube.DynaKube{ - ObjectMeta: metav1.ObjectMeta{ - Name: dkName, - }, - Spec: dynakube.DynaKubeSpec{ - APIURL: testAPIURL, - OneAgent: dynakube.OneAgentSpec{ - ApplicationMonitoring: buildValidApplicationMonitoringSpec(t), - }, - }, - Status: dynakube.DynaKubeStatus{ - CodeModules: dynakube.CodeModulesStatus{ - VersionStatus: status.VersionStatus{ - Version: "1.2.3", - }, - }, - }, - }, - ), - ), - gc: gc, - db: metadata.FakeMemoryDB(), - fs: afero.NewMemMapFs(), - } - result, err := provisioner.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: dkName}}) - require.EqualError(t, err, `secrets "`+dkName+`" not found`) - require.NotNil(t, result) - }) - t.Run("error when creating dynatrace client", func(t *testing.T) { - gc := reconcilermock.NewReconciler(t) - mockDtcBuilder := dtbuildermock.NewBuilder(t) - mockDtcBuilder.On("SetContext", mock.Anything).Return(mockDtcBuilder) - mockDtcBuilder.On("SetDynakube", mock.Anything).Return(mockDtcBuilder) - mockDtcBuilder.On("SetTokens", mock.Anything).Return(mockDtcBuilder) - mockDtcBuilder.On("Build").Return(nil, errors.New(errorMsg)) - - provisioner := &OneAgentProvisioner{ - apiReader: fake.NewClient( - addFakeTenantUUID( - &dynakube.DynaKube{ - ObjectMeta: metav1.ObjectMeta{ - Name: dkName, - }, - Spec: dynakube.DynaKubeSpec{ - APIURL: testAPIURL, - OneAgent: dynakube.OneAgentSpec{ - ApplicationMonitoring: buildValidApplicationMonitoringSpec(t), - }, - }, - Status: dynakube.DynaKubeStatus{ - CodeModules: dynakube.CodeModulesStatus{ - VersionStatus: status.VersionStatus{ - Version: "1.2.3", - }, - }, - }, - }, - ), - &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: dkName, - }, - Data: map[string][]byte{ - dtclient.ApiToken: []byte("test-value"), - }, - }, - ), - dynatraceClientBuilder: mockDtcBuilder, - gc: gc, - db: metadata.FakeMemoryDB(), - fs: afero.NewMemMapFs(), - } - result, err := provisioner.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: dkName}}) + t.Run("dynakube with version, issue with dtc => fail before installer creation", func(t *testing.T) { + dk := createDynaKubeWithVersion(t) + prov := createProvisioner(t, dk, createToken(t, dk)) + prov.dynatraceClientBuilder = mockFailingDtClientBuilder(t) - require.EqualError(t, err, "failed to create Dynatrace client: "+errorMsg) + result, err := prov.Reconcile(ctx, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(dk)}) + require.Error(t, err) require.NotNil(t, result) + + assert.True(t, areFsDirsCreated(t, prov, dk)) }) - t.Run("error creating directories", func(t *testing.T) { - gc := reconcilermock.NewReconciler(t) - errorfs := &mkDirAllErrorFs{ - Fs: afero.NewMemMapFs(), - } - mockDtcBuilder := dtbuildermock.NewBuilder(t) - provisioner := &OneAgentProvisioner{ - apiReader: fake.NewClient( - addFakeTenantUUID( - &dynakube.DynaKube{ - ObjectMeta: metav1.ObjectMeta{ - Name: dkName, - }, - Spec: dynakube.DynaKubeSpec{ - APIURL: testAPIURL, - OneAgent: dynakube.OneAgentSpec{ - ApplicationMonitoring: buildValidApplicationMonitoringSpec(t), - }, - }, - }, - ), - &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: dkName, - }, - Data: map[string][]byte{ - dtclient.ApiToken: []byte("api-token"), - }, - }, - ), - dynatraceClientBuilder: mockDtcBuilder, - fs: errorfs, - db: metadata.FakeMemoryDB(), - gc: gc, - } - result, err := provisioner.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: dkName}}) - require.EqualError(t, err, "failed to create directory "+tenantUUID+": "+errorMsg) + t.Run("dynakube with image => image installer used, dtclient not created, no error", func(t *testing.T) { + dk := createDynaKubeWithImage(t) + prov := createProvisioner(t, dk, createPMCSecret(t, dk)) + installer := createSuccessfulInstaller(t) + prov.imageInstallerBuilder = mockImageInstallerBuilder(t, installer) + createPMCSourceFile(t, prov, dk) + + result, err := prov.Reconcile(ctx, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(dk)}) + require.NoError(t, err) require.NotNil(t, result) - require.Equal(t, reconcile.Result{}, result) + assert.Equal(t, defaultRequeueDuration, result.RequeueAfter) - // Logging newline so go test can parse the output correctly - log.Info("") + assert.True(t, areFsDirsCreated(t, prov, dk)) + installer.AssertCalled(t, "InstallAgent", mock.Anything, mock.Anything) }) - t.Run("error getting latest agent version", func(t *testing.T) { - gc := reconcilermock.NewReconciler(t) - memFs := afero.NewMemMapFs() - mockDtcBuilder := dtbuildermock.NewBuilder(t) - dynakube := addFakeTenantUUID( - &dynakube.DynaKube{ - ObjectMeta: metav1.ObjectMeta{ - Name: dkName, - }, - Spec: dynakube.DynaKubeSpec{ - APIURL: testAPIURL, - OneAgent: dynakube.OneAgentSpec{ - ApplicationMonitoring: buildValidApplicationMonitoringSpec(t), - }, - }, - }, - ) - installerMock := installermock.NewInstaller(t) - - provisioner := &OneAgentProvisioner{ - apiReader: fake.NewClient( - dynakube, - &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: dkName, - }, - Data: map[string][]byte{ - dtclient.ApiToken: []byte("api-token"), - }, - }, - &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: dynakube.OneagentTenantSecret(), - }, - Data: map[string][]byte{ - connectioninfo.TenantTokenKey: []byte("tenant-token"), - }, - }, - ), - dynatraceClientBuilder: mockDtcBuilder, - fs: memFs, - db: metadata.FakeMemoryDB(), - recorder: &record.FakeRecorder{}, - gc: gc, - urlInstallerBuilder: mockUrlInstallerBuilder(installerMock), - } - - result, err := provisioner.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: dkName}}) - // "go test" breaks if the output does not end with a newline - // making sure one is printed here - log.Info("") + t.Run("dynakube with job => job installer used, dtclient not created, no error", func(t *testing.T) { + dk := createDynaKubeWithJobFF(t) + prov := createProvisioner(t, dk, createPMCSecret(t, dk)) + installer := createSuccessfulInstaller(t) + prov.jobInstallerBuilder = mockJobInstallerBuilder(t, installer) + createPMCSourceFile(t, prov, dk) + result, err := prov.Reconcile(ctx, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(dk)}) require.NoError(t, err) - require.NotEmpty(t, result) - - exists, err := afero.Exists(memFs, tenantUUID) + require.NotNil(t, result) + assert.Equal(t, defaultRequeueDuration, result.RequeueAfter) - require.NoError(t, err) - require.True(t, exists) + assert.True(t, areFsDirsCreated(t, prov, dk)) + installer.AssertCalled(t, "InstallAgent", mock.Anything, mock.Anything) }) - t.Run("error getting dynakube from db", func(t *testing.T) { - gc := reconcilermock.NewReconciler(t) - memFs := afero.NewMemMapFs() - mockDtcBuilder := dtbuildermock.NewBuilder(t) - - provisioner := &OneAgentProvisioner{ - apiReader: fake.NewClient( - &dynakube.DynaKube{ - ObjectMeta: metav1.ObjectMeta{ - Name: dkName, - }, - Spec: dynakube.DynaKubeSpec{ - APIURL: testAPIURL, - OneAgent: dynakube.OneAgentSpec{ - ApplicationMonitoring: buildValidApplicationMonitoringSpec(t), - }, - }, - Status: dynakube.DynaKubeStatus{ - CodeModules: dynakube.CodeModulesStatus{ - VersionStatus: status.VersionStatus{ - Version: "1.2.3", - }, - }, - }, - }, - &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: dkName, - }, - }, - ), - dynatraceClientBuilder: mockDtcBuilder, - fs: memFs, - db: &metadata.FakeFailDB{}, - gc: gc, - } - result, err := provisioner.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: dkName}}) + t.Run("dynakube with job => job installer used, back-off when not ready, no error", func(t *testing.T) { + dk := createDynaKubeWithJobFF(t) + prov := createProvisioner(t, dk, createPMCSecret(t, dk)) + installer := createNotReadyInstaller(t) + prov.jobInstallerBuilder = mockJobInstallerBuilder(t, installer) + createPMCSourceFile(t, prov, dk) - require.Error(t, err) - require.Empty(t, result) + result, err := prov.Reconcile(ctx, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(dk)}) + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, notReadyRequeueDuration, result.RequeueAfter) + + assert.True(t, areFsDirsCreated(t, prov, dk)) + installer.AssertCalled(t, "InstallAgent", mock.Anything, mock.Anything) }) - t.Run("correct directories are created", func(t *testing.T) { - gc := reconcilermock.NewReconciler(t) - memFs := afero.NewMemMapFs() - memDB := metadata.FakeMemoryDB() - dynakube := addFakeTenantUUID( - &dynakube.DynaKube{ - ObjectMeta: metav1.ObjectMeta{ - Name: dkName, - }, - Spec: dynakube.DynaKubeSpec{ - APIURL: testAPIURL, - OneAgent: dynakube.OneAgentSpec{ - HostMonitoring: &dynakube.HostInjectSpec{}, - }, - }, - }, - ) - - r := &OneAgentProvisioner{ - apiReader: fake.NewClient(dynakube), - fs: memFs, - db: memDB, - gc: gc, - } - result, err := r.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: dkName}}) + t.Run("dynakube with job => job installer used, with error", func(t *testing.T) { + dk := createDynaKubeWithJobFF(t) + prov := createProvisioner(t, dk, createPMCSecret(t, dk)) + installer := createFailingInstaller(t) + prov.jobInstallerBuilder = mockJobInstallerBuilder(t, installer) + createPMCSourceFile(t, prov, dk) - require.NoError(t, err) + result, err := prov.Reconcile(ctx, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(dk)}) + require.Error(t, err) require.NotNil(t, result) - exists, err := afero.Exists(memFs, tenantUUID) + assert.True(t, areFsDirsCreated(t, prov, dk)) + installer.AssertCalled(t, "InstallAgent", mock.Anything, mock.Anything) + }) - require.NoError(t, err) - require.True(t, exists) + t.Run("installer fails => error", func(t *testing.T) { + dk := createDynaKubeWithImage(t) + prov := createProvisioner(t, dk) + installer := createFailingInstaller(t) + prov.imageInstallerBuilder = mockImageInstallerBuilder(t, installer) - fileInfo, err := memFs.Stat(tenantUUID) + result, err := prov.Reconcile(ctx, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(dk)}) + require.Error(t, err) + require.NotNil(t, result) - require.NoError(t, err) - require.True(t, fileInfo.IsDir()) + assert.True(t, areFsDirsCreated(t, prov, dk)) + installer.AssertCalled(t, "InstallAgent", mock.Anything, mock.Anything) }) } -func buildValidApplicationMonitoringSpec(_ *testing.T) *dynakube.ApplicationMonitoringSpec { - return &dynakube.ApplicationMonitoringSpec{} -} +func areFsDirsCreated(t *testing.T, prov OneAgentProvisioner, dk *dynakube.DynaKube) bool { + t.Helper() -func TestProvisioner_CreateDynakube(t *testing.T) { - ctx := context.Background() - db := metadata.FakeMemoryDB() - expectedOtherDynakube := metadata.NewDynakube(otherDkName, tenantUUID, "v1", "", 0) - _ = db.InsertDynakube(ctx, expectedOtherDynakube) - provisioner := &OneAgentProvisioner{ - db: db, + neededFolders := []string{ + prov.path.DynaKubeDir(dk.GetName()), + prov.path.AgentSharedBinaryDirBase(), + } + for _, folder := range neededFolders { + stat, err := prov.fs.Stat(folder) + if err != nil || stat == nil || !stat.IsDir() { + return false + } } - oldDynakube := metadata.Dynakube{} - newDynakube := metadata.NewDynakube(dkName, tenantUUID, "v1", "", 0) - - err := provisioner.createOrUpdateDynakubeMetadata(ctx, oldDynakube, newDynakube) - require.NoError(t, err) + return true +} - dynakube, err := db.GetDynakube(ctx, dkName) - require.NoError(t, err) - require.NotNil(t, dynakube) - require.Equal(t, *newDynakube, *dynakube) +func createProvisioner(t *testing.T, objs ...client.Object) OneAgentProvisioner { + t.Helper() - otherDynakube, err := db.GetDynakube(ctx, otherDkName) - require.NoError(t, err) - require.NotNil(t, dynakube) - require.Equal(t, *expectedOtherDynakube, *otherDynakube) -} + fs := afero.NewMemMapFs() + path := metadata.PathResolver{} + apiReader := fake.NewClient(objs...) -func TestProvisioner_UpdateDynakube(t *testing.T) { - ctx := context.Background() - db := metadata.FakeMemoryDB() - oldDynakube := metadata.NewDynakube(dkName, tenantUUID, "v1", "", 0) - _ = db.InsertDynakube(ctx, oldDynakube) - expectedOtherDynakube := metadata.NewDynakube(otherDkName, tenantUUID, "v1", "", 0) - _ = db.InsertDynakube(ctx, expectedOtherDynakube) - - provisioner := &OneAgentProvisioner{ - db: db, + return OneAgentProvisioner{ + fs: fs, + path: path, + apiReader: apiReader, + cleaner: cleanup.New(afero.Afero{Fs: fs}, apiReader, path, mount.NewFakeMounter(nil)), } - newDynakube := metadata.NewDynakube(dkName, "new-uuid", "v2", "", 0) +} - err := provisioner.createOrUpdateDynakubeMetadata(ctx, *oldDynakube, newDynakube) - require.NoError(t, err) +func createDynaKubeWithVersion(t *testing.T) *dynakube.DynaKube { + t.Helper() - dynakube, err := db.GetDynakube(ctx, dkName) - require.NoError(t, err) - require.NotNil(t, dynakube) - require.Equal(t, *newDynakube, *dynakube) + dk := createDynaKubeBase(t) + version := "test-version" + dk.Spec.OneAgent = oneagent.Spec{ + ApplicationMonitoring: &oneagent.ApplicationMonitoringSpec{ + Version: version, + }, + } + dk.Status.CodeModules.Version = version - otherDynakube, err := db.GetDynakube(ctx, otherDkName) - require.NoError(t, err) - require.NotNil(t, dynakube) - require.Equal(t, *expectedOtherDynakube, *otherDynakube) + return dk } -func TestUpdateAgentInstallation(t *testing.T) { - ctx := context.Background() +func createDynaKubeWithImage(t *testing.T) *dynakube.DynaKube { + t.Helper() - t.Run("updateAgentInstallation with codeModules enabled", func(t *testing.T) { - dynakube := getDynakube() - enableCodeModules(dynakube) + dk := createDynaKubeBase(t) + imageId := "test-image" + dk.Spec.OneAgent = oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{ + AppInjectionSpec: oneagent.AppInjectionSpec{CodeModulesImage: imageId}, + }, + } + dk.Status.CodeModules.ImageID = imageId - mockDtcBuilder := dtbuildermock.NewBuilder(t) + return dk +} - var dtc dtclient.Client +func createDynaKubeWithJobFF(t *testing.T) *dynakube.DynaKube { + t.Helper() - mockDtcBuilder.On("Build").Return(dtc, nil) - dtc, err := mockDtcBuilder.Build() - require.NoError(t, err) + dk := createDynaKubeBase(t) + imageId := "test-image" + dk.Spec.OneAgent = oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{ + AppInjectionSpec: oneagent.AppInjectionSpec{CodeModulesImage: imageId}, + }, + } + dk.Status.CodeModules.ImageID = imageId + dk.Annotations = map[string]string{ + dynakube.AnnotationFeatureNodeImagePull: "true", + } - path := metadata.PathResolver{RootDir: "test"} - base64Image := base64.StdEncoding.EncodeToString([]byte(dynakube.CodeModulesImage())) - targetDir := path.AgentSharedBinaryDirForAgent(base64Image) - - mockK8sClient := createMockK8sClient(ctx, dynakube) - installerMock := installermock.NewInstaller(t) - installerMock. - On("InstallAgent", mock.AnythingOfType("context.backgroundCtx"), targetDir). - Return(true, nil) - - provisioner := &OneAgentProvisioner{ - db: metadata.FakeMemoryDB(), - dynatraceClientBuilder: mockDtcBuilder, - apiReader: mockK8sClient, - client: mockK8sClient, - path: path, - fs: afero.NewMemMapFs(), - imageInstallerBuilder: mockImageInstallerBuilder(installerMock), - recorder: &record.FakeRecorder{}, - } + return dk +} - ruxitAgentProcPath := filepath.Join(targetDir, "agent", "conf", "ruxitagentproc.conf") - sourceRuxitAgentProcPath := filepath.Join(targetDir, "agent", "conf", "_ruxitagentproc.conf") +func createNotReadyDynaKube(t *testing.T) *dynakube.DynaKube { + t.Helper() - setUpFS(provisioner.fs, ruxitAgentProcPath, sourceRuxitAgentProcPath) + dk := createDynaKubeBase(t) + dk.Spec.OneAgent = oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{}, + } - dynakubeMetadata := metadata.Dynakube{TenantUUID: tenantUUID, LatestVersion: agentVersion, Name: dkName} - isRequeue, err := provisioner.updateAgentInstallation(ctx, dtc, &dynakubeMetadata, dynakube) - require.NoError(t, err) + return dk +} - require.Equal(t, "", dynakubeMetadata.LatestVersion) - require.Equal(t, base64Image, dynakubeMetadata.ImageDigest) - assert.False(t, isRequeue) - }) - t.Run("updateAgentInstallation with codeModules enabled errors and requeues", func(t *testing.T) { - dynakube := getDynakube() - enableCodeModules(dynakube) +func createDynaKubeNoCSI(t *testing.T) *dynakube.DynaKube { + t.Helper() - mockDtcBuilder := dtbuildermock.NewBuilder(t) + dk := createDynaKubeBase(t) + dk.Spec.OneAgent = oneagent.Spec{ + ClassicFullStack: &oneagent.HostInjectSpec{}, + } - var dtc dtclient.Client + return dk +} - mockDtcBuilder.On("Build").Return(dtc, nil) - dtc, err := mockDtcBuilder.Build() - require.NoError(t, err) +func createDynaKubeBase(t *testing.T) *dynakube.DynaKube { + t.Helper() - path := metadata.PathResolver{RootDir: "test"} - base64Image := base64.StdEncoding.EncodeToString([]byte(dynakube.CodeModulesImage())) - targetDir := path.AgentSharedBinaryDirForAgent(base64Image) - - mockK8sClient := createMockK8sClient(ctx, dynakube) - installerMock := installermock.NewInstaller(t) - installerMock. - On("InstallAgent", mock.AnythingOfType("context.backgroundCtx"), targetDir). - Return(true, nil) - - provisioner := &OneAgentProvisioner{ - db: metadata.FakeMemoryDB(), - dynatraceClientBuilder: mockDtcBuilder, - apiReader: mockK8sClient, - client: mockK8sClient, - path: metadata.PathResolver{RootDir: "test"}, - fs: afero.NewMemMapFs(), - imageInstallerBuilder: mockImageInstallerBuilder(installerMock), - recorder: &record.FakeRecorder{}, - } + return &dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-dk", + Namespace: "test-ns", + }, + } +} - dynakubeMetadata := metadata.Dynakube{TenantUUID: tenantUUID, LatestVersion: agentVersion, Name: dkName} - isRequeue, err := provisioner.updateAgentInstallation(ctx, dtc, &dynakubeMetadata, dynakube) - require.NoError(t, err) +func createSuccessfulInstaller(t *testing.T) *installermock.Installer { + t.Helper() - require.Equal(t, "12345", dynakubeMetadata.LatestVersion) - require.Equal(t, "", dynakubeMetadata.ImageDigest) - assert.True(t, isRequeue) - }) - t.Run("updateAgentInstallation without codeModules", func(t *testing.T) { - dynakube := getDynakube() + m := installermock.NewInstaller(t) + m.On("InstallAgent", mock.Anything, mock.Anything).Return(true, nil) - mockDtcBuilder := dtbuildermock.NewBuilder(t) + return m +} - var dtc dtclient.Client +func createNotReadyInstaller(t *testing.T) *installermock.Installer { + t.Helper() - mockDtcBuilder.On("Build").Return(dtc, nil) - dtc, err := mockDtcBuilder.Build() - require.NoError(t, err) + m := installermock.NewInstaller(t) + m.On("InstallAgent", mock.Anything, mock.Anything).Return(false, nil) - mockK8sClient := createMockK8sClient(ctx, dynakube) - installerMock := installermock.NewInstaller(t) - installerMock. - On("InstallAgent", mock.AnythingOfType("context.backgroundCtx"), "test/codemodules"). - Return(true, nil) - - provisioner := &OneAgentProvisioner{ - db: metadata.FakeMemoryDB(), - dynatraceClientBuilder: mockDtcBuilder, - apiReader: mockK8sClient, - client: mockK8sClient, - path: metadata.PathResolver{RootDir: "test"}, - fs: afero.NewMemMapFs(), - recorder: &record.FakeRecorder{}, - urlInstallerBuilder: mockUrlInstallerBuilder(installerMock), - } - ruxitAgentProcPath := filepath.Join("test", "codemodules", "agent", "conf", "ruxitagentproc.conf") - sourceRuxitAgentProcPath := filepath.Join("test", "codemodules", "agent", "conf", "_ruxitagentproc.conf") + return m +} - setUpFS(provisioner.fs, ruxitAgentProcPath, sourceRuxitAgentProcPath) +func createFailingInstaller(t *testing.T) *installermock.Installer { + t.Helper() - dynakubeMetadata := metadata.Dynakube{TenantUUID: tenantUUID, LatestVersion: agentVersion, Name: dkName} - isRequeue, err := provisioner.updateAgentInstallation(ctx, dtc, &dynakubeMetadata, dynakube) - require.NoError(t, err) + m := installermock.NewInstaller(t) + m.On("InstallAgent", mock.Anything, mock.Anything).Return(false, errors.New("BOOM")) - require.Equal(t, "12345", dynakubeMetadata.LatestVersion) - require.Equal(t, "", dynakubeMetadata.ImageDigest) - assert.False(t, isRequeue) - }) - t.Run("updateAgentInstallation without codeModules errors and requeues", func(t *testing.T) { - dynakube := getDynakube() + return m +} - mockDtcBuilder := dtbuildermock.NewBuilder(t) +func mockUrlInstallerBuilder(t *testing.T, mockedInstaller *installermock.Installer) urlInstallerBuilder { + t.Helper() - var dtc dtclient.Client + return func(f afero.Fs, _ dtclient.Client, _ *url.Properties) installer.Installer { + return mockedInstaller + } +} - mockDtcBuilder.On("Build").Return(dtc, nil) - dtc, err := mockDtcBuilder.Build() - require.NoError(t, err) +func mockImageInstallerBuilder(t *testing.T, mockedInstaller *installermock.Installer) imageInstallerBuilder { + t.Helper() - mockK8sClient := createMockK8sClient(ctx, dynakube) - installerMock := installermock.NewInstaller(t) - installerMock. - On("InstallAgent", mock.AnythingOfType("context.backgroundCtx"), "test/codemodules"). - Return(true, nil) - - provisioner := &OneAgentProvisioner{ - db: metadata.FakeMemoryDB(), - dynatraceClientBuilder: mockDtcBuilder, - apiReader: mockK8sClient, - client: mockK8sClient, - path: metadata.PathResolver{RootDir: "test"}, - fs: afero.NewMemMapFs(), - recorder: &record.FakeRecorder{}, - urlInstallerBuilder: mockUrlInstallerBuilder(installerMock), - } + return func(_ context.Context, _ afero.Fs, _ *image.Properties) (installer.Installer, error) { + return mockedInstaller, nil + } +} - dynakubeMetadata := metadata.Dynakube{TenantUUID: tenantUUID, LatestVersion: agentVersion, Name: dkName} - isRequeue, err := provisioner.updateAgentInstallation(ctx, dtc, &dynakubeMetadata, dynakube) - require.NoError(t, err) +func mockJobInstallerBuilder(t *testing.T, mockedInstaller *installermock.Installer) jobInstallerBuilder { + t.Helper() - require.Equal(t, "12345", dynakubeMetadata.LatestVersion) - require.Equal(t, "", dynakubeMetadata.ImageDigest) - assert.True(t, isRequeue) - }) + return func(_ context.Context, _ afero.Fs, _ *job.Properties) installer.Installer { + return mockedInstaller + } } -func createMockK8sClient(ctx context.Context, dynakube *dynakube.DynaKube) client.Client { - mockK8sClient := fake.NewClient(dynakube) - mockK8sClient.Create(ctx, - &corev1.Secret{ - Data: map[string][]byte{processmoduleconfigsecret.SecretKeyProcessModuleConfig: []byte(`{"revision":0,"properties":[]}`)}, - ObjectMeta: metav1.ObjectMeta{ - Name: strings.Join([]string{dkName, "pmc-secret"}, "-"), - Namespace: "test-namespace", - }, - }, - ) +func createToken(t *testing.T, dk *dynakube.DynaKube) *corev1.Secret { + t.Helper() - return mockK8sClient + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: dk.Tokens(), + Namespace: dk.Namespace, + }, + Data: map[string][]byte{ + dtclient.ApiToken: []byte("this is a token"), + }, + } } -func getDynakube() *dynakube.DynaKube { - return addFakeTenantUUID(&dynakube.DynaKube{ +func createPMCSecret(t *testing.T, dk *dynakube.DynaKube) *corev1.Secret { + t.Helper() + + return &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: dkName, - Namespace: testNamespace, + Name: dk.GetName() + processmoduleconfigsecret.SecretSuffix, + Namespace: dk.Namespace, }, - Spec: dynakube.DynaKubeSpec{ - APIURL: testAPIURL, - OneAgent: dynakube.OneAgentSpec{}, + Data: map[string][]byte{ + processmoduleconfigsecret.SecretKeyProcessModuleConfig: getPMC(t), }, - }) + } +} + +func createPMCSourceFile(t *testing.T, prov OneAgentProvisioner, dk *dynakube.DynaKube) { + t.Helper() + + targetDir := prov.getTargetDir(*dk) + + pmcPath := filepath.Join(targetDir, processmoduleconfig.RuxitAgentProcPath) + pmcDir := filepath.Dir(pmcPath) + require.NoError(t, prov.fs.MkdirAll(pmcDir, os.ModePerm)) + + pmcFile, err := prov.fs.OpenFile(pmcPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.ModePerm) + require.NoError(t, err) + _, err = pmcFile.Write(getPMC(t)) + require.NoError(t, err) } -func enableCodeModules(dk *dynakube.DynaKube) { - dk.Status.CodeModules = dynakube.CodeModulesStatus{ - VersionStatus: status.VersionStatus{ - Version: testVersion, - ImageID: testImageID, +func getPMC(t *testing.T) []byte { + t.Helper() + + pmc := dtclient.ProcessModuleConfig{ + Revision: 0, + Properties: []dtclient.ProcessModuleProperty{ + {Section: "test-section", Key: "test-key", Value: "test-value"}, }, } + + pmcJson, err := json.Marshal(pmc) + require.NoError(t, err) + + return pmcJson } -func addFakeTenantUUID(dynakube *dynakube.DynaKube) *dynakube.DynaKube { - dynakube.Status.OneAgent.ConnectionInfoStatus.TenantUUID = tenantUUID +func mockFailingDtClientBuilder(t *testing.T) dynatraceclient.Builder { + t.Helper() - return dynakube + mockDtcBuilder := dtbuildermock.NewBuilder(t) + mockDtcBuilder.On("SetContext", mock.Anything).Return(mockDtcBuilder) + mockDtcBuilder.On("SetDynakube", mock.Anything).Return(mockDtcBuilder) + mockDtcBuilder.On("SetTokens", mock.Anything).Return(mockDtcBuilder) + mockDtcBuilder.On("Build", mock.Anything).Return(nil, errors.New("BOOM")) + + return mockDtcBuilder } -func setUpFS(fs afero.Fs, ruxitAgentProcPath string, sourceRuxitAgentProcPath string) { - _ = fs.MkdirAll(filepath.Base(sourceRuxitAgentProcPath), 0755) - _ = fs.MkdirAll(filepath.Base(ruxitAgentProcPath), 0755) - usedConf, _ := fs.OpenFile(ruxitAgentProcPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) - _, _ = usedConf.WriteString(testRuxitConf) +func mockSuccessfulDtClientBuilder(t *testing.T) dynatraceclient.Builder { + t.Helper() + + mockDtcBuilder := dtbuildermock.NewBuilder(t) + mockDtcBuilder.On("SetContext", mock.Anything).Return(mockDtcBuilder) + mockDtcBuilder.On("SetDynakube", mock.Anything).Return(mockDtcBuilder) + mockDtcBuilder.On("SetTokens", mock.Anything).Return(mockDtcBuilder) + mockDtcBuilder.On("Build", mock.Anything).Return(dtclientmock.NewClient(t), nil) + + return mockDtcBuilder } diff --git a/pkg/controllers/csi/provisioner/events.go b/pkg/controllers/csi/provisioner/events.go deleted file mode 100644 index 76637b9dc9..0000000000 --- a/pkg/controllers/csi/provisioner/events.go +++ /dev/null @@ -1,26 +0,0 @@ -package csiprovisioner - -import ( - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - corev1 "k8s.io/api/core/v1" - "k8s.io/client-go/tools/record" -) - -type updaterEventRecorder struct { - dk *dynakube.DynaKube - recorder record.EventRecorder -} - -func (event *updaterEventRecorder) sendFailedInstallAgentVersionEvent(version, tenantUUID string) { - event.recorder.Eventf(event.dk, - corev1.EventTypeWarning, - failedInstallAgentVersionEvent, - "Failed to install agent version: %s to tenant: %s", version, tenantUUID) -} - -func (event *updaterEventRecorder) sendInstalledAgentVersionEvent(version, tenantUUID string) { - event.recorder.Eventf(event.dk, - corev1.EventTypeNormal, - installAgentVersionEvent, - "Installed agent version: %s to tenant: %s", version, tenantUUID) -} diff --git a/pkg/controllers/csi/provisioner/install.go b/pkg/controllers/csi/provisioner/install.go index 18c4510ed6..79eeaaa2bf 100644 --- a/pkg/controllers/csi/provisioner/install.go +++ b/pkg/controllers/csi/provisioner/install.go @@ -3,117 +3,144 @@ package csiprovisioner import ( "context" "encoding/base64" + "errors" + "time" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/arch" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" - "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi/metadata" + "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/processmoduleconfigsecret" "github.com/Dynatrace/dynatrace-operator/pkg/injection/codemodule/installer" "github.com/Dynatrace/dynatrace-operator/pkg/injection/codemodule/installer/image" + "github.com/Dynatrace/dynatrace-operator/pkg/injection/codemodule/installer/job" + "github.com/Dynatrace/dynatrace-operator/pkg/injection/codemodule/installer/symlink" "github.com/Dynatrace/dynatrace-operator/pkg/injection/codemodule/installer/url" "github.com/Dynatrace/dynatrace-operator/pkg/injection/codemodule/processmoduleconfig" ) -func (provisioner *OneAgentProvisioner) installAgentImage( - ctx context.Context, - dk dynakube.DynaKube, - latestProcessModuleConfig *dtclient.ProcessModuleConfig, -) ( - string, - error, -) { - tenantUUID, err := dk.TenantUUIDFromApiUrl() - if err != nil { - return "", err - } +const notReadyRequeueDuration = 30 * time.Second - targetImage := dk.CodeModulesImage() - // An image URI often contains one or several /-s, which is problematic when trying to use it as a folder name. - // Easiest to just base64 encode it - base64Image := base64.StdEncoding.EncodeToString([]byte(targetImage)) - targetDir := provisioner.path.AgentSharedBinaryDirForAgent(base64Image) - targetConfigDir := provisioner.path.AgentConfigDir(tenantUUID, dk.GetName()) +var errNotReady = errors.New("download job is not ready yet") - props := &image.Properties{ - ImageUri: targetImage, - ApiReader: provisioner.apiReader, - Dynakube: &dk, - PathResolver: provisioner.path, - Metadata: provisioner.db, +func (provisioner *OneAgentProvisioner) installAgent(ctx context.Context, dk dynakube.DynaKube) error { + agentInstaller, err := provisioner.getInstaller(ctx, dk) + if err != nil { + log.Info("failed to create CodeModule installer", "dk", dk.GetName()) + + return err } - imageInstaller, err := provisioner.imageInstallerBuilder(ctx, provisioner.fs, props) + targetDir := provisioner.getTargetDir(dk) + + ready, err := agentInstaller.InstallAgent(ctx, targetDir) if err != nil { - return "", err + return err } - err = provisioner.installAgent(ctx, imageInstaller, dk, targetDir, targetImage, tenantUUID) - if err != nil { - return "", err + if !ready { + return errNotReady } - err = processmoduleconfig.CreateAgentConfigDir(provisioner.fs, targetConfigDir, targetDir, latestProcessModuleConfig) + err = provisioner.createLatestVersionSymlink(dk, targetDir) if err != nil { - return "", err + return err } - return base64Image, err + return provisioner.setupAgentConfigDir(ctx, dk, targetDir) } -func (provisioner *OneAgentProvisioner) installAgentZip(ctx context.Context, dk dynakube.DynaKube, dtc dtclient.Client, latestProcessModuleConfig *dtclient.ProcessModuleConfig) (string, error) { - tenantUUID, err := dk.TenantUUIDFromApiUrl() - if err != nil { - return "", err +func (provisioner *OneAgentProvisioner) getInstaller(ctx context.Context, dk dynakube.DynaKube) (installer.Installer, error) { + switch { + case dk.FeatureNodeImagePull(): + return provisioner.getJobInstaller(ctx, dk), nil + case dk.OneAgent().GetCustomCodeModulesImage() != "": + props := &image.Properties{ + ImageUri: dk.OneAgent().GetCodeModulesImage(), + ApiReader: provisioner.apiReader, + Dynakube: &dk, + PathResolver: provisioner.path, + } + + imageInstaller, err := provisioner.imageInstallerBuilder(ctx, provisioner.fs, props) + if err != nil { + return nil, err + } + + return imageInstaller, nil + default: + dtc, err := buildDtc(provisioner, ctx, dk) + if err != nil { + return nil, err + } + + props := &url.Properties{ + Os: dtclient.OsUnix, + Type: dtclient.InstallerTypePaaS, + Arch: arch.Arch, + Flavor: arch.Flavor, + Technologies: []string{"all"}, + TargetVersion: dk.OneAgent().GetCodeModulesVersion(), + SkipMetadata: true, + PathResolver: provisioner.path, + } + + urlInstaller := provisioner.urlInstallerBuilder(provisioner.fs, dtc, props) + + return urlInstaller, nil } +} - targetVersion := dk.CodeModulesVersion() - urlInstaller := provisioner.urlInstallerBuilder(provisioner.fs, dtc, getUrlProperties(targetVersion, provisioner.path)) - - targetDir := provisioner.path.AgentSharedBinaryDirForAgent(targetVersion) - targetConfigDir := provisioner.path.AgentConfigDir(tenantUUID, dk.GetName()) - - err = provisioner.installAgent(ctx, urlInstaller, dk, targetDir, targetVersion, tenantUUID) - if err != nil { - return "", err +func (provisioner *OneAgentProvisioner) getJobInstaller(ctx context.Context, dk dynakube.DynaKube) installer.Installer { + imageUri := dk.OneAgent().GetCustomCodeModulesImage() + if imageUri == "" { + imageUri = "public.ecr.aws/dynatrace/dynatrace-codemodules:" + dk.OneAgent().GetCodeModulesVersion() } - err = processmoduleconfig.CreateAgentConfigDir(provisioner.fs, targetConfigDir, targetDir, latestProcessModuleConfig) - if err != nil { - return "", err + props := &job.Properties{ + ImageUri: imageUri, + Owner: &dk, + PullSecrets: dk.PullSecretNames(), + ApiReader: provisioner.apiReader, + Client: provisioner.kubeClient, + PathResolver: provisioner.path, } - return targetVersion, nil + return provisioner.jobInstallerBuilder(ctx, provisioner.fs, props) } -func (provisioner *OneAgentProvisioner) installAgent(ctx context.Context, agentInstaller installer.Installer, dk dynakube.DynaKube, targetDir, targetVersion, tenantUUID string) error { //nolint:revive - eventRecorder := updaterEventRecorder{ - recorder: provisioner.recorder, - dk: &dk, +func (provisioner *OneAgentProvisioner) getTargetDir(dk dynakube.DynaKube) string { + var dirName string + + if dk.OneAgent().GetCustomCodeModulesImage() != "" { + // An image URI often contains one or several slashes, which is problematic when trying to use it as a folder name. + // Easiest to just base64 encode it + dirName = base64.StdEncoding.EncodeToString([]byte(dk.OneAgent().GetCodeModulesImage())) + } else { + dirName = dk.OneAgent().GetCodeModulesVersion() } - isNewlyInstalled, err := agentInstaller.InstallAgent(ctx, targetDir) - if err != nil { - eventRecorder.sendFailedInstallAgentVersionEvent(targetVersion, tenantUUID) + return provisioner.path.AgentSharedBinaryDirForAgent(dirName) +} +func (provisioner *OneAgentProvisioner) createLatestVersionSymlink(dk dynakube.DynaKube, targetDir string) error { + symlinkPath := provisioner.path.LatestAgentBinaryForDynaKube(dk.GetName()) + if err := symlink.Remove(provisioner.fs, symlinkPath); err != nil { return err } - if isNewlyInstalled { - eventRecorder.sendInstalledAgentVersionEvent(targetVersion, tenantUUID) + err := symlink.Create(provisioner.fs, targetDir, symlinkPath) + if err != nil { + return err } - return nil + return err } -func getUrlProperties(targetVersion string, pathResolver metadata.PathResolver) *url.Properties { - return &url.Properties{ - Os: dtclient.OsUnix, - Type: dtclient.InstallerTypePaaS, - Arch: arch.Arch, - Flavor: arch.Flavor, - Technologies: []string{"all"}, - TargetVersion: targetVersion, - SkipMetadata: true, - PathResolver: pathResolver, +func (provisioner *OneAgentProvisioner) setupAgentConfigDir(ctx context.Context, dk dynakube.DynaKube, targetDir string) error { + latestProcessModuleConfig, err := processmoduleconfigsecret.GetSecretData(ctx, provisioner.apiReader, dk.Name, dk.Namespace) + if err != nil { + return err } + + return processmoduleconfig.UpdateFromDir(provisioner.fs, provisioner.path.AgentConfigDir(dk.GetName()), targetDir, latestProcessModuleConfig) } diff --git a/pkg/controllers/csi/provisioner/install_test.go b/pkg/controllers/csi/provisioner/install_test.go index d7255f2956..0c46b4a71e 100644 --- a/pkg/controllers/csi/provisioner/install_test.go +++ b/pkg/controllers/csi/provisioner/install_test.go @@ -1,353 +1,27 @@ package csiprovisioner import ( - "context" "encoding/base64" - "fmt" - "path/filepath" "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" - "github.com/Dynatrace/dynatrace-operator/pkg/api/status" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" - "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi/metadata" - "github.com/Dynatrace/dynatrace-operator/pkg/injection/codemodule/installer" - "github.com/Dynatrace/dynatrace-operator/pkg/injection/codemodule/installer/image" - "github.com/Dynatrace/dynatrace-operator/pkg/injection/codemodule/installer/url" - "github.com/Dynatrace/dynatrace-operator/pkg/injection/codemodule/processmoduleconfig" - t_utils "github.com/Dynatrace/dynatrace-operator/pkg/util/testing" - dtclientmock "github.com/Dynatrace/dynatrace-operator/test/mocks/pkg/clients/dynatrace" - installermock "github.com/Dynatrace/dynatrace-operator/test/mocks/pkg/injection/codemodule/installer" - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/tools/record" - "sigs.k8s.io/controller-runtime/pkg/client" ) -const ( - testTenantUUID = "zib123" -) - -func TestUpdateAgent(t *testing.T) { - ctx := context.Background() - testVersion := "test" - testImage := "my-image/1223:123" - - t.Run("zip install", func(t *testing.T) { - dk := createTestDynaKubeWithZip(testVersion) - provisioner := createTestProvisioner() - targetDir := provisioner.path.AgentSharedBinaryDirForAgent(dk.CodeModulesVersion()) - - var revision uint = 3 - processModule := createTestProcessModuleConfig(revision) - installerMock := installermock.NewInstaller(t) - installerMock. - On("InstallAgent", mock.AnythingOfType("context.backgroundCtx"), targetDir). - Return(true, nil).Run(mockFsAfterInstall(provisioner, testVersion)) - - provisioner.urlInstallerBuilder = mockUrlInstallerBuilder(installerMock) - - currentVersion, err := provisioner.installAgentZip(ctx, dk, dtclientmock.NewClient(t), processModule) - require.NoError(t, err) - assert.Equal(t, testVersion, currentVersion) - t_utils.AssertEvents(t, - provisioner.recorder.(*record.FakeRecorder).Events, - t_utils.Events{ - t_utils.Event{ - EventType: corev1.EventTypeNormal, - Reason: installAgentVersionEvent, - }, - }, - ) - }) - t.Run("zip update", func(t *testing.T) { - dk := createTestDynaKubeWithZip(testVersion) - provisioner := createTestProvisioner() - previousTargetDir := provisioner.path.AgentSharedBinaryDirForAgent(dk.CodeModulesVersion()) - previousSourceConfigPath := filepath.Join(previousTargetDir, processmoduleconfig.RuxitAgentProcPath) - _ = provisioner.fs.MkdirAll(previousTargetDir, 0755) - _, _ = provisioner.fs.Create(previousSourceConfigPath) - - newVersion := "new" - dk.Status.CodeModules.Version = newVersion - newTargetDir := provisioner.path.AgentSharedBinaryDirForAgent(dk.CodeModulesVersion()) - - var revision uint = 3 - processModule := createTestProcessModuleConfig(revision) - installerMock := installermock.NewInstaller(t) - installerMock. - On("InstallAgent", mock.AnythingOfType("context.backgroundCtx"), newTargetDir). - Return(true, nil).Run(mockFsAfterInstall(provisioner, newVersion)) - - provisioner.urlInstallerBuilder = mockUrlInstallerBuilder(installerMock) - - currentVersion, err := provisioner.installAgentZip(ctx, dk, dtclientmock.NewClient(t), processModule) - require.NoError(t, err) - assert.Equal(t, newVersion, currentVersion) - }) - t.Run("only process module config update", func(t *testing.T) { - dk := createTestDynaKubeWithZip(testVersion) - provisioner := createTestProvisioner() - targetDir := provisioner.path.AgentSharedBinaryDirForAgent(dk.CodeModulesVersion()) - sourceConfigPath := filepath.Join(targetDir, processmoduleconfig.RuxitAgentProcPath) - _ = provisioner.fs.MkdirAll(targetDir, 0755) - _, _ = provisioner.fs.Create(sourceConfigPath) - - var revision uint = 3 - processModule := createTestProcessModuleConfig(revision) - installerMock := installermock.NewInstaller(t) - installerMock. - On("InstallAgent", mock.AnythingOfType("context.backgroundCtx"), targetDir). - Return(false, nil) - - provisioner.urlInstallerBuilder = mockUrlInstallerBuilder(installerMock) - currentVersion, err := provisioner.installAgentZip(ctx, dk, dtclientmock.NewClient(t), processModule) +func TestGetTargetDir(t *testing.T) { + t.Run("version set => folder is the version", func(t *testing.T) { + prov := createProvisioner(t) + dk := createDynaKubeWithVersion(t) - require.NoError(t, err) - assert.Equal(t, testVersion, currentVersion) + targetDir := prov.getTargetDir(*dk) + require.Contains(t, targetDir, dk.OneAgent().GetCodeModulesVersion()) }) - t.Run("failed install", func(t *testing.T) { - dockerconfigjsonContent := `{"auths":{}}` - dk := createTestDynaKubeWithImage(testImage) - provisioner := createTestProvisioner(createMockedPullSecret(dk, dockerconfigjsonContent)) - var revision uint = 3 - processModule := createTestProcessModuleConfig(revision) - targetDir := provisioner.path.AgentSharedBinaryDirForAgent(base64.StdEncoding.EncodeToString([]byte(testImage))) - installerMock := installermock.NewInstaller(t) - installerMock. - On("InstallAgent", mock.AnythingOfType("context.backgroundCtx"), targetDir). - Return(false, fmt.Errorf("BOOM")) + t.Run("image set => folder is the base64 of the imageURI", func(t *testing.T) { + prov := createProvisioner(t) + dk := createDynaKubeWithImage(t) - provisioner.imageInstallerBuilder = mockImageInstallerBuilder(installerMock) - - currentVersion, err := provisioner.installAgentImage(ctx, dk, processModule) - - require.Error(t, err) - assert.Equal(t, "", currentVersion) - t_utils.AssertEvents(t, - provisioner.recorder.(*record.FakeRecorder).Events, - t_utils.Events{ - t_utils.Event{ - EventType: corev1.EventTypeWarning, - Reason: failedInstallAgentVersionEvent, - }, - }, - ) - }) - t.Run("codeModulesImage set without custom pull secret", func(t *testing.T) { - dockerconfigjsonContent := `{"auths":{}}` - - var revision uint = 3 - processModule := createTestProcessModuleConfig(revision) - - dk := createTestDynaKubeWithImage(testImage) - provisioner := createTestProvisioner(createMockedPullSecret(dk, dockerconfigjsonContent)) - base64Image := base64.StdEncoding.EncodeToString([]byte(testImage)) - targetDir := provisioner.path.AgentSharedBinaryDirForAgent(base64Image) - installerMock := installermock.NewInstaller(t) - installerMock. - On("InstallAgent", mock.AnythingOfType("context.backgroundCtx"), targetDir). - Return(true, nil).Run(mockFsAfterInstall(provisioner, base64Image)) - - provisioner.imageInstallerBuilder = mockImageInstallerBuilder(installerMock) - - currentVersion, err := provisioner.installAgentImage(ctx, dk, processModule) - require.NoError(t, err) - assert.Equal(t, base64Image, currentVersion) + expectedDir := base64.StdEncoding.EncodeToString([]byte(dk.OneAgent().GetCodeModulesImage())) + targetDir := prov.getTargetDir(*dk) + require.Contains(t, targetDir, expectedDir) }) - t.Run("codeModulesImage set with custom pull secret", func(t *testing.T) { - pullSecretName := "test-pull-secret" - dockerconfigjsonContent := `{"auths":{}}` - - var revision uint = 3 - processModule := createTestProcessModuleConfig(revision) - - dk := createTestDynaKubeWithImage(testImage) - dk.Spec.CustomPullSecret = pullSecretName - - provisioner := createTestProvisioner(createMockedPullSecret(dk, dockerconfigjsonContent)) - base64Image := base64.StdEncoding.EncodeToString([]byte(testImage)) - targetDir := provisioner.path.AgentSharedBinaryDirForAgent(base64Image) - installerMock := installermock.NewInstaller(t) - installerMock. - On("InstallAgent", mock.AnythingOfType("context.backgroundCtx"), targetDir). - Return(true, nil).Run(mockFsAfterInstall(provisioner, base64Image)) - - provisioner.imageInstallerBuilder = mockImageInstallerBuilder(installerMock) - - currentVersion, err := provisioner.installAgentImage(ctx, dk, processModule) - require.NoError(t, err) - assert.Equal(t, base64Image, currentVersion) - }) - t.Run("codeModulesImage + trustedCA set", func(t *testing.T) { - pullSecretName := "test-pull-secret" - trustedCAName := "test-trusted-ca" - customCertContent := ` ------BEGIN CERTIFICATE----- -MIIDazCCAlOgAwIBAgIUdKGNuWxm1t7auCtk+RYAgMKC4wkwDQYJKoZIhvcNAQEL -BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM -GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMzA4MDcxMzUzMjBaFw0yNDA4 -MDYxMzUzMjBaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw -HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB -AQUAA4IBDwAwggEKAoIBAQDGkW280WZbTyHPiNQXHVaWW/C3ZbaKh5cuarQUkHZc -1SVfFELuJXm3YAA5ZOtwaIuqsSO9Yieao0kWYWWCSyFdwcOIl5H85n9YaZ1/8ki3 -af7TwH1UppA3Zh24eV9ME+uJKsmn4AkMVaM9EKUaOTybZD6Sc0jxsmec9yDuE4md -P0vqIshcd6VmxruPnzzmOEXggP3QPFF5s9017uPnQ7k2kU8b0MG19HS2opeeSO59 -R2+kg/Xkz8UnCa5y+OSORW20DHjwc7DUr/Gr78X49iiFBzBewBfeqxQKwtYcC9eB -DxiDWiXENUnsS0EkMs4jNFjgiAJTzx6rBa4xiwe7SJWfAgMBAAGjUzBRMB0GA1Ud -DgQWBBR+L23VHT1LLmpAwz4esbVmfSCOdDAfBgNVHSMEGDAWgBR+L23VHT1LLmpA -wz4esbVmfSCOdDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCj -jU/luq9dNiZi6fhgfhDQRuZEYnHSV8L3+hEDn1j6Gn02c9wNcCDjOBH4i8pJz8g2 -x+Z1SNALXFcr+bGJQx94lw7S1Vm84YxELyNbwVYuHo+7aLAUXSQ62RMIhEJ/NCzW -yN0j8PhweOTBwUtvzPa+71f1gNbDgkfXqgLSXBgjNvolcg/lefmKBs0pU8swOmX1 -q8nrWV12953Gf9sMJ0mFP5/Lcv4l1SdnFLOSdVjWF4RX+SjnVgiHSuJxp9k3QiXz -5dlfTqc9/qZa1PRq4hdq/3Rs42Hiwa3FTWSgqjM1qcDycQtTIAeZu2zfYDQDkYcI -NK85cEJwyxQ+wahdNGUD ------END CERTIFICATE----- -` - dockerconfigjsonContent := `{"auths":{}}` - - var revision uint = 3 - processModule := createTestProcessModuleConfig(revision) - - dk := createTestDynaKubeWithImage(testImage) - dk.Spec.CustomPullSecret = pullSecretName - dk.Spec.TrustedCAs = trustedCAName - - provisioner := createTestProvisioner(createMockedPullSecret(dk, dockerconfigjsonContent), createMockedCAConfigMap(dk, customCertContent)) - base64Image := base64.StdEncoding.EncodeToString([]byte(testImage)) - targetDir := provisioner.path.AgentSharedBinaryDirForAgent(base64Image) - installerMock := installermock.NewInstaller(t) - installerMock. - On("InstallAgent", mock.AnythingOfType("context.backgroundCtx"), targetDir). - Return(true, nil).Run(mockFsAfterInstall(provisioner, base64Image)) - - provisioner.imageInstallerBuilder = mockImageInstallerBuilder(installerMock) - - currentVersion, err := provisioner.installAgentImage(ctx, dk, processModule) - require.NoError(t, err) - assert.Equal(t, base64Image, currentVersion) - }) -} - -func mockFsAfterInstall(provisioner *OneAgentProvisioner, version string) func(mock.Arguments) { - return func(mock.Arguments) { - targetDir := provisioner.path.AgentSharedBinaryDirForAgent(version) - sourceConfigPath := filepath.Join(targetDir, processmoduleconfig.RuxitAgentProcPath) - _ = provisioner.fs.MkdirAll(targetDir, 0755) - _ = provisioner.fs.MkdirAll(filepath.Dir(sourceConfigPath), 0755) - _, _ = provisioner.fs.Create(sourceConfigPath) - } -} - -func createMockedPullSecret(dk dynakube.DynaKube, pullSecretContent string) *corev1.Secret { - return &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: dk.PullSecretName(), - Namespace: dk.Namespace, - }, - Data: map[string][]byte{ - ".dockerconfigjson": []byte(pullSecretContent), - }, - } -} - -func createMockedCAConfigMap(dk dynakube.DynaKube, certContent string) *corev1.ConfigMap { - return &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: dk.Spec.TrustedCAs, - Namespace: dk.Namespace, - }, - Data: map[string]string{ - dynakube.TrustedCAKey: certContent, - }, - } -} - -func createTestDynaKubeWithImage(image string) dynakube.DynaKube { - return *addFakeTenantUUID(&dynakube.DynaKube{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-dk", - Namespace: "test-ns", - }, - Spec: dynakube.DynaKubeSpec{ - APIURL: "https://" + testTenantUUID + ".dynatrace.com", - }, - Status: dynakube.DynaKubeStatus{ - CodeModules: dynakube.CodeModulesStatus{ - VersionStatus: status.VersionStatus{ - ImageID: image, - }, - }, - }, - }) -} - -func createTestDynaKubeWithZip(version string) dynakube.DynaKube { - return *addFakeTenantUUID(&dynakube.DynaKube{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-dk", - Namespace: "test-ns", - }, - Spec: dynakube.DynaKubeSpec{ - APIURL: "https://" + testTenantUUID + ".dynatrace.com", - }, - Status: dynakube.DynaKubeStatus{ - CodeModules: dynakube.CodeModulesStatus{ - VersionStatus: status.VersionStatus{ - Version: version, - }, - }, - }, - }) -} - -func createTestProvisioner(obj ...client.Object) *OneAgentProvisioner { - path := metadata.PathResolver{RootDir: "test"} - fs := afero.NewMemMapFs() - rec := record.NewFakeRecorder(10) - db := metadata.FakeMemoryDB() - - fakeClient := fake.NewClient(obj...) - provisioner := &OneAgentProvisioner{ - client: fakeClient, - apiReader: fakeClient, - fs: fs, - recorder: rec, - path: path, - db: db, - } - - return provisioner -} - -func mockImageInstallerBuilder(mock *installermock.Installer) imageInstallerBuilder { - return func(_ context.Context, _ afero.Fs, _ *image.Properties) (installer.Installer, error) { - return mock, nil - } -} - -func mockUrlInstallerBuilder(mock *installermock.Installer) urlInstallerBuilder { - return func(_ afero.Fs, _ dtclient.Client, _ *url.Properties) installer.Installer { - return mock - } -} - -func createTestProcessModuleConfig(revision uint) *dtclient.ProcessModuleConfig { - return &dtclient.ProcessModuleConfig{ - Revision: revision, - Properties: []dtclient.ProcessModuleProperty{ - { - Section: "test", - Key: "test", - Value: "test3", - }, - }, - } } diff --git a/pkg/controllers/dynakube/activegate.go b/pkg/controllers/dynakube/activegate.go index 9cff4ec612..dcced9d956 100644 --- a/pkg/controllers/dynakube/activegate.go +++ b/pkg/controllers/dynakube/activegate.go @@ -3,7 +3,7 @@ package dynakube import ( "context" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/istio" "github.com/pkg/errors" diff --git a/pkg/controllers/dynakube/activegate/capability/capability.go b/pkg/controllers/dynakube/activegate/capability/capability.go index 7bdbd72d66..c68a5ca992 100644 --- a/pkg/controllers/dynakube/activegate/capability/capability.go +++ b/pkg/controllers/dynakube/activegate/capability/capability.go @@ -4,11 +4,11 @@ import ( "fmt" "strings" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/consts" - "github.com/Dynatrace/dynatrace-operator/pkg/util/address" "k8s.io/utils/net" + "k8s.io/utils/ptr" ) type baseFunc func() *capabilityBase @@ -19,6 +19,7 @@ var ( activegate.RoutingCapability.DisplayName: routingBase, activegate.MetricsIngestCapability.DisplayName: metricsIngestBase, activegate.DynatraceApiCapability.DisplayName: dynatraceApiBase, + activegate.DebuggingCapability.DisplayName: debuggingBase, } ) @@ -80,7 +81,7 @@ func NewMultiCapability(dk *dynakube.DynaKube) Capability { mc.properties = &dk.Spec.ActiveGate.CapabilityProperties if len(dk.Spec.ActiveGate.Capabilities) == 0 && dk.IsExtensionsEnabled() { - mc.properties.Replicas = address.Of(int32(1)) + mc.properties.Replicas = ptr.To(int32(1)) } capabilityNames := []string{} @@ -99,7 +100,12 @@ func NewMultiCapability(dk *dynakube.DynaKube) Capability { if dk.IsExtensionsEnabled() { capabilityNames = append(capabilityNames, "extension_controller") - capabilityDisplayNames = append(capabilityDisplayNames, "extension_controller") + capabilityDisplayNames = append(capabilityDisplayNames, "extension-controller") + } + + if dk.TelemetryIngest().IsEnabled() { + capabilityNames = append(capabilityNames, "log_analytics_collector", "generic_ingest", "otlp_ingest") + capabilityDisplayNames = append(capabilityDisplayNames, "log_analytics-collector", "generic-ingest", "otlp-ingest") } mc.argName = strings.Join(capabilityNames, ",") @@ -148,6 +154,16 @@ func dynatraceApiBase() *capabilityBase { return &c } +func debuggingBase() *capabilityBase { + c := capabilityBase{ + shortName: activegate.DebuggingCapability.ShortName, + argName: activegate.DebuggingCapability.ArgumentName, + displayName: string(activegate.DebuggingCapability.DisplayName), + } + + return &c +} + func GenerateActiveGateCapabilities(dk *dynakube.DynaKube) []Capability { return []Capability{ NewMultiCapability(dk), diff --git a/pkg/controllers/dynakube/activegate/capability/capability_test.go b/pkg/controllers/dynakube/activegate/capability/capability_test.go index adb75d156f..5f3355c979 100644 --- a/pkg/controllers/dynakube/activegate/capability/capability_test.go +++ b/pkg/controllers/dynakube/activegate/capability/capability_test.go @@ -3,8 +3,9 @@ package capability import ( "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/telemetryingest" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/proxy" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -12,13 +13,15 @@ import ( ) const ( - testNamespace = "test-namespace" - testName = "test-name" - testApiUrl = "https://demo.dev.dynatracelabs.com/api" - expectedShortName = "activegate" - expectedArgName = "MSGrouter,kubernetes_monitoring,metrics_ingest,restInterface" - expectedArgNameWithExtensions = "MSGrouter,kubernetes_monitoring,metrics_ingest,restInterface,extension_controller" - expectedArgNameWithExtensionsOnly = "extension_controller" + testNamespace = "test-namespace" + testName = "test-name" + testApiUrl = "https://demo.dev.dynatracelabs.com/api" + expectedShortName = "activegate" + expectedArgName = "MSGrouter,kubernetes_monitoring,metrics_ingest,restInterface" + expectedArgNameWithDebugging = "MSGrouter,kubernetes_monitoring,metrics_ingest,restInterface,debugging" + expectedArgNameWithExtensions = "MSGrouter,kubernetes_monitoring,metrics_ingest,restInterface,extension_controller" + expectedArgNameWithExtensionsOnly = "extension_controller" + expectedArgNameWithTelemetryIngest = "MSGrouter,kubernetes_monitoring,metrics_ingest,restInterface,log_analytics_collector,generic_ingest,otlp_ingest" ) var capabilities = []activegate.CapabilityDisplayName{ @@ -28,12 +31,17 @@ var capabilities = []activegate.CapabilityDisplayName{ activegate.DynatraceApiCapability.DisplayName, } -func buildDynakube(capabilities []activegate.CapabilityDisplayName, enableExtensions bool) *dynakube.DynaKube { +func buildDynakube(capabilities []activegate.CapabilityDisplayName, enableExtensions bool, enableTelemetryIngest bool) *dynakube.DynaKube { extensionsSpec := &dynakube.ExtensionsSpec{} if !enableExtensions { extensionsSpec = nil } + telemetryIngestSpec := &telemetryingest.Spec{} + if !enableTelemetryIngest { + telemetryIngestSpec = nil + } + return &dynakube.DynaKube{ ObjectMeta: metav1.ObjectMeta{ Namespace: testNamespace, Name: testName, @@ -43,7 +51,8 @@ func buildDynakube(capabilities []activegate.CapabilityDisplayName, enableExtens ActiveGate: activegate.Spec{ Capabilities: capabilities, }, - Extensions: extensionsSpec, + Extensions: extensionsSpec, + TelemetryIngest: telemetryIngestSpec, }, } } @@ -68,7 +77,7 @@ func TestBuildServiceName(t *testing.T) { func TestNewMultiCapability(t *testing.T) { t.Run(`creates new multicapability`, func(t *testing.T) { - dk := buildDynakube(capabilities, false) + dk := buildDynakube(capabilities, false, false) mc := NewMultiCapability(dk) require.NotNil(t, mc) assert.True(t, mc.Enabled()) @@ -77,18 +86,18 @@ func TestNewMultiCapability(t *testing.T) { }) t.Run(`creates new multicapability without capabilities set in dynakube`, func(t *testing.T) { var emptyCapabilites []activegate.CapabilityDisplayName - dk := buildDynakube(emptyCapabilites, false) + dk := buildDynakube(emptyCapabilites, false, false) mc := NewMultiCapability(dk) require.NotNil(t, mc) assert.False(t, mc.Enabled()) assert.Equal(t, expectedShortName, mc.ShortName()) - assert.Equal(t, "", mc.ArgName()) + assert.Empty(t, mc.ArgName()) }) } func TestNewMultiCapabilityWithExtensions(t *testing.T) { t.Run(`creates new multicapability with Extensions enabled`, func(t *testing.T) { - dk := buildDynakube(capabilities, true) + dk := buildDynakube(capabilities, true, false) mc := NewMultiCapability(dk) require.NotNil(t, mc) assert.True(t, mc.Enabled()) @@ -97,7 +106,7 @@ func TestNewMultiCapabilityWithExtensions(t *testing.T) { }) t.Run(`creates new multicapability without capabilities set in dynakube and Extensions enabled`, func(t *testing.T) { var emptyCapabilites []activegate.CapabilityDisplayName - dk := buildDynakube(emptyCapabilites, true) + dk := buildDynakube(emptyCapabilites, true, false) mc := NewMultiCapability(dk) require.NotNil(t, mc) assert.True(t, mc.Enabled()) @@ -106,6 +115,37 @@ func TestNewMultiCapabilityWithExtensions(t *testing.T) { }) } +func TestNewMultiCapabilityWithTelemetryIngest(t *testing.T) { + t.Run(`creates new multicapability with TelemetryIngest enabled`, func(t *testing.T) { + dk := buildDynakube(capabilities, false, true) + mc := NewMultiCapability(dk) + require.NotNil(t, mc) + assert.True(t, mc.Enabled()) + assert.Equal(t, expectedShortName, mc.ShortName()) + assert.Equal(t, expectedArgNameWithTelemetryIngest, mc.ArgName()) + }) + t.Run(`creates new multicapability without capabilities set in dynakube and TelemetryIngest enabled`, func(t *testing.T) { + var emptyCapabilites []activegate.CapabilityDisplayName + dk := buildDynakube(emptyCapabilites, false, true) + mc := NewMultiCapability(dk) + require.NotNil(t, mc) + assert.False(t, mc.Enabled()) + assert.Equal(t, expectedShortName, mc.ShortName()) + assert.Empty(t, mc.ArgName()) + }) +} + +func TestNewMultiCapabilityWithDebugging(t *testing.T) { + t.Run(`creates new multicapability with debugging capability enabled`, func(t *testing.T) { + dk := buildDynakube(append(capabilities, activegate.DebuggingCapability.DisplayName), false, false) + mc := NewMultiCapability(dk) + require.NotNil(t, mc) + assert.True(t, mc.Enabled()) + assert.Equal(t, expectedShortName, mc.ShortName()) + assert.Equal(t, expectedArgNameWithDebugging, mc.ArgName()) + }) +} + func TestBuildServiceDomainNameForDNSEntryPoint(t *testing.T) { actual := buildServiceDomainName("test-name", "test-namespace", "test-component-feature") assert.NotEmpty(t, actual) diff --git a/pkg/controllers/dynakube/activegate/consts/consts.go b/pkg/controllers/dynakube/activegate/consts/consts.go index e973e56c6a..3798114423 100644 --- a/pkg/controllers/dynakube/activegate/consts/consts.go +++ b/pkg/controllers/dynakube/activegate/consts/consts.go @@ -28,6 +28,7 @@ const ( EnvDtHttpPort = "DT_HTTP_PORT" AnnotationActiveGateConfigurationHash = api.InternalFlagPrefix + "activegate-configuration-hash" + AnnotationActiveGateTenantTokenHash = api.InternalFlagPrefix + "activegate-tenant-token-hash" AnnotationActiveGateContainerAppArmor = "container.apparmor.security.beta.kubernetes.io/" + ActiveGateContainerName GatewayConfigVolumeName = "ag-lib-gateway-config" diff --git a/pkg/controllers/dynakube/activegate/internal/authtoken/reconciler.go b/pkg/controllers/dynakube/activegate/internal/authtoken/reconciler.go index ae4a64cec4..e688d48158 100644 --- a/pkg/controllers/dynakube/activegate/internal/authtoken/reconciler.go +++ b/pkg/controllers/dynakube/activegate/internal/authtoken/reconciler.go @@ -6,7 +6,7 @@ import ( "strings" "time" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/controllers" "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" diff --git a/pkg/controllers/dynakube/activegate/internal/authtoken/reconciler_test.go b/pkg/controllers/dynakube/activegate/internal/authtoken/reconciler_test.go index c6a7f7435e..66aa59ffed 100644 --- a/pkg/controllers/dynakube/activegate/internal/authtoken/reconciler_test.go +++ b/pkg/controllers/dynakube/activegate/internal/authtoken/reconciler_test.go @@ -5,8 +5,8 @@ import ( "testing" "time" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" dtclientmock "github.com/Dynatrace/dynatrace-operator/test/mocks/pkg/clients/dynatrace" diff --git a/pkg/controllers/dynakube/activegate/internal/capability/reconciler.go b/pkg/controllers/dynakube/activegate/internal/capability/reconciler.go index f79e5c4eb8..fef2fd909d 100644 --- a/pkg/controllers/dynakube/activegate/internal/capability/reconciler.go +++ b/pkg/controllers/dynakube/activegate/internal/capability/reconciler.go @@ -4,7 +4,7 @@ import ( "context" "reflect" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/controllers" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/capability" "github.com/pkg/errors" @@ -21,20 +21,22 @@ type Reconciler struct { capability capability.Capability statefulsetReconciler controllers.Reconciler customPropertiesReconciler controllers.Reconciler + tlsSecretReconciler controllers.Reconciler dk *dynakube.DynaKube } -func NewReconciler(clt client.Client, capability capability.Capability, dk *dynakube.DynaKube, statefulsetReconciler controllers.Reconciler, customPropertiesReconciler controllers.Reconciler) controllers.Reconciler { +func NewReconciler(clt client.Client, capability capability.Capability, dk *dynakube.DynaKube, statefulsetReconciler controllers.Reconciler, customPropertiesReconciler controllers.Reconciler, tlsSecretReconciler controllers.Reconciler) controllers.Reconciler { //nolint:revive return &Reconciler{ statefulsetReconciler: statefulsetReconciler, customPropertiesReconciler: customPropertiesReconciler, + tlsSecretReconciler: tlsSecretReconciler, capability: capability, dk: dk, client: clt, } } -type NewReconcilerFunc = func(clt client.Client, capability capability.Capability, dk *dynakube.DynaKube, statefulsetReconciler controllers.Reconciler, customPropertiesReconciler controllers.Reconciler) controllers.Reconciler +type NewReconcilerFunc = func(clt client.Client, capability capability.Capability, dk *dynakube.DynaKube, statefulsetReconciler controllers.Reconciler, customPropertiesReconciler controllers.Reconciler, tlsSecretReconciler controllers.Reconciler) controllers.Reconciler func (r *Reconciler) Reconcile(ctx context.Context) error { err := r.customPropertiesReconciler.Reconcile(ctx) @@ -42,18 +44,19 @@ func (r *Reconciler) Reconcile(ctx context.Context) error { return errors.WithStack(err) } - if r.dk.ActiveGate().NeedsService() { - err = r.createOrUpdateService(ctx) - if err != nil { - return err - } + err = r.createOrUpdateService(ctx) + if err != nil { + return err + } - err = r.setAGServiceIPs(ctx) - if err != nil { - return err - } - } else { - r.dk.Status.ActiveGate.ServiceIPs = []string{} + err = r.setAGServiceIPs(ctx) + if err != nil { + return err + } + + err = r.tlsSecretReconciler.Reconcile(ctx) + if err != nil { + return err } err = r.statefulsetReconciler.Reconcile(ctx) diff --git a/pkg/controllers/dynakube/activegate/internal/capability/reconciler_test.go b/pkg/controllers/dynakube/activegate/internal/capability/reconciler_test.go index ae32b9f6a4..cd1aa41ef5 100644 --- a/pkg/controllers/dynakube/activegate/internal/capability/reconciler_test.go +++ b/pkg/controllers/dynakube/activegate/internal/capability/reconciler_test.go @@ -5,8 +5,8 @@ import ( "testing" "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/capability" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/internal/authtoken" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubesystem" @@ -93,8 +93,9 @@ func TestReconcile(t *testing.T) { dk := buildDynakube(capabilitiesWithService) mockStatefulSetReconciler := getMockReconciler(t, nil) mockCustompropertiesReconciler := getMockReconciler(t, nil) + mockTlsSecretReconciler := getMockReconciler(t, nil) - r := NewReconciler(clt, capability.NewMultiCapability(dk), dk, mockStatefulSetReconciler, mockCustompropertiesReconciler).(*Reconciler) + r := NewReconciler(clt, capability.NewMultiCapability(dk), dk, mockStatefulSetReconciler, mockCustompropertiesReconciler, mockTlsSecretReconciler).(*Reconciler) verifyReconciler(t, r) err := r.Reconcile(context.Background()) @@ -107,8 +108,9 @@ func TestReconcile(t *testing.T) { dk := buildDynakube(capabilitiesWithoutService) mockStatefulSetReconciler := getMockReconciler(t, errors.New("")) mockCustompropertiesReconciler := getMockReconciler(t, nil) + mockTlsSecretReconciler := getMockReconciler(t, nil) - r := NewReconciler(clt, capability.NewMultiCapability(dk), dk, mockStatefulSetReconciler, mockCustompropertiesReconciler).(*Reconciler) + r := NewReconciler(clt, capability.NewMultiCapability(dk), dk, mockStatefulSetReconciler, mockCustompropertiesReconciler, mockTlsSecretReconciler).(*Reconciler) verifyReconciler(t, r) err := r.Reconcile(context.Background()) @@ -121,8 +123,9 @@ func TestReconcile(t *testing.T) { dk := buildDynakube(capabilitiesWithoutService) mockStatefulSetReconciler := getMockReconciler(t) mockCustompropertiesReconciler := getMockReconciler(t, errors.New("")) + mockTlsSecretReconciler := getMockReconciler(t, nil) - r := NewReconciler(clt, capability.NewMultiCapability(dk), dk, mockStatefulSetReconciler, mockCustompropertiesReconciler).(*Reconciler) + r := NewReconciler(clt, capability.NewMultiCapability(dk), dk, mockStatefulSetReconciler, mockCustompropertiesReconciler, mockTlsSecretReconciler).(*Reconciler) verifyReconciler(t, r) err := r.Reconcile(context.Background()) @@ -134,8 +137,9 @@ func TestReconcile(t *testing.T) { dk := buildDynakube(capabilitiesWithoutService) mockStatefulSetReconciler := getMockReconciler(t, errors.New("")) mockCustompropertiesReconciler := getMockReconciler(t, errors.New("")) + mockTlsSecretReconciler := getMockReconciler(t, nil) - r := NewReconciler(clt, capability.NewMultiCapability(dk), dk, mockStatefulSetReconciler, mockCustompropertiesReconciler).(*Reconciler) + r := NewReconciler(clt, capability.NewMultiCapability(dk), dk, mockStatefulSetReconciler, mockCustompropertiesReconciler, mockTlsSecretReconciler).(*Reconciler) verifyReconciler(t, r) err := r.Reconcile(context.Background()) @@ -145,8 +149,9 @@ func TestReconcile(t *testing.T) { dk := buildDynakube(capabilitiesWithService) mockStatefulSetReconciler := getMockReconciler(t, nil) mockCustompropertiesReconciler := getMockReconciler(t, nil) + mockTlsSecretReconciler := getMockReconciler(t, nil) - r := NewReconciler(clt, capability.NewMultiCapability(dk), dk, mockStatefulSetReconciler, mockCustompropertiesReconciler).(*Reconciler) + r := NewReconciler(clt, capability.NewMultiCapability(dk), dk, mockStatefulSetReconciler, mockCustompropertiesReconciler, mockTlsSecretReconciler).(*Reconciler) verifyReconciler(t, r) err := r.Reconcile(context.Background()) @@ -161,13 +166,14 @@ func TestReconcile(t *testing.T) { assert.NotNil(t, service) require.NoError(t, err) }) - t.Run(`service does not get created when missing capabilities`, func(t *testing.T) { + t.Run(`service is created even though capability does not need it`, func(t *testing.T) { clt := createClient() dk := buildDynakube(capabilitiesWithoutService) mockStatefulSetReconciler := getMockReconciler(t, nil) mockCustompropertiesReconciler := getMockReconciler(t, nil) + mockTlsSecretReconciler := getMockReconciler(t, nil) - r := NewReconciler(clt, capability.NewMultiCapability(dk), dk, mockStatefulSetReconciler, mockCustompropertiesReconciler).(*Reconciler) + r := NewReconciler(clt, capability.NewMultiCapability(dk), dk, mockStatefulSetReconciler, mockCustompropertiesReconciler, mockTlsSecretReconciler).(*Reconciler) verifyReconciler(t, r) err := r.Reconcile(context.Background()) @@ -179,8 +185,10 @@ func TestReconcile(t *testing.T) { service := corev1.Service{} err = r.client.Get(context.Background(), client.ObjectKey{Name: r.dk.Name + "-" + r.capability.ShortName(), Namespace: r.dk.Namespace}, &service) - assert.Empty(t, service) - require.Error(t, err) + require.NoError(t, err) + + assert.NotEmpty(t, service) + assert.Len(t, service.Spec.Ports, 2) }) } @@ -189,9 +197,10 @@ func TestCreateOrUpdateService(t *testing.T) { dk := buildDynakube(capabilitiesWithService) mockStatefulSetReconciler := getMockReconciler(t, nil) mockCustompropertiesReconciler := getMockReconciler(t, nil) + mockTlsSecretReconciler := getMockReconciler(t, nil) t.Run(`create service works`, func(t *testing.T) { - r := NewReconciler(clt, capability.NewMultiCapability(dk), dk, mockStatefulSetReconciler, mockCustompropertiesReconciler).(*Reconciler) + r := NewReconciler(clt, capability.NewMultiCapability(dk), dk, mockStatefulSetReconciler, mockCustompropertiesReconciler, mockTlsSecretReconciler).(*Reconciler) verifyReconciler(t, r) service := &corev1.Service{} @@ -208,7 +217,7 @@ func TestCreateOrUpdateService(t *testing.T) { assert.NotNil(t, service) }) t.Run(`ports get updated`, func(t *testing.T) { - r := NewReconciler(clt, capability.NewMultiCapability(dk), dk, mockStatefulSetReconciler, mockCustompropertiesReconciler).(*Reconciler) + r := NewReconciler(clt, capability.NewMultiCapability(dk), dk, mockStatefulSetReconciler, mockCustompropertiesReconciler, mockTlsSecretReconciler).(*Reconciler) verifyReconciler(t, r) err := r.createOrUpdateService(context.Background()) @@ -236,7 +245,7 @@ func TestCreateOrUpdateService(t *testing.T) { require.NotEqual(t, actualService.Spec.Ports, service.Spec.Ports) }) t.Run(`labels get updated`, func(t *testing.T) { - r := NewReconciler(clt, capability.NewMultiCapability(dk), dk, mockStatefulSetReconciler, mockCustompropertiesReconciler).(*Reconciler) + r := NewReconciler(clt, capability.NewMultiCapability(dk), dk, mockStatefulSetReconciler, mockCustompropertiesReconciler, mockTlsSecretReconciler).(*Reconciler) verifyReconciler(t, r) err := r.createOrUpdateService(context.Background()) @@ -268,8 +277,9 @@ func TestPortsAreOutdated(t *testing.T) { dk := buildDynakube(capabilitiesWithService) mockStatefulSetReconciler := getMockReconciler(t, nil) mockCustompropertiesReconciler := getMockReconciler(t, nil) + mockTlsSecretReconciler := getMockReconciler(t, nil) - r := NewReconciler(clt, capability.NewMultiCapability(dk), dk, mockStatefulSetReconciler, mockCustompropertiesReconciler).(*Reconciler) + r := NewReconciler(clt, capability.NewMultiCapability(dk), dk, mockStatefulSetReconciler, mockCustompropertiesReconciler, mockTlsSecretReconciler).(*Reconciler) verifyReconciler(t, r) desiredService := CreateService(r.dk, r.capability.ShortName()) @@ -296,7 +306,9 @@ func TestLabelsAreOutdated(t *testing.T) { dk := buildDynakube(capabilitiesWithService) mockStatefulSetReconciler := getMockReconciler(t, nil) mockCustompropertiesReconciler := getMockReconciler(t, nil) - r := NewReconciler(clt, capability.NewMultiCapability(dk), dk, mockStatefulSetReconciler, mockCustompropertiesReconciler).(*Reconciler) + mockTlsSecretReconciler := getMockReconciler(t, nil) + + r := NewReconciler(clt, capability.NewMultiCapability(dk), dk, mockStatefulSetReconciler, mockCustompropertiesReconciler, mockTlsSecretReconciler).(*Reconciler) verifyReconciler(t, r) desiredService := CreateService(r.dk, r.capability.ShortName()) diff --git a/pkg/controllers/dynakube/activegate/internal/capability/service.go b/pkg/controllers/dynakube/activegate/internal/capability/service.go index 51647ff441..76ef4f6444 100644 --- a/pkg/controllers/dynakube/activegate/internal/capability/service.go +++ b/pkg/controllers/dynakube/activegate/internal/capability/service.go @@ -1,7 +1,7 @@ package capability import ( - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/capability" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/consts" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/labels" @@ -13,24 +13,18 @@ import ( func CreateService(dk *dynakube.DynaKube, feature string) *corev1.Service { var ports []corev1.ServicePort - if dk.ActiveGate().NeedsService() { - ports = append(ports, - corev1.ServicePort{ - Name: consts.HttpsServicePortName, - Protocol: corev1.ProtocolTCP, - Port: consts.HttpsServicePort, - TargetPort: intstr.FromString(consts.HttpsServicePortName), - }, - ) - if dk.ActiveGate().IsMetricsIngestEnabled() { - ports = append(ports, corev1.ServicePort{ - Name: consts.HttpServicePortName, - Protocol: corev1.ProtocolTCP, - Port: consts.HttpServicePort, - TargetPort: intstr.FromString(consts.HttpServicePortName), - }) - } - } + ports = append(ports, + corev1.ServicePort{ + Name: consts.HttpsServicePortName, + Protocol: corev1.ProtocolTCP, + Port: consts.HttpsServicePort, + TargetPort: intstr.FromString(consts.HttpsServicePortName), + }, corev1.ServicePort{ + Name: consts.HttpServicePortName, + Protocol: corev1.ProtocolTCP, + Port: consts.HttpServicePort, + TargetPort: intstr.FromString(consts.HttpServicePortName), + }) coreLabels := labels.NewCoreLabels(dk.Name, labels.ActiveGateComponentLabel) diff --git a/pkg/controllers/dynakube/activegate/internal/capability/service_test.go b/pkg/controllers/dynakube/activegate/internal/capability/service_test.go index 8ac773305a..e7202d9583 100644 --- a/pkg/controllers/dynakube/activegate/internal/capability/service_test.go +++ b/pkg/controllers/dynakube/activegate/internal/capability/service_test.go @@ -3,8 +3,8 @@ package capability import ( "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/consts" agutil "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/activegate" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/labels" @@ -81,7 +81,7 @@ func TestCreateService(t *testing.T) { ports := service.Spec.Ports assert.Contains(t, ports, agHttpsPort) - assert.NotContains(t, ports, agHttpPort) + assert.Contains(t, ports, agHttpPort) }) t.Run("check AG service if metrics-ingest enabled", func(t *testing.T) { dk := createTestDynaKube() diff --git a/pkg/controllers/dynakube/activegate/internal/customproperties/reconciler.go b/pkg/controllers/dynakube/activegate/internal/customproperties/reconciler.go index f21d8e6f80..7908b010f6 100644 --- a/pkg/controllers/dynakube/activegate/internal/customproperties/reconciler.go +++ b/pkg/controllers/dynakube/activegate/internal/customproperties/reconciler.go @@ -6,7 +6,7 @@ import ( "strings" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/value" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/controllers" "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/secret" diff --git a/pkg/controllers/dynakube/activegate/internal/customproperties/reconciler_test.go b/pkg/controllers/dynakube/activegate/internal/customproperties/reconciler_test.go index 0c2dcda370..a902385fdb 100644 --- a/pkg/controllers/dynakube/activegate/internal/customproperties/reconciler_test.go +++ b/pkg/controllers/dynakube/activegate/internal/customproperties/reconciler_test.go @@ -6,7 +6,7 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/value" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" diff --git a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/authtoken.go b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/authtoken.go index b378c546aa..356b2f757d 100644 --- a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/authtoken.go +++ b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/authtoken.go @@ -1,7 +1,7 @@ package modifiers import ( - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/consts" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/internal/authtoken" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/internal/statefulset/builder" diff --git a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/certs.go b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/certs.go index df892934f7..bca47c6068 100644 --- a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/certs.go +++ b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/certs.go @@ -3,7 +3,7 @@ package modifiers import ( "path/filepath" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/consts" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/internal/statefulset/builder" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/container" @@ -48,7 +48,7 @@ func (mod CertificatesModifier) getVolumes() []corev1.Volume { Name: jettyCerts, VolumeSource: corev1.VolumeSource{ Secret: &corev1.SecretVolumeSource{ - SecretName: mod.dk.Spec.ActiveGate.TlsSecretName, + SecretName: mod.dk.ActiveGate().GetTLSSecretName(), }, }, }, diff --git a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/certs_test.go b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/certs_test.go index 7d3247f556..1d1e7e68d9 100644 --- a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/certs_test.go +++ b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/certs_test.go @@ -3,7 +3,7 @@ package modifiers import ( "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -18,9 +18,14 @@ func setCertUsage(dk *dynakube.DynaKube, isUsed bool) { } } +func disableAutomaticAGCertificate(dk *dynakube.DynaKube) { + dk.Annotations[dynakube.AnnotationFeatureActiveGateAutomaticTLSCertificate] = "false" +} + func TestCertEnabled(t *testing.T) { t.Run("true", func(t *testing.T) { dk := getBaseDynakube() + disableAutomaticAGCertificate(&dk) enableKubeMonCapability(&dk) setCertUsage(&dk, true) @@ -31,6 +36,7 @@ func TestCertEnabled(t *testing.T) { t.Run("false", func(t *testing.T) { dk := getBaseDynakube() + disableAutomaticAGCertificate(&dk) enableKubeMonCapability(&dk) setCertUsage(&dk, false) @@ -38,6 +44,26 @@ func TestCertEnabled(t *testing.T) { assert.False(t, mod.Enabled()) }) + + t.Run("true, AG cert enabled", func(t *testing.T) { + dk := getBaseDynakube() + enableKubeMonCapability(&dk) + setCertUsage(&dk, true) + + mod := NewCertificatesModifier(dk) + + assert.True(t, mod.Enabled()) + }) + + t.Run("false, AG cert enabled", func(t *testing.T) { + dk := getBaseDynakube() + enableKubeMonCapability(&dk) + setCertUsage(&dk, false) + + mod := NewCertificatesModifier(dk) + + assert.True(t, mod.Enabled()) + }) } func TestCertModify(t *testing.T) { diff --git a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/config.go b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/config.go index ca8e8a9590..94706a4589 100644 --- a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/config.go +++ b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/config.go @@ -1,7 +1,7 @@ package modifiers import ( - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/capability" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/internal/statefulset/builder" "github.com/Dynatrace/dynatrace-operator/pkg/util/prioritymap" diff --git a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/config_test.go b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/config_test.go index d52f1c11da..c747c1e183 100644 --- a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/config_test.go +++ b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/config_test.go @@ -3,8 +3,8 @@ package modifiers import ( "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/capability" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/consts" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/internal/statefulset/builder" @@ -78,7 +78,6 @@ func enableAllModifiers(dk *dynakube.DynaKube, capability capability.Capability) setCustomPropertyUsage(capability, true) setProxyUsage(dk, true) setKubernetesMonitoringUsage(dk, true) - setServicePortUsage(dk, true) } func TestNoConflict(t *testing.T) { diff --git a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/customprops.go b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/customprops.go index 2f8dd20f2e..752136db42 100644 --- a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/customprops.go +++ b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/customprops.go @@ -3,7 +3,7 @@ package modifiers import ( "fmt" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/capability" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/consts" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/internal/customproperties" diff --git a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/customprops_test.go b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/customprops_test.go index d6222c2d24..869a4be2f6 100644 --- a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/customprops_test.go +++ b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/customprops_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/value" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/capability" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/eec.go b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/eec.go index 32d32c1ef7..9658bc52bd 100644 --- a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/eec.go +++ b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/eec.go @@ -1,7 +1,7 @@ package modifiers import ( - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/consts" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/internal/statefulset/builder" eecconsts "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/extension/consts" @@ -54,7 +54,7 @@ func (mod EecModifier) getVolumes() []corev1.Volume { DefaultMode: &mode, Items: []corev1.KeyToPath{ { - Key: eecconsts.EecTokenSecretKey, + Key: eecconsts.TokenSecretKey, Path: eecFile, Mode: &mode, }, diff --git a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/eec_test.go b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/eec_test.go index fc4416f767..ab517a9dbc 100644 --- a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/eec_test.go +++ b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/eec_test.go @@ -3,7 +3,7 @@ package modifiers import ( "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/kspm.go b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/kspm.go index a5b872d382..576972ace0 100644 --- a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/kspm.go +++ b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/kspm.go @@ -2,8 +2,8 @@ package modifiers import ( "github.com/Dynatrace/dynatrace-operator/pkg/api" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/kspm" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/kspm" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/consts" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/internal/statefulset/builder" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/container" diff --git a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/kspm_test.go b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/kspm_test.go index a5b4d064e0..456666fb3a 100644 --- a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/kspm_test.go +++ b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/kspm_test.go @@ -3,8 +3,8 @@ package modifiers import ( "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/kspm" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/kspm" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/kubemon.go b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/kubemon.go index 525aed5f89..02e368342c 100644 --- a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/kubemon.go +++ b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/kubemon.go @@ -1,14 +1,14 @@ package modifiers import ( - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/capability" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/consts" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/internal/statefulset/builder" - "github.com/Dynatrace/dynatrace-operator/pkg/util/address" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/container" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + "k8s.io/utils/ptr" ) var _ volumeModifier = KubernetesMonitoringModifier{} @@ -123,11 +123,11 @@ func (mod KubernetesMonitoringModifier) getReadOnlyInitVolumeMounts() []corev1.V func GetSecurityContext(readOnlyRootFileSystem bool) *corev1.SecurityContext { securityContext := corev1.SecurityContext{ - Privileged: address.Of(false), - AllowPrivilegeEscalation: address.Of(false), - RunAsNonRoot: address.Of(true), - RunAsUser: address.Of(consts.DockerImageUser), - RunAsGroup: address.Of(consts.DockerImageGroup), + Privileged: ptr.To(false), + AllowPrivilegeEscalation: ptr.To(false), + RunAsNonRoot: ptr.To(true), + RunAsUser: ptr.To(consts.DockerImageUser), + RunAsGroup: ptr.To(consts.DockerImageGroup), Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{ "ALL", @@ -136,7 +136,7 @@ func GetSecurityContext(readOnlyRootFileSystem bool) *corev1.SecurityContext { SeccompProfile: &corev1.SeccompProfile{ Type: corev1.SeccompProfileTypeRuntimeDefault, }, - ReadOnlyRootFilesystem: address.Of(readOnlyRootFileSystem), + ReadOnlyRootFilesystem: ptr.To(readOnlyRootFileSystem), } return &securityContext diff --git a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/kubemon_test.go b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/kubemon_test.go index dec5446179..9b4e547274 100644 --- a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/kubemon_test.go +++ b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/kubemon_test.go @@ -3,7 +3,7 @@ package modifiers import ( "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/capability" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/proxy.go b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/proxy.go index 9016a85be7..4c428542af 100644 --- a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/proxy.go +++ b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/proxy.go @@ -1,7 +1,7 @@ package modifiers import ( - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/consts" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/internal/statefulset/builder" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/proxy" diff --git a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/proxy_test.go b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/proxy_test.go index 38f8e5f752..17144ec164 100644 --- a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/proxy_test.go +++ b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/proxy_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/value" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/rawimage.go b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/rawimage.go index 20754aa7de..bc355b713e 100644 --- a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/rawimage.go +++ b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/rawimage.go @@ -1,15 +1,15 @@ package modifiers import ( - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/consts" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/internal/statefulset/builder" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/connectioninfo" - "github.com/Dynatrace/dynatrace-operator/pkg/util/address" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/container" "github.com/Dynatrace/dynatrace-operator/pkg/util/prioritymap" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + "k8s.io/utils/ptr" ) var _ envModifier = RawImageModifier{} @@ -82,7 +82,7 @@ func (mod RawImageModifier) tenantUUIDEnvVar() corev1.EnvVar { Name: mod.dk.ActiveGate().GetConnectionInfoConfigMapName(), }, Key: connectioninfo.TenantUUIDKey, - Optional: address.Of(false), + Optional: ptr.To(false), }}} } @@ -94,7 +94,7 @@ func (mod RawImageModifier) communicationEndpointEnvVar() corev1.EnvVar { Name: mod.dk.ActiveGate().GetConnectionInfoConfigMapName(), }, Key: connectioninfo.CommunicationEndpointsKey, - Optional: address.Of(false), + Optional: ptr.To(false), }}, } } diff --git a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/readonly.go b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/readonly.go index 938158b0ad..513cdbd61f 100644 --- a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/readonly.go +++ b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/readonly.go @@ -1,13 +1,13 @@ package modifiers import ( - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/consts" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/internal/statefulset/builder" - "github.com/Dynatrace/dynatrace-operator/pkg/util/address" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/container" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + "k8s.io/utils/ptr" ) var _ volumeModifier = ReadOnlyModifier{} @@ -35,7 +35,7 @@ func (mod ReadOnlyModifier) Modify(sts *appsv1.StatefulSet) error { sts.Spec.Template.Spec.Volumes = append(sts.Spec.Template.Spec.Volumes, mod.getVolumes()...) baseContainer := container.FindContainerInPodSpec(&sts.Spec.Template.Spec, consts.ActiveGateContainerName) - baseContainer.SecurityContext.ReadOnlyRootFilesystem = address.Of(true) + baseContainer.SecurityContext.ReadOnlyRootFilesystem = ptr.To(true) mod.presentMounts = baseContainer.VolumeMounts baseContainer.VolumeMounts = append(baseContainer.VolumeMounts, mod.getVolumeMounts()...) @@ -62,12 +62,6 @@ func (mod ReadOnlyModifier) getVolumes() []corev1.Volume { EmptyDir: &corev1.EmptyDirVolumeSource{}, }, }, - { - Name: consts.GatewayTmpVolumeName, - VolumeSource: corev1.VolumeSource{ - EmptyDir: &corev1.EmptyDirVolumeSource{}, - }, - }, { Name: consts.GatewayConfigVolumeName, VolumeSource: corev1.VolumeSource{ @@ -94,11 +88,6 @@ func (mod ReadOnlyModifier) getVolumeMounts() []corev1.VolumeMount { Name: consts.GatewayLogVolumeName, MountPath: consts.GatewayLogMountPoint, }, - { - ReadOnly: false, - Name: consts.GatewayTmpVolumeName, - MountPath: consts.GatewayTmpMountPoint, - }, { ReadOnly: false, Name: consts.GatewayConfigVolumeName, diff --git a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/serviceport.go b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/serviceport.go index 592b1f4191..f0b43e6a03 100644 --- a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/serviceport.go +++ b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/serviceport.go @@ -1,7 +1,7 @@ package modifiers import ( - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/capability" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/consts" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/internal/statefulset/builder" @@ -30,7 +30,7 @@ type ServicePortModifier struct { } func (mod ServicePortModifier) Enabled() bool { - return mod.dk.ActiveGate().NeedsService() + return true } func (mod ServicePortModifier) Modify(sts *appsv1.StatefulSet) error { @@ -48,12 +48,10 @@ func (mod ServicePortModifier) getPorts() []corev1.ContainerPort { Name: consts.HttpsServicePortName, ContainerPort: consts.HttpsContainerPort, }, - } - if mod.dk.ActiveGate().IsMetricsIngestEnabled() { - ports = append(ports, corev1.ContainerPort{ + { Name: consts.HttpServicePortName, ContainerPort: consts.HttpContainerPort, - }) + }, } return ports diff --git a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/serviceport_test.go b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/serviceport_test.go index ec8fd4e70c..2c30cd7282 100644 --- a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/serviceport_test.go +++ b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/serviceport_test.go @@ -3,8 +3,6 @@ package modifiers import ( "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/capability" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/consts" "github.com/Dynatrace/dynatrace-operator/pkg/util/prioritymap" @@ -12,38 +10,9 @@ import ( "github.com/stretchr/testify/require" ) -func setServicePortUsage(dk *dynakube.DynaKube, isUsed bool) { - if isUsed { - dk.Spec.ActiveGate.Capabilities = append(dk.Spec.ActiveGate.Capabilities, activegate.MetricsIngestCapability.DisplayName) - } -} - -func TestServicePortEnabled(t *testing.T) { - t.Run("true", func(t *testing.T) { - dk := getBaseDynakube() - setServicePortUsage(&dk, true) - multiCapability := capability.NewMultiCapability(&dk) - - mod := NewServicePortModifier(dk, multiCapability, prioritymap.New()) - - assert.True(t, mod.Enabled()) - }) - - t.Run("false", func(t *testing.T) { - dk := getBaseDynakube() - setServicePortUsage(&dk, false) - multiCapability := capability.NewMultiCapability(&dk) - - mod := NewServicePortModifier(dk, multiCapability, prioritymap.New()) - - assert.False(t, mod.Enabled()) - }) -} - func TestServicePortModify(t *testing.T) { t.Run("successfully modified", func(t *testing.T) { dk := getBaseDynakube() - setServicePortUsage(&dk, true) multiCapability := capability.NewMultiCapability(&dk) mod := NewServicePortModifier(dk, multiCapability, prioritymap.New()) builder := createBuilderForTesting() diff --git a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/ssl_volume.go b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/ssl_volume.go index 644d048e4d..9485646237 100644 --- a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/ssl_volume.go +++ b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/ssl_volume.go @@ -1,7 +1,7 @@ package modifiers import ( - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/consts" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/internal/statefulset/builder" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/container" diff --git a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/ssl_volume_test.go b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/ssl_volume_test.go index af2537e2fc..0b53d7bdef 100644 --- a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/ssl_volume_test.go +++ b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/ssl_volume_test.go @@ -10,6 +10,7 @@ import ( func TestSSLVolumeEnabled(t *testing.T) { t.Run("true - TlsSecretName", func(t *testing.T) { dk := getBaseDynakube() + disableAutomaticAGCertificate(&dk) enableKubeMonCapability(&dk) dk.Spec.ActiveGate.TlsSecretName = testTlsSecretName @@ -20,6 +21,7 @@ func TestSSLVolumeEnabled(t *testing.T) { t.Run("true - TrustedCAs", func(t *testing.T) { dk := getBaseDynakube() + disableAutomaticAGCertificate(&dk) enableKubeMonCapability(&dk) dk.Spec.TrustedCAs = testTlsSecretName @@ -30,12 +32,42 @@ func TestSSLVolumeEnabled(t *testing.T) { t.Run("false", func(t *testing.T) { dk := getBaseDynakube() + disableAutomaticAGCertificate(&dk) enableKubeMonCapability(&dk) mod := NewSSLVolumeModifier(dk) assert.False(t, mod.Enabled()) }) + + t.Run("true - TlsSecretName, AG cert enabled", func(t *testing.T) { + dk := getBaseDynakube() + enableKubeMonCapability(&dk) + dk.Spec.ActiveGate.TlsSecretName = testTlsSecretName + + mod := NewSSLVolumeModifier(dk) + + assert.True(t, mod.Enabled()) + }) + + t.Run("true - TrustedCAs, AG cert enabled", func(t *testing.T) { + dk := getBaseDynakube() + enableKubeMonCapability(&dk) + dk.Spec.TrustedCAs = testTlsSecretName + + mod := NewSSLVolumeModifier(dk) + + assert.True(t, mod.Enabled()) + }) + + t.Run("false, AG cert enabled", func(t *testing.T) { + dk := getBaseDynakube() + enableKubeMonCapability(&dk) + + mod := NewSSLVolumeModifier(dk) + + assert.True(t, mod.Enabled()) + }) } func TestSSLVolumeModify(t *testing.T) { diff --git a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/trustedcas_volume.go b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/trustedcas_volume.go index 4b99be2ac2..74bdd54a2c 100644 --- a/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/trustedcas_volume.go +++ b/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers/trustedcas_volume.go @@ -1,7 +1,7 @@ package modifiers import ( - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/consts" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/internal/statefulset/builder" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/container" diff --git a/pkg/controllers/dynakube/activegate/internal/statefulset/reconciler.go b/pkg/controllers/dynakube/activegate/internal/statefulset/reconciler.go index e8b3b061f7..8a2041d2ff 100644 --- a/pkg/controllers/dynakube/activegate/internal/statefulset/reconciler.go +++ b/pkg/controllers/dynakube/activegate/internal/statefulset/reconciler.go @@ -5,7 +5,7 @@ import ( "strconv" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/value" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/controllers" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/capability" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/internal/authtoken" diff --git a/pkg/controllers/dynakube/activegate/internal/statefulset/reconciler_test.go b/pkg/controllers/dynakube/activegate/internal/statefulset/reconciler_test.go index 7958d6a965..32dba75c78 100644 --- a/pkg/controllers/dynakube/activegate/internal/statefulset/reconciler_test.go +++ b/pkg/controllers/dynakube/activegate/internal/statefulset/reconciler_test.go @@ -2,14 +2,15 @@ package statefulset import ( "context" + "errors" "fmt" "testing" "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme" dynafake "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/value" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/capability" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/internal/authtoken" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/internal/customproperties" @@ -141,7 +142,7 @@ func TestReconcile(t *testing.T) { r := createDefaultReconciler(t) fakeClient := dynafake.NewClientWithInterceptors(interceptor.Funcs{ Get: func(ctx context.Context, client client.WithWatch, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { - return fmt.Errorf("BOOM") + return errors.New("BOOM") }, }) r.apiReader = fakeClient diff --git a/pkg/controllers/dynakube/activegate/internal/statefulset/statefulset.go b/pkg/controllers/dynakube/activegate/internal/statefulset/statefulset.go index 7e75c2b22a..b6d7d6ee11 100644 --- a/pkg/controllers/dynakube/activegate/internal/statefulset/statefulset.go +++ b/pkg/controllers/dynakube/activegate/internal/statefulset/statefulset.go @@ -3,26 +3,31 @@ package statefulset import ( "strconv" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/status" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/capability" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/consts" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/internal/statefulset/builder" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/deploymentmetadata" - "github.com/Dynatrace/dynatrace-operator/pkg/util/address" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/labels" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/node" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/statefulset" maputils "github.com/Dynatrace/dynatrace-operator/pkg/util/map" "github.com/Dynatrace/dynatrace-operator/pkg/util/prioritymap" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/ptr" ) -const defaultEnvPriority = prioritymap.DefaultPriority -const customEnvPriority = prioritymap.HighPriority +const ( + defaultEnvPriority = prioritymap.DefaultPriority + customEnvPriority = prioritymap.HighPriority +) type Builder struct { capability capability.Capability @@ -63,6 +68,7 @@ func (statefulSetBuilder Builder) getBase() appsv1.StatefulSet { statefulSetBuilder.addUserAnnotations(&sts) statefulSetBuilder.addLabels(&sts) statefulSetBuilder.addTemplateSpec(&sts) + statefulSetBuilder.addPersistentVolumeClaim(&sts) if statefulSetBuilder.dynakube.FeatureActiveGateAppArmor() { sts.Spec.Template.ObjectMeta.Annotations[consts.AnnotationActiveGateContainerAppArmor] = "runtime/default" @@ -87,6 +93,7 @@ func (statefulSetBuilder Builder) getBaseSpec() appsv1.StatefulSetSpec { ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{ consts.AnnotationActiveGateConfigurationHash: statefulSetBuilder.configHash, + consts.AnnotationActiveGateTenantTokenHash: statefulSetBuilder.dynakube.Status.ActiveGate.ConnectionInfo.TenantTokenHash, }, }, }, @@ -113,21 +120,18 @@ func (statefulSetBuilder Builder) addUserAnnotations(sts *appsv1.StatefulSet) { func (statefulSetBuilder Builder) addTemplateSpec(sts *appsv1.StatefulSet) { podSpec := corev1.PodSpec{ - Containers: statefulSetBuilder.buildBaseContainer(), - NodeSelector: statefulSetBuilder.capability.Properties().NodeSelector, - ServiceAccountName: statefulSetBuilder.dynakube.ActiveGate().GetServiceAccountName(), - Affinity: nodeAffinity(), - Tolerations: statefulSetBuilder.capability.Properties().Tolerations, - SecurityContext: &corev1.PodSecurityContext{ - SeccompProfile: &corev1.SeccompProfile{ - Type: corev1.SeccompProfileTypeRuntimeDefault, - }, - }, - ImagePullSecrets: statefulSetBuilder.dynakube.ImagePullSecretReferences(), - PriorityClassName: statefulSetBuilder.dynakube.Spec.ActiveGate.PriorityClassName, - DNSPolicy: statefulSetBuilder.dynakube.Spec.ActiveGate.DNSPolicy, - - TopologySpreadConstraints: statefulSetBuilder.buildTopologySpreadConstraints(statefulSetBuilder.capability), + Containers: statefulSetBuilder.buildBaseContainer(), + NodeSelector: statefulSetBuilder.capability.Properties().NodeSelector, + ServiceAccountName: statefulSetBuilder.dynakube.ActiveGate().GetServiceAccountName(), + Affinity: statefulSetBuilder.nodeAffinity(), + Tolerations: statefulSetBuilder.capability.Properties().Tolerations, + SecurityContext: statefulSetBuilder.buildPodSecurityContext(), + ImagePullSecrets: statefulSetBuilder.dynakube.ImagePullSecretReferences(), + PriorityClassName: statefulSetBuilder.dynakube.Spec.ActiveGate.PriorityClassName, + DNSPolicy: statefulSetBuilder.dynakube.Spec.ActiveGate.DNSPolicy, + TerminationGracePeriodSeconds: statefulSetBuilder.dynakube.ActiveGate().GetTerminationGracePeriodSeconds(), + TopologySpreadConstraints: statefulSetBuilder.buildTopologySpreadConstraints(statefulSetBuilder.capability), + Volumes: statefulSetBuilder.buildVolumes(), } sts.Spec.Template.Spec = podSpec } @@ -140,6 +144,48 @@ func (statefulSetBuilder Builder) buildTopologySpreadConstraints(capability capa return statefulSetBuilder.defaultTopologyConstraints() } +func (statefulSetBuilder Builder) buildVolumes() []corev1.Volume { + volumes := []corev1.Volume{} + + if statefulSetBuilder.dynakube.Spec.ActiveGate.PersistentVolumeClaim == nil { + if !isDefaultPVCNeeded(statefulSetBuilder.dynakube) { + volumes = append(volumes, corev1.Volume{ + Name: consts.GatewayTmpVolumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }) + } + } + + return volumes +} + +func (statefulSetBuilder Builder) buildVolumeMounts() []corev1.VolumeMount { + var volumeMounts []corev1.VolumeMount + + volumeMounts = append(volumeMounts, corev1.VolumeMount{ + Name: consts.GatewayTmpVolumeName, + MountPath: consts.GatewayTmpMountPoint, + }) + + return volumeMounts +} + +func (statefulSetBuilder Builder) buildPodSecurityContext() *corev1.PodSecurityContext { + sc := corev1.PodSecurityContext{ + SeccompProfile: &corev1.SeccompProfile{ + Type: corev1.SeccompProfileTypeRuntimeDefault, + }, + } + + if !statefulSetBuilder.dynakube.Spec.ActiveGate.UseEphemeralVolume { + sc.FSGroup = ptr.To(consts.DockerImageGroup) + } + + return &sc +} + func (statefulSetBuilder Builder) defaultTopologyConstraints() []corev1.TopologySpreadConstraint { appLabels := statefulSetBuilder.buildAppLabels() @@ -180,6 +226,7 @@ func (statefulSetBuilder Builder) buildBaseContainer() []corev1.Container { TimeoutSeconds: 2, }, SecurityContext: modifiers.GetSecurityContext(false), + VolumeMounts: statefulSetBuilder.buildVolumeMounts(), } return []corev1.Container{container} @@ -200,9 +247,10 @@ func (statefulSetBuilder Builder) buildCommonEnvs() []corev1.EnvVar { Name: deploymentmetadata.GetDeploymentMetadataConfigMapName(statefulSetBuilder.dynakube.Name), }, Key: deploymentmetadata.ActiveGateMetadataKey, - Optional: address.Of(false), + Optional: ptr.To(false), }, }}, + {Name: consts.EnvDtHttpPort, Value: strconv.Itoa(consts.HttpContainerPort)}, }) if statefulSetBuilder.capability.Properties().Group != "" { @@ -213,25 +261,69 @@ func (statefulSetBuilder Builder) buildCommonEnvs() []corev1.EnvVar { prioritymap.Append(statefulSetBuilder.envMap, corev1.EnvVar{Name: consts.EnvDtNetworkZone, Value: statefulSetBuilder.dynakube.Spec.NetworkZone}) } - if statefulSetBuilder.dynakube.ActiveGate().IsMetricsIngestEnabled() { - prioritymap.Append(statefulSetBuilder.envMap, corev1.EnvVar{Name: consts.EnvDtHttpPort, Value: strconv.Itoa(consts.HttpContainerPort)}) - } - prioritymap.Append(statefulSetBuilder.envMap, statefulSetBuilder.capability.Properties().Env, prioritymap.WithPriority(customEnvPriority)) return statefulSetBuilder.envMap.AsEnvVars() } -func nodeAffinity() *corev1.Affinity { - return &corev1.Affinity{ - NodeAffinity: &corev1.NodeAffinity{ - RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{ - NodeSelectorTerms: []corev1.NodeSelectorTerm{ - { - MatchExpressions: node.AffinityNodeRequirementForSupportedArches(), - }, +func (statefulSetBuilder Builder) nodeAffinity() *corev1.Affinity { + var affinity corev1.Affinity + if statefulSetBuilder.dynakube.Status.ActiveGate.Source == status.TenantRegistryVersionSource || statefulSetBuilder.dynakube.Status.ActiveGate.Source == status.CustomVersionVersionSource { + affinity = node.AMDOnlyAffinity() + } else { + affinity = node.Affinity() + } + + return &affinity +} + +func isDefaultPVCNeeded(dk dynakube.DynaKube) bool { + return dk.TelemetryIngest().IsEnabled() && !dk.Spec.ActiveGate.UseEphemeralVolume +} + +func (statefulSetBuilder Builder) addPersistentVolumeClaim(sts *appsv1.StatefulSet) { + if statefulSetBuilder.dynakube.Spec.ActiveGate.PersistentVolumeClaim != nil { + // validation webhook ensures that statefulSetBuilder.dynakube.Spec.ActiveGate.UseEphemeralVolume is false at this point + sts.Spec.VolumeClaimTemplates = []corev1.PersistentVolumeClaim{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: consts.GatewayTmpVolumeName, + }, + Spec: *statefulSetBuilder.dynakube.Spec.ActiveGate.PersistentVolumeClaim, + }, + } + sts.Spec.PersistentVolumeClaimRetentionPolicy = defaultPVCRetentionPolicy() + } else if isDefaultPVCNeeded(statefulSetBuilder.dynakube) { + sts.Spec.VolumeClaimTemplates = []corev1.PersistentVolumeClaim{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: consts.GatewayTmpVolumeName, }, + Spec: defaultPVCSpec(), + }, + } + sts.Spec.PersistentVolumeClaimRetentionPolicy = defaultPVCRetentionPolicy() + } + + statefulset.SetPVCAnnotation()(sts) +} + +func defaultPVCSpec() corev1.PersistentVolumeClaimSpec { + return corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{ + corev1.ReadWriteOnce, + }, + Resources: corev1.VolumeResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: resource.MustParse("1Gi"), }, }, } } + +func defaultPVCRetentionPolicy() *appsv1.StatefulSetPersistentVolumeClaimRetentionPolicy { + return &appsv1.StatefulSetPersistentVolumeClaimRetentionPolicy{ + WhenDeleted: appsv1.DeletePersistentVolumeClaimRetentionPolicyType, + WhenScaled: appsv1.DeletePersistentVolumeClaimRetentionPolicyType, + } +} diff --git a/pkg/controllers/dynakube/activegate/internal/statefulset/statefulset_test.go b/pkg/controllers/dynakube/activegate/internal/statefulset/statefulset_test.go index ada84d3849..45df16a836 100644 --- a/pkg/controllers/dynakube/activegate/internal/statefulset/statefulset_test.go +++ b/pkg/controllers/dynakube/activegate/internal/statefulset/statefulset_test.go @@ -5,29 +5,33 @@ import ( "strconv" "testing" + "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/communication" "github.com/Dynatrace/dynatrace-operator/pkg/api/status" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/telemetryingest" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/capability" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/consts" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/internal/statefulset/builder" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/internal/statefulset/builder/modifiers" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/deploymentmetadata" - "github.com/Dynatrace/dynatrace-operator/pkg/util/address" agutil "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/activegate" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/env" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/labels" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/statefulset" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/exp/slices" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" ) const ( testKubeUID = "test-uid" testConfigHash = "test-hash" + testTokenHash = "test-hash-token" testDynakubeName = "test-dynakube" testNamespaceName = "test-namespace" ) @@ -49,13 +53,16 @@ func getTestDynakube() dynakube.DynaKube { activegate.RoutingCapability.DisplayName, }, CapabilityProperties: activegate.CapabilityProperties{ - Replicas: address.Of(testReplicas), + Replicas: ptr.To(testReplicas), }, }, }, Status: dynakube.DynaKubeStatus{ ActiveGate: activegate.Status{ VersionStatus: status.VersionStatus{}, + ConnectionInfo: communication.ConnectionInfo{ + TenantTokenHash: testTokenHash, + }, }, }, } @@ -81,12 +88,40 @@ func TestGetBaseObjectMeta(t *testing.T) { sts, _ := builder.CreateStatefulSet(nil) expectedTemplateAnnotations := map[string]string{ consts.AnnotationActiveGateConfigurationHash: testConfigHash, + consts.AnnotationActiveGateTenantTokenHash: testTokenHash, } require.NotEmpty(t, sts.Spec.Template.Labels) assert.Equal(t, expectedTemplateAnnotations, sts.Spec.Template.Annotations) }) - t.Run("has default node affinity", func(t *testing.T) { + t.Run("has default(tenant-registry) node affinity", func(t *testing.T) { + dk := getTestDynakube() + dk.Status.ActiveGate.VersionStatus.Source = status.TenantRegistryVersionSource + multiCapability := capability.NewMultiCapability(&dk) + builder := NewStatefulSetBuilder(testKubeUID, testConfigHash, dk, multiCapability) + sts, _ := builder.CreateStatefulSet(nil) + expectedNodeSelectorTerms := []corev1.NodeSelectorTerm{ + { + MatchExpressions: []corev1.NodeSelectorRequirement{ + { + Key: "kubernetes.io/arch", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"amd64"}, + }, + { + Key: "kubernetes.io/os", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"linux"}, + }, + }, + }} + + require.NotEmpty(t, sts.Spec.Template.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms) + assert.Contains(t, sts.Spec.Template.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms, expectedNodeSelectorTerms[0]) + }) + t.Run("has none tenant-registry node affinity", func(t *testing.T) { + dk := getTestDynakube() + dk.Status.ActiveGate.VersionStatus.Source = status.CustomImageVersionSource multiCapability := capability.NewMultiCapability(&dk) builder := NewStatefulSetBuilder(testKubeUID, testConfigHash, dk, multiCapability) sts, _ := builder.CreateStatefulSet(nil) @@ -118,6 +153,7 @@ func TestGetBaseObjectMeta(t *testing.T) { sts, _ := builder.CreateStatefulSet(nil) expectedTemplateAnnotations := map[string]string{ consts.AnnotationActiveGateConfigurationHash: testConfigHash, + consts.AnnotationActiveGateTenantTokenHash: testTokenHash, "test": "test", } @@ -139,6 +175,7 @@ func TestGetBaseSpec(t *testing.T) { assert.Equal(t, &testReplicas, stsSpec.Replicas) require.NotNil(t, stsSpec.Template.Annotations) assert.Equal(t, testConfigHash, stsSpec.Template.Annotations[consts.AnnotationActiveGateConfigurationHash]) + assert.Equal(t, testTokenHash, stsSpec.Template.Annotations[consts.AnnotationActiveGateTenantTokenHash]) }) } @@ -191,7 +228,7 @@ func TestAddTemplateSpec(t *testing.T) { assert.NotEmpty(t, spec.Containers) assert.NotEmpty(t, spec.Affinity) - assert.Equal(t, len(dk.PullSecretNames()), len(spec.ImagePullSecrets)) + assert.Len(t, dk.PullSecretNames(), len(spec.ImagePullSecrets)) assert.Equal(t, dk.PullSecretNames()[0], spec.ImagePullSecrets[0].Name) }) @@ -344,15 +381,14 @@ func TestBuildCommonEnvs(t *testing.T) { require.NotNil(t, idEnv) assert.Equal(t, testKubeUID, idEnv.Value) + dtHttpPortEnv := env.FindEnvVar(envs, consts.EnvDtHttpPort) + require.NotNil(t, dtHttpPortEnv) + metadataEnv := env.FindEnvVar(envs, deploymentmetadata.EnvDtDeploymentMetadata) require.NotNil(t, metadataEnv) assert.NotEmpty(t, metadataEnv.ValueFrom.ConfigMapKeyRef) assert.Equal(t, deploymentmetadata.ActiveGateMetadataKey, metadataEnv.ValueFrom.ConfigMapKeyRef.Key) assert.Equal(t, deploymentmetadata.GetDeploymentMetadataConfigMapName(dk.Name), metadataEnv.ValueFrom.ConfigMapKeyRef.Name) - - // metrics-ingest disabled -> HTTP port disabled - dtHttpPortEnv := env.FindEnvVar(envs, consts.EnvDtHttpPort) - require.Nil(t, dtHttpPortEnv) }) t.Run("adds extra envs with overrides", func(t *testing.T) { @@ -454,3 +490,190 @@ func TestSecurityContexts(t *testing.T) { require.Truef(t, reflect.DeepEqual(sts.Spec.Template.Spec.InitContainers[0].SecurityContext, sts.Spec.Template.Spec.Containers[0].SecurityContext), "InitContainer and Container have different SecurityContexts") }) } + +func TestTempVolume(t *testing.T) { + myPVCSpec := corev1.PersistentVolumeClaimSpec{ + StorageClassName: ptr.To("test"), + VolumeName: "foo-pv", + } + + tests := []struct { + name string + telemetryIngest *telemetryingest.Spec + pvc *corev1.PersistentVolumeClaimSpec + useEphemeral bool + emptyDirExpected bool + pvcExpected bool + expectedPvcSpec corev1.PersistentVolumeClaimSpec + }{ + { + name: "EmptyDir and no PVC when PersistentVolumeClaim = nil, TelemetryIngest enabled, UseEphemeralVolume = true", + pvc: nil, + telemetryIngest: &telemetryingest.Spec{}, + useEphemeral: true, + emptyDirExpected: true, + pvcExpected: false, + }, + { + name: "default PVC and no EmptyDir when PersistentVolumeClaim = nil, TelemetryIngest enabled, UseEphemeralVolume = false", + pvc: nil, + telemetryIngest: &telemetryingest.Spec{}, + useEphemeral: false, + emptyDirExpected: false, + pvcExpected: true, + expectedPvcSpec: defaultPVCSpec(), + }, + { + name: "EmptyDir and no PVC when PersistentVolumeClaim = nil, TelemetryIngest not enabled, UseEphemeralVolume = true", + pvc: nil, + telemetryIngest: nil, + useEphemeral: true, + emptyDirExpected: true, + pvcExpected: false, + }, + { + name: "EmptyDir and no PVC when PersistentVolumeClaim = nil, TelemetryIngest not enabled, UseEphemeralVolume = false", + pvc: nil, + telemetryIngest: nil, + useEphemeral: false, + emptyDirExpected: true, + pvcExpected: false, + }, + { + name: "custom PVC and no EmptyDir when PersistentVolumeClaim != nil, TelemetryIngest enabled, UseEphemeralVolume = false", + pvc: &myPVCSpec, + telemetryIngest: &telemetryingest.Spec{}, + useEphemeral: false, + emptyDirExpected: false, + pvcExpected: true, + expectedPvcSpec: myPVCSpec, + }, + { + name: "custom PVC and no EmptyDir when PersistentVolumeClaim != nil, TelemetryIngest enabled, UseEphemeralVolume = true", + pvc: &myPVCSpec, + telemetryIngest: &telemetryingest.Spec{}, + useEphemeral: true, + emptyDirExpected: false, + pvcExpected: true, + expectedPvcSpec: myPVCSpec, + }, + { + name: "custom PVC and no EmptyDir when PersistentVolumeClaim != nil, TelemetryIngest not enabled, UseEphemeralVolume = false", + pvc: &myPVCSpec, + telemetryIngest: nil, + useEphemeral: false, + emptyDirExpected: false, + pvcExpected: true, + expectedPvcSpec: myPVCSpec, + }, + { + name: "custom PVC and no EmptyDir when PersistentVolumeClaim != nil, TelemetryIngest not enabled, UseEphemeralVolume = true", + pvc: &myPVCSpec, + telemetryIngest: nil, + useEphemeral: true, + emptyDirExpected: false, + pvcExpected: true, + expectedPvcSpec: myPVCSpec, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + dk := getTestDynakube() + + dk.Spec.ActiveGate.PersistentVolumeClaim = test.pvc + dk.Spec.TelemetryIngest = test.telemetryIngest + dk.Spec.ActiveGate.UseEphemeralVolume = test.useEphemeral + + multiCapability := capability.NewMultiCapability(&dk) + statefulsetBuilder := NewStatefulSetBuilder(testKubeUID, testConfigHash, dk, multiCapability) + sts, _ := statefulsetBuilder.CreateStatefulSet([]builder.Modifier{ + modifiers.NewKubernetesMonitoringModifier(dk, multiCapability), + modifiers.NewReadOnlyModifier(dk), + }) + + require.NotEmpty(t, sts) + + expectedEmptyDirVolume := corev1.Volume{ + Name: consts.GatewayTmpVolumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + } + + if test.emptyDirExpected { + require.Contains(t, sts.Spec.Template.Spec.Volumes, expectedEmptyDirVolume) + } else { + require.NotContains(t, sts.Spec.Template.Spec.Volumes, expectedEmptyDirVolume) + } + + if test.pvcExpected { + require.Len(t, sts.Spec.VolumeClaimTemplates, 1) + assert.Equal(t, test.expectedPvcSpec, sts.Spec.VolumeClaimTemplates[0].Spec) + assert.Equal(t, consts.GatewayTmpVolumeName, sts.Spec.VolumeClaimTemplates[0].Name) + assert.Equal(t, defaultPVCRetentionPolicy(), sts.Spec.PersistentVolumeClaimRetentionPolicy) + + assert.Contains(t, sts.Annotations, statefulset.AnnotationPVCHash) + } else { + require.Empty(t, sts.Spec.VolumeClaimTemplates) + assert.NotContains(t, sts.Annotations, statefulset.AnnotationPVCHash) + } + }) + } +} + +func TestVolumeMounts(t *testing.T) { + t.Run("volume mount is presented in Container volumeMount list", func(t *testing.T) { + dk := getTestDynakube() + multiCapability := capability.NewMultiCapability(&dk) + statefulsetBuilder := NewStatefulSetBuilder(testKubeUID, testConfigHash, dk, multiCapability) + sts, _ := statefulsetBuilder.CreateStatefulSet([]builder.Modifier{ + modifiers.NewKubernetesMonitoringModifier(dk, multiCapability), + modifiers.NewReadOnlyModifier(dk), + }) + + require.NotEmpty(t, sts) + + expectedVolumeMount := corev1.VolumeMount{ + Name: consts.GatewayTmpVolumeName, + MountPath: consts.GatewayTmpMountPoint, + } + require.Contains(t, sts.Spec.Template.Spec.Containers[0].VolumeMounts, expectedVolumeMount) + }) +} + +func TestTerminationGracePeriodSeconds(t *testing.T) { + tests := []struct { + name string + gracePeriodSeconds *int64 + }{ + { + name: "gracePeriodSeconds is zero", + gracePeriodSeconds: ptr.To(int64(0)), + }, + { + name: "gracePeriodSeconds is positive", + gracePeriodSeconds: ptr.To(int64(1)), + }, + { + name: "gracePeriodSeconds is negative", + gracePeriodSeconds: ptr.To(int64(-1)), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + dk := getTestDynakube() + dk.ActiveGate().TerminationGracePeriodSeconds = test.gracePeriodSeconds + assert.Equal(t, *test.gracePeriodSeconds, *dk.ActiveGate().GetTerminationGracePeriodSeconds()) + }) + } +} + +func TestTerminationGracePeriodSecondsNil(t *testing.T) { + t.Run("gracePeriodSeconds is nil", func(t *testing.T) { + dk := getTestDynakube() + dk.ActiveGate().TerminationGracePeriodSeconds = nil + assert.Nil(t, dk.ActiveGate().GetTerminationGracePeriodSeconds()) + }) +} diff --git a/pkg/controllers/dynakube/activegate/internal/tls/conditions.go b/pkg/controllers/dynakube/activegate/internal/tls/conditions.go new file mode 100644 index 0000000000..5a61bbe7f1 --- /dev/null +++ b/pkg/controllers/dynakube/activegate/internal/tls/conditions.go @@ -0,0 +1,5 @@ +package tls + +const ( + conditionType = "TLSSecret" +) diff --git a/pkg/controllers/dynakube/activegate/internal/tls/config.go b/pkg/controllers/dynakube/activegate/internal/tls/config.go new file mode 100644 index 0000000000..fca05602f6 --- /dev/null +++ b/pkg/controllers/dynakube/activegate/internal/tls/config.go @@ -0,0 +1,7 @@ +package tls + +import "github.com/Dynatrace/dynatrace-operator/pkg/logd" + +var ( + log = logd.Get().WithName("dynakube-activegate-tls-secret") +) diff --git a/pkg/controllers/dynakube/activegate/internal/tls/reconciler.go b/pkg/controllers/dynakube/activegate/internal/tls/reconciler.go new file mode 100644 index 0000000000..639fb24a49 --- /dev/null +++ b/pkg/controllers/dynakube/activegate/internal/tls/reconciler.go @@ -0,0 +1,172 @@ +package tls + +import ( + "context" + "crypto/x509" + "net" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/consts" + "github.com/Dynatrace/dynatrace-operator/pkg/util/certificates" + "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" + k8slabels "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/labels" + k8ssecret "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/secret" + "github.com/Dynatrace/dynatrace-operator/pkg/util/timeprovider" + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + activeGateSelfSignedTLSCommonNameSuffix = "activegate" + + tlsCrtDataName = "server.crt" +) + +type Reconciler struct { + client client.Client + apiReader client.Reader + dk *dynakube.DynaKube + timeProvider *timeprovider.Provider +} + +type ReconcilerBuilder func(client client.Client, apiReader client.Reader, dk *dynakube.DynaKube) *Reconciler + +func NewReconciler(client client.Client, apiReader client.Reader, dk *dynakube.DynaKube) *Reconciler { + return &Reconciler{ + client: client, + dk: dk, + apiReader: apiReader, + timeProvider: timeprovider.New(), + } +} + +func (r *Reconciler) Reconcile(ctx context.Context) error { + if r.dk.ActiveGate().IsEnabled() && r.dk.ActiveGate().IsAutomaticTlsSecretEnabled() && r.dk.ActiveGate().TlsSecretName == "" { + return r.reconcileSelfSignedTLSSecret(ctx) + } + + if meta.FindStatusCondition(*r.dk.Conditions(), conditionType) == nil { + return nil + } + defer meta.RemoveStatusCondition(r.dk.Conditions(), conditionType) + + return r.deleteSelfSignedTLSSecret(ctx) +} + +func (r *Reconciler) reconcileSelfSignedTLSSecret(ctx context.Context) error { + query := k8ssecret.Query(r.client, r.client, log) + + _, err := query.Get(ctx, types.NamespacedName{ + Name: r.dk.ActiveGate().GetTLSSecretName(), + Namespace: r.dk.Namespace, + }) + + if err != nil && k8serrors.IsNotFound(err) { + return r.createSelfSignedTLSSecret(ctx) + } + + if err != nil { + conditions.SetKubeApiError(r.dk.Conditions(), conditionType, err) + + return err + } + + return nil +} + +func (r *Reconciler) deleteSelfSignedTLSSecret(ctx context.Context) error { + query := k8ssecret.Query(r.client, r.client, log) + + return query.Delete(ctx, &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: r.dk.ActiveGate().GetTLSSecretName(), + Namespace: r.dk.Namespace, + }, + }) +} + +func (r *Reconciler) createSelfSignedTLSSecret(ctx context.Context) error { + cert, err := certificates.New(r.timeProvider) + if err != nil { + conditions.SetSecretGenFailed(r.dk.Conditions(), conditionType, err) + + return err + } + + cert.Cert.DNSNames = certificates.AltNames(r.dk.Name, r.dk.Namespace, activeGateSelfSignedTLSCommonNameSuffix) + cert.Cert.KeyUsage = x509.KeyUsageKeyEncipherment | x509.KeyUsageDataEncipherment + cert.Cert.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth} + cert.Cert.Subject.CommonName = certificates.CommonName(r.dk.Name, r.dk.Namespace, activeGateSelfSignedTLSCommonNameSuffix) + + ipAddresses, err := getCertificateAltIPs(r.dk.Status.ActiveGate.ServiceIPs) + if err != nil { + conditions.SetSecretGenFailed(r.dk.Conditions(), conditionType, err) + + return err + } + + cert.Cert.IPAddresses = ipAddresses + + err = cert.SelfSign() + if err != nil { + conditions.SetSecretGenFailed(r.dk.Conditions(), conditionType, err) + + return err + } + + pemCert, pemPk, err := cert.ToPEM() + if err != nil { + conditions.SetSecretGenFailed(r.dk.Conditions(), conditionType, err) + + return err + } + + coreLabels := k8slabels.NewCoreLabels(r.dk.Name, k8slabels.ActiveGateComponentLabel) + secretData := map[string][]byte{ + consts.TLSCrtDataName: pemCert, + consts.TLSKeyDataName: pemPk, + tlsCrtDataName: pemCert, + } + + secret, err := k8ssecret.Build(r.dk, r.dk.ActiveGate().GetTLSSecretName(), secretData, k8ssecret.SetLabels(coreLabels.BuildLabels())) + if err != nil { + conditions.SetSecretGenFailed(r.dk.Conditions(), conditionType, err) + + return err + } + + secret.Type = corev1.SecretTypeOpaque + + query := k8ssecret.Query(r.client, r.client, log) + + err = query.Create(ctx, secret) + if err != nil { + conditions.SetKubeApiError(r.dk.Conditions(), conditionType, err) + + return err + } + + conditions.SetSecretCreated(r.dk.Conditions(), conditionType, secret.Name) + + return nil +} + +func getCertificateAltIPs(ips []string) ([]net.IP, error) { + altIPs := []net.IP{} + + for _, ip := range ips { + netIP := net.ParseIP(ip) + if netIP == nil { + return nil, errors.Errorf("failed to parse '%s' IP address", ip) + } + + altIPs = append(altIPs, netIP) + } + + return altIPs, nil +} diff --git a/pkg/controllers/dynakube/activegate/internal/tls/reconciler_test.go b/pkg/controllers/dynakube/activegate/internal/tls/reconciler_test.go new file mode 100644 index 0000000000..c81a7d6afd --- /dev/null +++ b/pkg/controllers/dynakube/activegate/internal/tls/reconciler_test.go @@ -0,0 +1,131 @@ +package tls + +import ( + "context" + "fmt" + "testing" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + testNamespace = "test-namespace" + testDynakubeName = "test-dynakube" +) + +func TestReconciler_Reconcile(t *testing.T) { + t.Run(`ActiveGate disabled`, func(t *testing.T) { + dk := &dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNamespace, + Name: testDynakubeName, + }, + } + fakeClient := fake.NewClient() + r := NewReconciler(fakeClient, fakeClient, dk) + err := r.Reconcile(context.Background()) + require.NoError(t, err) + + agTLSSecret := corev1.Secret{} + err = r.client.Get(context.Background(), client.ObjectKey{Name: r.dk.ActiveGate().GetTLSSecretName(), Namespace: r.dk.Namespace}, &agTLSSecret) + + require.Error(t, err) + assert.True(t, k8serrors.IsNotFound(err)) + }) + + t.Run(`custom ActiveGate TLS secret exists`, func(t *testing.T) { + dk := &dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNamespace, + Name: testDynakubeName, + }, + Spec: dynakube.DynaKubeSpec{ + ActiveGate: activegate.Spec{ + Capabilities: []activegate.CapabilityDisplayName{ + activegate.RoutingCapability.DisplayName, + }, + TlsSecretName: "test", + }, + }, + } + fakeClient := fake.NewClient() + r := NewReconciler(fakeClient, fakeClient, dk) + err := r.Reconcile(context.Background()) + require.NoError(t, err) + + agTLSSecret := corev1.Secret{} + err = r.client.Get(context.Background(), client.ObjectKey{Name: r.dk.ActiveGate().GetTLSSecretName(), Namespace: r.dk.Namespace}, &agTLSSecret) + + require.Error(t, err) + assert.True(t, k8serrors.IsNotFound(err)) + }) + + t.Run(`automatic-tls-certificate feature disabled`, func(t *testing.T) { + dk := &dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNamespace, + Name: testDynakubeName, + Annotations: map[string]string{ + dynakube.AnnotationFeatureActiveGateAutomaticTLSCertificate: "false", + }, + }, + Spec: dynakube.DynaKubeSpec{ + ActiveGate: activegate.Spec{ + Capabilities: []activegate.CapabilityDisplayName{ + activegate.RoutingCapability.DisplayName, + }, + }, + }, + } + fakeClient := fake.NewClient() + r := NewReconciler(fakeClient, fakeClient, dk) + err := r.Reconcile(context.Background()) + require.NoError(t, err) + + agTLSSecret := corev1.Secret{} + err = r.client.Get(context.Background(), client.ObjectKey{Name: r.dk.ActiveGate().GetTLSSecretName(), Namespace: r.dk.Namespace}, &agTLSSecret) + + require.Error(t, err) + assert.True(t, k8serrors.IsNotFound(err)) + }) + + t.Run(`secret created`, func(t *testing.T) { + dk := &dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNamespace, + Name: testDynakubeName, + }, + Spec: dynakube.DynaKubeSpec{ + ActiveGate: activegate.Spec{ + Capabilities: []activegate.CapabilityDisplayName{ + activegate.RoutingCapability.DisplayName, + }, + }, + }, + } + fakeClient := fake.NewClient() + r := NewReconciler(fakeClient, fakeClient, dk) + err := r.Reconcile(context.Background()) + require.NoError(t, err) + + agTLSSecret := corev1.Secret{} + err = r.client.Get(context.Background(), client.ObjectKey{Name: r.dk.ActiveGate().GetTLSSecretName(), Namespace: r.dk.Namespace}, &agTLSSecret) + + require.NoError(t, err) + + condition := meta.FindStatusCondition(r.dk.Status.Conditions, conditionType) + assert.Equal(t, metav1.ConditionTrue, condition.Status) + assert.Equal(t, conditions.SecretCreatedReason, condition.Reason) + assert.Equal(t, fmt.Sprintf("%s created", agTLSSecret.Name), condition.Message) + }) +} diff --git a/pkg/controllers/dynakube/activegate/reconciler.go b/pkg/controllers/dynakube/activegate/reconciler.go index add8f3b148..e73b6458c0 100644 --- a/pkg/controllers/dynakube/activegate/reconciler.go +++ b/pkg/controllers/dynakube/activegate/reconciler.go @@ -2,7 +2,7 @@ package activegate import ( "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/value" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/controllers" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/capability" @@ -10,6 +10,7 @@ import ( capabilityInternal "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/internal/capability" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/internal/customproperties" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/internal/statefulset" + "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/internal/tls" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/connectioninfo" agconnectioninfo "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/connectioninfo/activegate" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/dtpullsecret" @@ -180,8 +181,9 @@ func extractPublicData(dk *dynakube.DynaKube) map[string]string { func (r *Reconciler) createCapability(ctx context.Context, agCapability capability.Capability) error { customPropertiesReconciler := r.newCustomPropertiesReconcilerFunc(r.dk.ActiveGate().GetServiceAccountOwner(), agCapability.Properties().CustomProperties) //nolint:typeCheck statefulsetReconciler := r.newStatefulsetReconcilerFunc(r.client, r.apiReader, r.dk, agCapability) //nolint:typeCheck + tlsSecretReconciler := tls.NewReconciler(r.client, r.apiReader, r.dk) - capabilityReconciler := r.newCapabilityReconcilerFunc(r.client, agCapability, r.dk, statefulsetReconciler, customPropertiesReconciler) + capabilityReconciler := r.newCapabilityReconcilerFunc(r.client, agCapability, r.dk, statefulsetReconciler, customPropertiesReconciler, tlsSecretReconciler) return capabilityReconciler.Reconcile(ctx) } @@ -199,10 +201,6 @@ func (r *Reconciler) deleteCapability(ctx context.Context, agCapability capabili } func (r *Reconciler) deleteService(ctx context.Context, agCapability capability.Capability) error { - if r.dk.ActiveGate().NeedsService() { - return nil - } - svc := corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: capability.BuildServiceName(r.dk.Name, agCapability.ShortName()), diff --git a/pkg/controllers/dynakube/activegate/reconciler_test.go b/pkg/controllers/dynakube/activegate/reconciler_test.go index 1a2b746ac1..6bf264d1d0 100644 --- a/pkg/controllers/dynakube/activegate/reconciler_test.go +++ b/pkg/controllers/dynakube/activegate/reconciler_test.go @@ -7,8 +7,8 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/communication" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/value" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/controllers" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/capability" @@ -163,7 +163,7 @@ func TestReconciler_Reconcile(t *testing.T) { newStatefulsetReconcilerFunc: func(_ client.Client, _ client.Reader, _ *dynakube.DynaKube, _ capability.Capability) controllers.Reconciler { return fakeReconciler }, - newCapabilityReconcilerFunc: func(_ client.Client, _ capability.Capability, _ *dynakube.DynaKube, _ controllers.Reconciler, _ controllers.Reconciler) controllers.Reconciler { + newCapabilityReconcilerFunc: func(_ client.Client, _ capability.Capability, _ *dynakube.DynaKube, _ controllers.Reconciler, _ controllers.Reconciler, _ controllers.Reconciler) controllers.Reconciler { return fakeReconciler }, newCustomPropertiesReconcilerFunc: func(_ string, customPropertiesSource *value.Source) controllers.Reconciler { @@ -184,7 +184,7 @@ func TestReconciler_Reconcile(t *testing.T) { newStatefulsetReconcilerFunc: func(_ client.Client, _ client.Reader, _ *dynakube.DynaKube, _ capability.Capability) controllers.Reconciler { return fakeReconciler }, - newCapabilityReconcilerFunc: func(_ client.Client, _ capability.Capability, _ *dynakube.DynaKube, _ controllers.Reconciler, _ controllers.Reconciler) controllers.Reconciler { + newCapabilityReconcilerFunc: func(_ client.Client, _ capability.Capability, _ *dynakube.DynaKube, _ controllers.Reconciler, _ controllers.Reconciler, _ controllers.Reconciler) controllers.Reconciler { return fakeReconciler }, newCustomPropertiesReconcilerFunc: func(_ string, customPropertiesSource *value.Source) controllers.Reconciler { @@ -435,19 +435,24 @@ func TestServiceCreation(t *testing.T) { }, } - t.Run("service exposes correct ports for single capabilities", func(t *testing.T) { + t.Run("service exposes all ports for every capabilities", func(t *testing.T) { expectedCapabilityPorts := map[activegate.CapabilityDisplayName][]string{ activegate.RoutingCapability.DisplayName: { + consts.HttpServicePortName, consts.HttpsServicePortName, }, activegate.MetricsIngestCapability.DisplayName: { - consts.HttpsServicePortName, consts.HttpServicePortName, + consts.HttpsServicePortName, }, activegate.DynatraceApiCapability.DisplayName: { + consts.HttpServicePortName, + consts.HttpsServicePortName, + }, + activegate.KubeMonCapability.DisplayName: { + consts.HttpServicePortName, consts.HttpsServicePortName, }, - activegate.KubeMonCapability.DisplayName: {}, } for capName, expectedPorts := range expectedCapabilityPorts { @@ -477,28 +482,6 @@ func TestServiceCreation(t *testing.T) { assertContainsAllPorts(t, expectedPorts, activegateService.Spec.Ports) } }) - - t.Run("service exposes correct ports for multiple capabilities", func(t *testing.T) { - fakeClient := fake.NewClient(testKubeSystemNamespace) - - reconciler := NewReconciler(fakeClient, fakeClient, dk, dynatraceClient, nil, nil).(*Reconciler) - reconciler.connectionReconciler = createGenericReconcilerMock(t) - reconciler.versionReconciler = createVersionReconcilerMock(t) - reconciler.pullSecretReconciler = createGenericReconcilerMock(t) - - dk.Spec.ActiveGate.Capabilities = []activegate.CapabilityDisplayName{ - activegate.RoutingCapability.DisplayName, - } - expectedPorts := []string{ - consts.HttpsServicePortName, - } - - err := reconciler.Reconcile(context.Background()) - require.NoError(t, err) - - activegateService := getTestActiveGateService(t, fakeClient) - assertContainsAllPorts(t, expectedPorts, activegateService.Spec.Ports) - }) } func assertContainsAllPorts(t *testing.T, expectedPorts []string, servicePorts []corev1.ServicePort) { @@ -562,7 +545,7 @@ func TestReconcile_ActivegateConfigMap(t *testing.T) { newStatefulsetReconcilerFunc: func(_ client.Client, _ client.Reader, _ *dynakube.DynaKube, _ capability.Capability) controllers.Reconciler { return fakeReconciler }, - newCapabilityReconcilerFunc: func(_ client.Client, _ capability.Capability, _ *dynakube.DynaKube, _ controllers.Reconciler, _ controllers.Reconciler) controllers.Reconciler { + newCapabilityReconcilerFunc: func(_ client.Client, _ capability.Capability, _ *dynakube.DynaKube, _ controllers.Reconciler, _ controllers.Reconciler, _ controllers.Reconciler) controllers.Reconciler { return fakeReconciler }, newCustomPropertiesReconcilerFunc: func(_ string, _ *value.Source) controllers.Reconciler { diff --git a/pkg/controllers/dynakube/activegate_test.go b/pkg/controllers/dynakube/activegate_test.go index 25cbaf44f3..566ba17210 100644 --- a/pkg/controllers/dynakube/activegate_test.go +++ b/pkg/controllers/dynakube/activegate_test.go @@ -6,8 +6,8 @@ import ( "testing" "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/apimonitoring" controllermock "github.com/Dynatrace/dynatrace-operator/test/mocks/pkg/controllers" diff --git a/pkg/controllers/dynakube/apimonitoring/reconciler.go b/pkg/controllers/dynakube/apimonitoring/reconciler.go index a59bc4c087..084f8dddaa 100644 --- a/pkg/controllers/dynakube/apimonitoring/reconciler.go +++ b/pkg/controllers/dynakube/apimonitoring/reconciler.go @@ -3,7 +3,7 @@ package apimonitoring import ( "context" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/monitoredentities" "github.com/pkg/errors" diff --git a/pkg/controllers/dynakube/apimonitoring/reconciler_test.go b/pkg/controllers/dynakube/apimonitoring/reconciler_test.go index 2c6e52569d..7968db2bde 100644 --- a/pkg/controllers/dynakube/apimonitoring/reconciler_test.go +++ b/pkg/controllers/dynakube/apimonitoring/reconciler_test.go @@ -2,10 +2,10 @@ package apimonitoring import ( "context" - "fmt" "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" dtclientmock "github.com/Dynatrace/dynatrace-operator/test/mocks/pkg/clients/dynatrace" "github.com/pkg/errors" @@ -72,11 +72,11 @@ func createReadOnlyReconciler(t *testing.T, dk *dynakube.DynaKube, monitoredEnti mock.AnythingOfType("string")). Return(getSettingsResponse, nil) mockClient.On("CreateOrUpdateKubernetesSetting", mock.AnythingOfType("context.backgroundCtx"), testName, testUID, "KUBERNETES_CLUSTER-119C75CCDA94799F"). - Return("", fmt.Errorf("BOOM, readonly only client is used")) + Return("", errors.New("BOOM, readonly only client is used")) mockClient.On("CreateOrUpdateKubernetesSetting", mock.AnythingOfType("context.backgroundCtx"), testName, testUID, "test-MEID"). - Return("", fmt.Errorf("BOOM, readonly only client is used")) + Return("", errors.New("BOOM, readonly only client is used")) mockClient.On("CreateOrUpdateKubernetesAppSetting", mock.AnythingOfType("context.backgroundCtx"), mock.AnythingOfType("string")). - Return("", fmt.Errorf("BOOM, readonly only client is used")) + Return("", errors.New("BOOM, readonly only client is used")) for _, call := range mockClient.ExpectedCalls { call.Maybe() @@ -173,7 +173,7 @@ func TestReconcile(t *testing.T) { // assert require.NoError(t, err) - assert.Equal(t, "", actual) + assert.Empty(t, actual) }) } @@ -191,7 +191,7 @@ func TestReconcileErrors(t *testing.T) { // assert require.Error(t, err) - assert.Equal(t, "", actual) + assert.Empty(t, actual) }) t.Run("don't create setting when get entities api response is error", func(t *testing.T) { @@ -203,7 +203,7 @@ func TestReconcileErrors(t *testing.T) { // assert require.Error(t, err) - assert.Equal(t, "", actual) + assert.Empty(t, actual) }) t.Run("don't create setting when get settings api response is error", func(t *testing.T) { @@ -215,7 +215,7 @@ func TestReconcileErrors(t *testing.T) { // assert require.Error(t, err) - assert.Equal(t, "", actual) + assert.Empty(t, actual) }) t.Run("don't create setting when create settings api response is error", func(t *testing.T) { @@ -227,7 +227,7 @@ func TestReconcileErrors(t *testing.T) { // assert require.Error(t, err) - assert.Equal(t, "", actual) + assert.Empty(t, actual) }) t.Run("create settings successful in case of CreateOrUpdateKubernetesAppSetting error", func(t *testing.T) { @@ -324,8 +324,8 @@ func newDynaKube() *dynakube.DynaKube { }, }, Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - HostMonitoring: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + HostMonitoring: &oneagent.HostInjectSpec{}, }, }, Status: dynakube.DynaKubeStatus{ diff --git a/pkg/controllers/dynakube/conditions.go b/pkg/controllers/dynakube/conditions.go index 57c8548f33..6bf05dcfad 100644 --- a/pkg/controllers/dynakube/conditions.go +++ b/pkg/controllers/dynakube/conditions.go @@ -1,7 +1,7 @@ package dynakube import ( - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) diff --git a/pkg/controllers/dynakube/connectioninfo/activegate/communication_hosts.go b/pkg/controllers/dynakube/connectioninfo/activegate/communication_hosts.go index 97c8b481c5..aa0de5de7c 100644 --- a/pkg/controllers/dynakube/connectioninfo/activegate/communication_hosts.go +++ b/pkg/controllers/dynakube/connectioninfo/activegate/communication_hosts.go @@ -5,7 +5,7 @@ import ( "net/url" "strings" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/pkg/errors" ) diff --git a/pkg/controllers/dynakube/connectioninfo/activegate/communication_hosts_test.go b/pkg/controllers/dynakube/connectioninfo/activegate/communication_hosts_test.go index d63c6fbf7c..d88d440966 100644 --- a/pkg/controllers/dynakube/connectioninfo/activegate/communication_hosts_test.go +++ b/pkg/controllers/dynakube/connectioninfo/activegate/communication_hosts_test.go @@ -4,7 +4,8 @@ import ( "testing" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/communication" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -16,8 +17,8 @@ func TestParseCommunicationHostsFromActiveGateEndpoints(t *testing.T) { Name: "test-name", }, Status: dynakube.DynaKubeStatus{ - OneAgent: dynakube.OneAgentStatus{ - ConnectionInfoStatus: dynakube.OneAgentConnectionInfoStatus{ + OneAgent: oneagent.Status{ + ConnectionInfoStatus: oneagent.ConnectionInfoStatus{ ConnectionInfo: communication.ConnectionInfo{}, }, }, diff --git a/pkg/controllers/dynakube/connectioninfo/activegate/reconciler.go b/pkg/controllers/dynakube/connectioninfo/activegate/reconciler.go index ce132605b0..9cf0c4fe77 100644 --- a/pkg/controllers/dynakube/connectioninfo/activegate/reconciler.go +++ b/pkg/controllers/dynakube/connectioninfo/activegate/reconciler.go @@ -4,11 +4,12 @@ import ( "context" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/communication" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/controllers" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/connectioninfo" "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" + "github.com/Dynatrace/dynatrace-operator/pkg/util/hasher" k8ssecret "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/secret" "github.com/Dynatrace/dynatrace-operator/pkg/util/timeprovider" "github.com/pkg/errors" @@ -110,6 +111,11 @@ func (r *reconciler) reconcileConnectionInfo(ctx context.Context) error { return err } + r.dk.Status.ActiveGate.ConnectionInfo.TenantTokenHash, err = hasher.GenerateHash(connectionInfo.ConnectionInfo.TenantToken) + if err != nil { + return errors.Wrap(err, "failed to generate TenantTokenHash") + } + log.Info("activegate connection info updated") return nil diff --git a/pkg/controllers/dynakube/connectioninfo/activegate/reconciler_test.go b/pkg/controllers/dynakube/connectioninfo/activegate/reconciler_test.go index 663898b72d..38020bb26e 100644 --- a/pkg/controllers/dynakube/connectioninfo/activegate/reconciler_test.go +++ b/pkg/controllers/dynakube/connectioninfo/activegate/reconciler_test.go @@ -2,18 +2,19 @@ package activegate import ( "context" - "fmt" "testing" "time" "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/communication" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/connectioninfo" "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" + "github.com/Dynatrace/dynatrace-operator/pkg/util/hasher" dtclientmock "github.com/Dynatrace/dynatrace-operator/test/mocks/pkg/clients/dynatrace" + "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -108,8 +109,12 @@ func TestReconcile(t *testing.T) { assert.Equal(t, metav1.ConditionTrue, condition.Status) assert.Equal(t, conditions.SecretCreatedReason, condition.Reason) + tenantTokenHash, err := hasher.GenerateHash(testTenantToken) + + require.NoError(t, err) assert.Equal(t, testTenantUUID, dk.Status.ActiveGate.ConnectionInfo.TenantUUID) assert.Equal(t, testTenantEndpoints, dk.Status.ActiveGate.ConnectionInfo.Endpoints) + assert.Equal(t, tenantTokenHash, dk.Status.ActiveGate.ConnectionInfo.TenantTokenHash) var actualSecret corev1.Secret err = fakeClient.Get(ctx, client.ObjectKey{Name: dk.ActiveGate().GetTenantSecretName(), Namespace: testNamespace}, &actualSecret) @@ -149,7 +154,7 @@ func TestReconcile(t *testing.T) { dk := getTestDynakube() fakeClient := fake.NewClientWithInterceptors(interceptor.Funcs{ Get: func(ctx context.Context, client client.WithWatch, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { - return fmt.Errorf("BOOM") + return errors.New("BOOM") }, }) diff --git a/pkg/controllers/dynakube/connectioninfo/oneagent/communication_hosts.go b/pkg/controllers/dynakube/connectioninfo/oneagent/communication_hosts.go index df2b9f27e0..2ec59c751f 100644 --- a/pkg/controllers/dynakube/connectioninfo/oneagent/communication_hosts.go +++ b/pkg/controllers/dynakube/connectioninfo/oneagent/communication_hosts.go @@ -1,7 +1,7 @@ package oaconnectioninfo import ( - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" ) diff --git a/pkg/controllers/dynakube/connectioninfo/oneagent/communication_hosts_test.go b/pkg/controllers/dynakube/connectioninfo/oneagent/communication_hosts_test.go index ec6b41c7ff..2d3cd2ad5e 100644 --- a/pkg/controllers/dynakube/connectioninfo/oneagent/communication_hosts_test.go +++ b/pkg/controllers/dynakube/connectioninfo/oneagent/communication_hosts_test.go @@ -4,7 +4,8 @@ import ( "testing" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/communication" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -17,8 +18,8 @@ func TestGetCommunicationHosts(t *testing.T) { Name: testName, }, Status: dynakube.DynaKubeStatus{ - OneAgent: dynakube.OneAgentStatus{ - ConnectionInfoStatus: dynakube.OneAgentConnectionInfoStatus{ + OneAgent: oneagent.Status{ + ConnectionInfoStatus: oneagent.ConnectionInfoStatus{ ConnectionInfo: communication.ConnectionInfo{}, }, }, @@ -39,7 +40,7 @@ func TestGetCommunicationHosts(t *testing.T) { }) t.Run(`communication-hosts field found`, func(t *testing.T) { - dk.Status.OneAgent.ConnectionInfoStatus.CommunicationHosts = []dynakube.CommunicationHostStatus{ + dk.Status.OneAgent.ConnectionInfoStatus.CommunicationHosts = []oneagent.CommunicationHostStatus{ { Protocol: "protocol", Host: "host", diff --git a/pkg/controllers/dynakube/connectioninfo/oneagent/reconciler.go b/pkg/controllers/dynakube/connectioninfo/oneagent/reconciler.go index 6cf7703832..52b5106035 100644 --- a/pkg/controllers/dynakube/connectioninfo/oneagent/reconciler.go +++ b/pkg/controllers/dynakube/connectioninfo/oneagent/reconciler.go @@ -1,7 +1,8 @@ package oaconnectioninfo import ( - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/controllers" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/connectioninfo" @@ -43,20 +44,20 @@ func NewReconciler(clt client.Client, apiReader client.Reader, dtc dtclient.Clie var NoOneAgentCommunicationHostsError = errors.New("no communication hosts for OneAgent are available") func (r *reconciler) Reconcile(ctx context.Context) error { - if !r.dk.NeedAppInjection() && !r.dk.NeedsOneAgent() && !r.dk.LogMonitoring().IsEnabled() { + if !r.dk.OneAgent().IsAppInjectionNeeded() && !r.dk.OneAgent().IsDaemonsetRequired() && !r.dk.LogMonitoring().IsEnabled() { if meta.FindStatusCondition(*r.dk.Conditions(), oaConnectionInfoConditionType) == nil { return nil // no condition == nothing is there to clean up } query := k8ssecret.Query(r.client, r.apiReader, log) - err := query.Delete(ctx, &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: r.dk.OneagentTenantSecret(), Namespace: r.dk.Namespace}}) + err := query.Delete(ctx, &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: r.dk.OneAgent().GetTenantSecret(), Namespace: r.dk.Namespace}}) if err != nil { log.Error(err, "failed to clean-up OneAgent tenant-secret") } meta.RemoveStatusCondition(r.dk.Conditions(), oaConnectionInfoConditionType) - r.dk.Status.OneAgent.ConnectionInfoStatus = dynakube.OneAgentConnectionInfoStatus{} + r.dk.Status.OneAgent.ConnectionInfoStatus = oneagent.ConnectionInfoStatus{} return nil // clean-up shouldn't cause a failure } @@ -79,7 +80,7 @@ func (r *reconciler) Reconcile(ctx context.Context) error { } func (r *reconciler) reconcileConnectionInfo(ctx context.Context) error { - secretNamespacedName := types.NamespacedName{Name: r.dk.OneagentTenantSecret(), Namespace: r.dk.Namespace} + secretNamespacedName := types.NamespacedName{Name: r.dk.OneAgent().GetTenantSecret(), Namespace: r.dk.Namespace} if !conditions.IsOutdated(r.timeProvider, r.dk, oaConnectionInfoConditionType) { isSecretPresent, err := connectioninfo.IsTenantSecretPresent(ctx, r.apiReader, secretNamespacedName, log) @@ -122,11 +123,16 @@ func (r *reconciler) reconcileConnectionInfo(ctx context.Context) error { return NoOneAgentCommunicationHostsError } - err = r.createTenantTokenSecret(ctx, r.dk.OneagentTenantSecret(), connectionInfo.ConnectionInfo) + err = r.createTenantTokenSecret(ctx, r.dk.OneAgent().GetTenantSecret(), connectionInfo.ConnectionInfo) if err != nil { return err } + r.dk.Status.OneAgent.ConnectionInfoStatus.TenantTokenHash, err = hasher.GenerateHash(connectionInfo.ConnectionInfo.TenantToken) + if err != nil { + return errors.Wrap(err, "failed to generate TenantTokenHash") + } + log.Info("received OneAgent communication hosts", "communication hosts", connectionInfo.CommunicationHosts, "tenant", connectionInfo.TenantUUID) return nil @@ -138,10 +144,10 @@ func (r *reconciler) setDynakubeStatus(connectionInfo dtclient.OneAgentConnectio copyCommunicationHosts(&r.dk.Status.OneAgent.ConnectionInfoStatus, connectionInfo.CommunicationHosts) } -func copyCommunicationHosts(dest *dynakube.OneAgentConnectionInfoStatus, src []dtclient.CommunicationHost) { - dest.CommunicationHosts = make([]dynakube.CommunicationHostStatus, 0, len(src)) +func copyCommunicationHosts(dest *oneagent.ConnectionInfoStatus, src []dtclient.CommunicationHost) { + dest.CommunicationHosts = make([]oneagent.CommunicationHostStatus, 0, len(src)) for _, host := range src { - dest.CommunicationHosts = append(dest.CommunicationHosts, dynakube.CommunicationHostStatus{ + dest.CommunicationHosts = append(dest.CommunicationHosts, oneagent.CommunicationHostStatus{ Protocol: host.Protocol, Host: host.Host, Port: host.Port, diff --git a/pkg/controllers/dynakube/connectioninfo/oneagent/reconciler_test.go b/pkg/controllers/dynakube/connectioninfo/oneagent/reconciler_test.go index d1ab62ca84..480f1e55d0 100644 --- a/pkg/controllers/dynakube/connectioninfo/oneagent/reconciler_test.go +++ b/pkg/controllers/dynakube/connectioninfo/oneagent/reconciler_test.go @@ -7,10 +7,12 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/communication" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/connectioninfo" "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" + "github.com/Dynatrace/dynatrace-operator/pkg/util/hasher" dtclientmock "github.com/Dynatrace/dynatrace-operator/test/mocks/pkg/clients/dynatrace" "github.com/pkg/errors" "github.com/stretchr/testify/assert" @@ -38,7 +40,7 @@ func TestReconcile(t *testing.T) { t.Run("cleanup when oneagent is not needed", func(t *testing.T) { dk := getTestDynakube() - dk.Status.OneAgent.ConnectionInfoStatus = dynakube.OneAgentConnectionInfoStatus{ + dk.Status.OneAgent.ConnectionInfoStatus = oneagent.ConnectionInfoStatus{ ConnectionInfo: communication.ConnectionInfo{ TenantUUID: testOutdated, Endpoints: testOutdated, @@ -48,7 +50,7 @@ func TestReconcile(t *testing.T) { dk.Spec = dynakube.DynaKubeSpec{} - fakeClient := fake.NewClient(&corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: dk.OneagentTenantSecret(), Namespace: dk.Namespace}}) + fakeClient := fake.NewClient(&corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: dk.OneAgent().GetTenantSecret(), Namespace: dk.Namespace}}) dtc := dtclientmock.NewClient(t) r := NewReconciler(fakeClient, fakeClient, dtc, dk) @@ -60,21 +62,21 @@ func TestReconcile(t *testing.T) { require.Nil(t, condition) var actualSecret corev1.Secret - err = fakeClient.Get(ctx, client.ObjectKey{Name: dk.OneagentTenantSecret(), Namespace: testNamespace}, &actualSecret) + err = fakeClient.Get(ctx, client.ObjectKey{Name: dk.OneAgent().GetTenantSecret(), Namespace: testNamespace}, &actualSecret) require.Error(t, err) assert.True(t, k8serrors.IsNotFound(err)) }) t.Run("does not cleanup when only host oneagent is needed", func(t *testing.T) { dk := getTestDynakube() - dk.Status.OneAgent.ConnectionInfoStatus = dynakube.OneAgentConnectionInfoStatus{ + dk.Status.OneAgent.ConnectionInfoStatus = oneagent.ConnectionInfoStatus{ ConnectionInfo: communication.ConnectionInfo{ TenantUUID: testOutdated, Endpoints: testOutdated, }, } dk.Spec = dynakube.DynaKubeSpec{} - dk.Spec.OneAgent.ClassicFullStack = &dynakube.HostInjectSpec{} + dk.Spec.OneAgent.ClassicFullStack = &oneagent.HostInjectSpec{} conditions.SetSecretCreated(dk.Conditions(), oaConnectionInfoConditionType, "testing") @@ -130,12 +132,16 @@ func TestReconcile(t *testing.T) { err := r.Reconcile(ctx) require.NoError(t, err) + tenantTokenHash, err := hasher.GenerateHash(testTenantToken) + + require.NoError(t, err) assert.Equal(t, testTenantUUID, dk.Status.OneAgent.ConnectionInfoStatus.TenantUUID) assert.Equal(t, testTenantEndpoints, dk.Status.OneAgent.ConnectionInfoStatus.Endpoints) assert.Equal(t, getTestCommunicationHosts(), dk.Status.OneAgent.ConnectionInfoStatus.CommunicationHosts) + assert.Equal(t, tenantTokenHash, dk.Status.OneAgent.ConnectionInfoStatus.TenantTokenHash) var actualSecret corev1.Secret - err = fakeClient.Get(ctx, client.ObjectKey{Name: dk.OneagentTenantSecret(), Namespace: testNamespace}, &actualSecret) + err = fakeClient.Get(ctx, client.ObjectKey{Name: dk.OneAgent().GetTenantSecret(), Namespace: testNamespace}, &actualSecret) require.NoError(t, err) assert.Equal(t, []byte(testTenantToken), actualSecret.Data[connectioninfo.TenantTokenKey]) @@ -150,7 +156,7 @@ func TestReconcile(t *testing.T) { dtc := dtclientmock.NewClient(t) dtc.On("GetOneAgentConnectionInfo", mock.AnythingOfType("context.backgroundCtx")).Return(getTestOneAgentConnectionInfo(), nil) - dk.Status.OneAgent.ConnectionInfoStatus = dynakube.OneAgentConnectionInfoStatus{ + dk.Status.OneAgent.ConnectionInfoStatus = oneagent.ConnectionInfoStatus{ ConnectionInfo: communication.ConnectionInfo{ TenantUUID: testOutdated, Endpoints: testOutdated, @@ -169,7 +175,7 @@ func TestReconcile(t *testing.T) { assert.Equal(t, testTenantEndpoints, dk.Status.OneAgent.ConnectionInfoStatus.Endpoints) var actualSecret corev1.Secret - err = fakeClient.Get(ctx, client.ObjectKey{Name: dk.OneagentTenantSecret(), Namespace: testNamespace}, &actualSecret) + err = fakeClient.Get(ctx, client.ObjectKey{Name: dk.OneAgent().GetTenantSecret(), Namespace: testNamespace}, &actualSecret) require.NoError(t, err) assert.Equal(t, []byte(testTenantToken), actualSecret.Data[connectioninfo.TenantTokenKey]) @@ -184,7 +190,7 @@ func TestReconcile(t *testing.T) { fakeClient := fake.NewClient(dk, buildOneAgentTenantSecret(dk, testOutdated)) dtc := dtclientmock.NewClient(t) - dk.Status.OneAgent.ConnectionInfoStatus = dynakube.OneAgentConnectionInfoStatus{ + dk.Status.OneAgent.ConnectionInfoStatus = oneagent.ConnectionInfoStatus{ ConnectionInfo: communication.ConnectionInfo{ TenantUUID: testOutdated, Endpoints: testOutdated, @@ -200,7 +206,7 @@ func TestReconcile(t *testing.T) { assert.Equal(t, testOutdated, dk.Status.OneAgent.ConnectionInfoStatus.Endpoints) var actualSecret corev1.Secret - err = fakeClient.Get(ctx, client.ObjectKey{Name: dk.OneagentTenantSecret(), Namespace: testNamespace}, &actualSecret) + err = fakeClient.Get(ctx, client.ObjectKey{Name: dk.OneAgent().GetTenantSecret(), Namespace: testNamespace}, &actualSecret) require.NoError(t, err) assert.Equal(t, []byte(testOutdated), actualSecret.Data[connectioninfo.TenantTokenKey]) @@ -216,7 +222,7 @@ func TestReconcile(t *testing.T) { dtc := dtclientmock.NewClient(t) dtc.On("GetOneAgentConnectionInfo", mock.AnythingOfType("context.backgroundCtx")).Return(getTestOneAgentConnectionInfo(), nil) - dk.Status.OneAgent.ConnectionInfoStatus = dynakube.OneAgentConnectionInfoStatus{ + dk.Status.OneAgent.ConnectionInfoStatus = oneagent.ConnectionInfoStatus{ ConnectionInfo: communication.ConnectionInfo{ TenantUUID: testOutdated, Endpoints: testOutdated, @@ -232,7 +238,7 @@ func TestReconcile(t *testing.T) { assert.Equal(t, testTenantEndpoints, dk.Status.OneAgent.ConnectionInfoStatus.Endpoints) var actualSecret corev1.Secret - err = fakeClient.Get(ctx, client.ObjectKey{Name: dk.OneagentTenantSecret(), Namespace: testNamespace}, &actualSecret) + err = fakeClient.Get(ctx, client.ObjectKey{Name: dk.OneAgent().GetTenantSecret(), Namespace: testNamespace}, &actualSecret) require.NoError(t, err) assert.Equal(t, []byte(testTenantToken), actualSecret.Data[connectioninfo.TenantTokenKey]) @@ -249,7 +255,7 @@ func TestReconcile(t *testing.T) { dtc := dtclientmock.NewClient(t) dtc.On("GetOneAgentConnectionInfo", mock.AnythingOfType("context.backgroundCtx")).Return(getTestOneAgentConnectionInfo(), nil) - dk.Status.OneAgent.ConnectionInfoStatus = dynakube.OneAgentConnectionInfoStatus{ + dk.Status.OneAgent.ConnectionInfoStatus = oneagent.ConnectionInfoStatus{ ConnectionInfo: communication.ConnectionInfo{ TenantUUID: testOutdated, Endpoints: testOutdated, @@ -265,7 +271,7 @@ func TestReconcile(t *testing.T) { assert.Equal(t, testTenantEndpoints, dk.Status.OneAgent.ConnectionInfoStatus.Endpoints) var actualSecret corev1.Secret - err = fakeClient.Get(ctx, client.ObjectKey{Name: dk.OneagentTenantSecret(), Namespace: testNamespace}, &actualSecret) + err = fakeClient.Get(ctx, client.ObjectKey{Name: dk.OneAgent().GetTenantSecret(), Namespace: testNamespace}, &actualSecret) require.NoError(t, err) assert.Equal(t, []byte(testTenantToken), actualSecret.Data[connectioninfo.TenantTokenKey]) @@ -284,8 +290,8 @@ func TestReconcile_NoOneAgentCommunicationHosts(t *testing.T) { Name: testName, }, Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{}, + OneAgent: oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{}, }, }, } @@ -323,8 +329,8 @@ func getTestDynakube() *dynakube.DynaKube { Name: testName, }, Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{}, + OneAgent: oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{}, }, }, } @@ -333,7 +339,7 @@ func getTestDynakube() *dynakube.DynaKube { func buildOneAgentTenantSecret(dk *dynakube.DynaKube, token string) *corev1.Secret { return &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: dk.OneagentTenantSecret(), + Name: dk.OneAgent().GetTenantSecret(), Namespace: testNamespace, }, Data: map[string][]byte{ @@ -342,8 +348,8 @@ func buildOneAgentTenantSecret(dk *dynakube.DynaKube, token string) *corev1.Secr } } -func getTestCommunicationHosts() []dynakube.CommunicationHostStatus { - return []dynakube.CommunicationHostStatus{ +func getTestCommunicationHosts() []oneagent.CommunicationHostStatus { + return []oneagent.CommunicationHostStatus{ { Protocol: "http", Host: "dummyhost", diff --git a/pkg/controllers/dynakube/controller.go b/pkg/controllers/dynakube/controller.go index b82ae22675..45b3ab5053 100644 --- a/pkg/controllers/dynakube/controller.go +++ b/pkg/controllers/dynakube/controller.go @@ -7,7 +7,7 @@ import ( "time" dynatracestatus "github.com/Dynatrace/dynatrace-operator/pkg/api/status" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/apimonitoring" @@ -20,7 +20,9 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/istio" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/kspm" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/logmonitoring" + logmondaemonset "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/logmonitoring/daemonset" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/oneagent" + "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/otelc" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/proxy" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/token" "github.com/Dynatrace/dynatrace-operator/pkg/injection/namespace/mapper" @@ -77,6 +79,7 @@ func NewDynaKubeController(kubeClient client.Client, apiReader client.Reader, co injectionReconcilerBuilder: injection.NewReconciler, istioReconcilerBuilder: istio.NewReconciler, extensionReconcilerBuilder: extension.NewReconciler, + otelcReconcilerBuilder: otelc.NewReconciler, logMonitoringReconcilerBuilder: logmonitoring.NewReconciler, proxyReconcilerBuilder: proxy.NewReconciler, kspmReconcilerBuilder: kspm.NewReconciler, @@ -113,6 +116,7 @@ type Controller struct { injectionReconcilerBuilder injection.ReconcilerBuilder istioReconcilerBuilder istio.ReconcilerBuilder extensionReconcilerBuilder extension.ReconcilerBuilder + otelcReconcilerBuilder otelc.ReconcilerBuilder logMonitoringReconcilerBuilder logmonitoring.ReconcilerBuilder proxyReconcilerBuilder proxy.ReconcilerBuilder kspmReconcilerBuilder kspm.ReconcilerBuilder @@ -332,15 +336,24 @@ func (controller *Controller) reconcileComponents(ctx context.Context, dynatrace componentErrors = append(componentErrors, err) } + log.Info("start reconciling otel-collector") + + otelcReconciler := controller.otelcReconcilerBuilder(controller.client, controller.apiReader, dk) + + err = otelcReconciler.Reconcile(ctx) + if err != nil { + log.Info("could not reconcile otelc") + + componentErrors = append(componentErrors, err) + } + log.Info("start reconciling LogMonitoring") logMonitoringReconciler := controller.logMonitoringReconcilerBuilder(controller.client, controller.apiReader, dynatraceClient, dk) err = logMonitoringReconciler.Reconcile(ctx) if err != nil { - if errors.Is(err, oaconnectioninfo.NoOneAgentCommunicationHostsError) { - // missing communication hosts is not an error per se, just make sure next the reconciliation is happening ASAP - // this situation will clear itself after AG has been started + if errors.Is(err, oaconnectioninfo.NoOneAgentCommunicationHostsError) || errors.Is(err, logmondaemonset.KubernetesSettingsNotAvailableError) { controller.setRequeueAfterIfNewIsShorter(fastUpdateInterval) return goerrors.Join(componentErrors...) diff --git a/pkg/controllers/dynakube/controller_system_test.go b/pkg/controllers/dynakube/controller_system_test.go index 1f95f5695c..23e9b2cd9b 100644 --- a/pkg/controllers/dynakube/controller_system_test.go +++ b/pkg/controllers/dynakube/controller_system_test.go @@ -8,9 +8,10 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/value" "github.com/Dynatrace/dynatrace-operator/pkg/api/status" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/logmonitoring" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/logmonitoring" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" ag "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/capability" @@ -20,7 +21,8 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/injection" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/kspm" logmon "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/logmonitoring" - "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/oneagent" + oneagentcontroller "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/oneagent" + "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/otelc" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/proxy" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/labels" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubesystem" @@ -290,7 +292,7 @@ func TestAPIError(t *testing.T) { }, Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{HostInjectSpec: dynakube.HostInjectSpec{}}}, + OneAgent: oneagent.Spec{CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{HostInjectSpec: oneagent.HostInjectSpec{}}}, ActiveGate: activegate.Spec{ Capabilities: []activegate.CapabilityDisplayName{ activegate.KubeMonCapability.DisplayName, @@ -432,10 +434,11 @@ func createFakeClientAndReconciler(t *testing.T, mockClient dtclient.Client, dk activeGateReconcilerBuilder: ag.NewReconciler, apiMonitoringReconcilerBuilder: apimonitoring.NewReconciler, injectionReconcilerBuilder: injection.NewReconciler, - oneAgentReconcilerBuilder: oneagent.NewReconciler, + oneAgentReconcilerBuilder: oneagentcontroller.NewReconciler, logMonitoringReconcilerBuilder: logmon.NewReconciler, proxyReconcilerBuilder: proxy.NewReconciler, extensionReconcilerBuilder: extension.NewReconciler, + otelcReconcilerBuilder: otelc.NewReconciler, kspmReconcilerBuilder: kspm.NewReconciler, clusterID: testUID, } diff --git a/pkg/controllers/dynakube/controller_test.go b/pkg/controllers/dynakube/controller_test.go index 8491e86e0f..dec95523e0 100644 --- a/pkg/controllers/dynakube/controller_test.go +++ b/pkg/controllers/dynakube/controller_test.go @@ -10,8 +10,9 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/communication" "github.com/Dynatrace/dynatrace-operator/pkg/api/status" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/controllers" ag "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate" @@ -22,7 +23,8 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/istio" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/kspm" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/logmonitoring" - "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/oneagent" + oneagentcontroller "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/oneagent" + "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/otelc" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/token" dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" dtclientmock "github.com/Dynatrace/dynatrace-operator/test/mocks/pkg/clients/dynatrace" @@ -326,7 +328,7 @@ func TestReconcileComponents(t *testing.T) { }, Spec: dynakube.DynaKubeSpec{ APIURL: "this-is-an-api-url", - OneAgent: dynakube.OneAgentSpec{CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{}}, + OneAgent: oneagent.Spec{CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{}}, ActiveGate: activegate.Spec{Capabilities: []activegate.CapabilityDisplayName{activegate.KubeMonCapability.DisplayName}}, }, } @@ -350,6 +352,9 @@ func TestReconcileComponents(t *testing.T) { mockExtensionReconciler := controllermock.NewReconciler(t) mockExtensionReconciler.On("Reconcile", mock.Anything).Return(errors.New("BOOM")) + mockOtelcReconciler := controllermock.NewReconciler(t) + mockOtelcReconciler.On("Reconcile", mock.Anything).Return(errors.New("BOOM")) + mockKSPMReconciler := controllermock.NewReconciler(t) mockKSPMReconciler.On("Reconcile", mock.Anything).Return(errors.New("BOOM")) @@ -363,6 +368,7 @@ func TestReconcileComponents(t *testing.T) { oneAgentReconcilerBuilder: createOneAgentReconcilerBuilder(mockOneAgentReconciler), logMonitoringReconcilerBuilder: createLogMonitoringReconcilerBuilder(mockLogMonitoringReconciler), extensionReconcilerBuilder: createExtensionReconcilerBuilder(mockExtensionReconciler), + otelcReconcilerBuilder: createOtelcReconcilerBuilder(mockOtelcReconciler), kspmReconcilerBuilder: createKSPMReconcilerBuilder(mockKSPMReconciler), } mockedDtc := dtclientmock.NewClient(t) @@ -371,7 +377,7 @@ func TestReconcileComponents(t *testing.T) { require.Error(t, err) // goerrors.Join concats errors with \n - assert.Len(t, strings.Split(err.Error(), "\n"), 6) // ActiveGate, Extension, OneAgent LogMonitoring, and Injection reconcilers + assert.Len(t, strings.Split(err.Error(), "\n"), 7) // ActiveGate, Extension, OtelC, OneAgent LogMonitoring, and Injection reconcilers }) t.Run("exit early in case of no oneagent conncection info", func(t *testing.T) { @@ -384,6 +390,9 @@ func TestReconcileComponents(t *testing.T) { mockExtensionReconciler := controllermock.NewReconciler(t) mockExtensionReconciler.On("Reconcile", mock.Anything).Return(errors.New("BOOM")) + mockOtelcReconciler := controllermock.NewReconciler(t) + mockOtelcReconciler.On("Reconcile", mock.Anything).Return(errors.New("BOOM")) + mockLogMonitoringReconciler := injectionmock.NewReconciler(t) mockLogMonitoringReconciler.On("Reconcile", mock.Anything).Return(oaconnectioninfo.NoOneAgentCommunicationHostsError) @@ -394,6 +403,7 @@ func TestReconcileComponents(t *testing.T) { activeGateReconcilerBuilder: createActivegateReconcilerBuilder(mockActiveGateReconciler), logMonitoringReconcilerBuilder: createLogMonitoringReconcilerBuilder(mockLogMonitoringReconciler), extensionReconcilerBuilder: createExtensionReconcilerBuilder(mockExtensionReconciler), + otelcReconcilerBuilder: createOtelcReconcilerBuilder(mockOtelcReconciler), } mockedDtc := dtclientmock.NewClient(t) @@ -401,7 +411,7 @@ func TestReconcileComponents(t *testing.T) { require.Error(t, err) // goerrors.Join concats errors with \n - assert.Len(t, strings.Split(err.Error(), "\n"), 2) // ActiveGate, Extension, no OneAgent connection info is not an error + assert.Len(t, strings.Split(err.Error(), "\n"), 3) // ActiveGate, Extension, OtelC, no OneAgent connection info is not an error }) } @@ -411,7 +421,7 @@ func createActivegateReconcilerBuilder(reconciler controllers.Reconciler) ag.Rec } } -func createOneAgentReconcilerBuilder(reconciler controllers.Reconciler) oneagent.ReconcilerBuilder { +func createOneAgentReconcilerBuilder(reconciler controllers.Reconciler) oneagentcontroller.ReconcilerBuilder { return func(_ client.Client, _ client.Reader, _ dtclient.Client, _ *dynakube.DynaKube, _ token.Tokens, _ string) controllers.Reconciler { return reconciler } @@ -429,6 +439,12 @@ func createExtensionReconcilerBuilder(reconciler controllers.Reconciler) extensi } } +func createOtelcReconcilerBuilder(reconciler controllers.Reconciler) otelc.ReconcilerBuilder { + return func(_ client.Client, _ client.Reader, _ *dynakube.DynaKube) controllers.Reconciler { + return reconciler + } +} + func createInjectionReconcilerBuilder(reconciler *injectionmock.Reconciler) injection.ReconcilerBuilder { return func(_ client.Client, _ client.Reader, _ dtclient.Client, _ *istio.Client, _ *dynakube.DynaKube) controllers.Reconciler { return reconciler @@ -457,8 +473,8 @@ func TestGetDynakube(t *testing.T) { Namespace: testNamespace, }, Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{}, + OneAgent: oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{}, }, }, }) @@ -674,13 +690,13 @@ func getTestDynkubeStatus() *dynakube.DynaKubeStatus { Endpoints: "endpoint", }, }, - OneAgent: dynakube.OneAgentStatus{ - ConnectionInfoStatus: dynakube.OneAgentConnectionInfoStatus{ + OneAgent: oneagent.Status{ + ConnectionInfoStatus: oneagent.ConnectionInfoStatus{ ConnectionInfo: communication.ConnectionInfo{ TenantUUID: testUUID, Endpoints: "endpoint", }, - CommunicationHosts: []dynakube.CommunicationHostStatus{ + CommunicationHosts: []oneagent.CommunicationHostStatus{ { Protocol: "http", Host: "localhost", @@ -697,7 +713,7 @@ func createTenantSecrets(dk *dynakube.DynaKube) []client.Object { return []client.Object{ &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: dk.OneagentTenantSecret(), + Name: dk.OneAgent().GetTenantSecret(), Namespace: testNamespace, }, Data: map[string][]byte{ diff --git a/pkg/controllers/dynakube/deploymentmetadata/deploymentmetadata.go b/pkg/controllers/dynakube/deploymentmetadata/deploymentmetadata.go index 6f64886154..38574b1961 100644 --- a/pkg/controllers/dynakube/deploymentmetadata/deploymentmetadata.go +++ b/pkg/controllers/dynakube/deploymentmetadata/deploymentmetadata.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/version" ) @@ -15,13 +15,13 @@ type DeploymentMetadata struct { func GetOneAgentDeploymentType(dk dynakube.DynaKube) string { switch { - case dk.HostMonitoringMode(): + case dk.OneAgent().IsHostMonitoringMode(): return HostMonitoringDeploymentType - case dk.CloudNativeFullstackMode(): + case dk.OneAgent().IsCloudNativeFullstackMode(): return CloudNativeDeploymentType - case dk.ClassicFullStackMode(): + case dk.OneAgent().IsClassicFullStackMode(): return ClassicFullStackDeploymentType - case dk.ApplicationMonitoringMode(): + case dk.OneAgent().IsApplicationMonitoringMode(): return ApplicationMonitoringDeploymentType } diff --git a/pkg/controllers/dynakube/deploymentmetadata/deploymentmetadata_test.go b/pkg/controllers/dynakube/deploymentmetadata/deploymentmetadata_test.go index 06e6a7869b..96606d076c 100644 --- a/pkg/controllers/dynakube/deploymentmetadata/deploymentmetadata_test.go +++ b/pkg/controllers/dynakube/deploymentmetadata/deploymentmetadata_test.go @@ -3,7 +3,8 @@ package deploymentmetadata import ( "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" "github.com/stretchr/testify/assert" ) @@ -46,13 +47,13 @@ func TestFormatKeyValue(t *testing.T) { func TestGetOneAgentDeploymentType(t *testing.T) { tests := []struct { - oneAgentSpec dynakube.OneAgentSpec + oneAgentSpec oneagent.Spec expectedDeploymentType string }{ - {dynakube.OneAgentSpec{HostMonitoring: &dynakube.HostInjectSpec{}}, HostMonitoringDeploymentType}, - {dynakube.OneAgentSpec{ClassicFullStack: &dynakube.HostInjectSpec{}}, ClassicFullStackDeploymentType}, - {dynakube.OneAgentSpec{CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{}}, CloudNativeDeploymentType}, - {dynakube.OneAgentSpec{ApplicationMonitoring: &dynakube.ApplicationMonitoringSpec{}}, ApplicationMonitoringDeploymentType}, + {oneagent.Spec{HostMonitoring: &oneagent.HostInjectSpec{}}, HostMonitoringDeploymentType}, + {oneagent.Spec{ClassicFullStack: &oneagent.HostInjectSpec{}}, ClassicFullStackDeploymentType}, + {oneagent.Spec{CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{}}, CloudNativeDeploymentType}, + {oneagent.Spec{ApplicationMonitoring: &oneagent.ApplicationMonitoringSpec{}}, ApplicationMonitoringDeploymentType}, } for _, test := range tests { diff --git a/pkg/controllers/dynakube/deploymentmetadata/reconciler.go b/pkg/controllers/dynakube/deploymentmetadata/reconciler.go index f95edf532a..e715f1b211 100644 --- a/pkg/controllers/dynakube/deploymentmetadata/reconciler.go +++ b/pkg/controllers/dynakube/deploymentmetadata/reconciler.go @@ -3,7 +3,7 @@ package deploymentmetadata import ( "context" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/controllers" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/configmap" "github.com/Dynatrace/dynatrace-operator/pkg/version" @@ -40,7 +40,7 @@ func (r *Reconciler) Reconcile(ctx context.Context) error { } func (r *Reconciler) addOneAgentDeploymentMetadata(configMapData map[string]string) { - if !r.dk.NeedsOneAgent() { + if !r.dk.OneAgent().IsDaemonsetRequired() { return } @@ -56,7 +56,7 @@ func (r *Reconciler) addActiveGateDeploymentMetadata(configMapData map[string]st } func (r *Reconciler) addOperatorVersionInfo(configMapData map[string]string) { - if !r.dk.NeedsOneAgent() { // Currently only used for oneAgent args + if !r.dk.OneAgent().IsDaemonsetRequired() { // Currently only used for oneAgent args return } diff --git a/pkg/controllers/dynakube/deploymentmetadata/reconciler_test.go b/pkg/controllers/dynakube/deploymentmetadata/reconciler_test.go index 3781d6bf2a..d87a0022ef 100644 --- a/pkg/controllers/dynakube/deploymentmetadata/reconciler_test.go +++ b/pkg/controllers/dynakube/deploymentmetadata/reconciler_test.go @@ -4,8 +4,9 @@ import ( "context" "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" @@ -71,8 +72,8 @@ func TestReconcile(t *testing.T) { t.Run(`create configmap with 1 key, if only oneagent is needed`, func(t *testing.T) { dk := createTestDynakube( &dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{}, + OneAgent: oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{}, }, }) @@ -112,8 +113,8 @@ func TestReconcile(t *testing.T) { t.Run(`create configmap with 2 keys, if both oneagent and activegate is needed`, func(t *testing.T) { dk := createTestDynakube( &dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{}, + OneAgent: oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{}, }, ActiveGate: activegate.Spec{ Capabilities: []activegate.CapabilityDisplayName{ diff --git a/pkg/controllers/dynakube/dtpullsecret/find.go b/pkg/controllers/dynakube/dtpullsecret/find.go index 44de16c68b..8d85cc7963 100644 --- a/pkg/controllers/dynakube/dtpullsecret/find.go +++ b/pkg/controllers/dynakube/dtpullsecret/find.go @@ -3,7 +3,7 @@ package dtpullsecret import ( "context" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" diff --git a/pkg/controllers/dynakube/dtpullsecret/find_test.go b/pkg/controllers/dynakube/dtpullsecret/find_test.go index 8770bee11e..ab926aefbd 100644 --- a/pkg/controllers/dynakube/dtpullsecret/find_test.go +++ b/pkg/controllers/dynakube/dtpullsecret/find_test.go @@ -6,7 +6,7 @@ import ( "testing" "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/pkg/controllers/dynakube/dtpullsecret/generate.go b/pkg/controllers/dynakube/dtpullsecret/generate.go index da5c434d9c..f6f3596c18 100644 --- a/pkg/controllers/dynakube/dtpullsecret/generate.go +++ b/pkg/controllers/dynakube/dtpullsecret/generate.go @@ -52,7 +52,7 @@ func (r *Reconciler) GenerateData() (map[string][]byte, error) { return nil, errors.New("token secret does not contain a paas or api token, cannot generate docker config") } - tenantUUID, err := r.dk.TenantUUIDFromConnectionInfoStatus() + tenantUUID, err := r.dk.TenantUUID() if err != nil { return nil, errors.WithMessage(err, "cannot generate docker config") } diff --git a/pkg/controllers/dynakube/dtpullsecret/generate_test.go b/pkg/controllers/dynakube/dtpullsecret/generate_test.go index 6840d07917..5934a46aa2 100644 --- a/pkg/controllers/dynakube/dtpullsecret/generate_test.go +++ b/pkg/controllers/dynakube/dtpullsecret/generate_test.go @@ -7,7 +7,8 @@ import ( "testing" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/communication" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/token" "github.com/stretchr/testify/assert" @@ -39,8 +40,8 @@ func TestReconciler_GenerateData(t *testing.T) { APIURL: testApiUrl, }, Status: dynakube.DynaKubeStatus{ - OneAgent: dynakube.OneAgentStatus{ - ConnectionInfoStatus: dynakube.OneAgentConnectionInfoStatus{ + OneAgent: oneagent.Status{ + ConnectionInfoStatus: oneagent.ConnectionInfoStatus{ ConnectionInfo: communication.ConnectionInfo{ TenantUUID: testTenant, }, diff --git a/pkg/controllers/dynakube/dtpullsecret/reconciler.go b/pkg/controllers/dynakube/dtpullsecret/reconciler.go index 8be1154caf..287a7ec2e6 100644 --- a/pkg/controllers/dynakube/dtpullsecret/reconciler.go +++ b/pkg/controllers/dynakube/dtpullsecret/reconciler.go @@ -4,7 +4,7 @@ import ( "context" "reflect" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/token" "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/secret" @@ -39,7 +39,7 @@ func NewReconciler(clt client.Client, apiReader client.Reader, dk *dynakube.Dyna } func (r *Reconciler) Reconcile(ctx context.Context) error { - if !(r.dk.NeedsOneAgent() || r.dk.ActiveGate().IsEnabled()) { + if !r.dk.OneAgent().IsDaemonsetRequired() && !r.dk.ActiveGate().IsEnabled() { if meta.FindStatusCondition(*r.dk.Conditions(), PullSecretConditionType) == nil { return nil // no condition == nothing is there to clean up } diff --git a/pkg/controllers/dynakube/dtpullsecret/reconciler_test.go b/pkg/controllers/dynakube/dtpullsecret/reconciler_test.go index e18c64e2fc..b053c5eff4 100644 --- a/pkg/controllers/dynakube/dtpullsecret/reconciler_test.go +++ b/pkg/controllers/dynakube/dtpullsecret/reconciler_test.go @@ -7,7 +7,8 @@ import ( "time" "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/token" "github.com/stretchr/testify/assert" @@ -75,7 +76,7 @@ func TestReconciler_Reconcile(t *testing.T) { Name: testName, }, Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{}}, + OneAgent: oneagent.Spec{CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{}}, }, } fakeClient := errorClient{} @@ -230,7 +231,7 @@ func TestReconciler_Reconcile(t *testing.T) { require.NoError(t, err) - dk.Spec.OneAgent = dynakube.OneAgentSpec{} + dk.Spec.OneAgent = oneagent.Spec{} err = r.Reconcile(context.Background()) require.NoError(t, err) @@ -251,7 +252,7 @@ func createTestDynakube() *dynakube.DynaKube { }, Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{}}, + OneAgent: oneagent.Spec{CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{}}, }, }) } diff --git a/pkg/controllers/dynakube/dynatraceclient/builder.go b/pkg/controllers/dynakube/dynatraceclient/builder.go index 7dfe81f72a..ef212d172d 100644 --- a/pkg/controllers/dynakube/dynatraceclient/builder.go +++ b/pkg/controllers/dynakube/dynatraceclient/builder.go @@ -3,7 +3,7 @@ package dynatraceclient import ( "context" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/token" "github.com/Dynatrace/dynatrace-operator/pkg/util/timeprovider" @@ -76,7 +76,7 @@ func (dynatraceClientBuilder builder) Build() (dtclient.Client, error) { opts := newOptions(dynatraceClientBuilder.context()) opts.appendCertCheck(dynatraceClientBuilder.dk.Spec.SkipCertCheck) opts.appendNetworkZone(dynatraceClientBuilder.dk.Spec.NetworkZone) - opts.appendHostGroup(dynatraceClientBuilder.dk.HostGroup()) + opts.appendHostGroup(dynatraceClientBuilder.dk.OneAgent().GetHostGroup()) err := opts.appendProxySettings(apiReader, &dynatraceClientBuilder.dk) if err != nil { diff --git a/pkg/controllers/dynakube/dynatraceclient/builder_test.go b/pkg/controllers/dynakube/dynatraceclient/builder_test.go index ca4a564919..fa0db46c91 100644 --- a/pkg/controllers/dynakube/dynatraceclient/builder_test.go +++ b/pkg/controllers/dynakube/dynatraceclient/builder_test.go @@ -5,7 +5,7 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/value" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/token" "github.com/stretchr/testify/assert" diff --git a/pkg/controllers/dynakube/dynatraceclient/options.go b/pkg/controllers/dynakube/dynatraceclient/options.go index db0d08968e..97858cc577 100644 --- a/pkg/controllers/dynakube/dynatraceclient/options.go +++ b/pkg/controllers/dynakube/dynatraceclient/options.go @@ -3,7 +3,7 @@ package dynatraceclient import ( "context" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" diff --git a/pkg/controllers/dynakube/dynatraceclient/options_test.go b/pkg/controllers/dynakube/dynatraceclient/options_test.go index afd549c940..e96a22967e 100644 --- a/pkg/controllers/dynakube/dynatraceclient/options_test.go +++ b/pkg/controllers/dynakube/dynatraceclient/options_test.go @@ -6,7 +6,7 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/value" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" diff --git a/pkg/controllers/dynakube/extension/conditions.go b/pkg/controllers/dynakube/extension/conditions.go new file mode 100644 index 0000000000..32af03f3e4 --- /dev/null +++ b/pkg/controllers/dynakube/extension/conditions.go @@ -0,0 +1,6 @@ +package extension + +const ( + secretConditionType = "ExtensionsSecret" + serviceConditionType = "ExtensionsService" +) diff --git a/pkg/controllers/dynakube/extension/consts/consts.go b/pkg/controllers/dynakube/extension/consts/consts.go index e397da06c0..499ac460c0 100644 --- a/pkg/controllers/dynakube/extension/consts/consts.go +++ b/pkg/controllers/dynakube/extension/consts/consts.go @@ -1,33 +1,8 @@ package consts -import "github.com/Dynatrace/dynatrace-operator/pkg/api" - const ( - ExtensionsAnnotationSecretHash = api.InternalFlagPrefix + "secret-hash" - - // secret - EecTokenSecretKey = "eec.token" - EecTokenSecretValuePrefix = "EEC dt0x01" - - OtelcTokenSecretKey = "otelc.token" - OtelcTokenSecretValuePrefix = "dt0x01" - - // shared volume name between eec and OtelC - ExtensionsTokensVolumeName = "tokens" - - ExtensionsSecretConditionType = "ExtensionsSecret" - ExtensionsServiceConditionType = "ExtensionsService" - - ExtensionsControllerSuffix = "-extensions-controller" - ExtensionsCollectorComPort = 14599 - ExtensionsCollectorTargetPortName = "collector-com" - - ExtensionsSelfSignedTLSSecretSuffix = "-extensions-controller-tls" - ExtensionsSelfSignedTLSCommonNameSuffix = "-extensions-controller.dynatrace" - - // TLSKeyDataName is the key used to store a TLS private key in the secret's data field. - TLSKeyDataName = "tls.key" + TokenSecretKey = "eec.token" + TokenSecretValuePrefix = "EEC dt0x01" - // TLSCrtDataName is the key used to store a TLS certificate in the secret's data field. - TLSCrtDataName = "tls.crt" + ExtensionsControllerSuffix = "-extensions-controller" ) diff --git a/pkg/controllers/dynakube/extension/eec/reconciler.go b/pkg/controllers/dynakube/extension/eec/reconciler.go index 13204691cf..8a4c4bd468 100644 --- a/pkg/controllers/dynakube/extension/eec/reconciler.go +++ b/pkg/controllers/dynakube/extension/eec/reconciler.go @@ -1,7 +1,7 @@ package eec import ( - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/controllers" "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/statefulset" diff --git a/pkg/controllers/dynakube/extension/eec/reconciler_test.go b/pkg/controllers/dynakube/extension/eec/reconciler_test.go index 9c9cdd3641..286c022362 100644 --- a/pkg/controllers/dynakube/extension/eec/reconciler_test.go +++ b/pkg/controllers/dynakube/extension/eec/reconciler_test.go @@ -4,17 +4,18 @@ import ( "strconv" "testing" + "github.com/Dynatrace/dynatrace-operator/pkg/api" "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/communication" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/image" "github.com/Dynatrace/dynatrace-operator/pkg/api/status" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" - "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/extension/consts" - "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/extension/tls" - "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/extension/utils" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/consts" + eecConsts "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/extension/consts" "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/node" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/topology" maputils "github.com/Dynatrace/dynatrace-operator/pkg/util/map" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -84,7 +85,7 @@ func getStatefulset(t *testing.T, dk *dynakube.DynaKube) *appsv1.StatefulSet { } func mockTLSSecret(t *testing.T, client client.Client, dk *dynakube.DynaKube) client.Client { - tlsSecret := getTLSSecret(tls.GetTLSSecretName(dk), dk.Namespace, "super-cert", "super-key") + tlsSecret := getTLSSecret(dk.ExtensionsTLSSecretName(), dk.Namespace, "super-cert", "super-key") err := client.Create(context.Background(), &tlsSecret) require.NoError(t, err) @@ -105,6 +106,10 @@ func getTLSSecret(name string, namespace string, crt string, key string) corev1. } } +func disableAutomaticAGCertificate(dk *dynakube.DynaKube) { + dk.Annotations[dynakube.AnnotationFeatureActiveGateAutomaticTLSCertificate] = "false" +} + func TestConditions(t *testing.T) { t.Run("no kubeSystemUUID", func(t *testing.T) { dk := getTestDynakube() @@ -179,7 +184,7 @@ func TestSecretHashAnnotation(t *testing.T) { statefulSet := getStatefulset(t, dk) require.Len(t, statefulSet.Spec.Template.Annotations, 1) - assert.NotEmpty(t, statefulSet.Spec.Template.Annotations[consts.ExtensionsAnnotationSecretHash]) + assert.NotEmpty(t, statefulSet.Spec.Template.Annotations[api.AnnotationExtensionsSecretHash]) }) t.Run("annotation is set with tlsRefName", func(t *testing.T) { dk := getTestDynakube() @@ -187,7 +192,7 @@ func TestSecretHashAnnotation(t *testing.T) { statefulSet := getStatefulset(t, dk) require.Len(t, statefulSet.Spec.Template.Annotations, 1) - assert.NotEmpty(t, statefulSet.Spec.Template.Annotations[consts.ExtensionsAnnotationSecretHash]) + assert.NotEmpty(t, statefulSet.Spec.Template.Annotations[api.AnnotationExtensionsSecretHash]) }) t.Run("annotation is updated when TLS Secret gets updated", func(t *testing.T) { statefulSet := &appsv1.StatefulSet{} @@ -204,10 +209,10 @@ func TestSecretHashAnnotation(t *testing.T) { err = mockK8sClient.Get(context.Background(), client.ObjectKey{Name: dk.ExtensionsExecutionControllerStatefulsetName(), Namespace: dk.Namespace}, statefulSet) require.NoError(t, err) - originalSecretHash := statefulSet.Spec.Template.Annotations[consts.ExtensionsAnnotationSecretHash] + originalSecretHash := statefulSet.Spec.Template.Annotations[api.AnnotationExtensionsSecretHash] // then update the TLS Secret and call reconcile again - updatedTLSSecret := getTLSSecret(tls.GetTLSSecretName(dk), dk.Namespace, "updated-cert", "updated-key") + updatedTLSSecret := getTLSSecret(dk.ExtensionsTLSSecretName(), dk.Namespace, "updated-cert", "updated-key") err = mockK8sClient.Update(context.Background(), &updatedTLSSecret) require.NoError(t, err) @@ -216,7 +221,7 @@ func TestSecretHashAnnotation(t *testing.T) { err = mockK8sClient.Get(context.Background(), client.ObjectKey{Name: dk.ExtensionsExecutionControllerStatefulsetName(), Namespace: dk.Namespace}, statefulSet) require.NoError(t, err) - resultingSecretHash := statefulSet.Spec.Template.Annotations[consts.ExtensionsAnnotationSecretHash] + resultingSecretHash := statefulSet.Spec.Template.Annotations[api.AnnotationExtensionsSecretHash] // original hash and resulting hash should be different, value got updated on reconcile assert.NotEqual(t, originalSecretHash, resultingSecretHash) @@ -228,7 +233,7 @@ func TestTopologySpreadConstraints(t *testing.T) { dk := getTestDynakube() statefulSet := getStatefulset(t, dk) appLabels := buildAppLabels(dk.Name) - assert.Equal(t, utils.BuildTopologySpreadConstraints(dk.Spec.Templates.ExtensionExecutionController.TopologySpreadConstraints, appLabels), statefulSet.Spec.Template.Spec.TopologySpreadConstraints) + assert.Equal(t, topology.MaxOnePerNode(appLabels), statefulSet.Spec.Template.Spec.TopologySpreadConstraints) }) t.Run("custom TopologySpreadConstraints", func(t *testing.T) { @@ -263,12 +268,12 @@ func TestEnvironmentVariables(t *testing.T) { assert.Equal(t, corev1.EnvVar{Name: envTenantId, Value: dk.Status.ActiveGate.ConnectionInfo.TenantUUID}, statefulSet.Spec.Template.Spec.Containers[0].Env[0]) assert.Equal(t, corev1.EnvVar{Name: envServerUrl, Value: buildActiveGateServiceName(dk) + "." + dk.Namespace + ".svc.cluster.local:443"}, statefulSet.Spec.Template.Spec.Containers[0].Env[1]) - assert.Equal(t, corev1.EnvVar{Name: envEecTokenPath, Value: eecTokenMountPath + "/" + consts.EecTokenSecretKey}, statefulSet.Spec.Template.Spec.Containers[0].Env[2]) + assert.Equal(t, corev1.EnvVar{Name: envEecTokenPath, Value: eecTokenMountPath + "/" + eecConsts.TokenSecretKey}, statefulSet.Spec.Template.Spec.Containers[0].Env[2]) assert.Equal(t, corev1.EnvVar{Name: envEecIngestPort, Value: strconv.Itoa(int(collectorPort))}, statefulSet.Spec.Template.Spec.Containers[0].Env[3]) assert.Equal(t, corev1.EnvVar{Name: envExtensionsModuleExecPathName, Value: envExtensionsModuleExecPath}, statefulSet.Spec.Template.Spec.Containers[0].Env[4]) assert.Equal(t, corev1.EnvVar{Name: envDsInstallDirName, Value: envDsInstallDir}, statefulSet.Spec.Template.Spec.Containers[0].Env[5]) assert.Equal(t, corev1.EnvVar{Name: envK8sClusterId, Value: dk.Status.KubeSystemUUID}, statefulSet.Spec.Template.Spec.Containers[0].Env[6]) - assert.Equal(t, corev1.EnvVar{Name: envK8sExtServiceUrl, Value: "https://" + dk.Name + consts.ExtensionsControllerSuffix + "." + dk.Namespace}, statefulSet.Spec.Template.Spec.Containers[0].Env[7]) + assert.Equal(t, corev1.EnvVar{Name: envK8sExtServiceUrl, Value: "https://" + dk.Name + eecConsts.ExtensionsControllerSuffix + "." + dk.Namespace}, statefulSet.Spec.Template.Spec.Containers[0].Env[7]) assert.Equal(t, corev1.EnvVar{Name: envDSTokenPath, Value: eecTokenMountPath + "/" + consts.OtelcTokenSecretKey}, statefulSet.Spec.Template.Spec.Containers[0].Env[8]) assert.Equal(t, corev1.EnvVar{Name: envHttpsCertPathPem, Value: envEecHttpsCertPathPem}, statefulSet.Spec.Template.Spec.Containers[0].Env[9]) assert.Equal(t, corev1.EnvVar{Name: envHttpsPrivKeyPathPem, Value: envEecHttpsPrivKeyPathPem}, statefulSet.Spec.Template.Spec.Containers[0].Env[10]) @@ -306,6 +311,41 @@ func TestEnvironmentVariables(t *testing.T) { } func TestVolumeMounts(t *testing.T) { + t.Run("volume mounts, AG cert disabled", func(t *testing.T) { + dk := getTestDynakube() + disableAutomaticAGCertificate(dk) + statefulSet := getStatefulset(t, dk) + + expectedVolumeMounts := []corev1.VolumeMount{ + { + Name: consts.ExtensionsTokensVolumeName, + MountPath: eecTokenMountPath, + ReadOnly: true, + }, + { + Name: logVolumeName, + MountPath: logMountPath, + ReadOnly: false, + }, + { + Name: runtimeVolumeName, + MountPath: runtimeMountPath, + ReadOnly: false, + }, + { + Name: configurationVolumeName, + MountPath: configurationMountPath, + ReadOnly: false, + }, + { + Name: httpsCertVolumeName, + MountPath: httpsCertMountPath, + ReadOnly: true, + }, + } + assert.Equal(t, expectedVolumeMounts, statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts) + }) + t.Run("volume mounts", func(t *testing.T) { statefulSet := getStatefulset(t, getTestDynakube()) @@ -335,12 +375,18 @@ func TestVolumeMounts(t *testing.T) { MountPath: httpsCertMountPath, ReadOnly: true, }, + { + Name: activeGateTrustedCertVolumeName, + MountPath: activeGateTrustedCertMountPath, + ReadOnly: true, + }, } assert.Equal(t, expectedVolumeMounts, statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts) }) - t.Run("volume mounts with PVC", func(t *testing.T) { + t.Run("volume mounts with PVC, AG cert disabled", func(t *testing.T) { dk := getTestDynakube() + disableAutomaticAGCertificate(dk) dk.Spec.Templates.ExtensionExecutionController.PersistentVolumeClaim = &corev1.PersistentVolumeClaimSpec{ Resources: corev1.VolumeResourceRequirements{ Requests: corev1.ResourceList{ @@ -407,6 +453,48 @@ func TestVolumeMounts(t *testing.T) { assert.Contains(t, statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts, expectedVolumeMount) }) + t.Run("volume mounts with custom configuration, AG cert disabled", func(t *testing.T) { + dk := getTestDynakube() + disableAutomaticAGCertificate(dk) + dk.Spec.Templates.ExtensionExecutionController.CustomConfig = testCustomConfigConfigMapName + + statefulSet := getStatefulset(t, dk) + + expectedVolumeMounts := []corev1.VolumeMount{ + { + Name: consts.ExtensionsTokensVolumeName, + MountPath: eecTokenMountPath, + ReadOnly: true, + }, + { + Name: logVolumeName, + MountPath: logMountPath, + ReadOnly: false, + }, + { + Name: runtimeVolumeName, + MountPath: runtimeMountPath, + ReadOnly: false, + }, + { + Name: configurationVolumeName, + MountPath: configurationMountPath, + ReadOnly: false, + }, + { + Name: httpsCertVolumeName, + MountPath: httpsCertMountPath, + ReadOnly: true, + }, + { + Name: customConfigVolumeName, + MountPath: customConfigMountPath, + ReadOnly: true, + }, + } + assert.Equal(t, expectedVolumeMounts, statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts) + }) + t.Run("volume mounts with custom configuration", func(t *testing.T) { dk := getTestDynakube() dk.Spec.Templates.ExtensionExecutionController.CustomConfig = testCustomConfigConfigMapName @@ -444,6 +532,11 @@ func TestVolumeMounts(t *testing.T) { MountPath: customConfigMountPath, ReadOnly: true, }, + { + Name: activeGateTrustedCertVolumeName, + MountPath: activeGateTrustedCertMountPath, + ReadOnly: true, + }, } assert.Equal(t, expectedVolumeMounts, statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts) }) @@ -462,19 +555,8 @@ func TestAffinity(t *testing.T) { statefulSet := getStatefulset(t, dk) - expectedAffinity := &corev1.Affinity{ - NodeAffinity: &corev1.NodeAffinity{ - RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{ - NodeSelectorTerms: []corev1.NodeSelectorTerm{ - { - MatchExpressions: node.AffinityNodeRequirementForSupportedArches(), - }, - }, - }, - }, - } - - assert.Equal(t, expectedAffinity, statefulSet.Spec.Template.Spec.Affinity) + expectedAffinity := node.Affinity() + assert.Equal(t, expectedAffinity, *statefulSet.Spec.Template.Spec.Affinity) }) } @@ -556,9 +638,9 @@ func TestAnnotations(t *testing.T) { t.Run("the default annotations", func(t *testing.T) { statefulSet := getStatefulset(t, getTestDynakube()) - assert.Len(t, statefulSet.ObjectMeta.Annotations, 1) + assert.Len(t, statefulSet.ObjectMeta.Annotations, 2) require.Len(t, statefulSet.Spec.Template.ObjectMeta.Annotations, 1) - assert.NotEmpty(t, statefulSet.Spec.Template.ObjectMeta.Annotations[consts.ExtensionsAnnotationSecretHash]) + assert.NotEmpty(t, statefulSet.Spec.Template.ObjectMeta.Annotations[api.AnnotationExtensionsSecretHash]) }) t.Run("custom annotations", func(t *testing.T) { @@ -570,11 +652,11 @@ func TestAnnotations(t *testing.T) { statefulSet := getStatefulset(t, dk) - assert.Len(t, statefulSet.ObjectMeta.Annotations, 1) + assert.Len(t, statefulSet.ObjectMeta.Annotations, 2) assert.Empty(t, statefulSet.ObjectMeta.Annotations["a"]) require.Len(t, statefulSet.Spec.Template.ObjectMeta.Annotations, 2) assert.Equal(t, "b", statefulSet.Spec.Template.ObjectMeta.Annotations["a"]) - assert.NotEmpty(t, statefulSet.Spec.Template.ObjectMeta.Annotations[consts.ExtensionsAnnotationSecretHash]) + assert.NotEmpty(t, statefulSet.Spec.Template.ObjectMeta.Annotations[api.AnnotationExtensionsSecretHash]) }) } @@ -766,6 +848,55 @@ func TestUpdateStrategy(t *testing.T) { } func TestVolumes(t *testing.T) { + t.Run("volumes without PVC, AG cert disabled", func(t *testing.T) { + dk := getTestDynakube() + disableAutomaticAGCertificate(dk) + dk.Spec.Templates.ExtensionExecutionController.UseEphemeralVolume = true + + statefulSet := getStatefulset(t, dk) + + mode := int32(420) + expectedVolumes := []corev1.Volume{ + { + Name: consts.ExtensionsTokensVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: dk.ExtensionsTokenSecretName(), + DefaultMode: &mode, + }, + }, + }, + { + Name: logVolumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + { + Name: configurationVolumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + { + Name: httpsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: dk.ExtensionsTLSSecretName(), + }, + }, + }, + { + Name: runtimeVolumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + } + + assert.Equal(t, expectedVolumes, statefulSet.Spec.Template.Spec.Volumes) + }) + t.Run("volumes without PVC", func(t *testing.T) { dk := getTestDynakube() dk.Spec.Templates.ExtensionExecutionController.UseEphemeralVolume = true @@ -809,6 +940,70 @@ func TestVolumes(t *testing.T) { EmptyDir: &corev1.EmptyDirVolumeSource{}, }, }, + { + Name: activeGateTrustedCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + DefaultMode: &mode, + SecretName: dk.ActiveGate().GetTLSSecretName(), + Items: []corev1.KeyToPath{ + { + Key: activeGateTrustedCertSecretKeyPath, + Path: activeGateTrustedCertSecretKeyPath, + }, + }, + }, + }, + }, + } + + assert.Equal(t, expectedVolumes, statefulSet.Spec.Template.Spec.Volumes) + }) + + t.Run("volumes with PVC, AG cert disabled", func(t *testing.T) { + dk := getTestDynakube() + disableAutomaticAGCertificate(dk) + dk.Spec.Templates.ExtensionExecutionController.PersistentVolumeClaim = &corev1.PersistentVolumeClaimSpec{ + Resources: corev1.VolumeResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: resource.MustParse("1Gi"), + }, + }, + } + + statefulSet := getStatefulset(t, dk) + + mode := int32(420) + expectedVolumes := []corev1.Volume{ + { + Name: consts.ExtensionsTokensVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: dk.ExtensionsTokenSecretName(), + DefaultMode: &mode, + }, + }, + }, + { + Name: logVolumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + { + Name: configurationVolumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + { + Name: httpsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: dk.ExtensionsTLSSecretName(), + }, + }, + }, } assert.Equal(t, expectedVolumes, statefulSet.Spec.Template.Spec.Volumes) @@ -857,13 +1052,29 @@ func TestVolumes(t *testing.T) { }, }, }, + { + Name: activeGateTrustedCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + DefaultMode: &mode, + SecretName: dk.ActiveGate().GetTLSSecretName(), + Items: []corev1.KeyToPath{ + { + Key: activeGateTrustedCertSecretKeyPath, + Path: activeGateTrustedCertSecretKeyPath, + }, + }, + }, + }, + }, } assert.Equal(t, expectedVolumes, statefulSet.Spec.Template.Spec.Volumes) }) - t.Run("volumes without PVC and with custom configuration", func(t *testing.T) { + t.Run("volumes without PVC and with custom configuration, AG cert disabled", func(t *testing.T) { dk := getTestDynakube() + disableAutomaticAGCertificate(dk) dk.Spec.Templates.ExtensionExecutionController.UseEphemeralVolume = true dk.Spec.Templates.ExtensionExecutionController.CustomConfig = testCustomConfigConfigMapName @@ -906,7 +1117,65 @@ func TestVolumes(t *testing.T) { EmptyDir: &corev1.EmptyDirVolumeSource{}, }, }, + { + Name: customConfigVolumeName, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: testCustomConfigConfigMapName, + }, + }, + }, + }, + } + assert.Equal(t, expectedVolumes, statefulSet.Spec.Template.Spec.Volumes) + }) + + t.Run("volumes without PVC and with custom configuration", func(t *testing.T) { + dk := getTestDynakube() + dk.Spec.Templates.ExtensionExecutionController.UseEphemeralVolume = true + dk.Spec.Templates.ExtensionExecutionController.CustomConfig = testCustomConfigConfigMapName + + statefulSet := getStatefulset(t, dk) + + mode := int32(420) + expectedVolumes := []corev1.Volume{ + { + Name: consts.ExtensionsTokensVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: dk.ExtensionsTokenSecretName(), + DefaultMode: &mode, + }, + }, + }, + { + Name: logVolumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + { + Name: configurationVolumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + { + Name: httpsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: dk.ExtensionsTLSSecretName(), + }, + }, + }, + { + Name: runtimeVolumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, { Name: customConfigVolumeName, VolumeSource: corev1.VolumeSource{ @@ -917,10 +1186,26 @@ func TestVolumes(t *testing.T) { }, }, }, + { + Name: activeGateTrustedCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + DefaultMode: &mode, + SecretName: dk.ActiveGate().GetTLSSecretName(), + Items: []corev1.KeyToPath{ + { + Key: activeGateTrustedCertSecretKeyPath, + Path: activeGateTrustedCertSecretKeyPath, + }, + }, + }, + }, + }, } assert.Equal(t, expectedVolumes, statefulSet.Spec.Template.Spec.Volumes) }) + t.Run("Custom EEC tls certificate is mounted to EEC", func(t *testing.T) { dk := getTestDynakube() dk.Spec.Templates.ExtensionExecutionController.TlsRefName = "custom-tls" @@ -980,8 +1265,23 @@ func TestActiveGateVolumes(t *testing.T) { }, }, } + expectedAutoAgCertVolume := corev1.Volume{ + Name: activeGateTrustedCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + DefaultMode: &defaultMode, + SecretName: testDynakubeName + activegate.TlsSecretSuffix, + Items: []corev1.KeyToPath{ + { + Key: activeGateTrustedCertSecretKeyPath, + Path: activeGateTrustedCertSecretKeyPath, + }, + }, + }, + }, + } - t.Run("ActiveGate tls certificate is mounted to EEC", func(t *testing.T) { + t.Run("volumes with custom ActiveGate tls certificate", func(t *testing.T) { dk := getTestDynakube() dk.Spec.ActiveGate.TlsSecretName = tlsSecretName statefulSet := getStatefulset(t, dk) @@ -993,8 +1293,36 @@ func TestActiveGateVolumes(t *testing.T) { require.Contains(t, statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts, expectedVolumeMount) require.Contains(t, statefulSet.Spec.Template.Spec.Volumes, expectedVolume) }) - t.Run("ActiveGate tls certificate is not mounted to EEC", func(t *testing.T) { + + t.Run("volumes with automatically created ActiveGate tls certificate", func(t *testing.T) { + dk := getTestDynakube() + statefulSet := getStatefulset(t, dk) + + require.NotEmpty(t, statefulSet.Spec.Template.Spec.Containers) + require.NotEmpty(t, statefulSet.Spec.Template.Spec.Volumes) + + require.Contains(t, statefulSet.Spec.Template.Spec.Containers[0].Env, expectedEnvVar) + require.Contains(t, statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts, expectedVolumeMount) + require.Contains(t, statefulSet.Spec.Template.Spec.Volumes, expectedAutoAgCertVolume) + }) + + t.Run("volumes without custom ActiveGate tls certificate", func(t *testing.T) { + dk := getTestDynakube() + disableAutomaticAGCertificate(dk) + statefulSet := getStatefulset(t, dk) + + require.NotEmpty(t, statefulSet.Spec.Template.Spec.Containers) + require.NotEmpty(t, statefulSet.Spec.Template.Spec.Volumes) + + require.NotContains(t, statefulSet.Spec.Template.Spec.Containers[0].Env, expectedEnvVar) + require.NotContains(t, statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts, expectedVolumeMount) + require.NotContains(t, statefulSet.Spec.Template.Spec.Volumes, expectedVolume) + }) + + t.Run("volumes with TrustedCAs certificates, AG cert disabled", func(t *testing.T) { dk := getTestDynakube() + disableAutomaticAGCertificate(dk) + dk.Spec.TrustedCAs = "custom-tls" statefulSet := getStatefulset(t, dk) require.NotEmpty(t, statefulSet.Spec.Template.Spec.Containers) @@ -1004,4 +1332,17 @@ func TestActiveGateVolumes(t *testing.T) { require.NotContains(t, statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts, expectedVolumeMount) require.NotContains(t, statefulSet.Spec.Template.Spec.Volumes, expectedVolume) }) + + t.Run("volumes with TrustedCAs certificates and automatically created ActiveGate tls certificate", func(t *testing.T) { + dk := getTestDynakube() + dk.Spec.TrustedCAs = "custom-tls" + statefulSet := getStatefulset(t, dk) + + require.NotEmpty(t, statefulSet.Spec.Template.Spec.Containers) + require.NotEmpty(t, statefulSet.Spec.Template.Spec.Volumes) + + require.Contains(t, statefulSet.Spec.Template.Spec.Containers[0].Env, expectedEnvVar) + require.Contains(t, statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts, expectedVolumeMount) + require.Contains(t, statefulSet.Spec.Template.Spec.Volumes, expectedAutoAgCertVolume) + }) } diff --git a/pkg/controllers/dynakube/extension/eec/statefulset.go b/pkg/controllers/dynakube/extension/eec/statefulset.go index afcf6ce634..22cab0a70c 100644 --- a/pkg/controllers/dynakube/extension/eec/statefulset.go +++ b/pkg/controllers/dynakube/extension/eec/statefulset.go @@ -3,19 +3,18 @@ package eec import ( "strconv" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/consts" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/capability" - "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/extension/consts" - "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/extension/servicename" - "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/extension/tls" - "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/extension/utils" - "github.com/Dynatrace/dynatrace-operator/pkg/util/address" + eecConsts "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/extension/consts" "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" "github.com/Dynatrace/dynatrace-operator/pkg/util/hasher" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/labels" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/node" k8ssecret "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/secret" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/statefulset" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/topology" "golang.org/x/net/context" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -23,6 +22,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/ptr" ) const ( @@ -84,6 +84,11 @@ func (r *reconciler) createOrUpdateStatefulset(ctx context.Context) error { return err } + topologySpreadConstraints := topology.MaxOnePerNode(appLabels) + if len(r.dk.Spec.Templates.ExtensionExecutionController.TopologySpreadConstraints) > 0 { + topologySpreadConstraints = r.dk.Spec.Templates.ExtensionExecutionController.TopologySpreadConstraints + } + desiredSts, err := statefulset.Build(r.dk, r.dk.ExtensionsExecutionControllerStatefulsetName(), buildContainer(r.dk), statefulset.SetReplicas(1), statefulset.SetPodManagementPolicy(appsv1.ParallelPodManagement), @@ -91,10 +96,10 @@ func (r *reconciler) createOrUpdateStatefulset(ctx context.Context) error { statefulset.SetAllAnnotations(nil, templateAnnotations), statefulset.SetAffinity(buildAffinity()), statefulset.SetTolerations(r.dk.Spec.Templates.ExtensionExecutionController.Tolerations), - statefulset.SetTopologySpreadConstraints(utils.BuildTopologySpreadConstraints(r.dk.Spec.Templates.ExtensionExecutionController.TopologySpreadConstraints, appLabels)), + statefulset.SetTopologySpreadConstraints(topologySpreadConstraints), statefulset.SetServiceAccount(serviceAccountName), statefulset.SetSecurityContext(buildPodSecurityContext(r.dk)), - statefulset.SetUpdateStrategy(utils.BuildUpdateStrategy()), + statefulset.SetRollingUpdateStrategyType(), setImagePullSecrets(r.dk.ImagePullSecretReferences()), setVolumes(r.dk), setPersistentVolumeClaim(r.dk), @@ -135,7 +140,7 @@ func (r *reconciler) buildTemplateAnnotations(ctx context.Context) (map[string]s query := k8ssecret.Query(r.client, r.client, log) tlsSecret, err := query.Get(ctx, types.NamespacedName{ - Name: tls.GetTLSSecretName(r.dk), + Name: r.dk.ExtensionsTLSSecretName(), Namespace: r.dk.Namespace, }) if err != nil { @@ -147,7 +152,7 @@ func (r *reconciler) buildTemplateAnnotations(ctx context.Context) (map[string]s return nil, err } - templateAnnotations[consts.ExtensionsAnnotationSecretHash] = tlsSecretHash + templateAnnotations[api.AnnotationExtensionsSecretHash] = tlsSecretHash return templateAnnotations, nil } @@ -160,17 +165,7 @@ func buildAppLabels(dynakubeName string) *labels.AppLabels { } func buildAffinity() corev1.Affinity { - return corev1.Affinity{ - NodeAffinity: &corev1.NodeAffinity{ - RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{ - NodeSelectorTerms: []corev1.NodeSelectorTerm{ - { - MatchExpressions: node.AffinityNodeRequirementForSupportedArches(), - }, - }, - }, - }, - } + return node.Affinity() } func setImagePullSecrets(imagePullSecrets []corev1.LocalObjectReference) func(o *appsv1.StatefulSet) { @@ -218,12 +213,12 @@ func buildSecurityContext() *corev1.SecurityContext { "ALL", }, }, - Privileged: address.Of(false), - RunAsUser: address.Of(userGroupId), - RunAsGroup: address.Of(userGroupId), - RunAsNonRoot: address.Of(true), - ReadOnlyRootFilesystem: address.Of(true), - AllowPrivilegeEscalation: address.Of(false), + Privileged: ptr.To(false), + RunAsUser: ptr.To(userGroupId), + RunAsGroup: ptr.To(userGroupId), + RunAsNonRoot: ptr.To(true), + ReadOnlyRootFilesystem: ptr.To(true), + AllowPrivilegeEscalation: ptr.To(false), SeccompProfile: &corev1.SeccompProfile{ Type: corev1.SeccompProfileTypeRuntimeDefault, }, @@ -238,7 +233,7 @@ func buildPodSecurityContext(dk *dynakube.DynaKube) *corev1.PodSecurityContext { } if !dk.Spec.Templates.ExtensionExecutionController.UseEphemeralVolume { - podSecurityContext.FSGroup = address.Of(userGroupId) + podSecurityContext.FSGroup = ptr.To(userGroupId) } return podSecurityContext @@ -248,18 +243,18 @@ func buildContainerEnvs(dk *dynakube.DynaKube) []corev1.EnvVar { containerEnvs := []corev1.EnvVar{ {Name: envTenantId, Value: dk.Status.ActiveGate.ConnectionInfo.TenantUUID}, {Name: envServerUrl, Value: buildActiveGateServiceName(dk) + "." + dk.Namespace + ".svc.cluster.local:443"}, - {Name: envEecTokenPath, Value: eecTokenMountPath + "/" + consts.EecTokenSecretKey}, + {Name: envEecTokenPath, Value: eecTokenMountPath + "/" + eecConsts.TokenSecretKey}, {Name: envEecIngestPort, Value: strconv.Itoa(int(collectorPort))}, {Name: envExtensionsModuleExecPathName, Value: envExtensionsModuleExecPath}, {Name: envDsInstallDirName, Value: envDsInstallDir}, {Name: envK8sClusterId, Value: dk.Status.KubeSystemUUID}, - {Name: envK8sExtServiceUrl, Value: serviceUrlScheme + servicename.BuildFQDN(dk)}, + {Name: envK8sExtServiceUrl, Value: serviceUrlScheme + dk.ExtensionsServiceNameFQDN()}, {Name: envDSTokenPath, Value: eecTokenMountPath + "/" + consts.OtelcTokenSecretKey}, {Name: envHttpsCertPathPem, Value: envEecHttpsCertPathPem}, {Name: envHttpsPrivKeyPathPem, Value: envEecHttpsPrivKeyPathPem}, } - if dk.Spec.ActiveGate.TlsSecretName != "" { + if dk.ActiveGate().HasCaCert() { containerEnvs = append(containerEnvs, corev1.EnvVar{Name: envActiveGateTrustedCertName, Value: envActiveGateTrustedCert}) } @@ -317,7 +312,7 @@ func buildContainerVolumeMounts(dk *dynakube.DynaKube) []corev1.VolumeMount { }) } - if dk.Spec.ActiveGate.TlsSecretName != "" { + if dk.ActiveGate().HasCaCert() { volumeMounts = append(volumeMounts, corev1.VolumeMount{ Name: activeGateTrustedCertVolumeName, MountPath: activeGateTrustedCertMountPath, @@ -393,14 +388,14 @@ func setVolumes(dk *dynakube.DynaKube) func(o *appsv1.StatefulSet) { }) } - if dk.Spec.ActiveGate.TlsSecretName != "" { + if dk.ActiveGate().HasCaCert() { defaultMode := int32(420) o.Spec.Template.Spec.Volumes = append(o.Spec.Template.Spec.Volumes, corev1.Volume{ Name: activeGateTrustedCertVolumeName, VolumeSource: corev1.VolumeSource{ Secret: &corev1.SecretVolumeSource{ DefaultMode: &defaultMode, - SecretName: dk.Spec.ActiveGate.TlsSecretName, + SecretName: dk.ActiveGate().GetTLSSecretName(), Items: []corev1.KeyToPath{ { Key: activeGateTrustedCertSecretKeyPath, diff --git a/pkg/controllers/dynakube/extension/otel/conditions.go b/pkg/controllers/dynakube/extension/otel/conditions.go deleted file mode 100644 index 3d055c4a93..0000000000 --- a/pkg/controllers/dynakube/extension/otel/conditions.go +++ /dev/null @@ -1,3 +0,0 @@ -package otel - -const otelControllerStatefulSetConditionType string = "OtelStatefulSet" diff --git a/pkg/controllers/dynakube/extension/otel/reconciler.go b/pkg/controllers/dynakube/extension/otel/reconciler.go deleted file mode 100644 index 38ac866881..0000000000 --- a/pkg/controllers/dynakube/extension/otel/reconciler.go +++ /dev/null @@ -1,59 +0,0 @@ -package otel - -import ( - "context" - - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/controllers" - "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/statefulset" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/meta" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -type reconciler struct { - client client.Client - apiReader client.Reader - - dk *dynakube.DynaKube -} - -type ReconcilerBuilder func(clt client.Client, apiReader client.Reader, dk *dynakube.DynaKube) controllers.Reconciler - -var _ ReconcilerBuilder = NewReconciler - -func NewReconciler(clt client.Client, apiReader client.Reader, dk *dynakube.DynaKube) controllers.Reconciler { - return &reconciler{ - client: clt, - apiReader: apiReader, - dk: dk, - } -} - -func (r *reconciler) Reconcile(ctx context.Context) error { - if !r.dk.IsExtensionsEnabled() { - if meta.FindStatusCondition(*r.dk.Conditions(), otelControllerStatefulSetConditionType) == nil { - return nil - } - defer meta.RemoveStatusCondition(r.dk.Conditions(), otelControllerStatefulSetConditionType) - - sts, err := statefulset.Build(r.dk, r.dk.ExtensionsCollectorStatefulsetName(), corev1.Container{}) - if err != nil { - log.Error(err, "could not build "+r.dk.ExtensionsCollectorStatefulsetName()+" during cleanup") - - return err - } - - err = statefulset.Query(r.client, r.apiReader, log).Delete(ctx, sts) - - if err != nil { - log.Error(err, "failed to clean up "+r.dk.ExtensionsCollectorStatefulsetName()+" statufulset") - - return nil - } - - return nil - } - - return r.createOrUpdateStatefulset(ctx) -} diff --git a/pkg/controllers/dynakube/extension/otel/statefulset.go b/pkg/controllers/dynakube/extension/otel/statefulset.go deleted file mode 100644 index db84e338ec..0000000000 --- a/pkg/controllers/dynakube/extension/otel/statefulset.go +++ /dev/null @@ -1,342 +0,0 @@ -package otel - -import ( - "context" - "fmt" - "strconv" - - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/extension/consts" - "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/extension/servicename" - "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/extension/tls" - "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/extension/utils" - "github.com/Dynatrace/dynatrace-operator/pkg/util/address" - "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" - "github.com/Dynatrace/dynatrace-operator/pkg/util/hasher" - "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/labels" - "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/node" - k8ssecret "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/secret" - "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/statefulset" - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/types" -) - -const ( - serviceAccountName = "dynatrace-extensions-collector" - containerName = "collector" - - // default values - defaultImageRepo = "public.ecr.aws/dynatrace/dynatrace-otel-collector" - defaultImageTag = "latest" - defaultOLTPgrpcPort = "10001" - defaultOLTPhttpPort = "10002" - defaultReplicas = 1 - - // env variables - envShards = "SHARDS" - envShardId = "SHARD_ID" - envPodNamePrefix = "POD_NAME_PREFIX" - envPodName = "POD_NAME" - envOTLPgrpcPort = "OTLP_GRPC_PORT" - envOTLPhttpPort = "OTLP_HTTP_PORT" - envOTLPtoken = "OTLP_TOKEN" - envEECDStoken = "EEC_DS_TOKEN" - envTrustedCAs = "TRUSTED_CAS" - envK8sClusterName = "K8S_CLUSTER_NAME" - envK8sClusterUuid = "K8S_CLUSTER_UID" - envDTentityK8sCluster = "DT_ENTITY_KUBERNETES_CLUSTER" - // certDirEnv is the environment variable that identifies which directory - // to check for SSL certificate files. If set, this overrides the system default. - // It is a colon separated list of directories. - // See https://www.openssl.org/docs/man1.0.2/man1/c_rehash.html. - envCertDir = "SSL_CERT_DIR" - envEECcontrollerTLS = "EXTENSIONS_CONTROLLER_TLS" - - // Volume names and paths - caCertsVolumeName = "cacerts" - trustedCAVolumeMountPath = "/tls/custom/cacerts" - trustedCAVolumePath = trustedCAVolumeMountPath + "/certs" - customEecTLSCertificatePath = "/tls/custom/eec" - customEecTLSCertificateFullPath = customEecTLSCertificatePath + "/" + consts.TLSCrtDataName - secretsTokensPath = "/secrets/tokens" - otelcSecretTokenFilePath = secretsTokensPath + "/" + consts.OtelcTokenSecretKey - - // misc - trustedCAsFile = "rootca.pem" -) - -func (r *reconciler) createOrUpdateStatefulset(ctx context.Context) error { - appLabels := buildAppLabels(r.dk.Name) - - templateAnnotations, err := r.buildTemplateAnnotations(ctx) - if err != nil { - return err - } - - sts, err := statefulset.Build(r.dk, r.dk.ExtensionsCollectorStatefulsetName(), buildContainer(r.dk), - statefulset.SetReplicas(getReplicas(r.dk)), - statefulset.SetPodManagementPolicy(appsv1.ParallelPodManagement), - statefulset.SetAllLabels(appLabels.BuildLabels(), appLabels.BuildMatchLabels(), appLabels.BuildLabels(), r.dk.Spec.Templates.OpenTelemetryCollector.Labels), - statefulset.SetAllAnnotations(nil, templateAnnotations), - statefulset.SetAffinity(buildAffinity()), - statefulset.SetServiceAccount(serviceAccountName), - statefulset.SetTolerations(r.dk.Spec.Templates.OpenTelemetryCollector.Tolerations), - statefulset.SetTopologySpreadConstraints(utils.BuildTopologySpreadConstraints(r.dk.Spec.Templates.OpenTelemetryCollector.TopologySpreadConstraints, appLabels)), - statefulset.SetSecurityContext(buildPodSecurityContext()), - statefulset.SetUpdateStrategy(utils.BuildUpdateStrategy()), - setImagePullSecrets(r.dk.ImagePullSecretReferences()), - setVolumes(r.dk), - ) - - if err != nil { - conditions.SetKubeApiError(r.dk.Conditions(), otelControllerStatefulSetConditionType, err) - - return err - } - - if err := hasher.AddAnnotation(sts); err != nil { - conditions.SetKubeApiError(r.dk.Conditions(), otelControllerStatefulSetConditionType, err) - - return err - } - - _, err = statefulset.Query(r.client, r.apiReader, log).WithOwner(r.dk).CreateOrUpdate(ctx, sts) - if err != nil { - log.Info("failed to create/update " + r.dk.ExtensionsCollectorStatefulsetName() + " statefulset") - conditions.SetKubeApiError(r.dk.Conditions(), otelControllerStatefulSetConditionType, err) - - return err - } - - conditions.SetStatefulSetCreated(r.dk.Conditions(), otelControllerStatefulSetConditionType, sts.Name) - - return nil -} - -func (r *reconciler) buildTemplateAnnotations(ctx context.Context) (map[string]string, error) { - templateAnnotations := map[string]string{} - - if r.dk.Spec.Templates.OpenTelemetryCollector.Annotations != nil { - templateAnnotations = r.dk.Spec.Templates.OpenTelemetryCollector.Annotations - } - - query := k8ssecret.Query(r.client, r.client, log) - - tlsSecret, err := query.Get(ctx, types.NamespacedName{ - Name: tls.GetTLSSecretName(r.dk), - Namespace: r.dk.Namespace, - }) - if err != nil { - return nil, err - } - - tlsSecretHash, err := hasher.GenerateHash(tlsSecret.Data) - if err != nil { - return nil, err - } - - templateAnnotations[consts.ExtensionsAnnotationSecretHash] = tlsSecretHash - - return templateAnnotations, nil -} - -func getReplicas(dk *dynakube.DynaKube) int32 { - if dk.Spec.Templates.OpenTelemetryCollector.Replicas != nil { - return *dk.Spec.Templates.OpenTelemetryCollector.Replicas - } - - return defaultReplicas -} - -func buildContainer(dk *dynakube.DynaKube) corev1.Container { - imageRepo := dk.Spec.Templates.OpenTelemetryCollector.ImageRef.Repository - imageTag := dk.Spec.Templates.OpenTelemetryCollector.ImageRef.Tag - - if imageRepo == "" { - imageRepo = defaultImageRepo - } - - if imageTag == "" { - imageTag = defaultImageTag - } - - return corev1.Container{ - Name: containerName, - Image: imageRepo + ":" + imageTag, - ImagePullPolicy: corev1.PullAlways, - SecurityContext: buildSecurityContext(), - Env: buildContainerEnvs(dk), - Resources: dk.Spec.Templates.OpenTelemetryCollector.Resources, - Args: []string{fmt.Sprintf("--config=eec://%s:%d/otcconfig/prometheusMetrics#refresh-interval=5s&auth-file=%s", servicename.BuildFQDN(dk), consts.ExtensionsCollectorComPort, otelcSecretTokenFilePath)}, - VolumeMounts: buildContainerVolumeMounts(dk), - } -} - -func buildSecurityContext() *corev1.SecurityContext { - return &corev1.SecurityContext{ - SeccompProfile: &corev1.SeccompProfile{ - Type: corev1.SeccompProfileTypeRuntimeDefault, - }, - } -} - -func buildPodSecurityContext() *corev1.PodSecurityContext { - return &corev1.PodSecurityContext{ - SeccompProfile: &corev1.SeccompProfile{ - Type: corev1.SeccompProfileTypeRuntimeDefault, - }, - } -} - -func buildContainerEnvs(dk *dynakube.DynaKube) []corev1.EnvVar { - envs := []corev1.EnvVar{ - {Name: envShards, Value: strconv.Itoa(int(getReplicas(dk)))}, - {Name: envPodNamePrefix, Value: dk.ExtensionsCollectorStatefulsetName()}, - {Name: envPodName, ValueFrom: &corev1.EnvVarSource{ - FieldRef: &corev1.ObjectFieldSelector{ - FieldPath: "metadata.labels['statefulset.kubernetes.io/pod-name']", - }, - }, - }, - {Name: envShardId, ValueFrom: &corev1.EnvVarSource{ - FieldRef: &corev1.ObjectFieldSelector{ - FieldPath: "metadata.labels['apps.kubernetes.io/pod-index']", - }, - }, - }, - {Name: envOTLPgrpcPort, Value: defaultOLTPgrpcPort}, - {Name: envOTLPhttpPort, Value: defaultOLTPhttpPort}, - {Name: envOTLPtoken, ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{Name: dk.ExtensionsTokenSecretName()}, - Key: consts.OtelcTokenSecretKey, - }, - }, - }, - {Name: envEECDStoken, ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{Name: dk.ExtensionsTokenSecretName()}, - Key: consts.OtelcTokenSecretKey, - }, - }, - }, - {Name: envCertDir, Value: customEecTLSCertificatePath}, - {Name: envK8sClusterName, Value: dk.Name}, - {Name: envK8sClusterUuid, Value: dk.Status.KubeSystemUUID}, - {Name: envDTentityK8sCluster, Value: dk.Status.KubernetesClusterMEID}, - } - if dk.Spec.TrustedCAs != "" { - envs = append(envs, corev1.EnvVar{Name: envTrustedCAs, Value: trustedCAVolumePath}) - } - - envs = append(envs, corev1.EnvVar{Name: envEECcontrollerTLS, Value: customEecTLSCertificateFullPath}) - - return envs -} - -func buildAppLabels(dkName string) *labels.AppLabels { - // TODO: when version is available - version := "0.0.0" - - return labels.NewAppLabels(labels.CollectorComponentLabel, dkName, labels.CollectorComponentLabel, version) -} - -func buildAffinity() corev1.Affinity { - // TODO: implement new attributes in CR dk.Spec.Templates.OpenTelemetryCollector.Affinity - // otherwise to use defaults ones - return corev1.Affinity{ - NodeAffinity: &corev1.NodeAffinity{ - RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{ - NodeSelectorTerms: []corev1.NodeSelectorTerm{ - { - MatchExpressions: node.AffinityNodeRequirementForSupportedArches(), - }, - }, - }, - }, - } -} - -func setImagePullSecrets(imagePullSecrets []corev1.LocalObjectReference) func(o *appsv1.StatefulSet) { - return func(o *appsv1.StatefulSet) { - o.Spec.Template.Spec.ImagePullSecrets = imagePullSecrets - } -} - -func setVolumes(dk *dynakube.DynaKube) func(o *appsv1.StatefulSet) { - return func(o *appsv1.StatefulSet) { - o.Spec.Template.Spec.Volumes = []corev1.Volume{ - { - Name: consts.ExtensionsTokensVolumeName, - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: dk.ExtensionsTokenSecretName(), - Items: []corev1.KeyToPath{ - { - Key: consts.OtelcTokenSecretKey, - Path: consts.OtelcTokenSecretKey, - }, - }, - DefaultMode: address.Of(int32(420)), - }, - }, - }, - } - if dk.Spec.TrustedCAs != "" { - o.Spec.Template.Spec.Volumes = append(o.Spec.Template.Spec.Volumes, corev1.Volume{ - Name: caCertsVolumeName, - VolumeSource: corev1.VolumeSource{ - ConfigMap: &corev1.ConfigMapVolumeSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: dk.Spec.TrustedCAs, - }, - Items: []corev1.KeyToPath{ - { - Key: "certs", - Path: trustedCAsFile, - }, - }, - }, - }, - }) - } - - o.Spec.Template.Spec.Volumes = append(o.Spec.Template.Spec.Volumes, corev1.Volume{ - Name: dk.ExtensionsTLSSecretName(), - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: dk.ExtensionsTLSSecretName(), - Items: []corev1.KeyToPath{ - { - Key: consts.TLSCrtDataName, - Path: consts.TLSCrtDataName, - }, - }, - }, - }, - }) - } -} - -func buildContainerVolumeMounts(dk *dynakube.DynaKube) []corev1.VolumeMount { - vm := []corev1.VolumeMount{ - {Name: consts.ExtensionsTokensVolumeName, ReadOnly: true, MountPath: secretsTokensPath}, - } - - if dk.Spec.TrustedCAs != "" { - vm = append(vm, corev1.VolumeMount{ - Name: caCertsVolumeName, - MountPath: trustedCAVolumeMountPath, - ReadOnly: true, - }) - } - - vm = append(vm, corev1.VolumeMount{ - Name: dk.ExtensionsTLSSecretName(), - MountPath: customEecTLSCertificatePath, - ReadOnly: true, - }) - - return vm -} diff --git a/pkg/controllers/dynakube/extension/otel/statefulset_test.go b/pkg/controllers/dynakube/extension/otel/statefulset_test.go deleted file mode 100644 index 4b5a95cc2e..0000000000 --- a/pkg/controllers/dynakube/extension/otel/statefulset_test.go +++ /dev/null @@ -1,526 +0,0 @@ -package otel - -import ( - "context" - "fmt" - "testing" - - "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/extension/consts" - "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/extension/tls" - "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/extension/utils" - "github.com/Dynatrace/dynatrace-operator/pkg/util/address" - "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" - "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/node" - maputils "github.com/Dynatrace/dynatrace-operator/pkg/util/map" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -const ( - testDynakubeName = "dynakube" - testNamespaceName = "dynatrace" - testOtelPullSecret = "otel-pull-secret" -) - -func getTestDynakube() *dynakube.DynaKube { - return &dynakube.DynaKube{ - ObjectMeta: metav1.ObjectMeta{ - Name: testDynakubeName, - Namespace: testNamespaceName, - Annotations: map[string]string{}, - }, - Spec: dynakube.DynaKubeSpec{ - Extensions: &dynakube.ExtensionsSpec{}, - Templates: dynakube.TemplatesSpec{OpenTelemetryCollector: dynakube.OpenTelemetryCollectorSpec{}}, - }, - } -} - -func getStatefulset(t *testing.T, dk *dynakube.DynaKube) *appsv1.StatefulSet { - mockK8sClient := fake.NewClient(dk) - mockK8sClient = mockTLSSecret(t, mockK8sClient, dk) - - err := NewReconciler(mockK8sClient, mockK8sClient, dk).Reconcile(context.Background()) - require.NoError(t, err) - - statefulSet := &appsv1.StatefulSet{} - err = mockK8sClient.Get(context.Background(), client.ObjectKey{Name: dk.ExtensionsCollectorStatefulsetName(), Namespace: dk.Namespace}, statefulSet) - require.NoError(t, err) - - return statefulSet -} - -func mockTLSSecret(t *testing.T, client client.Client, dk *dynakube.DynaKube) client.Client { - tlsSecret := getTLSSecret(tls.GetTLSSecretName(dk), dk.Namespace, "super-cert", "super-key") - - err := client.Create(context.Background(), &tlsSecret) - require.NoError(t, err) - - return client -} - -func getTLSSecret(name string, namespace string, crt string, key string) corev1.Secret { - return corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - Data: map[string][]byte{ - consts.TLSCrtDataName: []byte(crt), - consts.TLSKeyDataName: []byte(key), - }, - } -} - -func TestConditions(t *testing.T) { - t.Run("extensions are disabled", func(t *testing.T) { - dk := getTestDynakube() - dk.Spec.Extensions = nil - conditions.SetStatefulSetCreated(dk.Conditions(), otelControllerStatefulSetConditionType, dk.ExtensionsCollectorStatefulsetName()) - - mockK8sClient := fake.NewClient(dk) - - err := NewReconciler(mockK8sClient, mockK8sClient, dk).Reconcile(context.Background()) - require.NoError(t, err) - - statefulSet := &appsv1.StatefulSet{} - err = mockK8sClient.Get(context.Background(), client.ObjectKey{Name: dk.ExtensionsCollectorStatefulsetName(), Namespace: dk.Namespace}, statefulSet) - require.Error(t, err) - - assert.True(t, errors.IsNotFound(err)) - - assert.Empty(t, dk.Conditions()) - }) -} - -func TestSecretHashAnnotation(t *testing.T) { - t.Run("annotation is set with self-signed tls secret", func(t *testing.T) { - dk := getTestDynakube() - dk.Spec.Templates.ExtensionExecutionController.TlsRefName = "" - statefulSet := getStatefulset(t, dk) - - require.Len(t, statefulSet.Spec.Template.Annotations, 1) - assert.NotEmpty(t, statefulSet.Spec.Template.Annotations[consts.ExtensionsAnnotationSecretHash]) - }) - t.Run("annotation is set with tlsRefName", func(t *testing.T) { - dk := getTestDynakube() - dk.Spec.Templates.ExtensionExecutionController.TlsRefName = "dummy-secret" - statefulSet := getStatefulset(t, dk) - - require.Len(t, statefulSet.Spec.Template.Annotations, 1) - assert.NotEmpty(t, statefulSet.Spec.Template.Annotations[consts.ExtensionsAnnotationSecretHash]) - }) - t.Run("annotation is updated when TLS Secret gets updated", func(t *testing.T) { - statefulSet := &appsv1.StatefulSet{} - dk := getTestDynakube() - - // first reconcile a basic setup - TLS Secret gets created - mockK8sClient := fake.NewClient(dk) - mockK8sClient = mockTLSSecret(t, mockK8sClient, dk) - - reconciler := NewReconciler(mockK8sClient, mockK8sClient, dk) - err := reconciler.Reconcile(context.Background()) - require.NoError(t, err) - - err = mockK8sClient.Get(context.Background(), client.ObjectKey{Name: dk.ExtensionsCollectorStatefulsetName(), Namespace: dk.Namespace}, statefulSet) - require.NoError(t, err) - - originalSecretHash := statefulSet.Spec.Template.Annotations[consts.ExtensionsAnnotationSecretHash] - - // then update the TLS Secret and call reconcile again - updatedTLSSecret := getTLSSecret(tls.GetTLSSecretName(dk), dk.Namespace, "updated-cert", "updated-key") - err = mockK8sClient.Update(context.Background(), &updatedTLSSecret) - require.NoError(t, err) - - err = reconciler.Reconcile(context.Background()) - require.NoError(t, err) - err = mockK8sClient.Get(context.Background(), client.ObjectKey{Name: dk.ExtensionsCollectorStatefulsetName(), Namespace: dk.Namespace}, statefulSet) - require.NoError(t, err) - - resultingSecretHash := statefulSet.Spec.Template.Annotations[consts.ExtensionsAnnotationSecretHash] - - // original hash and resulting hash should be different, value got updated on reconcile - assert.NotEqual(t, originalSecretHash, resultingSecretHash) - }) -} - -func TestStatefulsetBase(t *testing.T) { - t.Run("replicas", func(t *testing.T) { - statefulSet := getStatefulset(t, getTestDynakube()) - - assert.Equal(t, int32(1), *statefulSet.Spec.Replicas) - }) - - t.Run("pod management policy", func(t *testing.T) { - statefulSet := getStatefulset(t, getTestDynakube()) - - assert.Equal(t, appsv1.ParallelPodManagement, statefulSet.Spec.PodManagementPolicy) - }) -} - -func TestServiceAccountName(t *testing.T) { - t.Run("serviceAccountName is set", func(t *testing.T) { - statefulSet := getStatefulset(t, getTestDynakube()) - - assert.Equal(t, serviceAccountName, statefulSet.Spec.Template.Spec.ServiceAccountName) - assert.Equal(t, serviceAccountName, statefulSet.Spec.Template.Spec.DeprecatedServiceAccount) - }) -} - -func TestTopologySpreadConstraints(t *testing.T) { - t.Run("the default TopologySpreadConstraints", func(t *testing.T) { - dk := getTestDynakube() - statefulSet := getStatefulset(t, dk) - appLabels := buildAppLabels(dk.Name) - assert.Equal(t, utils.BuildTopologySpreadConstraints(dk.Spec.Templates.OpenTelemetryCollector.TopologySpreadConstraints, appLabels), statefulSet.Spec.Template.Spec.TopologySpreadConstraints) - }) - - t.Run("custom TopologySpreadConstraints", func(t *testing.T) { - dk := getTestDynakube() - - customTopologySpreadConstraints := []corev1.TopologySpreadConstraint{ - { - MaxSkew: 2, - TopologyKey: "kubernetes.io/hostname", - WhenUnsatisfiable: "DoNotSchedule", - LabelSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "a": "b", - }, - }, - }, - } - - dk.Spec.Templates.OpenTelemetryCollector.TopologySpreadConstraints = customTopologySpreadConstraints - - statefulSet := getStatefulset(t, dk) - - assert.Equal(t, customTopologySpreadConstraints, statefulSet.Spec.Template.Spec.TopologySpreadConstraints) - }) -} - -func TestEnvironmentVariables(t *testing.T) { - t.Run("environment variables", func(t *testing.T) { - dk := getTestDynakube() - - statefulSet := getStatefulset(t, dk) - - assert.Equal(t, corev1.EnvVar{Name: envShards, Value: fmt.Sprintf("%d", getReplicas(dk))}, statefulSet.Spec.Template.Spec.Containers[0].Env[0]) - assert.Equal(t, corev1.EnvVar{Name: envPodNamePrefix, Value: dk.Name + "-extensions-collector"}, statefulSet.Spec.Template.Spec.Containers[0].Env[1]) - assert.Equal(t, corev1.EnvVar{Name: envPodName, ValueFrom: &corev1.EnvVarSource{ - FieldRef: &corev1.ObjectFieldSelector{ - FieldPath: "metadata.labels['statefulset.kubernetes.io/pod-name']", - }, - }}, statefulSet.Spec.Template.Spec.Containers[0].Env[2]) - assert.Equal(t, corev1.EnvVar{Name: envShardId, ValueFrom: &corev1.EnvVarSource{ - FieldRef: &corev1.ObjectFieldSelector{ - FieldPath: "metadata.labels['apps.kubernetes.io/pod-index']", - }, - }}, statefulSet.Spec.Template.Spec.Containers[0].Env[3]) - assert.Equal(t, corev1.EnvVar{Name: envOTLPgrpcPort, Value: defaultOLTPgrpcPort}, statefulSet.Spec.Template.Spec.Containers[0].Env[4]) - assert.Equal(t, corev1.EnvVar{Name: envOTLPhttpPort, Value: defaultOLTPhttpPort}, statefulSet.Spec.Template.Spec.Containers[0].Env[5]) - assert.Equal(t, corev1.EnvVar{Name: envOTLPtoken, ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{Name: dk.ExtensionsTokenSecretName()}, - Key: consts.OtelcTokenSecretKey, - }, - }}, statefulSet.Spec.Template.Spec.Containers[0].Env[6]) - assert.Equal(t, corev1.EnvVar{Name: envEECDStoken, ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{Name: dk.ExtensionsTokenSecretName()}, - Key: consts.OtelcTokenSecretKey, - }, - }}, statefulSet.Spec.Template.Spec.Containers[0].Env[7]) - assert.Equal(t, corev1.EnvVar{Name: envCertDir, Value: customEecTLSCertificatePath}, statefulSet.Spec.Template.Spec.Containers[0].Env[8]) - assert.Equal(t, corev1.EnvVar{Name: envK8sClusterName, Value: dk.Name}, statefulSet.Spec.Template.Spec.Containers[0].Env[9]) - assert.Equal(t, corev1.EnvVar{Name: envK8sClusterUuid, Value: dk.Status.KubeSystemUUID}, statefulSet.Spec.Template.Spec.Containers[0].Env[10]) - assert.Equal(t, corev1.EnvVar{Name: envDTentityK8sCluster, Value: dk.Status.KubernetesClusterMEID}, statefulSet.Spec.Template.Spec.Containers[0].Env[11]) - }) - t.Run("environment variables with trustedCA", func(t *testing.T) { - dk := getTestDynakube() - dk.Spec.TrustedCAs = "test-trusted-ca" - - statefulSet := getStatefulset(t, dk) - - assert.Contains(t, statefulSet.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{Name: envTrustedCAs, Value: trustedCAVolumePath}) - }) - - t.Run("environment variables with custom EEC TLS certificate", func(t *testing.T) { - dk := getTestDynakube() - dk.Spec.Templates.ExtensionExecutionController.TlsRefName = "test-tls-ca" - - statefulSet := getStatefulset(t, dk) - - assert.Contains(t, statefulSet.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{Name: envEECcontrollerTLS, Value: customEecTLSCertificateFullPath}) - }) -} - -func TestAffinity(t *testing.T) { - t.Run("affinity", func(t *testing.T) { - dk := getTestDynakube() - statefulSet := getStatefulset(t, dk) - - expectedAffinity := &corev1.Affinity{ - NodeAffinity: &corev1.NodeAffinity{ - RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{ - NodeSelectorTerms: []corev1.NodeSelectorTerm{ - { - MatchExpressions: node.AffinityNodeRequirementForSupportedArches(), - }, - }, - }, - }, - } - - assert.Equal(t, expectedAffinity, statefulSet.Spec.Template.Spec.Affinity) - }) -} - -func TestImagePullSecrets(t *testing.T) { - t.Run("the default image pull secret only", func(t *testing.T) { - statefulSet := getStatefulset(t, getTestDynakube()) - - assert.Len(t, statefulSet.Spec.Template.Spec.ImagePullSecrets, 1) - }) - - t.Run("custom pull secret", func(t *testing.T) { - dk := getTestDynakube() - dk.Spec.CustomPullSecret = testOtelPullSecret - - statefulSet := getStatefulset(t, dk) - - require.Len(t, statefulSet.Spec.Template.Spec.ImagePullSecrets, 2) - assert.Equal(t, dk.Name+dynakube.PullSecretSuffix, statefulSet.Spec.Template.Spec.ImagePullSecrets[0].Name) - assert.Equal(t, dk.Spec.CustomPullSecret, statefulSet.Spec.Template.Spec.ImagePullSecrets[1].Name) - }) -} - -func TestResources(t *testing.T) { - t.Run("no resources", func(t *testing.T) { - dk := getTestDynakube() - statefulSet := getStatefulset(t, dk) - - assert.Empty(t, statefulSet.Spec.Template.Spec.Containers[0].Resources) - }) - - t.Run("custom resources", func(t *testing.T) { - dk := getTestDynakube() - dk.Spec.Templates.ExtensionExecutionController.Resources = corev1.ResourceRequirements{ - Requests: corev1.ResourceList{ - corev1.ResourceMemory: resource.MustParse("1Gi"), - }, - } - - statefulSet := getStatefulset(t, dk) - - assert.Equal(t, dk.Spec.Templates.OpenTelemetryCollector.Resources, statefulSet.Spec.Template.Spec.Containers[0].Resources) - }) -} - -func TestLabels(t *testing.T) { - t.Run("the default labels", func(t *testing.T) { - dk := getTestDynakube() - - statefulSet := getStatefulset(t, dk) - - appLabels := buildAppLabels(dk.Name) - - assert.Equal(t, appLabels.BuildLabels(), statefulSet.ObjectMeta.Labels) - assert.Equal(t, &metav1.LabelSelector{MatchLabels: appLabels.BuildMatchLabels()}, statefulSet.Spec.Selector) - assert.Equal(t, appLabels.BuildLabels(), statefulSet.Spec.Template.ObjectMeta.Labels) - }) - - t.Run("custom labels", func(t *testing.T) { - dk := getTestDynakube() - customLabels := map[string]string{ - "a": "b", - } - dk.Spec.Templates.OpenTelemetryCollector.Labels = customLabels - - statefulSet := getStatefulset(t, dk) - - appLabels := buildAppLabels(dk.Name) - podLabels := maputils.MergeMap(customLabels, appLabels.BuildLabels()) - - assert.Equal(t, appLabels.BuildLabels(), statefulSet.ObjectMeta.Labels) - assert.Equal(t, &metav1.LabelSelector{MatchLabels: appLabels.BuildMatchLabels()}, statefulSet.Spec.Selector) - assert.Equal(t, podLabels, statefulSet.Spec.Template.ObjectMeta.Labels) - }) -} - -func TestAnnotations(t *testing.T) { - t.Run("the default annotations", func(t *testing.T) { - statefulSet := getStatefulset(t, getTestDynakube()) - - assert.Len(t, statefulSet.ObjectMeta.Annotations, 1) - require.Len(t, statefulSet.Spec.Template.ObjectMeta.Annotations, 1) - assert.NotEmpty(t, statefulSet.Spec.Template.ObjectMeta.Annotations[consts.ExtensionsAnnotationSecretHash]) - }) - - t.Run("custom annotations", func(t *testing.T) { - dk := getTestDynakube() - customAnnotations := map[string]string{ - "a": "b", - } - dk.Spec.Templates.OpenTelemetryCollector.Annotations = customAnnotations - - statefulSet := getStatefulset(t, dk) - - require.Len(t, statefulSet.ObjectMeta.Annotations, 1) - assert.Empty(t, statefulSet.ObjectMeta.Annotations["a"]) - require.Len(t, statefulSet.Spec.Template.ObjectMeta.Annotations, 2) - assert.Equal(t, "b", statefulSet.Spec.Template.ObjectMeta.Annotations["a"]) - assert.NotEmpty(t, statefulSet.Spec.Template.ObjectMeta.Annotations[consts.ExtensionsAnnotationSecretHash]) - }) -} - -func TestTolerations(t *testing.T) { - t.Run("the default tolerations", func(t *testing.T) { - statefulSet := getStatefulset(t, getTestDynakube()) - - assert.Empty(t, statefulSet.Spec.Template.Spec.Tolerations) - }) - - t.Run("custom tolerations", func(t *testing.T) { - dk := getTestDynakube() - - customTolerations := []corev1.Toleration{ - { - Key: "a", - Operator: corev1.TolerationOpEqual, - Value: "b", - Effect: corev1.TaintEffectNoSchedule, - }, - } - dk.Spec.Templates.OpenTelemetryCollector.Tolerations = customTolerations - - statefulSet := getStatefulset(t, dk) - - assert.Equal(t, customTolerations, statefulSet.Spec.Template.Spec.Tolerations) - }) -} - -func TestSecurityContext(t *testing.T) { - t.Run("the default securityContext is set", func(t *testing.T) { - statefulSet := getStatefulset(t, getTestDynakube()) - - assert.NotNil(t, statefulSet.Spec.Template.Spec.SecurityContext) - assert.NotNil(t, statefulSet.Spec.Template.Spec.Containers[0].SecurityContext) - }) -} - -func TestUpdateStrategy(t *testing.T) { - t.Run("the default update strategy is set", func(t *testing.T) { - statefulSet := getStatefulset(t, getTestDynakube()) - - assert.NotNil(t, statefulSet.Spec.UpdateStrategy.RollingUpdate.Partition) - assert.NotEmpty(t, statefulSet.Spec.UpdateStrategy.Type) - }) -} - -func TestVolumes(t *testing.T) { - t.Run("volume mounts with trusted CAs", func(t *testing.T) { - dk := getTestDynakube() - dk.Spec.TrustedCAs = "test-trusted-cas" - statefulSet := getStatefulset(t, dk) - - expectedVolumeMount := corev1.VolumeMount{ - Name: caCertsVolumeName, - MountPath: trustedCAVolumeMountPath, - ReadOnly: true, - } - assert.Contains(t, statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts, expectedVolumeMount) - }) - t.Run("volume mounts without trusted CAs", func(t *testing.T) { - dk := getTestDynakube() - statefulSet := getStatefulset(t, dk) - - expectedVolumeMount := corev1.VolumeMount{ - Name: caCertsVolumeName, - MountPath: trustedCAVolumeMountPath, - ReadOnly: true, - } - - assert.NotContains(t, statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts, expectedVolumeMount) - }) - t.Run("volumes and volume mounts with custom EEC TLS certificate", func(t *testing.T) { - dk := getTestDynakube() - dk.Spec.Templates.ExtensionExecutionController.TlsRefName = "test-tls-name" - statefulSet := getStatefulset(t, dk) - - expectedVolumeMount := corev1.VolumeMount{ - Name: dk.ExtensionsTLSSecretName(), - MountPath: customEecTLSCertificatePath, - ReadOnly: true, - } - - expectedVolume := corev1.Volume{Name: dk.Spec.Templates.ExtensionExecutionController.TlsRefName, - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: dk.Spec.Templates.ExtensionExecutionController.TlsRefName, - Items: []corev1.KeyToPath{ - { - Key: consts.TLSCrtDataName, - Path: consts.TLSCrtDataName, - }, - }, - }, - }} - - assert.Contains(t, statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts, expectedVolumeMount) - assert.Contains(t, statefulSet.Spec.Template.Spec.Volumes, expectedVolume) - }) - t.Run("volumes with trusted CAs", func(t *testing.T) { - dk := getTestDynakube() - dk.Spec.TrustedCAs = "test-trusted-cas" - statefulSet := getStatefulset(t, dk) - - expectedVolume := corev1.Volume{ - Name: caCertsVolumeName, - VolumeSource: corev1.VolumeSource{ - ConfigMap: &corev1.ConfigMapVolumeSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: dk.Spec.TrustedCAs, - }, - Items: []corev1.KeyToPath{ - { - Key: "certs", - Path: trustedCAsFile, - }, - }, - }, - }, - } - assert.Contains(t, statefulSet.Spec.Template.Spec.Volumes, expectedVolume) - }) - t.Run("volumes with otel token", func(t *testing.T) { - dk := getTestDynakube() - statefulSet := getStatefulset(t, dk) - - expectedVolume := corev1.Volume{ - Name: consts.ExtensionsTokensVolumeName, - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: dk.ExtensionsTokenSecretName(), - Items: []corev1.KeyToPath{ - { - Key: consts.OtelcTokenSecretKey, - Path: consts.OtelcTokenSecretKey, - }, - }, - DefaultMode: address.Of(int32(420)), - }, - }, - } - - assert.Contains(t, statefulSet.Spec.Template.Spec.Volumes, expectedVolume) - }) -} diff --git a/pkg/controllers/dynakube/extension/reconciler.go b/pkg/controllers/dynakube/extension/reconciler.go index a6ac436216..da29ab9a8f 100644 --- a/pkg/controllers/dynakube/extension/reconciler.go +++ b/pkg/controllers/dynakube/extension/reconciler.go @@ -3,10 +3,9 @@ package extension import ( "context" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/controllers" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/extension/eec" - "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/extension/otel" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/extension/tls" "github.com/Dynatrace/dynatrace-operator/pkg/util/timeprovider" "sigs.k8s.io/controller-runtime/pkg/client" @@ -56,10 +55,5 @@ func (r *reconciler) Reconcile(ctx context.Context) error { return err } - err = otel.NewReconciler(r.client, r.apiReader, r.dk).Reconcile(ctx) - if err != nil { - return err - } - return nil } diff --git a/pkg/controllers/dynakube/extension/reconciler_test.go b/pkg/controllers/dynakube/extension/reconciler_test.go index 65f0710ea7..a1e4c2e814 100644 --- a/pkg/controllers/dynakube/extension/reconciler_test.go +++ b/pkg/controllers/dynakube/extension/reconciler_test.go @@ -6,10 +6,10 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/communication" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" - "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/extension/consts" - "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/extension/servicename" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/consts" + eecConsts "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/extension/consts" "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" "github.com/Dynatrace/dynatrace-operator/pkg/util/dttoken" k8ssecret "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/secret" @@ -50,12 +50,12 @@ func TestReconciler_Reconcile(t *testing.T) { dk := createDynakube() // mock SecretCreated condition - conditions.SetSecretCreated(dk.Conditions(), consts.ExtensionsSecretConditionType, dk.ExtensionsTokenSecretName()) + conditions.SetSecretCreated(dk.Conditions(), secretConditionType, dk.ExtensionsTokenSecretName()) // mock secret - secretToken, _ := dttoken.New(consts.EecTokenSecretValuePrefix) + secretToken, _ := dttoken.New(eecConsts.TokenSecretValuePrefix) secretData := map[string][]byte{ - consts.EecTokenSecretKey: []byte(secretToken.String()), + eecConsts.TokenSecretKey: []byte(secretToken.String()), } secretMock, _ := k8ssecret.Build(dk, testName+"-extensions-token", secretData) @@ -95,13 +95,13 @@ func TestReconciler_Reconcile(t *testing.T) { var secretFound corev1.Secret err = fakeClient.Get(context.Background(), client.ObjectKey{Name: testName + "-extensions-token", Namespace: testNamespace}, &secretFound) require.NoError(t, err) - require.NotEmpty(t, secretFound.Data[consts.EecTokenSecretKey]) + require.NotEmpty(t, secretFound.Data[eecConsts.TokenSecretKey]) require.NotEmpty(t, secretFound.Data[consts.OtelcTokenSecretKey]) // assert extensions token condition is added require.NotEmpty(t, dk.Conditions()) - condition := meta.FindStatusCondition(*dk.Conditions(), consts.ExtensionsSecretConditionType) + condition := meta.FindStatusCondition(*dk.Conditions(), secretConditionType) assert.Equal(t, metav1.ConditionTrue, condition.Status) assert.Equal(t, conditions.SecretCreatedReason, condition.Reason) assert.Equal(t, dk.ExtensionsTokenSecretName()+" created", condition.Message) @@ -118,7 +118,7 @@ func TestReconciler_Reconcile(t *testing.T) { // assert extensions token condition is added require.NotEmpty(t, dk.Conditions()) - condition := meta.FindStatusCondition(*dk.Conditions(), consts.ExtensionsSecretConditionType) + condition := meta.FindStatusCondition(*dk.Conditions(), secretConditionType) assert.Equal(t, metav1.ConditionFalse, condition.Status) assert.Equal(t, conditions.KubeApiErrorReason, condition.Reason) assert.Contains(t, condition.Message, "A problem occurred when using the Kubernetes API") @@ -136,17 +136,17 @@ func TestReconciler_Reconcile(t *testing.T) { require.NoError(t, err) var svc corev1.Service - err = mockK8sClient.Get(context.Background(), client.ObjectKey{Name: servicename.Build(r.dk), Namespace: testNamespace}, &svc) + err = mockK8sClient.Get(context.Background(), client.ObjectKey{Name: dk.ExtensionsServiceName(), Namespace: testNamespace}, &svc) require.NoError(t, err) assert.NotNil(t, svc) // assert extensions token condition is added require.NotEmpty(t, dk.Conditions()) - condition := meta.FindStatusCondition(*dk.Conditions(), consts.ExtensionsServiceConditionType) + condition := meta.FindStatusCondition(*dk.Conditions(), serviceConditionType) assert.Equal(t, metav1.ConditionTrue, condition.Status) assert.Equal(t, conditions.ServiceCreatedReason, condition.Reason) - assert.Equal(t, dk.Name+consts.ExtensionsControllerSuffix+" created", condition.Message) + assert.Equal(t, dk.Name+eecConsts.ExtensionsControllerSuffix+" created", condition.Message) }) t.Run("Don't create service when extensions are disabled with minimal setup", func(t *testing.T) { @@ -161,7 +161,7 @@ func TestReconciler_Reconcile(t *testing.T) { require.NoError(t, err) var svc corev1.Service - err = mockK8sClient.Get(context.Background(), client.ObjectKey{Name: servicename.Build(r.dk), Namespace: testNamespace}, &svc) + err = mockK8sClient.Get(context.Background(), client.ObjectKey{Name: dk.ExtensionsServiceName(), Namespace: testNamespace}, &svc) require.Error(t, err) assert.True(t, k8serrors.IsNotFound(err)) }) diff --git a/pkg/controllers/dynakube/extension/secret.go b/pkg/controllers/dynakube/extension/secret.go index f6bfa6fd6c..ef1523aac0 100644 --- a/pkg/controllers/dynakube/extension/secret.go +++ b/pkg/controllers/dynakube/extension/secret.go @@ -3,7 +3,8 @@ package extension import ( "context" - "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/extension/consts" + "github.com/Dynatrace/dynatrace-operator/pkg/consts" + eecConsts "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/extension/consts" "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" "github.com/Dynatrace/dynatrace-operator/pkg/util/dttoken" k8ssecret "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/secret" @@ -16,10 +17,10 @@ import ( func (r *reconciler) reconcileSecret(ctx context.Context) error { if !r.dk.IsExtensionsEnabled() { - if meta.FindStatusCondition(*r.dk.Conditions(), consts.ExtensionsSecretConditionType) == nil { + if meta.FindStatusCondition(*r.dk.Conditions(), secretConditionType) == nil { return nil } - defer meta.RemoveStatusCondition(r.dk.Conditions(), consts.ExtensionsSecretConditionType) + defer meta.RemoveStatusCondition(r.dk.Conditions(), secretConditionType) secret, err := r.buildSecret(dttoken.Token{}, dttoken.Token{}) if err != nil { @@ -41,16 +42,16 @@ func (r *reconciler) reconcileSecret(ctx context.Context) error { _, err := k8ssecret.Query(r.client, r.apiReader, log).Get(ctx, client.ObjectKey{Name: r.getSecretName(), Namespace: r.dk.Namespace}) if err != nil && !k8serrors.IsNotFound(err) { log.Info("failed to check existence of extension secret") - conditions.SetKubeApiError(r.dk.Conditions(), consts.ExtensionsSecretConditionType, err) + conditions.SetKubeApiError(r.dk.Conditions(), secretConditionType, err) return err } if k8serrors.IsNotFound(err) { - newEecToken, err := dttoken.New(consts.EecTokenSecretValuePrefix) + newEecToken, err := dttoken.New(eecConsts.TokenSecretValuePrefix) if err != nil { log.Info("failed to generate eec token") - conditions.SetSecretGenFailed(r.dk.Conditions(), consts.ExtensionsSecretConditionType, errors.Wrap(err, "error generating eec token")) + conditions.SetSecretGenFailed(r.dk.Conditions(), secretConditionType, errors.Wrap(err, "error generating eec token")) return err } @@ -58,7 +59,7 @@ func (r *reconciler) reconcileSecret(ctx context.Context) error { newOtelcToken, err := dttoken.New(consts.OtelcTokenSecretValuePrefix) if err != nil { log.Info("failed to generate otelc token") - conditions.SetSecretGenFailed(r.dk.Conditions(), consts.ExtensionsSecretConditionType, errors.Wrap(err, "error generating otelc token")) + conditions.SetSecretGenFailed(r.dk.Conditions(), secretConditionType, errors.Wrap(err, "error generating otelc token")) return err } @@ -66,7 +67,7 @@ func (r *reconciler) reconcileSecret(ctx context.Context) error { newSecret, err := r.buildSecret(*newEecToken, *newOtelcToken) if err != nil { log.Info("failed to generate extension secret") - conditions.SetSecretGenFailed(r.dk.Conditions(), consts.ExtensionsSecretConditionType, err) + conditions.SetSecretGenFailed(r.dk.Conditions(), secretConditionType, err) return err } @@ -74,20 +75,20 @@ func (r *reconciler) reconcileSecret(ctx context.Context) error { _, err = k8ssecret.Query(r.client, r.apiReader, log).CreateOrUpdate(ctx, newSecret) if err != nil { log.Info("failed to create/update extension secret") - conditions.SetKubeApiError(r.dk.Conditions(), consts.ExtensionsSecretConditionType, err) + conditions.SetKubeApiError(r.dk.Conditions(), secretConditionType, err) return err } } - conditions.SetSecretCreated(r.dk.Conditions(), consts.ExtensionsSecretConditionType, r.getSecretName()) + conditions.SetSecretCreated(r.dk.Conditions(), secretConditionType, r.getSecretName()) return nil } func (r *reconciler) buildSecret(eecToken dttoken.Token, otelcToken dttoken.Token) (*corev1.Secret, error) { secretData := map[string][]byte{ - consts.EecTokenSecretKey: []byte(eecToken.String()), + eecConsts.TokenSecretKey: []byte(eecToken.String()), consts.OtelcTokenSecretKey: []byte(otelcToken.String()), } diff --git a/pkg/controllers/dynakube/extension/service.go b/pkg/controllers/dynakube/extension/service.go index b7bee955a5..1477465559 100644 --- a/pkg/controllers/dynakube/extension/service.go +++ b/pkg/controllers/dynakube/extension/service.go @@ -3,8 +3,7 @@ package extension import ( "context" - "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/extension/consts" - "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/extension/servicename" + "github.com/Dynatrace/dynatrace-operator/pkg/consts" "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/labels" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/service" @@ -15,10 +14,10 @@ import ( func (r *reconciler) reconcileService(ctx context.Context) error { if !r.dk.IsExtensionsEnabled() { - if meta.FindStatusCondition(*r.dk.Conditions(), consts.ExtensionsServiceConditionType) == nil { + if meta.FindStatusCondition(*r.dk.Conditions(), serviceConditionType) == nil { return nil } - defer meta.RemoveStatusCondition(r.dk.Conditions(), consts.ExtensionsServiceConditionType) + defer meta.RemoveStatusCondition(r.dk.Conditions(), serviceConditionType) svc, err := r.buildService() if err != nil { @@ -44,7 +43,7 @@ func (r *reconciler) reconcileService(ctx context.Context) error { func (r *reconciler) createOrUpdateService(ctx context.Context) error { newService, err := r.buildService() if err != nil { - conditions.SetServiceGenFailed(r.dk.Conditions(), consts.ExtensionsServiceConditionType, err) + conditions.SetServiceGenFailed(r.dk.Conditions(), serviceConditionType, err) return err } @@ -52,12 +51,12 @@ func (r *reconciler) createOrUpdateService(ctx context.Context) error { _, err = service.Query(r.client, r.apiReader, log).CreateOrUpdate(ctx, newService) if err != nil { log.Info("failed to create/update extension service") - conditions.SetKubeApiError(r.dk.Conditions(), consts.ExtensionsServiceConditionType, err) + conditions.SetKubeApiError(r.dk.Conditions(), serviceConditionType, err) return err } - conditions.SetServiceCreated(r.dk.Conditions(), consts.ExtensionsServiceConditionType, servicename.Build(r.dk)) + conditions.SetServiceCreated(r.dk.Conditions(), serviceConditionType, r.dk.ExtensionsServiceName()) return nil } @@ -67,17 +66,19 @@ func (r *reconciler) buildService() (*corev1.Service, error) { // TODO: add proper version later on appLabels := labels.NewAppLabels(labels.ExtensionComponentLabel, r.dk.Name, labels.ExtensionComponentLabel, "") - svcPort := corev1.ServicePort{ - Name: servicename.BuildPortName(), - Port: consts.ExtensionsCollectorComPort, - Protocol: corev1.ProtocolTCP, - TargetPort: intstr.IntOrString{Type: intstr.String, StrVal: consts.ExtensionsCollectorTargetPortName}, + svcPorts := []corev1.ServicePort{ + { + Name: r.dk.ExtensionsPortName(), + Port: consts.OtelCollectorComPort, + Protocol: corev1.ProtocolTCP, + TargetPort: intstr.IntOrString{Type: intstr.String, StrVal: consts.ExtensionsCollectorTargetPortName}, + }, } return service.Build(r.dk, - servicename.Build(r.dk), + r.dk.ExtensionsServiceName(), appLabels.BuildMatchLabels(), - svcPort, + svcPorts, service.SetLabels(coreLabels.BuildLabels()), service.SetType(corev1.ServiceTypeClusterIP), ) diff --git a/pkg/controllers/dynakube/extension/service_test.go b/pkg/controllers/dynakube/extension/service_test.go index 68086ad346..075e461f62 100644 --- a/pkg/controllers/dynakube/extension/service_test.go +++ b/pkg/controllers/dynakube/extension/service_test.go @@ -4,7 +4,6 @@ import ( "testing" "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" - "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/extension/servicename" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/labels" "github.com/Dynatrace/dynatrace-operator/pkg/util/timeprovider" "github.com/Dynatrace/dynatrace-operator/pkg/version" @@ -38,6 +37,6 @@ func TestReconciler_prepareService(t *testing.T) { func TestFQDNNameGeneration(t *testing.T) { t.Run(`Check FQDN name generation`, func(t *testing.T) { dk := createDynakube() - assert.Equal(t, "test-name-extensions-controller.test-namespace", servicename.BuildFQDN(dk)) + assert.Equal(t, "test-name-extensions-controller.test-namespace", dk.ExtensionsServiceNameFQDN()) }) } diff --git a/pkg/controllers/dynakube/extension/servicename/naming.go b/pkg/controllers/dynakube/extension/servicename/naming.go deleted file mode 100644 index eb56f15f8f..0000000000 --- a/pkg/controllers/dynakube/extension/servicename/naming.go +++ /dev/null @@ -1,18 +0,0 @@ -package servicename - -import ( - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/extension/consts" -) - -func BuildPortName() string { - return "dynatrace" + consts.ExtensionsControllerSuffix + "-" + consts.ExtensionsCollectorTargetPortName -} - -func BuildFQDN(dk *dynakube.DynaKube) string { - return Build(dk) + "." + dk.Namespace -} - -func Build(dk *dynakube.DynaKube) string { - return dk.Name + consts.ExtensionsControllerSuffix -} diff --git a/pkg/controllers/dynakube/extension/tls/reconciler.go b/pkg/controllers/dynakube/extension/tls/reconciler.go index cbec6f81d4..6ef8a57bf4 100644 --- a/pkg/controllers/dynakube/extension/tls/reconciler.go +++ b/pkg/controllers/dynakube/extension/tls/reconciler.go @@ -4,9 +4,9 @@ import ( "context" "crypto/x509" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/consts" "github.com/Dynatrace/dynatrace-operator/pkg/controllers" - "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/extension/consts" "github.com/Dynatrace/dynatrace-operator/pkg/util/certificates" "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" k8slabels "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/labels" @@ -20,6 +20,10 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) +const ( + extensionsSelfSignedTLSCommonNameSuffix = "extensions-controller" +) + type reconciler struct { client client.Client apiReader client.Reader @@ -54,7 +58,7 @@ func (r *reconciler) reconcileSelfSignedTLSSecret(ctx context.Context) error { query := k8ssecret.Query(r.client, r.client, log) _, err := query.Get(ctx, types.NamespacedName{ - Name: getSelfSignedTLSSecretName(r.dk.Name), + Name: r.dk.ExtensionsSelfSignedTLSSecretName(), Namespace: r.dk.Namespace, }) @@ -76,7 +80,7 @@ func (r *reconciler) deleteSelfSignedTLSSecret(ctx context.Context) error { return query.Delete(ctx, &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: getSelfSignedTLSSecretName(r.dk.Name), + Name: r.dk.ExtensionsSelfSignedTLSSecretName(), Namespace: r.dk.Namespace, }, }) @@ -90,10 +94,10 @@ func (r *reconciler) createSelfSignedTLSSecret(ctx context.Context) error { return err } - cert.Cert.DNSNames = getCertificateAltNames(r.dk.Name) + cert.Cert.DNSNames = certificates.AltNames(r.dk.Name, r.dk.Namespace, extensionsSelfSignedTLSCommonNameSuffix) cert.Cert.KeyUsage = x509.KeyUsageKeyEncipherment | x509.KeyUsageDataEncipherment cert.Cert.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth} - cert.Cert.Subject.CommonName = r.dk.Name + consts.ExtensionsSelfSignedTLSCommonNameSuffix + cert.Cert.Subject.CommonName = certificates.CommonName(r.dk.Name, r.dk.Namespace, extensionsSelfSignedTLSCommonNameSuffix) err = cert.SelfSign() if err != nil { @@ -112,7 +116,7 @@ func (r *reconciler) createSelfSignedTLSSecret(ctx context.Context) error { coreLabels := k8slabels.NewCoreLabels(r.dk.Name, k8slabels.ExtensionComponentLabel) secretData := map[string][]byte{consts.TLSCrtDataName: pemCert, consts.TLSKeyDataName: pemPk} - secret, err := k8ssecret.Build(r.dk, getSelfSignedTLSSecretName(r.dk.Name), secretData, k8ssecret.SetLabels(coreLabels.BuildLabels())) + secret, err := k8ssecret.Build(r.dk, r.dk.ExtensionsSelfSignedTLSSecretName(), secretData, k8ssecret.SetLabels(coreLabels.BuildLabels())) if err != nil { conditions.SetSecretGenFailed(r.dk.Conditions(), conditionType, err) @@ -134,23 +138,3 @@ func (r *reconciler) createSelfSignedTLSSecret(ctx context.Context) error { return nil } - -func GetTLSSecretName(dk *dynakube.DynaKube) string { - if dk.ExtensionsNeedsSelfSignedTLS() { - return getSelfSignedTLSSecretName(dk.Name) - } - - return dk.ExtensionsTLSRefName() -} - -func getSelfSignedTLSSecretName(dkName string) string { - return dkName + consts.ExtensionsSelfSignedTLSSecretSuffix -} - -func getCertificateAltNames(dkName string) []string { - return []string{ - dkName + "-extensions-controller.dynatrace", - dkName + "-extensions-controller.dynatrace.svc", - dkName + "-extensions-controller.dynatrace.svc.cluster.local", - } -} diff --git a/pkg/controllers/dynakube/extension/tls/reconciler_test.go b/pkg/controllers/dynakube/extension/tls/reconciler_test.go index a4fa1a0985..0fd16d4e98 100644 --- a/pkg/controllers/dynakube/extension/tls/reconciler_test.go +++ b/pkg/controllers/dynakube/extension/tls/reconciler_test.go @@ -8,9 +8,9 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/communication" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/image" "github.com/Dynatrace/dynatrace-operator/pkg/api/status" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" - "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/extension/consts" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/consts" "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -31,8 +31,6 @@ const ( testCustomConfigConfigMapName = "eec-custom-config" ) -var SelfSignedTLSSecretObjectKey = client.ObjectKey{Name: getSelfSignedTLSSecretName(testDynakubeName), Namespace: testNamespaceName} - func TestReconcile(t *testing.T) { t.Run("self-signed tls secret is not generated", func(t *testing.T) { dk := getTestDynakube() @@ -45,7 +43,9 @@ func TestReconcile(t *testing.T) { require.NoError(t, err) var secret corev1.Secret - err = fakeClient.Get(context.Background(), SelfSignedTLSSecretObjectKey, &secret) + + key := client.ObjectKey{Name: dk.ExtensionsSelfSignedTLSSecretName(), Namespace: testNamespaceName} + err = fakeClient.Get(context.Background(), key, &secret) require.True(t, k8serrors.IsNotFound(err)) assert.Equal(t, corev1.Secret{}, secret) @@ -62,7 +62,9 @@ func TestReconcile(t *testing.T) { require.NoError(t, err) var secret corev1.Secret - err = fakeClient.Get(context.Background(), SelfSignedTLSSecretObjectKey, &secret) + + key := client.ObjectKey{Name: dk.ExtensionsSelfSignedTLSSecretName(), Namespace: testNamespaceName} + err = fakeClient.Get(context.Background(), key, &secret) require.NoError(t, err) assert.NotEmpty(t, secret) @@ -86,7 +88,9 @@ func TestReconcile(t *testing.T) { require.NoError(t, err) var secret corev1.Secret - err = fakeClient.Get(context.Background(), SelfSignedTLSSecretObjectKey, &secret) + + key := client.ObjectKey{Name: dk.ExtensionsSelfSignedTLSSecretName(), Namespace: testNamespaceName} + err = fakeClient.Get(context.Background(), key, &secret) require.NoError(t, err) require.NotEmpty(t, secret) @@ -106,7 +110,9 @@ func TestReconcile(t *testing.T) { require.NoError(t, err) var secret corev1.Secret - err = fakeClient.Get(context.Background(), SelfSignedTLSSecretObjectKey, &secret) + + key := client.ObjectKey{Name: dk.ExtensionsSelfSignedTLSSecretName(), Namespace: testNamespaceName} + err = fakeClient.Get(context.Background(), key, &secret) require.True(t, k8serrors.IsNotFound(err)) assert.Empty(t, secret) @@ -126,7 +132,9 @@ func TestReconcile(t *testing.T) { require.NoError(t, err) var secret corev1.Secret - err = fakeClient.Get(context.Background(), SelfSignedTLSSecretObjectKey, &secret) + + key := client.ObjectKey{Name: dk.ExtensionsSelfSignedTLSSecretName(), Namespace: testNamespaceName} + err = fakeClient.Get(context.Background(), key, &secret) require.True(t, k8serrors.IsNotFound(err)) assert.Equal(t, corev1.Secret{}, secret) @@ -139,16 +147,14 @@ func TestGetTLSSecretName(t *testing.T) { dk := getTestDynakube() dk.Spec.Templates.ExtensionExecutionController.TlsRefName = "" - secretName := GetTLSSecretName(dk) - - assert.Equal(t, getSelfSignedTLSSecretName(dk.Name), secretName) + secretName := dk.ExtensionsTLSSecretName() + assert.Equal(t, dk.ExtensionsSelfSignedTLSSecretName(), secretName) }) t.Run("tlsRefName secret", func(t *testing.T) { dk := getTestDynakube() dk.Spec.Templates.ExtensionExecutionController.TlsRefName = "dummy-value" - secretName := GetTLSSecretName(dk) - + secretName := dk.ExtensionsTLSSecretName() assert.Equal(t, "dummy-value", secretName) }) } @@ -196,7 +202,7 @@ func mockSelfSignedTLSSecret(t *testing.T, client client.Client, dk *dynakube.Dy func getSelfSignedTLSSecret(dk *dynakube.DynaKube) corev1.Secret { return corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: getSelfSignedTLSSecretName(dk.Name), + Name: dk.ExtensionsTLSSecretName(), Namespace: dk.Namespace, }, Data: map[string][]byte{ diff --git a/pkg/controllers/dynakube/extension/utils/utils.go b/pkg/controllers/dynakube/extension/utils/utils.go deleted file mode 100644 index 4e96d58f85..0000000000 --- a/pkg/controllers/dynakube/extension/utils/utils.go +++ /dev/null @@ -1,40 +0,0 @@ -package utils - -import ( - "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/labels" - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func BuildTopologySpreadConstraints(topologySpreadConstraints []corev1.TopologySpreadConstraint, appLabels *labels.AppLabels) []corev1.TopologySpreadConstraint { - if len(topologySpreadConstraints) > 0 { - return topologySpreadConstraints - } else { - return []corev1.TopologySpreadConstraint{ - { - MaxSkew: 1, - TopologyKey: "topology.kubernetes.io/zone", - WhenUnsatisfiable: "ScheduleAnyway", - LabelSelector: &metav1.LabelSelector{MatchLabels: appLabels.BuildMatchLabels()}, - }, - { - MaxSkew: 1, - TopologyKey: "kubernetes.io/hostname", - WhenUnsatisfiable: "DoNotSchedule", - LabelSelector: &metav1.LabelSelector{MatchLabels: appLabels.BuildMatchLabels()}, - }, - } - } -} - -func BuildUpdateStrategy() appsv1.StatefulSetUpdateStrategy { - partition := int32(0) - - return appsv1.StatefulSetUpdateStrategy{ - RollingUpdate: &appsv1.RollingUpdateStatefulSetStrategy{ - Partition: &partition, - }, - Type: appsv1.RollingUpdateStatefulSetStrategyType, - } -} diff --git a/pkg/controllers/dynakube/injection/reconciler.go b/pkg/controllers/dynakube/injection/reconciler.go index c8cb201644..d908c7c32f 100644 --- a/pkg/controllers/dynakube/injection/reconciler.go +++ b/pkg/controllers/dynakube/injection/reconciler.go @@ -5,7 +5,7 @@ import ( goerrors "errors" "github.com/Dynatrace/dynatrace-operator/pkg/api/status" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/controllers" oaconnectioninfo "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/connectioninfo/oneagent" @@ -14,6 +14,7 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/monitoredentities" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/processmoduleconfigsecret" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/version" + "github.com/Dynatrace/dynatrace-operator/pkg/injection/namespace/bootstrapperconfig" "github.com/Dynatrace/dynatrace-operator/pkg/injection/namespace/ingestendpoint" "github.com/Dynatrace/dynatrace-operator/pkg/injection/namespace/initgeneration" "github.com/Dynatrace/dynatrace-operator/pkg/injection/namespace/mapper" @@ -33,6 +34,7 @@ type reconciler struct { connectionInfoReconciler controllers.Reconciler monitoredEntitiesReconciler controllers.Reconciler enrichmentRulesReconciler controllers.Reconciler + dynatraceClient dynatrace.Client } type ReconcilerBuilder func( @@ -61,6 +63,7 @@ func NewReconciler( client: client, apiReader: apiReader, dk: dk, + dynatraceClient: dynatraceClient, istioReconciler: istioReconciler, versionReconciler: version.NewReconciler(apiReader, dynatraceClient, timeprovider.New().Freeze()), pmcSecretreconciler: processmoduleconfigsecret.NewReconciler( @@ -75,7 +78,7 @@ func (r *reconciler) Reconcile(ctx context.Context) error { // because the 2 injection type we have share the label that the webhook is listening to, we can only clean that label up if both are disabled // but we should only clean-up the labels after everything else is cleaned up because the clean-up for the secrets depend on the label still being there // but we have to do the mapping before everything when its necessary - if !r.dk.NeedAppInjection() && !r.dk.MetadataEnrichmentEnabled() { + if !r.dk.OneAgent().IsAppInjectionNeeded() && !r.dk.MetadataEnrichmentEnabled() { defer r.unmapDynakube(ctx) } else { dkMapper := r.createDynakubeMapper(ctx) @@ -145,22 +148,18 @@ func (r *reconciler) setupOneAgentInjection(ctx context.Context) error { } } - if !r.dk.NeedAppInjection() { + if !r.dk.OneAgent().IsAppInjectionNeeded() { r.cleanupOneAgentInjection(ctx) return nil } - err = initgeneration.NewInitGenerator(r.client, r.apiReader, r.dk.Namespace).GenerateForDynakube(ctx, r.dk) + err = r.generateCorrectInitSecret(ctx) if err != nil { - if conditions.IsKubeApiError(err) { - conditions.SetKubeApiError(r.dk.Conditions(), codeModulesInjectionConditionType, err) - } - return err } - if r.dk.ApplicationMonitoringMode() { + if r.dk.OneAgent().IsApplicationMonitoringMode() { r.dk.Status.SetPhase(status.Running) } @@ -169,6 +168,33 @@ func (r *reconciler) setupOneAgentInjection(ctx context.Context) error { return nil } +func (r *reconciler) generateCorrectInitSecret(ctx context.Context) error { + var err error + if r.dk.FeatureNodeImagePull() { + err = bootstrapperconfig.NewSecretGenerator(r.client, r.apiReader, r.dynatraceClient).GenerateForDynakube(ctx, r.dk) + if err != nil { + if conditions.IsKubeApiError(err) { + conditions.SetKubeApiError(r.dk.Conditions(), codeModulesInjectionConditionType, err) + } + + return err + } + } + + if !r.dk.FeatureNodeImagePull() || r.dk.OneAgent().IsCSIAvailable() { + err = initgeneration.NewInitGenerator(r.client, r.apiReader, r.dk.Namespace).GenerateForDynakube(ctx, r.dk) + if err != nil { + if conditions.IsKubeApiError(err) { + conditions.SetKubeApiError(r.dk.Conditions(), codeModulesInjectionConditionType, err) + } + + return err + } + } + + return err +} + func (r *reconciler) cleanupOneAgentInjection(ctx context.Context) { if meta.FindStatusCondition(*r.dk.Conditions(), codeModulesInjectionConditionType) != nil { defer meta.RemoveStatusCondition(r.dk.Conditions(), codeModulesInjectionConditionType) @@ -180,6 +206,11 @@ func (r *reconciler) cleanupOneAgentInjection(ctx context.Context) { return } + err = bootstrapperconfig.Cleanup(ctx, r.client, r.apiReader, namespaces, r.dk) + if err != nil { + log.Error(err, "failed to clean-up bootstrapper code module injection init-secrets") + } + err = initgeneration.NewInitGenerator(r.client, r.apiReader, r.dk.Namespace).Cleanup(ctx, namespaces) if err != nil { log.Error(err, "failed to clean-up code module injection init-secrets") diff --git a/pkg/controllers/dynakube/injection/reconciler_test.go b/pkg/controllers/dynakube/injection/reconciler_test.go index bf3100b0dc..3bebb69cbe 100644 --- a/pkg/controllers/dynakube/injection/reconciler_test.go +++ b/pkg/controllers/dynakube/injection/reconciler_test.go @@ -7,15 +7,17 @@ import ( "testing" "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/consts" "github.com/Dynatrace/dynatrace-operator/pkg/controllers" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/istio" versions "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/version" + "github.com/Dynatrace/dynatrace-operator/pkg/injection/namespace/bootstrapperconfig" "github.com/Dynatrace/dynatrace-operator/pkg/injection/namespace/mapper" "github.com/Dynatrace/dynatrace-operator/pkg/injection/startup" - "github.com/Dynatrace/dynatrace-operator/pkg/util/address" + "github.com/Dynatrace/dynatrace-operator/pkg/util/installconfig" dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" dtclientmock "github.com/Dynatrace/dynatrace-operator/test/mocks/pkg/clients/dynatrace" controllermock "github.com/Dynatrace/dynatrace-operator/test/mocks/pkg/controllers" @@ -30,6 +32,7 @@ import ( k8serrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/interceptor" ) @@ -85,9 +88,9 @@ func TestReconciler(t *testing.T) { }, Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{ - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{ - AppInjectionSpec: dynakube.AppInjectionSpec{ + OneAgent: oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{ + AppInjectionSpec: oneagent.AppInjectionSpec{ NamespaceSelector: metav1.LabelSelector{ MatchLabels: map[string]string{ testNamespaceSelectorLabel: testDynakube, @@ -97,7 +100,7 @@ func TestReconciler(t *testing.T) { }, }, MetadataEnrichment: dynakube.MetadataEnrichment{ - Enabled: address.Of(true), + Enabled: ptr.To(true), NamespaceSelector: metav1.LabelSelector{ MatchLabels: map[string]string{ testNamespaceSelectorLabel: testDynakube, @@ -130,7 +133,7 @@ func TestReconciler(t *testing.T) { err := rec.Reconcile(context.Background()) require.NoError(t, err) - assertSecretFound(t, clt, dk.OneagentTenantSecret(), dk.Namespace) + assertSecretFound(t, clt, dk.OneAgent().GetTenantSecret(), dk.Namespace) assertSecretFound(t, clt, consts.AgentInitSecretName, testNamespace) assertSecretNotFound(t, clt, consts.AgentInitSecretName, testNamespace2) assertSecretFound(t, clt, consts.EnrichmentEndpointSecretName, testNamespace) @@ -222,9 +225,9 @@ func TestReconciler(t *testing.T) { }, Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{ - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{ - AppInjectionSpec: dynakube.AppInjectionSpec{ + OneAgent: oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{ + AppInjectionSpec: oneagent.AppInjectionSpec{ NamespaceSelector: metav1.LabelSelector{ MatchLabels: map[string]string{ testNamespaceSelectorLabel: testDynakube, @@ -262,7 +265,7 @@ func TestReconciler(t *testing.T) { func TestRemoveAppInjection(t *testing.T) { clt := clientRemoveAppInjection() - rec := createReconciler(clt, testDynakube, testNamespaceDynatrace, dynakube.OneAgentSpec{ + rec := createReconciler(clt, testDynakube, testNamespaceDynatrace, oneagent.Spec{ CloudNativeFullStack: nil, }) rec.versionReconciler = createVersionReconcilerMock(t) @@ -296,8 +299,8 @@ func TestRemoveAppInjection(t *testing.T) { func TestSetupOneAgentInjection(t *testing.T) { t.Run(`no injection - ClassicFullStack`, func(t *testing.T) { clt := clientNoInjection() - rec := createReconciler(clt, testDynakube, testNamespaceDynatrace, dynakube.OneAgentSpec{ - ClassicFullStack: &dynakube.HostInjectSpec{}, + rec := createReconciler(clt, testDynakube, testNamespaceDynatrace, oneagent.Spec{ + ClassicFullStack: &oneagent.HostInjectSpec{}, }) rec.versionReconciler = createVersionReconcilerMock(t) rec.connectionInfoReconciler = createGenericReconcilerMock(t) @@ -313,8 +316,8 @@ func TestSetupOneAgentInjection(t *testing.T) { t.Run(`no injection - HostMonitoring`, func(t *testing.T) { clt := clientNoInjection() - rec := createReconciler(clt, testDynakube, testNamespaceDynatrace, dynakube.OneAgentSpec{ - HostMonitoring: &dynakube.HostInjectSpec{}, + rec := createReconciler(clt, testDynakube, testNamespaceDynatrace, oneagent.Spec{ + HostMonitoring: &oneagent.HostInjectSpec{}, }) rec.versionReconciler = createVersionReconcilerMock(t) rec.connectionInfoReconciler = createGenericReconcilerMock(t) @@ -330,8 +333,8 @@ func TestSetupOneAgentInjection(t *testing.T) { t.Run(`injection - ApplicationMonitoring`, func(t *testing.T) { clt := clientOneAgentInjection() - rec := createReconciler(clt, testDynakube, testNamespaceDynatrace, dynakube.OneAgentSpec{ - ApplicationMonitoring: &dynakube.ApplicationMonitoringSpec{}, + rec := createReconciler(clt, testDynakube, testNamespaceDynatrace, oneagent.Spec{ + ApplicationMonitoring: &oneagent.ApplicationMonitoringSpec{}, }) rec.versionReconciler = createVersionReconcilerMock(t) rec.connectionInfoReconciler = createGenericReconcilerMock(t) @@ -356,8 +359,8 @@ func TestSetupOneAgentInjection(t *testing.T) { t.Run(`injection - CloudNativeFullStack`, func(t *testing.T) { clt := clientOneAgentInjection() - rec := createReconciler(clt, testDynakube, testNamespaceDynatrace, dynakube.OneAgentSpec{ - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{}, + rec := createReconciler(clt, testDynakube, testNamespaceDynatrace, oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{}, }) rec.versionReconciler = createVersionReconcilerMock(t) rec.connectionInfoReconciler = createGenericReconcilerMock(t) @@ -379,17 +382,44 @@ func TestSetupOneAgentInjection(t *testing.T) { assertSecretNotFound(t, clt, consts.AgentInitSecretName, testNamespace2) }) + + t.Run(`no injection - ClassicFullStack`, func(t *testing.T) { + installconfig.SetModulesOverride(t, installconfig.Modules{CSIDriver: false}) + + clt := clientBootstrapperConfigInjection() + rec := createReconciler(clt, testDynakube, testNamespaceDynatrace, oneagent.Spec{ + ApplicationMonitoring: &oneagent.ApplicationMonitoringSpec{}, + }) + + dtClient := dtclientmock.NewClient(t) + dtClient.On("GetProcessModuleConfig", mock.AnythingOfType("context.backgroundCtx"), mock.AnythingOfType("uint")).Return(&dtclient.ProcessModuleConfig{}, nil) + + rec.dynatraceClient = dtClient + + rec.dk.Annotations = make(map[string]string) + rec.dk.Annotations[dynakube.AnnotationFeatureNodeImagePull] = "true" + rec.versionReconciler = createVersionReconcilerMock(t) + rec.connectionInfoReconciler = createGenericReconcilerMock(t) + rec.pmcSecretreconciler = createGenericReconcilerMock(t) + rec.monitoredEntitiesReconciler = createGenericReconcilerMock(t) + + err := rec.setupOneAgentInjection(context.Background()) + require.NoError(t, err) + + assertSecretNotFound(t, clt, consts.AgentInitSecretName, testNamespace) + assertSecretFound(t, clt, consts.BootstrapperInitSecretName, testNamespace) + }) } func TestSetupEnrichmentInjection(t *testing.T) { t.Run(`no enrichment injection`, func(t *testing.T) { clt := clientNoInjection() - rec := createReconciler(clt, testDynakube, testNamespaceDynatrace, dynakube.OneAgentSpec{ - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{}, + rec := createReconciler(clt, testDynakube, testNamespaceDynatrace, oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{}, }) rec.enrichmentRulesReconciler = createGenericReconcilerMock(t) rec.monitoredEntitiesReconciler = createGenericReconcilerMock(t) - rec.dk.Spec.MetadataEnrichment.Enabled = address.Of(false) + rec.dk.Spec.MetadataEnrichment.Enabled = ptr.To(false) err := rec.setupEnrichmentInjection(context.Background()) require.NoError(t, err) @@ -400,12 +430,12 @@ func TestSetupEnrichmentInjection(t *testing.T) { t.Run(`enrichment injection`, func(t *testing.T) { clt := clientEnrichmentInjection() - rec := createReconciler(clt, testDynakube, testNamespaceDynatrace, dynakube.OneAgentSpec{ - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{}, + rec := createReconciler(clt, testDynakube, testNamespaceDynatrace, oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{}, }) rec.enrichmentRulesReconciler = createGenericReconcilerMock(t) rec.monitoredEntitiesReconciler = createGenericReconcilerMock(t) - rec.dk.Spec.MetadataEnrichment.Enabled = address.Of(true) + rec.dk.Spec.MetadataEnrichment.Enabled = ptr.To(true) err := rec.setupEnrichmentInjection(context.Background()) require.NoError(t, err) @@ -415,6 +445,178 @@ func TestSetupEnrichmentInjection(t *testing.T) { }) } +func TestGenerateCorrectInitSecret(t *testing.T) { + ctx := context.Background() + dkBase := &dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-dynakube", + Namespace: "my-dynatrace", + }, + Spec: dynakube.DynaKubeSpec{}, + } + + namespaces := []*corev1.Namespace{ + clientInjectedNamespace("ns-1", dkBase.Name), + clientInjectedNamespace("ns-2", dkBase.Name), + } + + tokenSecret := clientSecret(dkBase.Name, dkBase.Namespace, map[string][]byte{ + dtclient.ApiToken: []byte("testAPIToken"), + dtclient.PaasToken: []byte("testPaasToken"), + }) + + tenantSecret := clientSecret(dkBase.OneAgent().GetTenantSecret(), dkBase.Namespace, map[string][]byte{ + "tenant-token": []byte("token"), + }) + + t.Run("default 1 == no node-image-pull + csi enabled => only init-secret", func(t *testing.T) { + dk := dkBase.DeepCopy() + + clt := fake.NewClientWithIndex( + tokenSecret, + dk, + namespaces[0], namespaces[1], + ) + r := reconciler{client: clt, apiReader: clt, dk: dk} + + err := r.generateCorrectInitSecret(ctx) + require.NoError(t, err) + + for _, ns := range namespaces { + assertSecretFound(t, clt, consts.AgentInitSecretName, ns.Name) + assertSecretNotFound(t, clt, consts.BootstrapperInitSecretName, ns.Name) + } + + assertSecretNotFound(t, clt, bootstrapperconfig.GetSourceSecretName(dk.Name), dk.Namespace) + }) + + t.Run("default 2 == no node-image-pull + no csi enabled => only init-secret", func(t *testing.T) { + installconfig.SetModulesOverride(t, installconfig.Modules{CSIDriver: false}) + + dk := dkBase.DeepCopy() + + clt := fake.NewClientWithIndex( + tokenSecret, + dk, + namespaces[0], namespaces[1], + ) + r := reconciler{client: clt, apiReader: clt, dk: dk} + + err := r.generateCorrectInitSecret(ctx) + require.NoError(t, err) + + for _, ns := range namespaces { + assertSecretFound(t, clt, consts.AgentInitSecretName, ns.Name) + assertSecretNotFound(t, clt, consts.BootstrapperInitSecretName, ns.Name) + } + + assertSecretNotFound(t, clt, bootstrapperconfig.GetSourceSecretName(dk.Name), dk.Namespace) + }) + + t.Run("node-image-pull + csi enabled => both init-secret and bootstrapper-secret", func(t *testing.T) { + dk := dkBase.DeepCopy() + dk.Annotations = map[string]string{ + dynakube.AnnotationFeatureNodeImagePull: "true", + } + + clt := fake.NewClientWithIndex( + tokenSecret, + tenantSecret, + dk, + namespaces[0], namespaces[1], + ) + + dtClient := dtclientmock.NewClient(t) + dtClient.On("GetProcessModuleConfig", mock.AnythingOfType("context.backgroundCtx"), mock.AnythingOfType("uint")).Return(&dtclient.ProcessModuleConfig{}, nil) + + r := reconciler{client: clt, apiReader: clt, dk: dk, dynatraceClient: dtClient} + + err := r.generateCorrectInitSecret(ctx) + require.NoError(t, err) + + for _, ns := range namespaces { + assertSecretFound(t, clt, consts.AgentInitSecretName, ns.Name) + assertSecretFound(t, clt, consts.BootstrapperInitSecretName, ns.Name) + } + + assertSecretFound(t, clt, bootstrapperconfig.GetSourceSecretName(dk.Name), dk.Namespace) + }) + + t.Run("node-image-pull + csi not enabled => only bootstrapper-secret", func(t *testing.T) { + installconfig.SetModulesOverride(t, installconfig.Modules{CSIDriver: false}) + + dk := dkBase.DeepCopy() + dk.Annotations = map[string]string{ + dynakube.AnnotationFeatureNodeImagePull: "true", + } + + clt := fake.NewClientWithIndex( + tokenSecret, + tenantSecret, + dk, + namespaces[0], namespaces[1], + ) + + dtClient := dtclientmock.NewClient(t) + dtClient.On("GetProcessModuleConfig", mock.AnythingOfType("context.backgroundCtx"), mock.AnythingOfType("uint")).Return(&dtclient.ProcessModuleConfig{}, nil) + + r := reconciler{client: clt, apiReader: clt, dk: dk, dynatraceClient: dtClient} + + err := r.generateCorrectInitSecret(ctx) + require.NoError(t, err) + + for _, ns := range namespaces { + assertSecretNotFound(t, clt, consts.AgentInitSecretName, ns.Name) + assertSecretFound(t, clt, consts.BootstrapperInitSecretName, ns.Name) + } + + assertSecretFound(t, clt, bootstrapperconfig.GetSourceSecretName(dk.Name), dk.Namespace) + }) +} + +func TestCleanupOneAgentInjection(t *testing.T) { + ctx := context.Background() + dkBase := &dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-dynakube", + Namespace: "my-dynatrace", + }, + Spec: dynakube.DynaKubeSpec{}, + } + + t.Run("remove everything", func(t *testing.T) { + dk := dkBase.DeepCopy() + namespaces := []*corev1.Namespace{ + clientInjectedNamespace("ns-1", dk.Name), + clientInjectedNamespace("ns-2", dk.Name), + } + + setCodeModulesInjectionCreatedCondition(dk.Conditions()) + + clt := fake.NewClientWithIndex( + clientSecret(consts.AgentInitSecretName, namespaces[0].Name, nil), + clientSecret(consts.AgentInitSecretName, namespaces[1].Name, nil), + clientSecret(consts.BootstrapperInitSecretName, namespaces[0].Name, nil), + clientSecret(consts.BootstrapperInitSecretName, namespaces[1].Name, nil), + clientSecret(bootstrapperconfig.GetSourceSecretName(dk.Name), dk.Namespace, nil), + dk, + namespaces[0], namespaces[1], + ) + r := reconciler{client: clt, apiReader: clt, dk: dk} + + r.cleanupOneAgentInjection(ctx) + + for _, ns := range namespaces { + assertSecretNotFound(t, clt, consts.AgentInitSecretName, ns.Name) + assertSecretNotFound(t, clt, consts.BootstrapperInitSecretName, ns.Name) + } + + assertSecretNotFound(t, clt, bootstrapperconfig.GetSourceSecretName(dk.Name), dk.Namespace) + + assert.Empty(t, dk.Conditions()) + }) +} + func newIstioTestingClient(fakeClient *fakeistio.Clientset, dk *dynakube.DynaKube) *istio.Client { return &istio.Client{ IstioClientset: fakeClient, @@ -422,7 +624,7 @@ func newIstioTestingClient(fakeClient *fakeistio.Clientset, dk *dynakube.DynaKub } } -func createReconciler(clt client.Client, dynakubeName string, dynakubeNamespace string, oneAgentSpec dynakube.OneAgentSpec) reconciler { +func createReconciler(clt client.Client, dynakubeName string, dynakubeNamespace string, oneAgentSpec oneagent.Spec) reconciler { return reconciler{ client: clt, apiReader: clt, @@ -467,6 +669,19 @@ func clientOneAgentInjection() client.Client { ) } +func clientBootstrapperConfigInjection() client.Client { + return fake.NewClientWithIndex( + clientInjectedNamespace(testNamespace, testDynakube), + clientSecret(testDynakube, testNamespaceDynatrace, map[string][]byte{ + dtclient.ApiToken: []byte(testAPIToken), + dtclient.PaasToken: []byte(testPaasToken), + }), + clientSecret("test-dynakube-oneagent-tenant-secret", testNamespaceDynatrace, map[string][]byte{ + "tenant-token": []byte(testTenantToken), + }), + ) +} + func clientEnrichmentInjection() client.Client { return fake.NewClientWithIndex( clientInjectedNamespace(testNamespace, testDynakube), diff --git a/pkg/controllers/dynakube/istio/reconciler.go b/pkg/controllers/dynakube/istio/reconciler.go index 10fc874808..4ca44c1ac1 100644 --- a/pkg/controllers/dynakube/istio/reconciler.go +++ b/pkg/controllers/dynakube/istio/reconciler.go @@ -6,7 +6,7 @@ import ( "net" "strings" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/connectioninfo/activegate" oaconnectioninfo "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/connectioninfo/oneagent" @@ -67,7 +67,7 @@ func (r *reconciler) ReconcileCodeModuleCommunicationHosts(ctx context.Context, return errors.New("can't reconcile oneagent communication hosts of nil dynakube") } - if !dk.NeedAppInjection() { + if !dk.OneAgent().IsAppInjectionNeeded() { if isIstioConfigured(dk, CodeModuleComponent) { log.Info("appinjection disabled, cleaning up") diff --git a/pkg/controllers/dynakube/istio/reconciler_test.go b/pkg/controllers/dynakube/istio/reconciler_test.go index a5be3cc00f..468a3a0e0a 100644 --- a/pkg/controllers/dynakube/istio/reconciler_test.go +++ b/pkg/controllers/dynakube/istio/reconciler_test.go @@ -7,10 +7,10 @@ import ( "testing" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/communication" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" - "github.com/Dynatrace/dynatrace-operator/pkg/util/address" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" istiov1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" @@ -19,6 +19,7 @@ import ( "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" fakediscovery "k8s.io/client-go/discovery/fake" + "k8s.io/utils/ptr" ) func TestSplitCommunicationHost(t *testing.T) { @@ -300,7 +301,7 @@ func TestReconcileOneAgentCommunicationHosts(t *testing.T) { require.Equal(t, "IstioForCodeModuleChanged", statusCondition.Reason) dk.Spec.OneAgent.CloudNativeFullStack = nil - dk.Spec.OneAgent.HostMonitoring = &dynakube.HostInjectSpec{} + dk.Spec.OneAgent.HostMonitoring = &oneagent.HostInjectSpec{} err = r.ReconcileCodeModuleCommunicationHosts(ctx, dk) require.NoError(t, err) @@ -490,15 +491,15 @@ func createTestDynaKube() *dynakube.DynaKube { activegate.RoutingCapability.DisplayName, }, }, - OneAgent: dynakube.OneAgentSpec{ - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{}, + OneAgent: oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{}, }, - DynatraceApiRequestThreshold: address.Of(uint16(15)), + DynatraceApiRequestThreshold: ptr.To(uint16(15)), }, Status: dynakube.DynaKubeStatus{ - OneAgent: dynakube.OneAgentStatus{ - ConnectionInfoStatus: dynakube.OneAgentConnectionInfoStatus{ - CommunicationHosts: []dynakube.CommunicationHostStatus{ + OneAgent: oneagent.Status{ + ConnectionInfoStatus: oneagent.ConnectionInfoStatus{ + CommunicationHosts: []oneagent.CommunicationHostStatus{ { Protocol: fqdnHost.Protocol, Host: fqdnHost.Host, diff --git a/pkg/controllers/dynakube/istio/serviceentry_test.go b/pkg/controllers/dynakube/istio/serviceentry_test.go index 36fdb18d76..e9ceef4bc6 100644 --- a/pkg/controllers/dynakube/istio/serviceentry_test.go +++ b/pkg/controllers/dynakube/istio/serviceentry_test.go @@ -50,7 +50,7 @@ func TestServiceEntryGeneration(t *testing.T) { Protocol: protocolHttps, }} result := buildServiceEntryFQDNs(buildObjectMeta(testName, testNamespace, buildTestLabels()), commHosts1) - assert.EqualValues(t, expected, result) + assert.Equal(t, expected, result) commHosts2 := []dtclient.CommunicationHost{{ Host: testHost1, @@ -58,7 +58,7 @@ func TestServiceEntryGeneration(t *testing.T) { Protocol: protocolHttps, }} result = buildServiceEntryFQDNs(buildObjectMeta(testName, testNamespace, buildTestLabels()), commHosts2) - assert.NotEqualValues(t, expected, result) + assert.NotEqual(t, expected, result) }) t.Run(`generate with two different hostnames and same port`, func(t *testing.T) { expected := &istiov1beta1.ServiceEntry{ @@ -89,7 +89,7 @@ func TestServiceEntryGeneration(t *testing.T) { Protocol: protocolHttps, }} result := buildServiceEntryFQDNs(buildObjectMeta(testName, testNamespace, buildTestLabels()), commHosts1) - assert.EqualValues(t, expected, result) + assert.Equal(t, expected, result) }) t.Run(`generate with Ip`, func(t *testing.T) { const testIp = "42.42.42.42" @@ -117,7 +117,7 @@ func TestServiceEntryGeneration(t *testing.T) { Protocol: protocolHttps, }} result := buildServiceEntryIPs(buildObjectMeta(testName, testNamespace, buildTestLabels()), commHosts1) - assert.EqualValues(t, expected, result) + assert.Equal(t, expected, result) commHosts2 := []dtclient.CommunicationHost{{ Host: testIp, @@ -125,7 +125,7 @@ func TestServiceEntryGeneration(t *testing.T) { Protocol: protocolHttps, }} result = buildServiceEntryIPs(buildObjectMeta(testName, testNamespace, buildTestLabels()), commHosts2) - assert.NotEqualValues(t, expected, result) + assert.NotEqual(t, expected, result) }) t.Run(`generate with two different Ips and same ports`, func(t *testing.T) { const ( @@ -162,7 +162,7 @@ func TestServiceEntryGeneration(t *testing.T) { Protocol: protocolHttps, }} result := buildServiceEntryIPs(buildObjectMeta(testName, testNamespace, buildTestLabels()), commHosts1) - assert.EqualValues(t, expected, result) + assert.Equal(t, expected, result) }) } @@ -174,7 +174,7 @@ func TestBuildServiceEntryForHostname(t *testing.T) { Protocol: protocolHttp, }} result := buildServiceEntryFQDNs(buildObjectMeta(testName, testNamespace, buildTestLabels()), commHosts1) - assert.EqualValues(t, expected, result) + assert.Equal(t, expected, result) commHosts2 := []dtclient.CommunicationHost{{ Host: testHost2, @@ -182,7 +182,7 @@ func TestBuildServiceEntryForHostname(t *testing.T) { Protocol: protocolHttp, }} result = buildServiceEntryFQDNs(buildObjectMeta(testName, testNamespace, buildTestLabels()), commHosts2) - assert.NotEqualValues(t, expected, result) + assert.NotEqual(t, expected, result) } func TestBuildServiceEntryIp(t *testing.T) { @@ -192,14 +192,14 @@ func TestBuildServiceEntryIp(t *testing.T) { Port: testPort1, }} result := buildServiceEntryIPs(buildObjectMeta(testName, testNamespace, buildTestLabels()), commHosts1) - assert.EqualValues(t, expected, result) + assert.Equal(t, expected, result) commHosts2 := []dtclient.CommunicationHost{{ Host: testIP2, Port: testPort2, }} result = buildServiceEntryIPs(buildObjectMeta(testName, testNamespace, buildTestLabels()), commHosts2) - assert.NotEqualValues(t, expected, result) + assert.NotEqual(t, expected, result) } func buildExpectedServiceEntryForHostname(_ *testing.T) *istiov1beta1.ServiceEntry { diff --git a/pkg/controllers/dynakube/istio/virtualservice_test.go b/pkg/controllers/dynakube/istio/virtualservice_test.go index bed3e9a49a..38f44463fd 100644 --- a/pkg/controllers/dynakube/istio/virtualservice_test.go +++ b/pkg/controllers/dynakube/istio/virtualservice_test.go @@ -57,7 +57,7 @@ func TestVirtualServiceGeneration(t *testing.T) { }} result := buildVirtualService(buildObjectMeta(testName, testNamespace, buildTestLabels()), commHosts) - assert.EqualValues(t, expected, result) + assert.Equal(t, expected, result) }) t.Run("generate for http connection", func(t *testing.T) { expected := &istiov1beta1.VirtualService{ @@ -87,7 +87,7 @@ func TestVirtualServiceGeneration(t *testing.T) { }} result := buildVirtualService(buildObjectMeta(testName, testNamespace, buildTestLabels()), commHosts) - assert.EqualValues(t, expected, result) + assert.Equal(t, expected, result) }) t.Run("generate for invalid protocol", func(t *testing.T) { commHosts := []dtclient.CommunicationHost{{ diff --git a/pkg/controllers/dynakube/kspm/daemonset/certs.go b/pkg/controllers/dynakube/kspm/daemonset/certs.go index 2211faca51..3ee281d980 100644 --- a/pkg/controllers/dynakube/kspm/daemonset/certs.go +++ b/pkg/controllers/dynakube/kspm/daemonset/certs.go @@ -1,7 +1,7 @@ package daemonset import ( - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" corev1 "k8s.io/api/core/v1" ) @@ -16,7 +16,7 @@ func getCertVolume(dk dynakube.DynaKube) corev1.Volume { Name: certVolumeName, VolumeSource: corev1.VolumeSource{ Secret: &corev1.SecretVolumeSource{ - SecretName: dk.ActiveGate().TlsSecretName, + SecretName: dk.ActiveGate().GetTLSSecretName(), }, }, } diff --git a/pkg/controllers/dynakube/kspm/daemonset/certs_test.go b/pkg/controllers/dynakube/kspm/daemonset/certs_test.go index ca1b012e2f..e982cf83f1 100644 --- a/pkg/controllers/dynakube/kspm/daemonset/certs_test.go +++ b/pkg/controllers/dynakube/kspm/daemonset/certs_test.go @@ -3,8 +3,8 @@ package daemonset import ( "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" ) func getDynaKubeWithCerts(t *testing.T) dynakube.DynaKube { @@ -16,3 +16,12 @@ func getDynaKubeWithCerts(t *testing.T) dynakube.DynaKube { return dk } + +func getDynaKubeWithAutomaticCerts(t *testing.T) dynakube.DynaKube { + t.Helper() + + dk := dynakube.DynaKube{} + dk.ActiveGate().Capabilities = []activegate.CapabilityDisplayName{activegate.KubeMonCapability.DisplayName} + + return dk +} diff --git a/pkg/controllers/dynakube/kspm/daemonset/container.go b/pkg/controllers/dynakube/kspm/daemonset/container.go index 3723c5fbcd..f2c05546e9 100644 --- a/pkg/controllers/dynakube/kspm/daemonset/container.go +++ b/pkg/controllers/dynakube/kspm/daemonset/container.go @@ -1,10 +1,10 @@ package daemonset import ( - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/util/address" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/resources" corev1 "k8s.io/api/core/v1" + "k8s.io/utils/ptr" ) const ( @@ -12,7 +12,7 @@ const ( defaultImageTag = "latest" containerName = "node-config-collector" - runAs int64 = 0 + runAs int64 = 65532 ) func getContainer(dk dynakube.DynaKube, tenantUUID string) corev1.Container { @@ -33,13 +33,13 @@ func getContainer(dk dynakube.DynaKube, tenantUUID string) corev1.Container { func getSecurityContext() corev1.SecurityContext { securityContext := corev1.SecurityContext{ - Privileged: address.Of(false), - AllowPrivilegeEscalation: address.Of(false), - RunAsUser: address.Of(runAs), - RunAsGroup: address.Of(runAs), - RunAsNonRoot: address.Of(false), - ReadOnlyRootFilesystem: address.Of(true), - Capabilities: &corev1.Capabilities{Drop: []corev1.Capability{"ALL"}}, + Privileged: ptr.To(false), + AllowPrivilegeEscalation: ptr.To(false), + RunAsUser: ptr.To(runAs), + RunAsGroup: ptr.To(runAs), + RunAsNonRoot: ptr.To(true), + ReadOnlyRootFilesystem: ptr.To(true), + Capabilities: &corev1.Capabilities{Add: []corev1.Capability{"DAC_OVERRIDE"}, Drop: []corev1.Capability{"ALL"}}, } return securityContext diff --git a/pkg/controllers/dynakube/kspm/daemonset/container_test.go b/pkg/controllers/dynakube/kspm/daemonset/container_test.go index 8cf94e8d0f..b7e2197cd6 100644 --- a/pkg/controllers/dynakube/kspm/daemonset/container_test.go +++ b/pkg/controllers/dynakube/kspm/daemonset/container_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/image" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/pkg/controllers/dynakube/kspm/daemonset/env.go b/pkg/controllers/dynakube/kspm/daemonset/env.go index 687cb8e995..a38a1112c2 100644 --- a/pkg/controllers/dynakube/kspm/daemonset/env.go +++ b/pkg/controllers/dynakube/kspm/daemonset/env.go @@ -3,7 +3,7 @@ package daemonset import ( "fmt" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/capability" agconsts "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/consts" corev1 "k8s.io/api/core/v1" diff --git a/pkg/controllers/dynakube/kspm/daemonset/env_test.go b/pkg/controllers/dynakube/kspm/daemonset/env_test.go index 40d6acc84a..63252b605f 100644 --- a/pkg/controllers/dynakube/kspm/daemonset/env_test.go +++ b/pkg/controllers/dynakube/kspm/daemonset/env_test.go @@ -3,7 +3,7 @@ package daemonset import ( "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" diff --git a/pkg/controllers/dynakube/kspm/daemonset/reconciler.go b/pkg/controllers/dynakube/kspm/daemonset/reconciler.go index ff06fb3305..351eb11e92 100644 --- a/pkg/controllers/dynakube/kspm/daemonset/reconciler.go +++ b/pkg/controllers/dynakube/kspm/daemonset/reconciler.go @@ -4,7 +4,7 @@ import ( "context" "maps" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" "github.com/Dynatrace/dynatrace-operator/pkg/util/hasher" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/daemonset" @@ -18,7 +18,6 @@ import ( ) const ( - nameSuffix = "-node-config-collector" serviceAccountName = "dynatrace-node-config-collector" ) @@ -77,7 +76,7 @@ func (r *Reconciler) Reconcile(ctx context.Context) error { } func (r *Reconciler) generateDaemonSet() (*appsv1.DaemonSet, error) { - tenantUUID, err := r.dk.TenantUUIDFromConnectionInfoStatus() + tenantUUID, err := r.dk.TenantUUID() if err != nil { return nil, err } @@ -90,8 +89,9 @@ func (r *Reconciler) generateDaemonSet() (*appsv1.DaemonSet, error) { daemonset.SetAllLabels(labels.BuildLabels(), labels.BuildMatchLabels(), labels.BuildLabels(), r.dk.KSPM().Labels), daemonset.SetAllAnnotations(r.dk.KSPM().Annotations, templateAnnotations), daemonset.SetServiceAccount(serviceAccountName), - daemonset.SetAffinity(node.Affinity()), + daemonset.SetAffinity(node.AMDOnlyAffinity()), daemonset.SetPriorityClass(r.dk.KSPM().PriorityClassName), + daemonset.SetNodeSelector(r.dk.KSPM().NodeSelector), daemonset.SetTolerations(r.dk.KSPM().Tolerations), daemonset.SetPullSecret(r.dk.ImagePullSecretReferences()...), daemonset.SetUpdateStrategy(r.getUpdateStrategy()), diff --git a/pkg/controllers/dynakube/kspm/daemonset/reconciler_test.go b/pkg/controllers/dynakube/kspm/daemonset/reconciler_test.go index ce874891aa..b2d606be8b 100644 --- a/pkg/controllers/dynakube/kspm/daemonset/reconciler_test.go +++ b/pkg/controllers/dynakube/kspm/daemonset/reconciler_test.go @@ -6,9 +6,9 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/communication" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/kspm" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/kspm" "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" "github.com/Dynatrace/dynatrace-operator/pkg/util/hasher" "github.com/pkg/errors" @@ -220,6 +220,21 @@ func TestGenerateDaemonSet(t *testing.T) { assert.Equal(t, daemonset.Spec.Template.Spec.Tolerations, customTolerations) }) + t.Run("respect custom nodeSelector", func(t *testing.T) { + customNodeSelector := map[string]string{ + "some.nodeSelector.key": "true", + } + + dk := createDynakube(true) + dk.KSPM().NodeSelector = customNodeSelector + reconciler := NewReconciler(nil, + nil, dk) + daemonset, err := reconciler.generateDaemonSet() + require.NoError(t, err) + require.NotNil(t, daemonset) + + assert.Equal(t, daemonset.Spec.Template.Spec.NodeSelector, customNodeSelector) + }) } func createDynakube(isEnabled bool) *dynakube.DynaKube { diff --git a/pkg/controllers/dynakube/kspm/daemonset/volumes.go b/pkg/controllers/dynakube/kspm/daemonset/volumes.go index 3616408abb..6a3f695521 100644 --- a/pkg/controllers/dynakube/kspm/daemonset/volumes.go +++ b/pkg/controllers/dynakube/kspm/daemonset/volumes.go @@ -2,8 +2,8 @@ package daemonset import ( "github.com/Dynatrace/dynatrace-operator/pkg/api" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/kspm" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/kspm" corev1 "k8s.io/api/core/v1" ) diff --git a/pkg/controllers/dynakube/kspm/daemonset/volumes_test.go b/pkg/controllers/dynakube/kspm/daemonset/volumes_test.go index 864d7558ed..652591319b 100644 --- a/pkg/controllers/dynakube/kspm/daemonset/volumes_test.go +++ b/pkg/controllers/dynakube/kspm/daemonset/volumes_test.go @@ -3,7 +3,7 @@ package daemonset import ( "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -39,6 +39,19 @@ func TestGetMounts(t *testing.T) { assert.NotEmpty(t, mount.MountPath) } }) + + t.Run("get cert mount with automatic AG cert", func(t *testing.T) { + dk := getDynaKubeWithAutomaticCerts(t) + mounts := getMounts(dk) + + require.NotEmpty(t, mounts) + assert.Len(t, mounts, expectedMountLen+1) + + for _, mount := range mounts { + assert.NotEmpty(t, mount.Name) + assert.NotEmpty(t, mount.MountPath) + } + }) } func TestGetVolumes(t *testing.T) { @@ -64,7 +77,28 @@ func TestGetVolumes(t *testing.T) { for _, volume := range volumes { assert.NotEmpty(t, volume.Name) - assert.NotEmpty(t, volume.VolumeSource) + require.NotEmpty(t, volume.VolumeSource) + + if volume.Name == certVolumeName { + assert.NotEmpty(t, volume.VolumeSource.Secret.SecretName) + } + } + }) + + t.Run("add cert volume with automatic AG cert", func(t *testing.T) { + dk := getDynaKubeWithAutomaticCerts(t) + volumes := getVolumes(dk) + + require.NotEmpty(t, volumes) + assert.Len(t, volumes, expectedMountLen+1) + + for _, volume := range volumes { + assert.NotEmpty(t, volume.Name) + require.NotEmpty(t, volume.VolumeSource) + + if volume.Name == certVolumeName { + assert.NotEmpty(t, volume.VolumeSource.Secret.SecretName) + } } }) } diff --git a/pkg/controllers/dynakube/kspm/reconciler.go b/pkg/controllers/dynakube/kspm/reconciler.go index a221adc340..8db939959f 100644 --- a/pkg/controllers/dynakube/kspm/reconciler.go +++ b/pkg/controllers/dynakube/kspm/reconciler.go @@ -3,7 +3,7 @@ package kspm import ( "context" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/controllers" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/kspm/daemonset" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/kspm/token" diff --git a/pkg/controllers/dynakube/kspm/token/reconciler.go b/pkg/controllers/dynakube/kspm/token/reconciler.go index 6be82a311b..bf076a7ac3 100644 --- a/pkg/controllers/dynakube/kspm/token/reconciler.go +++ b/pkg/controllers/dynakube/kspm/token/reconciler.go @@ -3,8 +3,8 @@ package token import ( "context" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/kspm" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/kspm" "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" "github.com/Dynatrace/dynatrace-operator/pkg/util/dttoken" "github.com/Dynatrace/dynatrace-operator/pkg/util/hasher" @@ -74,6 +74,10 @@ func ensureKSPMSecret(ctx context.Context, client client.Client, apiReader clien dk.KSPM().TokenSecretHash = tokenHash conditions.SetSecretCreated(dk.Conditions(), kspmConditionType, dk.KSPM().GetTokenSecretName()) + } else if err != nil { + conditions.SetKubeApiError(dk.Conditions(), kspmConditionType, err) + + return err } return nil diff --git a/pkg/controllers/dynakube/kspm/token/reconciler_test.go b/pkg/controllers/dynakube/kspm/token/reconciler_test.go index 635da14da5..0d47b8a275 100644 --- a/pkg/controllers/dynakube/kspm/token/reconciler_test.go +++ b/pkg/controllers/dynakube/kspm/token/reconciler_test.go @@ -2,11 +2,12 @@ package token import ( "context" + "errors" "testing" dtfake "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/kspm" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/kspm" "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -16,6 +17,7 @@ import ( "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/client/interceptor" ) func TestTokenCreation(t *testing.T) { @@ -38,6 +40,16 @@ func TestTokenCreation(t *testing.T) { assert.NotEmpty(t, dk.KSPM().TokenSecretHash) }) + t.Run("unexpected error -> return error", func(t *testing.T) { + clt := createFailK8sClient(t) + + dk := createDynaKube(true) + + err := ensureKSPMSecret(ctx, clt, clt, &dk) + require.Error(t, err) + assert.Equal(t, conditions.KubeApiErrorReason, meta.FindStatusCondition(*dk.Conditions(), kspmConditionType).Reason) + }) + t.Run("removes secret if exists", func(t *testing.T) { dk := createDynaKube(false) dk.KSPM().TokenSecretHash = "something" @@ -71,6 +83,27 @@ func TestTokenCreation(t *testing.T) { }) } +func createFailK8sClient(t *testing.T) client.Client { + t.Helper() + + boomClient := dtfake.NewClientWithInterceptors(interceptor.Funcs{ + Create: func(ctx context.Context, client client.WithWatch, obj client.Object, opts ...client.CreateOption) error { + return errors.New("BOOM") + }, + Delete: func(ctx context.Context, client client.WithWatch, obj client.Object, opts ...client.DeleteOption) error { + return errors.New("BOOM") + }, + Update: func(ctx context.Context, client client.WithWatch, obj client.Object, opts ...client.UpdateOption) error { + return errors.New("BOOM") + }, + Get: func(ctx context.Context, client client.WithWatch, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { + return errors.New("BOOM") + }, + }) + + return boomClient +} + func createDynaKube(kspmEnabled bool) dynakube.DynaKube { dk := dynakube.DynaKube{ ObjectMeta: metav1.ObjectMeta{ diff --git a/pkg/controllers/dynakube/logmonitoring/configsecret/conditions.go b/pkg/controllers/dynakube/logmonitoring/configsecret/conditions.go index 0ded47ba63..2b0f8d9b89 100644 --- a/pkg/controllers/dynakube/logmonitoring/configsecret/conditions.go +++ b/pkg/controllers/dynakube/logmonitoring/configsecret/conditions.go @@ -1,5 +1,5 @@ package configsecret const ( - lmcConditionType = "LogMonitoringConfig" + LmcConditionType = "LogMonitoringConfig" ) diff --git a/pkg/controllers/dynakube/logmonitoring/configsecret/reconciler.go b/pkg/controllers/dynakube/logmonitoring/configsecret/reconciler.go index e6f88b4479..b47d789317 100644 --- a/pkg/controllers/dynakube/logmonitoring/configsecret/reconciler.go +++ b/pkg/controllers/dynakube/logmonitoring/configsecret/reconciler.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/connectioninfo" "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" k8slabels "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/labels" @@ -44,7 +44,7 @@ func NewReconciler(clt client.Client, func (r *Reconciler) Reconcile(ctx context.Context) error { if !r.dk.LogMonitoring().IsStandalone() { - if meta.FindStatusCondition(*r.dk.Conditions(), lmcConditionType) == nil { + if meta.FindStatusCondition(*r.dk.Conditions(), LmcConditionType) == nil { return nil // no condition == nothing is there to clean up } @@ -55,7 +55,7 @@ func (r *Reconciler) Reconcile(ctx context.Context) error { log.Error(err, "failed to clean-up LogMonitoring config-secret") } - meta.RemoveStatusCondition(r.dk.Conditions(), lmcConditionType) + meta.RemoveStatusCondition(r.dk.Conditions(), LmcConditionType) return nil // clean-up shouldn't cause a failure } @@ -73,14 +73,14 @@ func (r *Reconciler) reconcileSecret(ctx context.Context) error { changed, err := query.CreateOrUpdate(ctx, newSecret) if err != nil { - conditions.SetKubeApiError(r.dk.Conditions(), lmcConditionType, err) + conditions.SetKubeApiError(r.dk.Conditions(), LmcConditionType, err) return err } else if changed { - conditions.SetSecretOutdated(r.dk.Conditions(), lmcConditionType, newSecret.Name) // needed so the timestamp updates, will never actually show up in the status + conditions.SetSecretOutdated(r.dk.Conditions(), LmcConditionType, newSecret.Name) // needed so the timestamp updates, will never actually show up in the status } - conditions.SetSecretCreated(r.dk.Conditions(), lmcConditionType, newSecret.Name) + conditions.SetSecretCreated(r.dk.Conditions(), LmcConditionType, newSecret.Name) return nil } @@ -99,7 +99,7 @@ func (r *Reconciler) prepareSecret(ctx context.Context) (*corev1.Secret, error) k8ssecret.SetLabels(coreLabels), ) if err != nil { - conditions.SetSecretGenFailed(r.dk.Conditions(), lmcConditionType, err) + conditions.SetSecretGenFailed(r.dk.Conditions(), LmcConditionType, err) return nil, err } @@ -109,24 +109,24 @@ func (r *Reconciler) prepareSecret(ctx context.Context) (*corev1.Secret, error) func (r *Reconciler) getSecretData(ctx context.Context) (map[string][]byte, error) { tenantToken, err := k8ssecret.GetDataFromSecretName(ctx, r.apiReader, types.NamespacedName{ - Name: r.dk.OneagentTenantSecret(), + Name: r.dk.OneAgent().GetTenantSecret(), Namespace: r.dk.Namespace, }, connectioninfo.TenantTokenKey, log) if err != nil { - conditions.SetKubeApiError(r.dk.Conditions(), lmcConditionType, err) + conditions.SetKubeApiError(r.dk.Conditions(), LmcConditionType, err) return nil, err } - tenantUUID, err := r.dk.TenantUUIDFromConnectionInfoStatus() + tenantUUID, err := r.dk.TenantUUID() if err != nil { - conditions.SetSecretGenFailed(r.dk.Conditions(), lmcConditionType, err) + conditions.SetSecretGenFailed(r.dk.Conditions(), LmcConditionType, err) return nil, err } deploymentConfigContent := map[string]string{ - serverKey: fmt.Sprintf("{%s}", r.dk.OneAgentEndpoints()), + serverKey: fmt.Sprintf("{%s}", r.dk.OneAgent().GetEndpoints()), tenantKey: tenantUUID, tenantTokenKey: tenantToken, hostIdSourceKey: "k8s-node-name", diff --git a/pkg/controllers/dynakube/logmonitoring/configsecret/reconciler_test.go b/pkg/controllers/dynakube/logmonitoring/configsecret/reconciler_test.go index f34ed4aad9..888586c5e4 100644 --- a/pkg/controllers/dynakube/logmonitoring/configsecret/reconciler_test.go +++ b/pkg/controllers/dynakube/logmonitoring/configsecret/reconciler_test.go @@ -8,8 +8,9 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/communication" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/logmonitoring" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/logmonitoring" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/connectioninfo" "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" "github.com/pkg/errors" @@ -35,8 +36,8 @@ func TestReconcile(t *testing.T) { t.Run("Only clean up if not standalone", func(t *testing.T) { dk := createDynakube(true) - dk.Spec.OneAgent.CloudNativeFullStack = &dynakube.CloudNativeFullStackSpec{} - conditions.SetSecretCreated(dk.Conditions(), lmcConditionType, "testing") + dk.Spec.OneAgent.CloudNativeFullStack = &oneagent.CloudNativeFullStackSpec{} + conditions.SetSecretCreated(dk.Conditions(), LmcConditionType, "testing") mockK8sClient := createK8sClientWithConfigSecret() @@ -49,7 +50,7 @@ func TestReconcile(t *testing.T) { err = mockK8sClient.Get(context.Background(), client.ObjectKey{Name: GetSecretName((dk.Name)), Namespace: dk.Namespace}, &secret) require.True(t, k8serrors.IsNotFound(err)) - condition := meta.FindStatusCondition(*dk.Conditions(), lmcConditionType) + condition := meta.FindStatusCondition(*dk.Conditions(), LmcConditionType) require.Nil(t, condition) }) @@ -65,7 +66,7 @@ func TestReconcile(t *testing.T) { checkSecretForValue(t, mockK8sClient, dk) - condition := meta.FindStatusCondition(*dk.Conditions(), lmcConditionType) + condition := meta.FindStatusCondition(*dk.Conditions(), LmcConditionType) require.NotNil(t, condition) oldTransitionTime := condition.LastTransitionTime require.NotEmpty(t, oldTransitionTime) @@ -81,7 +82,7 @@ func TestReconcile(t *testing.T) { dk := createDynakube(false) mockK8sClient := createK8sClientWithOneAgentTenantSecret(dk, tokenValue) - conditions.SetSecretCreated(dk.Conditions(), lmcConditionType, "this is a test") + conditions.SetSecretCreated(dk.Conditions(), LmcConditionType, "this is a test") reconciler := NewReconciler(mockK8sClient, mockK8sClient, dk) err := reconciler.Reconcile(ctx) @@ -109,7 +110,7 @@ func TestReconcile(t *testing.T) { require.Error(t, err) require.Len(t, *dk.Conditions(), 1) - condition := meta.FindStatusCondition(*dk.Conditions(), lmcConditionType) + condition := meta.FindStatusCondition(*dk.Conditions(), LmcConditionType) assert.Equal(t, conditions.KubeApiErrorReason, condition.Reason) assert.Equal(t, metav1.ConditionFalse, condition.Status) }) @@ -123,7 +124,7 @@ func checkSecretForValue(t *testing.T, k8sClient client.Client, dk *dynakube.Dyn deploymentConfig, ok := secret.Data[DeploymentConfigFilename] require.True(t, ok) - tenantUUID, err := dk.TenantUUIDFromConnectionInfoStatus() + tenantUUID, err := dk.TenantUUID() require.NoError(t, err) expectedLines := []string{ @@ -157,8 +158,8 @@ func createDynakube(isLogMonitoringEnabled bool) *dynakube.DynaKube { LogMonitoring: logMonitoringSpec, }, Status: dynakube.DynaKubeStatus{ - OneAgent: dynakube.OneAgentStatus{ - ConnectionInfoStatus: dynakube.OneAgentConnectionInfoStatus{ + OneAgent: oneagent.Status{ + ConnectionInfoStatus: oneagent.ConnectionInfoStatus{ ConnectionInfo: communication.ConnectionInfo{ TenantUUID: "test-uuid", Endpoints: "https://endpoint1.com;https://endpoint2.com", @@ -194,7 +195,7 @@ func createK8sClientWithOneAgentTenantSecret(dk *dynakube.DynaKube, token string &corev1.Secret{ Data: map[string][]byte{connectioninfo.TenantTokenKey: []byte(token)}, ObjectMeta: metav1.ObjectMeta{ - Name: dk.OneagentTenantSecret(), + Name: dk.OneAgent().GetTenantSecret(), Namespace: dkNamespace, }, }, diff --git a/pkg/controllers/dynakube/logmonitoring/daemonset/annotations.go b/pkg/controllers/dynakube/logmonitoring/daemonset/annotations.go new file mode 100644 index 0000000000..1716ccb173 --- /dev/null +++ b/pkg/controllers/dynakube/logmonitoring/daemonset/annotations.go @@ -0,0 +1,22 @@ +package daemonset + +import ( + "github.com/Dynatrace/dynatrace-operator/pkg/api" + maputils "github.com/Dynatrace/dynatrace-operator/pkg/util/map" +) + +const annotationTenantTokenHash = api.InternalFlagPrefix + "tenant-token-hash" + +func (r *Reconciler) getAnnotations() map[string]string { + return maputils.MergeMap( + r.dk.LogMonitoring().Template().Annotations, + r.buildTenantTokenHashAnnotation()) +} + +func (r *Reconciler) buildTenantTokenHashAnnotation() map[string]string { + annotations := map[string]string{ + annotationTenantTokenHash: r.dk.Status.OneAgent.ConnectionInfoStatus.ConnectionInfo.TenantTokenHash, + } + + return annotations +} diff --git a/pkg/controllers/dynakube/logmonitoring/daemonset/args.go b/pkg/controllers/dynakube/logmonitoring/daemonset/args.go index 3681205c97..eeb841cee6 100644 --- a/pkg/controllers/dynakube/logmonitoring/daemonset/args.go +++ b/pkg/controllers/dynakube/logmonitoring/daemonset/args.go @@ -3,7 +3,7 @@ package daemonset import ( "fmt" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" ) func getInitArgs(dk dynakube.DynaKube) []string { @@ -14,11 +14,12 @@ func getInitArgs(dk dynakube.DynaKube) []string { fmt.Sprintf("-p dt.entity.kubernetes_cluster=$(%s)", entityEnv), fmt.Sprintf("-c k8s_fullpodname $(%s)", podNameEnv), fmt.Sprintf("-c k8s_poduid $(%s)", podUIDEnv), - fmt.Sprintf("-c k8s_containername %s", containerName), //nolint:perfsprint - fmt.Sprintf("-c k8s_basepodname %s", dk.LogMonitoring().GetDaemonSetName()), //nolint:perfsprint + fmt.Sprintf("-c k8s_basepodname $(%s)", basePodNameEnv), fmt.Sprintf("-c k8s_namespace $(%s)", namespaceNameEnv), fmt.Sprintf("-c k8s_node_name $(%s)", nodeNameEnv), fmt.Sprintf("-c k8s_cluster_id $(%s)", clusterUIDEnv), + "-c k8s_containername " + containerName, + "-l " + dtLogVolumePath, } return append(baseArgs, dk.LogMonitoring().Template().Args...) diff --git a/pkg/controllers/dynakube/logmonitoring/daemonset/args_test.go b/pkg/controllers/dynakube/logmonitoring/daemonset/args_test.go index b75b846cd2..8804d9f83f 100644 --- a/pkg/controllers/dynakube/logmonitoring/daemonset/args_test.go +++ b/pkg/controllers/dynakube/logmonitoring/daemonset/args_test.go @@ -3,13 +3,13 @@ package daemonset import ( "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/logmonitoring" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/logmonitoring" "github.com/stretchr/testify/assert" ) const ( - expectedBaseInitArgsLen = 11 + expectedBaseInitArgsLen = 12 ) func TestGetInitArgs(t *testing.T) { diff --git a/pkg/controllers/dynakube/logmonitoring/daemonset/conditions.go b/pkg/controllers/dynakube/logmonitoring/daemonset/conditions.go index 5183d59171..18d466cb85 100644 --- a/pkg/controllers/dynakube/logmonitoring/daemonset/conditions.go +++ b/pkg/controllers/dynakube/logmonitoring/daemonset/conditions.go @@ -1,5 +1,5 @@ package daemonset const ( - conditionType = "LogMonitoringDaemonSet" + ConditionType = "LogMonitoringDaemonSet" ) diff --git a/pkg/controllers/dynakube/logmonitoring/daemonset/container.go b/pkg/controllers/dynakube/logmonitoring/daemonset/container.go index 145a6f5d2e..5e881de3dd 100644 --- a/pkg/controllers/dynakube/logmonitoring/daemonset/container.go +++ b/pkg/controllers/dynakube/logmonitoring/daemonset/container.go @@ -1,9 +1,9 @@ package daemonset import ( - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/util/address" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" corev1 "k8s.io/api/core/v1" + "k8s.io/utils/ptr" ) const ( @@ -27,7 +27,7 @@ var ( } ) -func getContainer(dk dynakube.DynaKube) corev1.Container { +func getContainer(dk dynakube.DynaKube, tenantUUID string) corev1.Container { securityContext := getBaseSecurityContext(dk) securityContext.Capabilities.Add = neededCapabilities @@ -35,7 +35,7 @@ func getContainer(dk dynakube.DynaKube) corev1.Container { Name: containerName, Image: dk.LogMonitoring().Template().ImageRef.StringWithDefaults(defaultImageRepo, defaultImageTag), ImagePullPolicy: corev1.PullAlways, - VolumeMounts: getVolumeMounts(), + VolumeMounts: getVolumeMounts(tenantUUID), Env: getEnvs(), SecurityContext: &securityContext, } @@ -43,7 +43,7 @@ func getContainer(dk dynakube.DynaKube) corev1.Container { return container } -func getInitContainer(dk dynakube.DynaKube) corev1.Container { +func getInitContainer(dk dynakube.DynaKube, tenantUUID string) corev1.Container { securityContext := getBaseSecurityContext(dk) securityContext.Capabilities.Add = neededInitCapabilities @@ -51,7 +51,7 @@ func getInitContainer(dk dynakube.DynaKube) corev1.Container { Name: initContainerName, Image: dk.LogMonitoring().Template().ImageRef.StringWithDefaults(defaultImageRepo, defaultImageTag), ImagePullPolicy: corev1.PullAlways, - VolumeMounts: getDTVolumeMounts(), + VolumeMounts: []corev1.VolumeMount{getDTVolumeMounts(tenantUUID)}, Command: []string{bootstrapCommand}, Env: getInitEnvs(dk), Args: getInitArgs(dk), @@ -63,12 +63,12 @@ func getInitContainer(dk dynakube.DynaKube) corev1.Container { func getBaseSecurityContext(dk dynakube.DynaKube) corev1.SecurityContext { securityContext := corev1.SecurityContext{ - Privileged: address.Of(false), - ReadOnlyRootFilesystem: address.Of(true), - AllowPrivilegeEscalation: address.Of(false), - RunAsUser: address.Of(runAs), - RunAsGroup: address.Of(runAs), - RunAsNonRoot: address.Of(true), + Privileged: ptr.To(false), + ReadOnlyRootFilesystem: ptr.To(true), + AllowPrivilegeEscalation: ptr.To(false), + RunAsUser: ptr.To(runAs), + RunAsGroup: ptr.To(runAs), + RunAsNonRoot: ptr.To(true), Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{"ALL"}, }, @@ -79,5 +79,10 @@ func getBaseSecurityContext(dk dynakube.DynaKube) corev1.SecurityContext { securityContext.SeccompProfile = &corev1.SeccompProfile{LocalhostProfile: &seccomp} } + if dk.OneAgent().IsPrivilegedNeeded() { + securityContext.Privileged = ptr.To(true) + securityContext.AllowPrivilegeEscalation = ptr.To(true) + } + return securityContext } diff --git a/pkg/controllers/dynakube/logmonitoring/daemonset/container_test.go b/pkg/controllers/dynakube/logmonitoring/daemonset/container_test.go index 2c7018b638..e4ed2f6f96 100644 --- a/pkg/controllers/dynakube/logmonitoring/daemonset/container_test.go +++ b/pkg/controllers/dynakube/logmonitoring/daemonset/container_test.go @@ -4,16 +4,18 @@ import ( "testing" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/image" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/logmonitoring" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/logmonitoring" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestGetContainer(t *testing.T) { + tenantUUID := "test-uuid" + t.Run("get main container", func(t *testing.T) { dk := dynakube.DynaKube{} - mainContainer := getContainer(dk) + mainContainer := getContainer(dk, tenantUUID) require.NotEmpty(t, mainContainer) @@ -37,7 +39,7 @@ func TestGetContainer(t *testing.T) { Tag: expectedTag, }, } - mainContainer := getContainer(dk) + mainContainer := getContainer(dk, tenantUUID) require.NotEmpty(t, mainContainer) assert.NotEmpty(t, mainContainer.Image) @@ -46,9 +48,11 @@ func TestGetContainer(t *testing.T) { } func TestGetInitContainer(t *testing.T) { + tenantUUID := "test-uuid" + t.Run("get main container", func(t *testing.T) { dk := dynakube.DynaKube{} - initContainer := getInitContainer(dk) + initContainer := getInitContainer(dk, tenantUUID) require.NotEmpty(t, initContainer) @@ -75,7 +79,7 @@ func TestGetInitContainer(t *testing.T) { Tag: expectedTag, }, } - initContainer := getContainer(dk) + initContainer := getContainer(dk, tenantUUID) require.NotEmpty(t, initContainer) assert.NotEmpty(t, initContainer.Image) @@ -84,6 +88,8 @@ func TestGetInitContainer(t *testing.T) { } func TestSecurityContext(t *testing.T) { + tenantUUID := "test-uuid" + t.Run("get base securityContext", func(t *testing.T) { dk := dynakube.DynaKube{} sc := getBaseSecurityContext(dk) @@ -118,8 +124,8 @@ func TestSecurityContext(t *testing.T) { t.Run("main and init container securityContext differ only in capabilities", func(t *testing.T) { dk := dynakube.DynaKube{} - initContainer := getInitContainer(dk) - mainContainer := getContainer(dk) + initContainer := getInitContainer(dk, tenantUUID) + mainContainer := getContainer(dk, tenantUUID) require.NotNil(t, initContainer) require.NotNil(t, mainContainer) @@ -129,4 +135,18 @@ func TestSecurityContext(t *testing.T) { mainContainer.SecurityContext.Capabilities = nil assert.Equal(t, *initContainer.SecurityContext, *mainContainer.SecurityContext) }) + + t.Run("ocp scenario, OA needs to be privileged", func(t *testing.T) { + dk := dynakube.DynaKube{} + dk.Annotations = map[string]string{ + dynakube.AnnotationFeatureRunOneAgentContainerPrivileged: "true", + } + + sc := getBaseSecurityContext(dk) + + require.NotNil(t, sc) + require.NotEmpty(t, sc) + assert.True(t, *sc.Privileged) + assert.True(t, *sc.AllowPrivilegeEscalation) + }) } diff --git a/pkg/controllers/dynakube/logmonitoring/daemonset/env.go b/pkg/controllers/dynakube/logmonitoring/daemonset/env.go index 1c2e6dd689..54f5d7404d 100644 --- a/pkg/controllers/dynakube/logmonitoring/daemonset/env.go +++ b/pkg/controllers/dynakube/logmonitoring/daemonset/env.go @@ -1,7 +1,7 @@ package daemonset import ( - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" corev1 "k8s.io/api/core/v1" ) @@ -14,13 +14,14 @@ const ( namespaceNameEnv = "K8S_NAMESPACE_NAME" clusterUIDEnv = "K8S_CLUSTER_UID" clusterNameEnv = "K8S_CLUSTER_NAME" + basePodNameEnv = "K8S_BASEPODNAME" entityEnv = "DT_ENTITY_KUBERNETES_CLUSTER" // main container envs - apiNodeNameEnv = "KUBELET_API_NODENAME" - apiIPAddressEnv = "KUBELET_API_ADDRESS" - dtStorageEnv = "DT_STORAGE" - ruxitConfigEnv = "APMNG_PA_CONFIG_PATH" + KubeletNodeNameEnv = "KUBELET_API_NODENAME" + KubeletIPAddressEnv = "KUBELET_API_ADDRESS" + dtStorageEnv = "DT_STORAGE" + ruxitConfigEnv = "APMNG_PA_CONFIG_PATH" dtStoragePath = "/var/lib/dynatrace/oneagent" ruxitConfigPath = "/var/lib/dynatrace/oneagent/agent/config/ruxitagentproc.conf" @@ -72,13 +73,17 @@ func getInitEnvs(dk dynakube.DynaKube) []corev1.EnvVar { Name: entityEnv, Value: dk.Status.KubernetesClusterMEID, }, + { + Name: basePodNameEnv, + Value: dk.LogMonitoring().GetDaemonSetName(), + }, } } -func getEnvs() []corev1.EnvVar { +func GetKubeletEnvs() []corev1.EnvVar { return []corev1.EnvVar{ { - Name: apiNodeNameEnv, + Name: KubeletNodeNameEnv, ValueFrom: &corev1.EnvVarSource{ FieldRef: &corev1.ObjectFieldSelector{ FieldPath: "spec.nodeName", @@ -86,13 +91,19 @@ func getEnvs() []corev1.EnvVar { }, }, { - Name: apiIPAddressEnv, + Name: KubeletIPAddressEnv, ValueFrom: &corev1.EnvVarSource{ FieldRef: &corev1.ObjectFieldSelector{ FieldPath: "status.hostIP", }, }, }, + } +} + +func getEnvs() []corev1.EnvVar { + apiEnvs := GetKubeletEnvs() + standaloneEnvs := []corev1.EnvVar{ { Name: dtStorageEnv, Value: dtStoragePath, @@ -102,4 +113,6 @@ func getEnvs() []corev1.EnvVar { Value: ruxitConfigPath, }, } + + return append(apiEnvs, standaloneEnvs...) } diff --git a/pkg/controllers/dynakube/logmonitoring/daemonset/env_test.go b/pkg/controllers/dynakube/logmonitoring/daemonset/env_test.go index 90ee153df3..e05927cb84 100644 --- a/pkg/controllers/dynakube/logmonitoring/daemonset/env_test.go +++ b/pkg/controllers/dynakube/logmonitoring/daemonset/env_test.go @@ -3,14 +3,14 @@ package daemonset import ( "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" ) const ( - expectedBaseInitEnvLen = 7 + expectedBaseInitEnvLen = 8 expectedBaseEnvLen = 4 ) diff --git a/pkg/controllers/dynakube/logmonitoring/daemonset/reconciler.go b/pkg/controllers/dynakube/logmonitoring/daemonset/reconciler.go index 6014cfbf86..34d626bcc4 100644 --- a/pkg/controllers/dynakube/logmonitoring/daemonset/reconciler.go +++ b/pkg/controllers/dynakube/logmonitoring/daemonset/reconciler.go @@ -3,7 +3,7 @@ package daemonset import ( "context" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" "github.com/Dynatrace/dynatrace-operator/pkg/util/hasher" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/daemonset" @@ -37,9 +37,11 @@ func NewReconciler(clt client.Client, } } +var KubernetesSettingsNotAvailableError = errors.New("the status of the DynaKube is missing information about the kubernetes monitored-entity, skipping LogMonitoring deployment until it is ready") + func (r *Reconciler) Reconcile(ctx context.Context) error { if !r.dk.LogMonitoring().IsStandalone() { - if meta.FindStatusCondition(*r.dk.Conditions(), conditionType) == nil { + if meta.FindStatusCondition(*r.dk.Conditions(), ConditionType) == nil { return nil // no condition == nothing is there to clean up } @@ -50,13 +52,15 @@ func (r *Reconciler) Reconcile(ctx context.Context) error { log.Error(err, "failed to clean-up LogMonitoring config-secret") } - meta.RemoveStatusCondition(r.dk.Conditions(), conditionType) + meta.RemoveStatusCondition(r.dk.Conditions(), ConditionType) return nil // clean-up shouldn't cause a failure } if !r.isMEConfigured() { - return errors.New("the status of the DynaKube is missing information about the kubernetes monitored-entity, skipping LogMonitoring deployment") + log.Info("Kubernetes settings are not yet available, which are needed for LogMonitoring, will requeue") + + return KubernetesSettingsNotAvailableError } ds, err := r.generateDaemonSet() @@ -66,21 +70,21 @@ func (r *Reconciler) Reconcile(ctx context.Context) error { updated, err := daemonset.Query(r.client, r.apiReader, log).WithOwner(r.dk).CreateOrUpdate(ctx, ds) if err != nil { - conditions.SetKubeApiError(r.dk.Conditions(), conditionType, err) + conditions.SetKubeApiError(r.dk.Conditions(), ConditionType, err) return err } if updated { - conditions.SetDaemonSetOutdated(r.dk.Conditions(), conditionType, r.dk.LogMonitoring().GetDaemonSetName()) // needed to reset the timestamp - conditions.SetDaemonSetCreated(r.dk.Conditions(), conditionType, r.dk.LogMonitoring().GetDaemonSetName()) + conditions.SetDaemonSetOutdated(r.dk.Conditions(), ConditionType, r.dk.LogMonitoring().GetDaemonSetName()) // needed to reset the timestamp + conditions.SetDaemonSetCreated(r.dk.Conditions(), ConditionType, r.dk.LogMonitoring().GetDaemonSetName()) } return nil } func (r *Reconciler) generateDaemonSet() (*appsv1.DaemonSet, error) { - tenantUUID, err := r.dk.TenantUUIDFromConnectionInfoStatus() + tenantUUID, err := r.dk.TenantUUID() if err != nil { return nil, err } @@ -89,14 +93,15 @@ func (r *Reconciler) generateDaemonSet() (*appsv1.DaemonSet, error) { maxUnavailable := intstr.FromInt(r.dk.FeatureOneAgentMaxUnavailable()) - ds, err := daemonset.Build(r.dk, r.dk.LogMonitoring().GetDaemonSetName(), getContainer(*r.dk), - daemonset.SetInitContainer(getInitContainer(*r.dk)), + ds, err := daemonset.Build(r.dk, r.dk.LogMonitoring().GetDaemonSetName(), getContainer(*r.dk, tenantUUID), + daemonset.SetInitContainer(getInitContainer(*r.dk, tenantUUID)), daemonset.SetAllLabels(labels.BuildLabels(), labels.BuildMatchLabels(), labels.BuildLabels(), r.dk.LogMonitoring().Template().Labels), - daemonset.SetAllAnnotations(nil, r.dk.LogMonitoring().Template().Annotations), + daemonset.SetAllAnnotations(nil, r.getAnnotations()), daemonset.SetServiceAccount(serviceAccountName), daemonset.SetDNSPolicy(r.dk.LogMonitoring().Template().DNSPolicy), daemonset.SetAffinity(node.Affinity()), daemonset.SetPriorityClass(r.dk.LogMonitoring().Template().PriorityClassName), + daemonset.SetNodeSelector(r.dk.LogMonitoring().Template().NodeSelector), daemonset.SetTolerations(r.dk.LogMonitoring().Template().Tolerations), daemonset.SetPullSecret(r.dk.ImagePullSecretReferences()...), daemonset.SetUpdateStrategy(appsv1.DaemonSetUpdateStrategy{ @@ -104,7 +109,7 @@ func (r *Reconciler) generateDaemonSet() (*appsv1.DaemonSet, error) { MaxUnavailable: &maxUnavailable, }, }), - daemonset.SetVolumes(getVolumes(r.dk.Name, tenantUUID)), + daemonset.SetVolumes(getVolumes(r.dk.Name)), ) if err != nil { return nil, err diff --git a/pkg/controllers/dynakube/logmonitoring/daemonset/reconciler_test.go b/pkg/controllers/dynakube/logmonitoring/daemonset/reconciler_test.go index 0db87b730b..4be62af0ca 100644 --- a/pkg/controllers/dynakube/logmonitoring/daemonset/reconciler_test.go +++ b/pkg/controllers/dynakube/logmonitoring/daemonset/reconciler_test.go @@ -6,8 +6,9 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/communication" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/logmonitoring" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/logmonitoring" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" "github.com/Dynatrace/dynatrace-operator/pkg/util/hasher" "github.com/pkg/errors" @@ -34,8 +35,8 @@ func TestReconcile(t *testing.T) { t.Run("Only clean up if not standalone", func(t *testing.T) { dk := createDynakube(true) - dk.Spec.OneAgent.CloudNativeFullStack = &dynakube.CloudNativeFullStackSpec{} - conditions.SetDaemonSetCreated(dk.Conditions(), conditionType, "testing") + dk.Spec.OneAgent.CloudNativeFullStack = &oneagent.CloudNativeFullStackSpec{} + conditions.SetDaemonSetCreated(dk.Conditions(), ConditionType, "testing") previousDaemonSet := appsv1.DaemonSet{} previousDaemonSet.Name = dk.LogMonitoring().GetDaemonSetName() @@ -54,7 +55,7 @@ func TestReconcile(t *testing.T) { }, &daemonset) require.True(t, k8serrors.IsNotFound(err)) - condition := meta.FindStatusCondition(*dk.Conditions(), conditionType) + condition := meta.FindStatusCondition(*dk.Conditions(), ConditionType) require.Nil(t, condition) }) @@ -68,7 +69,7 @@ func TestReconcile(t *testing.T) { err := reconciler.Reconcile(ctx) require.NoError(t, err) - condition := meta.FindStatusCondition(*dk.Conditions(), conditionType) + condition := meta.FindStatusCondition(*dk.Conditions(), ConditionType) require.NotNil(t, condition) oldTransitionTime := condition.LastTransitionTime require.NotEmpty(t, oldTransitionTime) @@ -94,7 +95,7 @@ func TestReconcile(t *testing.T) { previousDaemonSet.Namespace = dk.Namespace mockK8sClient := fake.NewClient(&previousDaemonSet) - conditions.SetDaemonSetCreated(dk.Conditions(), conditionType, "this is a test") + conditions.SetDaemonSetCreated(dk.Conditions(), ConditionType, "this is a test") reconciler := NewReconciler(mockK8sClient, mockK8sClient, dk) err := reconciler.Reconcile(ctx) @@ -122,7 +123,7 @@ func TestReconcile(t *testing.T) { require.Error(t, err) require.Len(t, *dk.Conditions(), 1) - condition := meta.FindStatusCondition(*dk.Conditions(), conditionType) + condition := meta.FindStatusCondition(*dk.Conditions(), ConditionType) assert.Equal(t, conditions.KubeApiErrorReason, condition.Reason) assert.Equal(t, metav1.ConditionFalse, condition.Status) }) @@ -146,8 +147,7 @@ func TestGenerateDaemonSet(t *testing.T) { t.Run("generate daemonset", func(t *testing.T) { dk := createDynakube(true) - reconciler := NewReconciler(nil, - nil, dk) + reconciler := NewReconciler(nil, fake.NewClient(), dk) daemonset, err := reconciler.generateDaemonSet() require.NoError(t, err) require.NotNil(t, daemonset) @@ -163,6 +163,8 @@ func TestGenerateDaemonSet(t *testing.T) { assert.Subset(t, daemonset.Spec.Template.Labels, daemonset.Spec.Selector.MatchLabels) require.Len(t, daemonset.Annotations, 1) assert.Contains(t, daemonset.Annotations, hasher.AnnotationHash) + require.Len(t, daemonset.Spec.Template.Annotations, 1) + assert.Contains(t, daemonset.Spec.Template.Annotations, annotationTenantTokenHash) assert.Equal(t, serviceAccountName, daemonset.Spec.Template.Spec.ServiceAccountName) assert.Empty(t, daemonset.Spec.Template.Spec.DNSPolicy) assert.Empty(t, daemonset.Spec.Template.Spec.PriorityClassName) @@ -182,8 +184,7 @@ func TestGenerateDaemonSet(t *testing.T) { Labels: customLabels, } - reconciler := NewReconciler(nil, - nil, dk) + reconciler := NewReconciler(nil, fake.NewClient(), dk) daemonset, err := reconciler.generateDaemonSet() require.NoError(t, err) require.NotNil(t, daemonset) @@ -191,23 +192,25 @@ func TestGenerateDaemonSet(t *testing.T) { assert.Subset(t, daemonset.Spec.Template.Labels, customLabels) }) - t.Run("respect custom annotations", func(t *testing.T) { + t.Run("respect annotations", func(t *testing.T) { customAnnotations := map[string]string{ "custom": "annotation", } + testTokenHash := "testTokenHash" dk := createDynakube(true) dk.Spec.Templates.LogMonitoring = &logmonitoring.TemplateSpec{ Annotations: customAnnotations, } + dk.Status.OneAgent.ConnectionInfoStatus.TenantTokenHash = testTokenHash - reconciler := NewReconciler(nil, - nil, dk) + reconciler := NewReconciler(nil, fake.NewClient(), dk) daemonset, err := reconciler.generateDaemonSet() require.NoError(t, err) require.NotNil(t, daemonset) assert.Subset(t, daemonset.Spec.Template.Annotations, customAnnotations) + assert.Equal(t, testTokenHash, daemonset.Spec.Template.Annotations[annotationTenantTokenHash]) }) t.Run("respect dns policy", func(t *testing.T) { @@ -218,8 +221,7 @@ func TestGenerateDaemonSet(t *testing.T) { DNSPolicy: customPolicy, } - reconciler := NewReconciler(nil, - nil, dk) + reconciler := NewReconciler(nil, fake.NewClient(), dk) daemonset, err := reconciler.generateDaemonSet() require.NoError(t, err) require.NotNil(t, daemonset) @@ -235,8 +237,7 @@ func TestGenerateDaemonSet(t *testing.T) { PriorityClassName: customClass, } - reconciler := NewReconciler(nil, - nil, dk) + reconciler := NewReconciler(nil, fake.NewClient(), dk) daemonset, err := reconciler.generateDaemonSet() require.NoError(t, err) require.NotNil(t, daemonset) @@ -250,8 +251,7 @@ func TestGenerateDaemonSet(t *testing.T) { dk := createDynakube(true) dk.Spec.CustomPullSecret = customPullSecret - reconciler := NewReconciler(nil, - nil, dk) + reconciler := NewReconciler(nil, fake.NewClient(), dk) daemonset, err := reconciler.generateDaemonSet() require.NoError(t, err) require.NotNil(t, daemonset) @@ -272,14 +272,29 @@ func TestGenerateDaemonSet(t *testing.T) { dk.Spec.Templates.LogMonitoring = &logmonitoring.TemplateSpec{ Tolerations: customTolerations, } - reconciler := NewReconciler(nil, - nil, dk) + reconciler := NewReconciler(nil, fake.NewClient(), dk) daemonset, err := reconciler.generateDaemonSet() require.NoError(t, err) require.NotNil(t, daemonset) assert.Equal(t, daemonset.Spec.Template.Spec.Tolerations, customTolerations) }) + t.Run("respect custom nodeselector", func(t *testing.T) { + customNodeSelector := map[string]string{ + "some.nodeSelector.key": "true", + } + + dk := createDynakube(true) + dk.Spec.Templates.LogMonitoring = &logmonitoring.TemplateSpec{ + NodeSelector: customNodeSelector, + } + reconciler := NewReconciler(nil, fake.NewClient(), dk) + daemonset, err := reconciler.generateDaemonSet() + require.NoError(t, err) + require.NotNil(t, daemonset) + + assert.Equal(t, daemonset.Spec.Template.Spec.NodeSelector, customNodeSelector) + }) } func createDynakube(isEnabled bool) *dynakube.DynaKube { @@ -298,8 +313,8 @@ func createDynakube(isEnabled bool) *dynakube.DynaKube { LogMonitoring: logMonitoring, }, Status: dynakube.DynaKubeStatus{ - OneAgent: dynakube.OneAgentStatus{ - ConnectionInfoStatus: dynakube.OneAgentConnectionInfoStatus{ + OneAgent: oneagent.Status{ + ConnectionInfoStatus: oneagent.ConnectionInfoStatus{ ConnectionInfo: communication.ConnectionInfo{ TenantUUID: "test-uuid", }, diff --git a/pkg/controllers/dynakube/logmonitoring/daemonset/volumes.go b/pkg/controllers/dynakube/logmonitoring/daemonset/volumes.go index 755bd19340..0f841cb55b 100644 --- a/pkg/controllers/dynakube/logmonitoring/daemonset/volumes.go +++ b/pkg/controllers/dynakube/logmonitoring/daemonset/volumes.go @@ -5,6 +5,7 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/logmonitoring/configsecret" corev1 "k8s.io/api/core/v1" + "k8s.io/utils/ptr" ) const ( @@ -13,19 +14,17 @@ const ( configVolumeMountPath = "/var/lib/dynatrace/oneagent/agent/config/deployment.conf" // for the logmonitoring to read/write - dtLibVolumeName = "dynatrace-lib" - dtLibVolumeMountPath = "/var/lib/dynatrace" - dtLibVolumePathTemplate = "/tmp/dynatrace-logmonitoring-%s" - dtLogVolumeName = "dynatrace-logs" - dtLogVolumeMountPath = "/var/log/dynatrace" + dtLibVolumeName = "dynatrace-lib" + dtLibVolumeMountPath = "/var/lib/dynatrace" + dtSubPathTemplate = "logmonitoring-%s" + dtLogVolumePath = "/tmp/dynatrace" + dtLogVolumeName = "dynatrace-logs" // for the logs that the logmonitoring will ingest - podLogsVolumeName = "var-log-pods" - podLogsVolumePath = "/var/log/pods" - dockerLogsVolumeName = "docker-container-logs" - dockerLogsVolumePath = "/var/lib/docker/containers" - containerLogsVolumeName = "container-logs" - containerLogsVolumePath = "/var/log/containers" + dockerLogsVolumeName = "docker-container-logs" + dockerLogsVolumePath = "/var/lib/docker/containers" + logsVolumeHostPath = "/var/log" + logsVolumeName = "var-log" ) // getConfigVolumeMount provides the VolumeMount for the deployment.conf @@ -50,27 +49,34 @@ func getConfigVolume(dkName string) corev1.Volume { } // getDTVolumeMounts provides the VolumeMounts for the dynatrace specific folders -func getDTVolumeMounts() []corev1.VolumeMount { - return []corev1.VolumeMount{ - { - Name: dtLibVolumeName, - MountPath: dtLibVolumeMountPath, - }, - { - Name: dtLogVolumeName, - MountPath: dtLogVolumeMountPath, - }, +func getDTVolumeMounts(tenantUUID string) corev1.VolumeMount { + return corev1.VolumeMount{ + + Name: dtLibVolumeName, + SubPath: fmt.Sprintf(dtSubPathTemplate, tenantUUID), + MountPath: dtLibVolumeMountPath, + } +} + +// getDTVolumeMounts provides the VolumeMounts for the dynatrace specific folders +func getDTLogVolumeMounts(tenantUUID string) corev1.VolumeMount { + return corev1.VolumeMount{ + + Name: dtLogVolumeName, + SubPath: fmt.Sprintf(dtSubPathTemplate, tenantUUID), + MountPath: dtLogVolumePath, } } // getDTVolumes provides the Volumes for the dynatrace specific folders -func getDTVolumes(tenantUUID string) []corev1.Volume { +func getDTVolumes() []corev1.Volume { return []corev1.Volume{ { Name: dtLibVolumeName, VolumeSource: corev1.VolumeSource{ HostPath: &corev1.HostPathVolumeSource{ - Path: fmt.Sprintf(dtLibVolumePathTemplate, tenantUUID), + Path: dtLogVolumePath, + Type: ptr.To(corev1.HostPathDirectoryOrCreate), }, }, }, @@ -90,13 +96,8 @@ func getIngestVolumeMounts() []corev1.VolumeMount { ReadOnly: true, }, { - Name: podLogsVolumeName, - MountPath: podLogsVolumePath, - ReadOnly: true, - }, - { - Name: containerLogsVolumeName, - MountPath: containerLogsVolumePath, + Name: logsVolumeName, + MountPath: logsVolumeHostPath, ReadOnly: true, }, } @@ -110,41 +111,36 @@ func getIngestVolumes() []corev1.Volume { VolumeSource: corev1.VolumeSource{ HostPath: &corev1.HostPathVolumeSource{ Path: dockerLogsVolumePath, + Type: ptr.To(corev1.HostPathDirectoryOrCreate), }, }, }, { - Name: podLogsVolumeName, - VolumeSource: corev1.VolumeSource{ - HostPath: &corev1.HostPathVolumeSource{ - Path: podLogsVolumePath, - }, - }, - }, - { - Name: containerLogsVolumeName, + Name: logsVolumeName, VolumeSource: corev1.VolumeSource{ HostPath: &corev1.HostPathVolumeSource{ - Path: containerLogsVolumePath, + Path: logsVolumeHostPath, + Type: ptr.To(corev1.HostPathDirectory), }, }, }, } } -func getVolumeMounts() []corev1.VolumeMount { - mounts := []corev1.VolumeMount{} +func getVolumeMounts(tenantUUID string) []corev1.VolumeMount { + var mounts []corev1.VolumeMount mounts = append(mounts, getConfigVolumeMount()) - mounts = append(mounts, getDTVolumeMounts()...) + mounts = append(mounts, getDTVolumeMounts(tenantUUID)) + mounts = append(mounts, getDTLogVolumeMounts(tenantUUID)) mounts = append(mounts, getIngestVolumeMounts()...) return mounts } -func getVolumes(dkName, tenantUUID string) []corev1.Volume { - volumes := []corev1.Volume{} +func getVolumes(dkName string) []corev1.Volume { + var volumes []corev1.Volume volumes = append(volumes, getConfigVolume(dkName)) - volumes = append(volumes, getDTVolumes(tenantUUID)...) + volumes = append(volumes, getDTVolumes()...) volumes = append(volumes, getIngestVolumes()...) return volumes diff --git a/pkg/controllers/dynakube/logmonitoring/daemonset/volumes_test.go b/pkg/controllers/dynakube/logmonitoring/daemonset/volumes_test.go index 3df9cff563..63790a3840 100644 --- a/pkg/controllers/dynakube/logmonitoring/daemonset/volumes_test.go +++ b/pkg/controllers/dynakube/logmonitoring/daemonset/volumes_test.go @@ -8,14 +8,15 @@ import ( ) const ( - expectedMountLen = 6 - expectedInitMountLen = 2 - expectedVolumeLen = 6 + expectedMountLen = 5 + expectedInitMountLen = 1 ) func TestGetVolumeMounts(t *testing.T) { + tenantUUID := "test-uuid" + t.Run("get volume mounts", func(t *testing.T) { - mounts := getVolumeMounts() + mounts := getVolumeMounts(tenantUUID) require.NotEmpty(t, mounts) assert.Len(t, mounts, expectedMountLen) @@ -29,10 +30,9 @@ func TestGetVolumeMounts(t *testing.T) { func TestGetVolumes(t *testing.T) { dkName := "test-dk" - tenantUUID := "test-uuid" t.Run("get volumes", func(t *testing.T) { - volumes := getVolumes(dkName, tenantUUID) + volumes := getVolumes(dkName) require.NotEmpty(t, volumes) assert.Len(t, volumes, expectedMountLen) diff --git a/pkg/controllers/dynakube/logmonitoring/logmonsettings/conditions.go b/pkg/controllers/dynakube/logmonitoring/logmonsettings/conditions.go index 27b3ff7b16..597e2da073 100644 --- a/pkg/controllers/dynakube/logmonitoring/logmonsettings/conditions.go +++ b/pkg/controllers/dynakube/logmonitoring/logmonsettings/conditions.go @@ -7,8 +7,9 @@ import ( const ( settingsExistReason = "LogMonSettingsExist" + settingsErrorReason = "LogMonSettingsError" - conditionType = "LogMonitoringSettings" + ConditionType = "LogMonitoringSettings" ) func setLogMonitoringSettingExists(conditions *[]metav1.Condition, conditionType string) { @@ -20,3 +21,13 @@ func setLogMonitoringSettingExists(conditions *[]metav1.Condition, conditionType } _ = meta.SetStatusCondition(conditions, condition) } + +func setLogMonitoringSettingError(conditions *[]metav1.Condition, conditionType, message string) { + condition := metav1.Condition{ + Type: conditionType, + Status: metav1.ConditionFalse, + Reason: settingsErrorReason, + Message: "LogMonitoring settings could not be created: " + message, + } + _ = meta.SetStatusCondition(conditions, condition) +} diff --git a/pkg/controllers/dynakube/logmonitoring/logmonsettings/reconciler.go b/pkg/controllers/dynakube/logmonitoring/logmonsettings/reconciler.go index 7085b668a9..a93a02789a 100644 --- a/pkg/controllers/dynakube/logmonitoring/logmonsettings/reconciler.go +++ b/pkg/controllers/dynakube/logmonitoring/logmonsettings/reconciler.go @@ -3,10 +3,11 @@ package logmonsettings import ( "context" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/logmonitoring" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/logmonitoring" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/controllers" + "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/logmonitoring/daemonset" "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" "github.com/Dynatrace/dynatrace-operator/pkg/util/timeprovider" "github.com/pkg/errors" @@ -33,14 +34,18 @@ func NewReconciler(dtc dtclient.Client, dk *dynakube.DynaKube) controllers.Recon } func (r *reconciler) Reconcile(ctx context.Context) error { - if !conditions.IsOutdated(r.timeProvider, r.dk, conditionType) { + if !conditions.IsOutdated(r.timeProvider, r.dk, ConditionType) { return nil } if !r.dk.LogMonitoring().IsEnabled() { - meta.RemoveStatusCondition(r.dk.Conditions(), conditionType) + meta.RemoveStatusCondition(r.dk.Conditions(), ConditionType) return nil + } else if r.dk.Status.KubernetesClusterMEID == "" { + log.Info("Kubernetes settings are not yet available, which are needed for LogMonitoring, will requeue") + + return daemonset.KubernetesSettingsNotAvailableError } err := r.checkLogMonitoringSettings(ctx) @@ -56,13 +61,15 @@ func (r *reconciler) checkLogMonitoringSettings(ctx context.Context) error { logMonitoringSettings, err := r.dtc.GetSettingsForLogModule(ctx, r.dk.Status.KubernetesClusterMEID) if err != nil { + setLogMonitoringSettingError(r.dk.Conditions(), ConditionType, err.Error()) + return errors.WithMessage(err, "error trying to check if setting exists") } if logMonitoringSettings.TotalCount > 0 { log.Info("there are already settings", "settings", logMonitoringSettings) - setLogMonitoringSettingExists(r.dk.Conditions(), conditionType) + setLogMonitoringSettingExists(r.dk.Conditions(), ConditionType) return nil } @@ -75,7 +82,9 @@ func (r *reconciler) checkLogMonitoringSettings(ctx context.Context) error { objectId, err := r.dtc.CreateLogMonitoringSetting(ctx, r.dk.Status.KubernetesClusterMEID, r.dk.Status.KubernetesClusterName, matchers) if err != nil { - return errors.WithMessage(err, "error when creating log monitoring setting") + setLogMonitoringSettingError(r.dk.Conditions(), ConditionType, err.Error()) + + return err } log.Info("logmonitoring setting created", "settings", objectId) diff --git a/pkg/controllers/dynakube/logmonitoring/logmonsettings/reconciler_test.go b/pkg/controllers/dynakube/logmonitoring/logmonsettings/reconciler_test.go index cb90233c07..b6b0725f39 100644 --- a/pkg/controllers/dynakube/logmonitoring/logmonsettings/reconciler_test.go +++ b/pkg/controllers/dynakube/logmonitoring/logmonsettings/reconciler_test.go @@ -5,8 +5,8 @@ import ( "errors" "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/logmonitoring" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/logmonitoring" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" dtclientmock "github.com/Dynatrace/dynatrace-operator/test/mocks/pkg/clients/dynatrace" "github.com/stretchr/testify/mock" diff --git a/pkg/controllers/dynakube/logmonitoring/reconciler.go b/pkg/controllers/dynakube/logmonitoring/reconciler.go index 6c91997f11..8c8806e766 100644 --- a/pkg/controllers/dynakube/logmonitoring/reconciler.go +++ b/pkg/controllers/dynakube/logmonitoring/reconciler.go @@ -3,7 +3,7 @@ package logmonitoring import ( "context" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/controllers" oaconnectioninfo "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/connectioninfo/oneagent" diff --git a/pkg/controllers/dynakube/logmonitoring/reconciler_test.go b/pkg/controllers/dynakube/logmonitoring/reconciler_test.go index 4e95719e7d..5b9a850c9c 100644 --- a/pkg/controllers/dynakube/logmonitoring/reconciler_test.go +++ b/pkg/controllers/dynakube/logmonitoring/reconciler_test.go @@ -5,8 +5,8 @@ import ( "errors" "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/logmonitoring" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/logmonitoring" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" dtclientmock "github.com/Dynatrace/dynatrace-operator/test/mocks/pkg/clients/dynatrace" controllermock "github.com/Dynatrace/dynatrace-operator/test/mocks/pkg/controllers" diff --git a/pkg/controllers/dynakube/metadata/rules/reconciler.go b/pkg/controllers/dynakube/metadata/rules/reconciler.go index 8df4a7e208..d188dd4f2d 100644 --- a/pkg/controllers/dynakube/metadata/rules/reconciler.go +++ b/pkg/controllers/dynakube/metadata/rules/reconciler.go @@ -4,7 +4,7 @@ import ( "context" "errors" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/controllers" "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" diff --git a/pkg/controllers/dynakube/metadata/rules/reconciler_test.go b/pkg/controllers/dynakube/metadata/rules/reconciler_test.go index e034dca635..9be41c516f 100644 --- a/pkg/controllers/dynakube/metadata/rules/reconciler_test.go +++ b/pkg/controllers/dynakube/metadata/rules/reconciler_test.go @@ -6,9 +6,8 @@ import ( "testing" "time" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" - "github.com/Dynatrace/dynatrace-operator/pkg/util/address" "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" "github.com/Dynatrace/dynatrace-operator/pkg/util/timeprovider" dtclientmock "github.com/Dynatrace/dynatrace-operator/test/mocks/pkg/clients/dynatrace" @@ -16,6 +15,7 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" ) func TestReconcile(t *testing.T) { @@ -23,7 +23,7 @@ func TestReconcile(t *testing.T) { t.Run("no error if not enabled", func(t *testing.T) { dk := createDynaKube() - dk.Spec.MetadataEnrichment.Enabled = address.Of(false) + dk.Spec.MetadataEnrichment.Enabled = ptr.To(false) reconciler := NewReconciler(nil, &dk) @@ -34,7 +34,7 @@ func TestReconcile(t *testing.T) { t.Run("clean-up if previously enabled", func(t *testing.T) { dk := createDynaKube() - dk.Spec.MetadataEnrichment.Enabled = address.Of(false) + dk.Spec.MetadataEnrichment.Enabled = ptr.To(false) dk.Status.MetadataEnrichment.Rules = createRules() conditions.SetStatusUpdated(dk.Conditions(), conditionType, "TESTING") @@ -127,7 +127,7 @@ func createDynaKube() dynakube.DynaKube { }, Spec: dynakube.DynaKubeSpec{ MetadataEnrichment: dynakube.MetadataEnrichment{ - Enabled: address.Of(true), + Enabled: ptr.To(true), }, }, Status: dynakube.DynaKubeStatus{ diff --git a/pkg/controllers/dynakube/monitoredentities/reconciler.go b/pkg/controllers/dynakube/monitoredentities/reconciler.go index 15c1a2ec17..11bdf3de58 100644 --- a/pkg/controllers/dynakube/monitoredentities/reconciler.go +++ b/pkg/controllers/dynakube/monitoredentities/reconciler.go @@ -3,7 +3,7 @@ package monitoredentities import ( "context" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/controllers" "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" diff --git a/pkg/controllers/dynakube/monitoredentities/reconciler_test.go b/pkg/controllers/dynakube/monitoredentities/reconciler_test.go index d9314a7e55..bee91b643a 100644 --- a/pkg/controllers/dynakube/monitoredentities/reconciler_test.go +++ b/pkg/controllers/dynakube/monitoredentities/reconciler_test.go @@ -4,13 +4,13 @@ import ( "context" "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" - "github.com/Dynatrace/dynatrace-operator/pkg/util/address" dtclientmock "github.com/Dynatrace/dynatrace-operator/test/mocks/pkg/clients/dynatrace" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" ) func TestReconcile(t *testing.T) { @@ -21,7 +21,7 @@ func TestReconcile(t *testing.T) { clt.On("GetMonitoredEntitiesForKubeSystemUUID", mock.AnythingOfType("context.backgroundCtx"), "kube-system-uuid").Return([]dtclient.MonitoredEntity{{EntityId: "KUBERNETES_CLUSTER-0E30FE4BF2007587", DisplayName: "operator test entity 1", LastSeenTms: 1639483869085}}, nil) dk := createDynaKube() - dk.Spec.MetadataEnrichment.Enabled = address.Of(false) + dk.Spec.MetadataEnrichment.Enabled = ptr.To(false) reconciler := NewReconciler(clt, &dk) @@ -66,7 +66,7 @@ func createDynaKube() dynakube.DynaKube { }, Spec: dynakube.DynaKubeSpec{ MetadataEnrichment: dynakube.MetadataEnrichment{ - Enabled: address.Of(true), + Enabled: ptr.To(true), }, }, Status: dynakube.DynaKubeStatus{ diff --git a/pkg/controllers/dynakube/oneagent/daemonset/affinity.go b/pkg/controllers/dynakube/oneagent/daemonset/affinity.go index d63b098614..8025aaca7e 100644 --- a/pkg/controllers/dynakube/oneagent/daemonset/affinity.go +++ b/pkg/controllers/dynakube/oneagent/daemonset/affinity.go @@ -1,30 +1,18 @@ package daemonset import ( + "github.com/Dynatrace/dynatrace-operator/pkg/api/status" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/node" corev1 "k8s.io/api/core/v1" ) func (b *builder) affinity() *corev1.Affinity { - return &corev1.Affinity{ - NodeAffinity: &corev1.NodeAffinity{ - RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{ - NodeSelectorTerms: b.affinityNodeSelectorTerms(), - }, - }, - } -} - -func (b *builder) affinityNodeSelectorTerms() []corev1.NodeSelectorTerm { - nodeSelectorTerms := []corev1.NodeSelectorTerm{ - kubernetesArchOsSelectorTerm(), + var affinity corev1.Affinity + if b.dk.Status.OneAgent.VersionStatus.Source == status.TenantRegistryVersionSource || b.dk.Status.OneAgent.VersionStatus.Source == status.CustomVersionVersionSource { + affinity = node.AMDOnlyAffinity() + } else { + affinity = node.Affinity() } - return nodeSelectorTerms -} - -func kubernetesArchOsSelectorTerm() corev1.NodeSelectorTerm { - return corev1.NodeSelectorTerm{ - MatchExpressions: node.AffinityNodeRequirementForSupportedArches(), - } + return &affinity } diff --git a/pkg/controllers/dynakube/oneagent/daemonset/affinity_test.go b/pkg/controllers/dynakube/oneagent/daemonset/affinity_test.go index e16e3096ab..360497ac60 100644 --- a/pkg/controllers/dynakube/oneagent/daemonset/affinity_test.go +++ b/pkg/controllers/dynakube/oneagent/daemonset/affinity_test.go @@ -3,39 +3,66 @@ package daemonset import ( "testing" + "github.com/Dynatrace/dynatrace-operator/pkg/api/status" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" ) func TestAffinity(t *testing.T) { - dsBuilder := builder{} - affinity := dsBuilder.affinity() - assert.NotContains(t, affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms, corev1.NodeSelectorTerm{ - MatchExpressions: []corev1.NodeSelectorRequirement{ - { - Key: "beta.kubernetes.io/arch", - Operator: corev1.NodeSelectorOpIn, - Values: []string{"amd64", "arm64", "ppc64le", "s390x"}, + t.Run("none tenant-registry DynaKube has all the architectures", func(t *testing.T) { + dk := dynakube.DynaKube{} + dk.Status.OneAgent.VersionStatus.Source = status.CustomImageVersionSource + dsBuilder := builder{dk: &dk} + affinity := dsBuilder.affinity() + assert.NotContains(t, affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms, corev1.NodeSelectorTerm{ + MatchExpressions: []corev1.NodeSelectorRequirement{ + { + Key: "beta.kubernetes.io/arch", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"amd64", "arm64", "ppc64le", "s390x"}, + }, + { + Key: "beta.kubernetes.io/os", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"linux"}, + }, }, - { - Key: "beta.kubernetes.io/os", - Operator: corev1.NodeSelectorOpIn, - Values: []string{"linux"}, + }) + assert.Contains(t, affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms, corev1.NodeSelectorTerm{ + MatchExpressions: []corev1.NodeSelectorRequirement{ + { + Key: "kubernetes.io/arch", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"amd64", "arm64", "ppc64le", "s390x"}, + }, + { + Key: "kubernetes.io/os", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"linux"}, + }, }, - }, + }) }) - assert.Contains(t, affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms, corev1.NodeSelectorTerm{ - MatchExpressions: []corev1.NodeSelectorRequirement{ - { - Key: "kubernetes.io/arch", - Operator: corev1.NodeSelectorOpIn, - Values: []string{"amd64", "arm64", "ppc64le", "s390x"}, - }, - { - Key: "kubernetes.io/os", - Operator: corev1.NodeSelectorOpIn, - Values: []string{"linux"}, + + t.Run("tenant-registry DynaKube has only AMD architectures", func(t *testing.T) { + dk := dynakube.DynaKube{} + dk.Status.OneAgent.VersionStatus.Source = status.TenantRegistryVersionSource + dsBuilder := builder{dk: &dk} + affinity := dsBuilder.affinity() + assert.Contains(t, affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms, corev1.NodeSelectorTerm{ + MatchExpressions: []corev1.NodeSelectorRequirement{ + { + Key: "kubernetes.io/arch", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"amd64"}, + }, + { + Key: "kubernetes.io/os", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"linux"}, + }, }, - }, + }) }) } diff --git a/pkg/controllers/dynakube/oneagent/daemonset/arguments.go b/pkg/controllers/dynakube/oneagent/daemonset/arguments.go index a41ec89c52..4971298b52 100644 --- a/pkg/controllers/dynakube/oneagent/daemonset/arguments.go +++ b/pkg/controllers/dynakube/oneagent/daemonset/arguments.go @@ -18,10 +18,12 @@ func (b *builder) arguments() ([]string, error) { argMap := prioritymap.New( prioritymap.WithSeparator(prioritymap.DefaultSeparator), prioritymap.WithPriority(defaultArgumentPriority), - prioritymap.WithAllowDuplicates(), + prioritymap.WithAvoidDuplicates(), + prioritymap.WithAllowDuplicatesFor("--set-host-property"), + prioritymap.WithAllowDuplicatesFor("--set-host-tag"), ) - isProxyAsEnvDeprecated, err := isProxyAsEnvVarDeprecated(b.dk.OneAgentVersion()) + isProxyAsEnvDeprecated, err := isProxyAsEnvVarDeprecated(b.dk.OneAgent().GetVersion()) if err != nil { return []string{}, err } @@ -38,9 +40,9 @@ func (b *builder) arguments() ([]string, error) { appendOperatorVersionArg(argMap) appendImmutableImageArgs(argMap) - if b.dk.ClassicFullStackMode() { + if b.dk.OneAgent().IsClassicFullStackMode() { argMap.Append(argumentPrefix+"set-host-id-source", classicHostIdSource) - } else if b.dk.HostMonitoringMode() || b.dk.CloudNativeFullstackMode() { + } else if b.dk.OneAgent().IsHostMonitoringMode() || b.dk.OneAgent().IsCloudNativeFullstackMode() { argMap.Append(argumentPrefix+"set-host-id-source", inframonHostIdSource) } diff --git a/pkg/controllers/dynakube/oneagent/daemonset/arguments_test.go b/pkg/controllers/dynakube/oneagent/daemonset/arguments_test.go index 351c688fab..d992947683 100644 --- a/pkg/controllers/dynakube/oneagent/daemonset/arguments_test.go +++ b/pkg/controllers/dynakube/oneagent/daemonset/arguments_test.go @@ -6,8 +6,9 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/value" "github.com/Dynatrace/dynatrace-operator/pkg/api/status" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/deploymentmetadata" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -49,8 +50,8 @@ func TestArguments(t *testing.T) { dk := dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ APIURL: testURL, - OneAgent: dynakube.OneAgentSpec{ - ClassicFullStack: &dynakube.HostInjectSpec{ + OneAgent: oneagent.Spec{ + ClassicFullStack: &oneagent.HostInjectSpec{ Args: []string{testValue}, }, }, @@ -72,7 +73,7 @@ func TestArguments(t *testing.T) { args := []string{testValue} builder := builder{ dk: &dynakube.DynaKube{}, - hostInjectSpec: &dynakube.HostInjectSpec{Args: args}, + hostInjectSpec: &oneagent.HostInjectSpec{Args: args}, } arguments, _ := builder.arguments() @@ -87,16 +88,29 @@ func TestArguments(t *testing.T) { } assert.Equal(t, expectedDefaultArguments, arguments) }) - t.Run("when injected arguments are provided then they come last", func(t *testing.T) { - args := []string{ + t.Run("when injected arguments are provided then they come last, only allowed duplicates and no duplicate key/value pairs", func(t *testing.T) { + custArgs := []string{ "--set-app-log-content-access=true", - "--set-host-id-source=lustiglustig", + "--set-host-id-source=fqdn", "--set-host-group=APP_LUSTIG_PETER", "--set-server=https://hyper.super.com:9999", + "--set-host-property=prop1", + "--set-host-property=prop1", + "--set-host-property=prop1", + "--set-host-property=prop2", + "--set-host-property=prop2", + "--set-host-property=prop3", + "--set-host-property=prop3", + "--set-host-property=prop3", + "--set-host-tag=tag1", + "--set-host-tag=tag2", + "--set-host-tag=tag2", + "--set-host-tag=tag2", + "--set-host-tag=tag3", } builder := builder{ - dk: &dynakube.DynaKube{}, - hostInjectSpec: &dynakube.HostInjectSpec{Args: args}, + dk: &dynakube.DynaKube{Spec: dynakube.DynaKubeSpec{OneAgent: oneagent.Spec{ClassicFullStack: &oneagent.HostInjectSpec{}}}}, + hostInjectSpec: &oneagent.HostInjectSpec{Args: custArgs}, } arguments, _ := builder.arguments() @@ -104,11 +118,16 @@ func TestArguments(t *testing.T) { expectedDefaultArguments := []string{ "--set-app-log-content-access=true", "--set-host-group=APP_LUSTIG_PETER", - "--set-host-id-source=lustiglustig", + "--set-host-id-source=fqdn", "--set-host-property=OperatorVersion=$(DT_OPERATOR_VERSION)", + "--set-host-property=prop1", + "--set-host-property=prop2", + "--set-host-property=prop3", + "--set-host-tag=tag1", + "--set-host-tag=tag2", + "--set-host-tag=tag3", "--set-no-proxy=", "--set-proxy=", - "--set-server={$(DT_SERVER)}", "--set-server=https://hyper.super.com:9999", "--set-tenant=$(DT_TENANT)", } @@ -121,7 +140,7 @@ func TestArguments(t *testing.T) { Proxy: &value.Source{Value: "something"}, }, Status: dynakube.DynaKubeStatus{ - OneAgent: dynakube.OneAgentStatus{ + OneAgent: oneagent.Status{ VersionStatus: status.VersionStatus{ Version: "1.285.0.20240122-141707", }, @@ -201,7 +220,7 @@ func TestArguments(t *testing.T) { } builder := builder{ dk: &dynakube.DynaKube{}, - hostInjectSpec: &dynakube.HostInjectSpec{Args: args}, + hostInjectSpec: &oneagent.HostInjectSpec{Args: args}, } arguments, _ := builder.arguments() @@ -216,7 +235,6 @@ func TestArguments(t *testing.T) { "--set-host-property=item2=value2", "--set-no-proxy=", "--set-proxy=", - "--set-server={$(DT_SERVER)}", "--set-server=https://hyper.super.com:9999", "--set-tenant=$(DT_TENANT)", } @@ -231,8 +249,8 @@ func TestArguments(t *testing.T) { }, Spec: dynakube.DynaKubeSpec{ Proxy: &value.Source{Value: testValue}, - OneAgent: dynakube.OneAgentSpec{ - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{}, + OneAgent: oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{}, }, ActiveGate: activegate.Spec{ Capabilities: []activegate.CapabilityDisplayName{activegate.RoutingCapability.DisplayName}, @@ -248,13 +266,43 @@ func TestArguments(t *testing.T) { assert.Contains(t, arguments, "--set-no-proxy=*.dev.dynatracelabs.com,dynakube-activegate.dynatrace") }) + t.Run("allow arguments without value, but deduplicate", func(t *testing.T) { + custArgs := []string{ + "--enable-feature-a", + "--enable-feature-b", + "--enable-feature-c", + "--enable-feature-c", + "--enable-feature-a", + "--enable-feature-b", + } + builder := builder{ + dk: &dynakube.DynaKube{Spec: dynakube.DynaKubeSpec{OneAgent: oneagent.Spec{ClassicFullStack: &oneagent.HostInjectSpec{}}}}, + hostInjectSpec: &oneagent.HostInjectSpec{Args: custArgs}, + deploymentType: deploymentmetadata.CloudNativeDeploymentType, + } + + arguments, _ := builder.arguments() + + expectedDefaultArguments := []string{ + "--enable-feature-a", + "--enable-feature-b", + "--enable-feature-c", + "--set-host-id-source=auto", + "--set-host-property=OperatorVersion=$(DT_OPERATOR_VERSION)", + "--set-no-proxy=", + "--set-proxy=", + "--set-server={$(DT_SERVER)}", + "--set-tenant=$(DT_TENANT)", + } + assert.Equal(t, expectedDefaultArguments, arguments) + }) } func TestPodSpec_Arguments(t *testing.T) { dk := &dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - ClassicFullStack: &dynakube.HostInjectSpec{ + OneAgent: oneagent.Spec{ + ClassicFullStack: &oneagent.HostInjectSpec{ Args: []string{testKey, testValue, testUID}, }, }, @@ -317,8 +365,8 @@ func TestPodSpec_Arguments(t *testing.T) { t.Run(`has host-id-source arg for hostMonitoring`, func(t *testing.T) { hostMonInstance := &dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - HostMonitoring: &dynakube.HostInjectSpec{ + OneAgent: oneagent.Spec{ + HostMonitoring: &oneagent.HostInjectSpec{ Args: []string{testKey, testValue, testUID}, }, }, @@ -341,9 +389,9 @@ func TestPodSpec_Arguments(t *testing.T) { t.Run(`has host-id-source arg for cloudNativeFullstack`, func(t *testing.T) { cloudNativeInstance := &dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{ - HostInjectSpec: dynakube.HostInjectSpec{Args: []string{testKey, testValue, testUID}}, + OneAgent: oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{ + HostInjectSpec: oneagent.HostInjectSpec{Args: []string{testKey, testValue, testUID}}, }, }, }, @@ -363,9 +411,9 @@ func TestPodSpec_Arguments(t *testing.T) { t.Run(`has host-group for classicFullstack`, func(t *testing.T) { classicInstance := &dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ + OneAgent: oneagent.Spec{ HostGroup: testNewHostGroupName, - ClassicFullStack: &dynakube.HostInjectSpec{ + ClassicFullStack: &oneagent.HostInjectSpec{ Args: []string{testOldHostGroupArgument}, }, }, @@ -385,10 +433,10 @@ func TestPodSpec_Arguments(t *testing.T) { t.Run(`has host-group for cloudNativeFullstack`, func(t *testing.T) { cloudNativeInstance := &dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ + OneAgent: oneagent.Spec{ HostGroup: testNewHostGroupName, - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{ - HostInjectSpec: dynakube.HostInjectSpec{Args: []string{testOldHostGroupArgument}}, + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{ + HostInjectSpec: oneagent.HostInjectSpec{Args: []string{testOldHostGroupArgument}}, }, }, }, @@ -407,9 +455,9 @@ func TestPodSpec_Arguments(t *testing.T) { t.Run(`has host-group for HostMonitoring`, func(t *testing.T) { hostMonitoringInstance := &dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ + OneAgent: oneagent.Spec{ HostGroup: testNewHostGroupName, - HostMonitoring: &dynakube.HostInjectSpec{ + HostMonitoring: &oneagent.HostInjectSpec{ Args: []string{testOldHostGroupArgument}, }, }, diff --git a/pkg/controllers/dynakube/oneagent/daemonset/daemonset.go b/pkg/controllers/dynakube/oneagent/daemonset/daemonset.go index 6c3e569f85..c7713a4e79 100644 --- a/pkg/controllers/dynakube/oneagent/daemonset/daemonset.go +++ b/pkg/controllers/dynakube/oneagent/daemonset/daemonset.go @@ -1,10 +1,11 @@ package daemonset import ( + "github.com/Dynatrace/dynatrace-operator/pkg/api" "github.com/Dynatrace/dynatrace-operator/pkg/api/status" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/deploymentmetadata" - "github.com/Dynatrace/dynatrace-operator/pkg/util/address" "github.com/Dynatrace/dynatrace-operator/pkg/util/dtversion" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/labels" maputils "github.com/Dynatrace/dynatrace-operator/pkg/util/map" @@ -15,11 +16,13 @@ import ( "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/ptr" ) const ( annotationUnprivileged = "container.apparmor.security.beta.kubernetes.io/dynatrace-oneagent" annotationUnprivilegedValue = "unconfined" + annotationTenantTokenHash = api.InternalFlagPrefix + "tenant-token-hash" serviceAccountName = "dynatrace-dynakube-oneagent" @@ -38,6 +41,8 @@ const ( csiStorageVolumeName = "osagent-storage" csiStorageVolumeMount = "/mnt/volume_storage_mount" + storageVolumeName = "volume-storage" + podName = "dynatrace-oneagent" inframonHostIdSource = "k8s-node-name" @@ -59,7 +64,7 @@ type classicFullStack struct { type builder struct { dk *dynakube.DynaKube - hostInjectSpec *dynakube.HostInjectSpec + hostInjectSpec *oneagent.HostInjectSpec clusterID string deploymentType string } @@ -107,7 +112,7 @@ func (hm *hostMonitoring) BuildDaemonSet() (*appsv1.DaemonSet, error) { return nil, err } - daemonSet.Name = hm.dk.OneAgentDaemonsetName() + daemonSet.Name = hm.dk.OneAgent().GetDaemonsetName() if len(daemonSet.Spec.Template.Spec.Containers) > 0 { hm.appendInfraMonEnvVars(daemonSet) @@ -122,7 +127,7 @@ func (classic *classicFullStack) BuildDaemonSet() (*appsv1.DaemonSet, error) { return nil, err } - result.Name = classic.dk.OneAgentDaemonsetName() + result.Name = classic.dk.OneAgent().GetDaemonsetName() return result, nil } @@ -135,7 +140,7 @@ func (b *builder) BuildDaemonSet() (*appsv1.DaemonSet, error) { return nil, err } - versionLabelValue := dk.OneAgentVersion() + versionLabelValue := dk.OneAgent().GetVersion() appLabels := labels.NewAppLabels(labels.OneAgentComponentLabel, dk.Name, b.deploymentType, versionLabelValue) @@ -147,6 +152,7 @@ func (b *builder) BuildDaemonSet() (*appsv1.DaemonSet, error) { annotations := map[string]string{ annotationUnprivileged: annotationUnprivilegedValue, webhook.AnnotationDynatraceInject: "false", + annotationTenantTokenHash: dk.Status.OneAgent.ConnectionInfoStatus.TenantTokenHash, } annotations = maputils.MergeMap(annotations, b.hostInjectSpec.Annotations) @@ -221,11 +227,14 @@ func (b *builder) podSpec() (corev1.PodSpec, error) { DNSPolicy: dnsPolicy, Volumes: volumes, Affinity: affinity, - TerminationGracePeriodSeconds: address.Of(defaultTerminationGracePeriod), + TerminationGracePeriodSeconds: ptr.To(defaultTerminationGracePeriod), } - if b.dk.NeedsOneAgentProbe() { + if b.dk.OneAgent().IsReadinessProbeNeeded() { podSpec.Containers[0].ReadinessProbe = b.getReadinessProbe() + } + + if b.dk.OneAgent().IsLivenessProbeNeeded() { podSpec.Containers[0].LivenessProbe = b.getDefaultProbeFromStatus() } @@ -237,7 +246,7 @@ func (b *builder) immutableOneAgentImage() string { return "" } - return b.dk.OneAgentImage() + return b.dk.OneAgent().GetImage() } func (b *builder) tolerations() []corev1.Toleration { @@ -312,35 +321,35 @@ func (b *builder) imagePullSecrets() []corev1.LocalObjectReference { func (b *builder) securityContext() *corev1.SecurityContext { var securityContext corev1.SecurityContext - if b.dk != nil && b.dk.UseReadOnlyOneAgents() { - securityContext.RunAsNonRoot = address.Of(true) - securityContext.RunAsUser = address.Of(int64(1000)) - securityContext.RunAsGroup = address.Of(int64(1000)) - securityContext.ReadOnlyRootFilesystem = address.Of(b.isRootFsReadonly()) + if b.dk != nil && b.dk.OneAgent().IsReadOnlyFSSupported() { + securityContext.RunAsNonRoot = ptr.To(true) + securityContext.RunAsUser = ptr.To(int64(1000)) + securityContext.RunAsGroup = ptr.To(int64(1000)) + securityContext.ReadOnlyRootFilesystem = ptr.To(b.isRootFsReadonly()) } else { - securityContext.ReadOnlyRootFilesystem = address.Of(false) + securityContext.ReadOnlyRootFilesystem = ptr.To(false) } - if b.dk != nil && b.dk.NeedsOneAgentPrivileged() { - securityContext.Privileged = address.Of(true) + if b.dk != nil && b.dk.OneAgent().IsPrivilegedNeeded() { + securityContext.Privileged = ptr.To(true) } else { securityContext.Capabilities = defaultSecurityContextCapabilities() if b.dk != nil { switch { - case b.dk.HostMonitoringMode() && b.dk.Spec.OneAgent.HostMonitoring.SecCompProfile != "": + case b.dk.OneAgent().IsHostMonitoringMode() && b.dk.Spec.OneAgent.HostMonitoring.SecCompProfile != "": secCompName := b.dk.Spec.OneAgent.HostMonitoring.SecCompProfile securityContext.SeccompProfile = &corev1.SeccompProfile{ Type: corev1.SeccompProfileTypeLocalhost, LocalhostProfile: &secCompName, } - case b.dk.ClassicFullStackMode() && b.dk.Spec.OneAgent.ClassicFullStack.SecCompProfile != "": + case b.dk.OneAgent().IsClassicFullStackMode() && b.dk.Spec.OneAgent.ClassicFullStack.SecCompProfile != "": secCompName := b.dk.Spec.OneAgent.ClassicFullStack.SecCompProfile securityContext.SeccompProfile = &corev1.SeccompProfile{ Type: corev1.SeccompProfileTypeLocalhost, LocalhostProfile: &secCompName, } - case b.dk.CloudNativeFullstackMode() && b.dk.Spec.OneAgent.CloudNativeFullStack.SecCompProfile != "": + case b.dk.OneAgent().IsCloudNativeFullstackMode() && b.dk.Spec.OneAgent.CloudNativeFullStack.SecCompProfile != "": secCompName := b.dk.Spec.OneAgent.CloudNativeFullStack.SecCompProfile securityContext.SeccompProfile = &corev1.SeccompProfile{ Type: corev1.SeccompProfileTypeLocalhost, @@ -408,12 +417,12 @@ func (b *builder) getReadinessProbe() *corev1.Probe { // if the version is not set, ie.: unknown, we consider the OneAgent to support `ReadOnlyRootFilesystem`. func (b *builder) isRootFsReadonly() bool { if b.dk != nil && - b.dk.UseReadOnlyOneAgents() && - b.dk.OneAgentVersion() != "" && - b.dk.OneAgentVersion() != string(status.CustomImageVersionSource) { - agentSemver, err := dtversion.ToSemver(b.dk.OneAgentVersion()) + b.dk.OneAgent().IsReadOnlyFSSupported() && + b.dk.OneAgent().GetVersion() != "" && + b.dk.OneAgent().GetVersion() != string(status.CustomImageVersionSource) { + agentSemver, err := dtversion.ToSemver(b.dk.OneAgent().GetVersion()) if err != nil { - log.Debug("Unable to determine OneAgent version to enable readonly pod filesystem, skipping", "version", b.dk.OneAgentVersion(), "error", err.Error()) + log.Debug("Unable to determine OneAgent version to enable readonly pod filesystem, skipping", "version", b.dk.OneAgent().GetVersion(), "error", err.Error()) return true } diff --git a/pkg/controllers/dynakube/oneagent/daemonset/daemonset_test.go b/pkg/controllers/dynakube/oneagent/daemonset/daemonset_test.go index 483bac5b3b..fbdc1e1a90 100644 --- a/pkg/controllers/dynakube/oneagent/daemonset/daemonset_test.go +++ b/pkg/controllers/dynakube/oneagent/daemonset/daemonset_test.go @@ -6,9 +6,9 @@ import ( "time" "github.com/Dynatrace/dynatrace-operator/pkg/api/status" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/deploymentmetadata" - "github.com/Dynatrace/dynatrace-operator/pkg/util/address" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/labels" "github.com/Dynatrace/dynatrace-operator/pkg/version" "github.com/Dynatrace/dynatrace-operator/pkg/webhook" @@ -18,10 +18,12 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" ) const ( - testImageTag = "1.203.0.0-0" + testImageTag = "1.203.0.0-0" + testTokenHash = "test-token-hash" ) func TestUseImmutableImage(t *testing.T) { @@ -30,12 +32,12 @@ func TestUseImmutableImage(t *testing.T) { dk := dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ APIURL: testURL, - OneAgent: dynakube.OneAgentSpec{ - ClassicFullStack: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + ClassicFullStack: &oneagent.HostInjectSpec{}, }, }, Status: dynakube.DynaKubeStatus{ - OneAgent: dynakube.OneAgentStatus{ + OneAgent: oneagent.Status{ VersionStatus: status.VersionStatus{ ImageID: imageID, }, @@ -59,12 +61,12 @@ func TestLabels(t *testing.T) { dk := dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ APIURL: testURL, - OneAgent: dynakube.OneAgentSpec{ - ClassicFullStack: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + ClassicFullStack: &oneagent.HostInjectSpec{}, }, }, Status: dynakube.DynaKubeStatus{ - OneAgent: dynakube.OneAgentStatus{ + OneAgent: oneagent.Status{ VersionStatus: status.VersionStatus{ Version: testImageTag, }, @@ -97,8 +99,8 @@ func TestLabels(t *testing.T) { dk := dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ APIURL: testURL, - OneAgent: dynakube.OneAgentSpec{ - ClassicFullStack: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + ClassicFullStack: &oneagent.HostInjectSpec{}, }, }, } @@ -135,8 +137,8 @@ func TestCustomPullSecret(t *testing.T) { }, Spec: dynakube.DynaKubeSpec{ APIURL: testURL, - OneAgent: dynakube.OneAgentSpec{ - ClassicFullStack: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + ClassicFullStack: &oneagent.HostInjectSpec{}, }, CustomPullSecret: testName, }, @@ -157,8 +159,8 @@ func TestResources(t *testing.T) { dk := dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ APIURL: testURL, - OneAgent: dynakube.OneAgentSpec{ - ClassicFullStack: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + ClassicFullStack: &oneagent.HostInjectSpec{}, }, }, } @@ -182,8 +184,8 @@ func TestResources(t *testing.T) { dk := dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ APIURL: testURL, - OneAgent: dynakube.OneAgentSpec{ - ClassicFullStack: &dynakube.HostInjectSpec{ + OneAgent: oneagent.Spec{ + ClassicFullStack: &oneagent.HostInjectSpec{ OneAgentResources: corev1.ResourceRequirements{ Requests: corev1.ResourceList{ corev1.ResourceCPU: *cpuRequest, @@ -256,8 +258,8 @@ func TestHostMonitoring_SecurityContext(t *testing.T) { dk := dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ APIURL: testURL, - OneAgent: dynakube.OneAgentSpec{ - HostMonitoring: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + HostMonitoring: &oneagent.HostInjectSpec{}, }, }, } @@ -270,9 +272,9 @@ func TestHostMonitoring_SecurityContext(t *testing.T) { securityContext := ds.Spec.Template.Spec.Containers[0].SecurityContext assert.NotNil(t, securityContext) - assert.Equal(t, address.Of(int64(1000)), securityContext.RunAsUser) - assert.Equal(t, address.Of(int64(1000)), securityContext.RunAsGroup) - assert.Equal(t, address.Of(true), securityContext.RunAsNonRoot) + assert.Equal(t, ptr.To(int64(1000)), securityContext.RunAsUser) + assert.Equal(t, ptr.To(int64(1000)), securityContext.RunAsGroup) + assert.Equal(t, ptr.To(true), securityContext.RunAsNonRoot) assert.NotEmpty(t, securityContext.Capabilities) assert.Nil(t, securityContext.SeccompProfile) require.NotNil(t, securityContext.ReadOnlyRootFilesystem) @@ -283,12 +285,12 @@ func TestHostMonitoring_SecurityContext(t *testing.T) { dk := dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ APIURL: testURL, - OneAgent: dynakube.OneAgentSpec{ - HostMonitoring: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + HostMonitoring: &oneagent.HostInjectSpec{}, }, }, Status: dynakube.DynaKubeStatus{ - OneAgent: dynakube.OneAgentStatus{ + OneAgent: oneagent.Status{ VersionStatus: status.VersionStatus{ Version: "1.290.18.20240520-124108", }, @@ -312,12 +314,12 @@ func TestHostMonitoring_SecurityContext(t *testing.T) { dk := dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ APIURL: testURL, - OneAgent: dynakube.OneAgentSpec{ - HostMonitoring: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + HostMonitoring: &oneagent.HostInjectSpec{}, }, }, Status: dynakube.DynaKubeStatus{ - OneAgent: dynakube.OneAgentStatus{ + OneAgent: oneagent.Status{ VersionStatus: status.VersionStatus{ Version: "1.291.18.20240520-124108", }, @@ -346,8 +348,8 @@ func TestHostMonitoring_SecurityContext(t *testing.T) { }, Spec: dynakube.DynaKubeSpec{ APIURL: testURL, - OneAgent: dynakube.OneAgentSpec{ - HostMonitoring: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + HostMonitoring: &oneagent.HostInjectSpec{}, }, }, } @@ -360,10 +362,10 @@ func TestHostMonitoring_SecurityContext(t *testing.T) { securityContext := ds.Spec.Template.Spec.Containers[0].SecurityContext assert.NotNil(t, securityContext) - assert.Equal(t, address.Of(int64(1000)), securityContext.RunAsUser) - assert.Equal(t, address.Of(int64(1000)), securityContext.RunAsGroup) - assert.Equal(t, address.Of(true), securityContext.RunAsNonRoot) - assert.Equal(t, address.Of(true), securityContext.Privileged) + assert.Equal(t, ptr.To(int64(1000)), securityContext.RunAsUser) + assert.Equal(t, ptr.To(int64(1000)), securityContext.RunAsGroup) + assert.Equal(t, ptr.To(true), securityContext.RunAsNonRoot) + assert.Equal(t, ptr.To(true), securityContext.Privileged) assert.Empty(t, securityContext.Capabilities) assert.Nil(t, securityContext.SeccompProfile) }) @@ -377,8 +379,8 @@ func TestHostMonitoring_SecurityContext(t *testing.T) { }, Spec: dynakube.DynaKubeSpec{ APIURL: testURL, - OneAgent: dynakube.OneAgentSpec{ - ClassicFullStack: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + ClassicFullStack: &oneagent.HostInjectSpec{}, }, }, } @@ -394,7 +396,7 @@ func TestHostMonitoring_SecurityContext(t *testing.T) { assert.Nil(t, securityContext.RunAsUser) assert.Nil(t, securityContext.RunAsGroup) assert.Nil(t, securityContext.RunAsNonRoot) - assert.Equal(t, address.Of(true), securityContext.Privileged) + assert.Equal(t, ptr.To(true), securityContext.Privileged) assert.Empty(t, securityContext.Capabilities) assert.Nil(t, securityContext.SeccompProfile) }) @@ -405,8 +407,8 @@ func TestHostMonitoring_SecurityContext(t *testing.T) { ObjectMeta: metav1.ObjectMeta{}, Spec: dynakube.DynaKubeSpec{ APIURL: testURL, - OneAgent: dynakube.OneAgentSpec{ - ClassicFullStack: &dynakube.HostInjectSpec{ + OneAgent: oneagent.Spec{ + ClassicFullStack: &oneagent.HostInjectSpec{ SecCompProfile: customSecCompProfile, }, }, @@ -440,8 +442,8 @@ func TestHostMonitoring_SecurityContext(t *testing.T) { }, Spec: dynakube.DynaKubeSpec{ APIURL: testURL, - OneAgent: dynakube.OneAgentSpec{ - ClassicFullStack: &dynakube.HostInjectSpec{ + OneAgent: oneagent.Spec{ + ClassicFullStack: &oneagent.HostInjectSpec{ SecCompProfile: customSecCompProfile, }, }, @@ -459,7 +461,7 @@ func TestHostMonitoring_SecurityContext(t *testing.T) { assert.Nil(t, securityContext.RunAsUser) assert.Nil(t, securityContext.RunAsGroup) assert.Nil(t, securityContext.RunAsNonRoot) - assert.Equal(t, address.Of(true), securityContext.Privileged) + assert.Equal(t, ptr.To(true), securityContext.Privileged) assert.Empty(t, securityContext.Capabilities) assert.Nil(t, securityContext.SeccompProfile) }) @@ -518,7 +520,7 @@ func TestPodSpecProbes(t *testing.T) { builder := builder{ dk: &dynakube.DynaKube{ Status: dynakube.DynaKubeStatus{ - OneAgent: dynakube.OneAgentStatus{ + OneAgent: oneagent.Status{ Healthcheck: &expectedHealthcheck, }, }, @@ -551,7 +553,7 @@ func TestPodSpecProbes(t *testing.T) { builder := builder{ dk: &dynakube.DynaKube{ Status: dynakube.DynaKubeStatus{ - OneAgent: dynakube.OneAgentStatus{ + OneAgent: oneagent.Status{ Healthcheck: updatedHealthCheck, }, }, @@ -576,6 +578,26 @@ func TestPodSpecProbes(t *testing.T) { assert.Nil(t, podSpec.Containers[0].ReadinessProbe) assert.Nil(t, podSpec.Containers[0].LivenessProbe) }) + t.Run("no livenessProbe when skip featureFlag is set", func(t *testing.T) { + builder := builder{ + dk: &dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + dynakube.AnnotationFeatureOneAgentSkipLivenessProbe: "true", + }, + }, + Status: dynakube.DynaKubeStatus{ + OneAgent: oneagent.Status{ + Healthcheck: &expectedHealthcheck, + }, + }, + }, + } + podSpec, _ := builder.podSpec() + + assert.NotNil(t, podSpec.Containers[0].ReadinessProbe) + assert.Nil(t, podSpec.Containers[0].LivenessProbe) + }) } func TestOneAgentResources(t *testing.T) { @@ -587,7 +609,7 @@ func TestOneAgentResources(t *testing.T) { }) t.Run("get resources if hostInjection spec is set", func(t *testing.T) { builder := builder{ - hostInjectSpec: &dynakube.HostInjectSpec{ + hostInjectSpec: &oneagent.HostInjectSpec{ OneAgentResources: corev1.ResourceRequirements{ Requests: map[corev1.ResourceName]resource.Quantity{ corev1.ResourceCPU: *resource.NewScaledQuantity(2, 1), @@ -623,7 +645,7 @@ func TestNodeSelector(t *testing.T) { }) t.Run("returns nodeselector", func(t *testing.T) { dsBuilder := builder{ - hostInjectSpec: &dynakube.HostInjectSpec{ + hostInjectSpec: &oneagent.HostInjectSpec{ NodeSelector: map[string]string{testKey: testValue}, }, } @@ -638,11 +660,11 @@ func TestPriorityClass(t *testing.T) { dsBuilder := builder{} priorityClassName := dsBuilder.priorityClassName() - assert.Equal(t, "", priorityClassName) + assert.Empty(t, priorityClassName) }) t.Run("returns nodeselector", func(t *testing.T) { dsBuilder := builder{ - hostInjectSpec: &dynakube.HostInjectSpec{ + hostInjectSpec: &oneagent.HostInjectSpec{ PriorityClassName: testName, }, } @@ -661,7 +683,7 @@ func TestTolerations(t *testing.T) { }) t.Run("returns tolerations", func(t *testing.T) { dsBuilder := builder{ - hostInjectSpec: &dynakube.HostInjectSpec{ + hostInjectSpec: &oneagent.HostInjectSpec{ Tolerations: []corev1.Toleration{ { Key: testKey, @@ -732,7 +754,7 @@ func TestImmutableOneAgentImage(t *testing.T) { } image := dsBuilder.immutableOneAgentImage() - assert.Equal(t, dsBuilder.dk.OneAgentImage(), image) + assert.Equal(t, dsBuilder.dk.OneAgent().GetImage(), image) }) } @@ -740,16 +762,18 @@ func TestAnnotations(t *testing.T) { t.Run("cloud native has apparmor annotation by default", func(t *testing.T) { dk := dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{ - HostInjectSpec: dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{ + HostInjectSpec: oneagent.HostInjectSpec{}, }, }, }, } + dk.Status.OneAgent.ConnectionInfoStatus.TenantTokenHash = testTokenHash expectedAnnotations := map[string]string{ webhook.AnnotationDynatraceInject: "false", annotationUnprivileged: annotationUnprivilegedValue, + annotationTenantTokenHash: testTokenHash, } builder := NewCloudNativeFullStack(&dk, testClusterID) @@ -762,14 +786,16 @@ func TestAnnotations(t *testing.T) { t.Run("host monitoring has apparmor annotation by default", func(t *testing.T) { dk := dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - HostMonitoring: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + HostMonitoring: &oneagent.HostInjectSpec{}, }, }, } + dk.Status.OneAgent.ConnectionInfoStatus.TenantTokenHash = testTokenHash expectedAnnotations := map[string]string{ webhook.AnnotationDynatraceInject: "false", annotationUnprivileged: annotationUnprivilegedValue, + annotationTenantTokenHash: testTokenHash, } builder := NewHostMonitoring(&dk, testClusterID) @@ -782,14 +808,16 @@ func TestAnnotations(t *testing.T) { t.Run("classic fullstack has apparmor annotation by default", func(t *testing.T) { dk := dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - ClassicFullStack: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + ClassicFullStack: &oneagent.HostInjectSpec{}, }, }, } + dk.Status.OneAgent.ConnectionInfoStatus.TenantTokenHash = testTokenHash expectedAnnotations := map[string]string{ webhook.AnnotationDynatraceInject: "false", annotationUnprivileged: annotationUnprivilegedValue, + annotationTenantTokenHash: testTokenHash, } builder := NewClassicFullStack(&dk, testClusterID) @@ -802,9 +830,9 @@ func TestAnnotations(t *testing.T) { t.Run("annotations are added with cloud native", func(t *testing.T) { dk := dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{ - HostInjectSpec: dynakube.HostInjectSpec{ + OneAgent: oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{ + HostInjectSpec: oneagent.HostInjectSpec{ Annotations: map[string]string{ testKey: testName, }, @@ -813,10 +841,12 @@ func TestAnnotations(t *testing.T) { }, }, } + dk.Status.OneAgent.ConnectionInfoStatus.TenantTokenHash = testTokenHash expectedAnnotations := map[string]string{ webhook.AnnotationDynatraceInject: "false", annotationUnprivileged: annotationUnprivilegedValue, testKey: testName, + annotationTenantTokenHash: testTokenHash, } builder := NewCloudNativeFullStack(&dk, testClusterID) @@ -829,8 +859,8 @@ func TestAnnotations(t *testing.T) { t.Run("annotations are added with host monitoring", func(t *testing.T) { dk := dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - HostMonitoring: &dynakube.HostInjectSpec{ + OneAgent: oneagent.Spec{ + HostMonitoring: &oneagent.HostInjectSpec{ Annotations: map[string]string{ testKey: testName, }, @@ -838,10 +868,12 @@ func TestAnnotations(t *testing.T) { }, }, } + dk.Status.OneAgent.ConnectionInfoStatus.TenantTokenHash = testTokenHash expectedAnnotations := map[string]string{ webhook.AnnotationDynatraceInject: "false", annotationUnprivileged: annotationUnprivilegedValue, testKey: testName, + annotationTenantTokenHash: testTokenHash, } builder := NewHostMonitoring(&dk, testClusterID) @@ -854,8 +886,8 @@ func TestAnnotations(t *testing.T) { t.Run("annotations are added with classic fullstack", func(t *testing.T) { dk := dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - ClassicFullStack: &dynakube.HostInjectSpec{ + OneAgent: oneagent.Spec{ + ClassicFullStack: &oneagent.HostInjectSpec{ Annotations: map[string]string{ testKey: testName, }, @@ -863,10 +895,12 @@ func TestAnnotations(t *testing.T) { }, }, } + dk.Status.OneAgent.ConnectionInfoStatus.TenantTokenHash = testTokenHash expectedAnnotations := map[string]string{ webhook.AnnotationDynatraceInject: "false", annotationUnprivileged: annotationUnprivilegedValue, testKey: testName, + annotationTenantTokenHash: testTokenHash, } builder := NewClassicFullStack(&dk, testClusterID) @@ -882,9 +916,9 @@ func TestOneAgentHostGroup(t *testing.T) { t.Run("cloud native - host group settings", func(t *testing.T) { dk := dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{ - HostInjectSpec: dynakube.HostInjectSpec{ + OneAgent: oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{ + HostInjectSpec: oneagent.HostInjectSpec{ Args: []string{ "--set-host-group=oldgroup", }, @@ -900,7 +934,7 @@ func TestOneAgentHostGroup(t *testing.T) { require.NoError(t, err) require.NotNil(t, daemonset) - assert.Equal(t, "--set-host-group=newgroup", daemonset.Spec.Template.Spec.Containers[0].Args[1]) + assert.Equal(t, "--set-host-group=newgroup", daemonset.Spec.Template.Spec.Containers[0].Args[0]) }) } @@ -915,12 +949,12 @@ func TestDefaultArguments(t *testing.T) { Spec: dynakube.DynaKubeSpec{ APIURL: "https://ENVIRONMENTID.live.dynatrace.com/api", Tokens: name, - OneAgent: dynakube.OneAgentSpec{ - ClassicFullStack: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + ClassicFullStack: &oneagent.HostInjectSpec{}, }, }, } - base.Status.OneAgent.ConnectionInfoStatus.CommunicationHosts = []dynakube.CommunicationHostStatus{ + base.Status.OneAgent.ConnectionInfoStatus.CommunicationHosts = []oneagent.CommunicationHostStatus{ { Protocol: "http", Host: "dummyhost", @@ -944,12 +978,10 @@ func TestDefaultArguments(t *testing.T) { expectedDefaultArguments := []string{ "--set-app-log-content-access=true", "--set-host-group=APP_LUSTIG_PETER", - "--set-host-id-source=auto", "--set-host-id-source=fqdn", "--set-host-property=OperatorVersion=$(DT_OPERATOR_VERSION)", "--set-no-proxy=", "--set-proxy=", - "--set-server={$(DT_SERVER)}", "--set-server=https://hyper.super.com:9999", "--set-tenant=$(DT_TENANT)", } @@ -975,7 +1007,6 @@ func TestDefaultArguments(t *testing.T) { "--set-host-property=OperatorVersion=$(DT_OPERATOR_VERSION)", "--set-no-proxy=", "--set-proxy=", - "--set-server={$(DT_SERVER)}", "--set-server=https://hyper.super.com:9999", "--set-tenant=$(DT_TENANT)", } diff --git a/pkg/controllers/dynakube/oneagent/daemonset/env_vars.go b/pkg/controllers/dynakube/oneagent/daemonset/env_vars.go index e6c23321dd..0bc50a8736 100644 --- a/pkg/controllers/dynakube/oneagent/daemonset/env_vars.go +++ b/pkg/controllers/dynakube/oneagent/daemonset/env_vars.go @@ -2,14 +2,15 @@ package daemonset import ( "github.com/Dynatrace/dynatrace-operator/pkg/api/status" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/connectioninfo" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/deploymentmetadata" - "github.com/Dynatrace/dynatrace-operator/pkg/util/address" + logmonitoring "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/logmonitoring/daemonset" "github.com/Dynatrace/dynatrace-operator/pkg/util/prioritymap" "github.com/Dynatrace/dynatrace-operator/pkg/version" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + "k8s.io/utils/ptr" ) const ( @@ -42,8 +43,9 @@ func (b *builder) environmentVariables() ([]corev1.EnvVar, error) { b.addOperatorVersionInfoEnv(envMap) b.addConnectionInfoEnvs(envMap) b.addReadOnlyEnv(envMap) + b.addLogMonitoringEnv(envMap) - isProxyAsEnvDeprecated, err := isProxyAsEnvVarDeprecated(b.dk.OneAgentVersion()) + isProxyAsEnvDeprecated, err := isProxyAsEnvVarDeprecated(b.dk.OneAgent().GetVersion()) if err != nil { return []corev1.EnvVar{}, err } @@ -70,7 +72,7 @@ func (b *builder) addDeploymentMetadataEnv(envVarMap *prioritymap.Map) { Name: deploymentmetadata.GetDeploymentMetadataConfigMapName(b.dk.Name), }, Key: deploymentmetadata.OneAgentMetadataKey, - Optional: address.Of(false), + Optional: ptr.To(false), }}) } @@ -80,24 +82,24 @@ func (b *builder) addOperatorVersionInfoEnv(envVarMap *prioritymap.Map) { Name: deploymentmetadata.GetDeploymentMetadataConfigMapName(b.dk.Name), }, Key: deploymentmetadata.OperatorVersionKey, - Optional: address.Of(false), + Optional: ptr.To(false), }}) } func (b *builder) addConnectionInfoEnvs(envVarMap *prioritymap.Map) { addDefaultValueSource(envVarMap, connectioninfo.EnvDtTenant, &corev1.EnvVarSource{ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ LocalObjectReference: corev1.LocalObjectReference{ - Name: b.dk.OneAgentConnectionInfoConfigMapName(), + Name: b.dk.OneAgent().GetConnectionInfoConfigMapName(), }, Key: connectioninfo.TenantUUIDKey, - Optional: address.Of(false), + Optional: ptr.To(false), }}) addDefaultValueSource(envVarMap, connectioninfo.EnvDtServer, &corev1.EnvVarSource{ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ LocalObjectReference: corev1.LocalObjectReference{ - Name: b.dk.OneAgentConnectionInfoConfigMapName(), + Name: b.dk.OneAgent().GetConnectionInfoConfigMapName(), }, Key: connectioninfo.CommunicationEndpointsKey, - Optional: address.Of(false), + Optional: ptr.To(false), }}) } @@ -120,11 +122,19 @@ func (b *builder) addProxyEnv(envVarMap *prioritymap.Map) { } func (b *builder) addReadOnlyEnv(envVarMap *prioritymap.Map) { - if b.dk != nil && b.dk.UseReadOnlyOneAgents() { + if b.dk != nil && b.dk.OneAgent().IsReadOnlyFSSupported() { addDefaultValue(envVarMap, oneagentReadOnlyMode, "true") } } +func (b *builder) addLogMonitoringEnv(envVarMap *prioritymap.Map) { + if b.dk != nil && b.dk.LogMonitoring().IsEnabled() { + for _, env := range logmonitoring.GetKubeletEnvs() { + prioritymap.Append(envVarMap, env) + } + } +} + func (b *hostMonitoring) appendInfraMonEnvVars(daemonset *appsv1.DaemonSet) { envVars := prioritymap.New() prioritymap.Append(envVars, daemonset.Spec.Template.Spec.Containers[0].Env) diff --git a/pkg/controllers/dynakube/oneagent/daemonset/env_vars_test.go b/pkg/controllers/dynakube/oneagent/daemonset/env_vars_test.go index 001de1c438..7aa1aa08a4 100644 --- a/pkg/controllers/dynakube/oneagent/daemonset/env_vars_test.go +++ b/pkg/controllers/dynakube/oneagent/daemonset/env_vars_test.go @@ -5,9 +5,12 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/value" "github.com/Dynatrace/dynatrace-operator/pkg/api/status" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/logmonitoring" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/connectioninfo" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/deploymentmetadata" + logmonitoringds "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/logmonitoring/daemonset" k8senv "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/env" "github.com/Dynatrace/dynatrace-operator/pkg/util/prioritymap" "github.com/stretchr/testify/assert" @@ -36,9 +39,10 @@ func TestEnvironmentVariables(t *testing.T) { Proxy: &value.Source{ Value: "test", }, - OneAgent: dynakube.OneAgentSpec{ - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{}, + OneAgent: oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{}, }, + LogMonitoring: &logmonitoring.Spec{}, }, } dsBuilder := builder{ @@ -53,6 +57,7 @@ func TestEnvironmentVariables(t *testing.T) { assertDeploymentMetadataEnv(t, envVars, dk.Name) assertReadOnlyEnv(t, envVars) + assertLogMonitoringEnv(t, envVars) }) t.Run("when injected envvars are provided then they will not be overridden", func(t *testing.T) { potentiallyOverriddenEnvVars := []corev1.EnvVar{ @@ -66,7 +71,7 @@ func TestEnvironmentVariables(t *testing.T) { } builder := builder{ dk: &dynakube.DynaKube{}, - hostInjectSpec: &dynakube.HostInjectSpec{Env: potentiallyOverriddenEnvVars}, + hostInjectSpec: &oneagent.HostInjectSpec{Env: potentiallyOverriddenEnvVars}, } envVars, _ := builder.environmentVariables() @@ -172,7 +177,7 @@ func assertConnectionInfoEnv(t *testing.T, envs []corev1.EnvVar, dk *dynakube.Dy env := k8senv.FindEnvVar(envs, connectioninfo.EnvDtTenant) assert.Equal(t, connectioninfo.EnvDtTenant, env.Name) assert.Equal(t, - dk.OneAgentConnectionInfoConfigMapName(), + dk.OneAgent().GetConnectionInfoConfigMapName(), env.ValueFrom.ConfigMapKeyRef.Name, ) assert.Equal(t, @@ -183,7 +188,7 @@ func assertConnectionInfoEnv(t *testing.T, envs []corev1.EnvVar, dk *dynakube.Dy env = k8senv.FindEnvVar(envs, connectioninfo.EnvDtServer) assert.Equal(t, connectioninfo.EnvDtServer, env.Name) assert.Equal(t, - dk.OneAgentConnectionInfoConfigMapName(), + dk.OneAgent().GetConnectionInfoConfigMapName(), env.ValueFrom.ConfigMapKeyRef.Name, ) assert.Equal(t, @@ -254,8 +259,8 @@ func TestAddReadOnlyEnv(t *testing.T) { Name: "test", }, Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{}, + OneAgent: oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{}, }, }, } @@ -290,6 +295,15 @@ func assertReadOnlyEnv(t *testing.T, envs []corev1.EnvVar) { assert.Equal(t, "true", env.Value) } +func assertLogMonitoringEnv(t *testing.T, envs []corev1.EnvVar) { + env := k8senv.FindEnvVar(envs, logmonitoringds.KubeletIPAddressEnv) + require.NotNil(t, env) + assert.NotEmpty(t, env.ValueFrom) + env = k8senv.FindEnvVar(envs, logmonitoringds.KubeletNodeNameEnv) + require.NotNil(t, env) + assert.NotEmpty(t, env.ValueFrom) +} + func TestIsProxyAsEnvVarDeprecated(t *testing.T) { tests := []struct { name string diff --git a/pkg/controllers/dynakube/oneagent/daemonset/volumes.go b/pkg/controllers/dynakube/oneagent/daemonset/volumes.go index 029a292fc1..043c27404b 100644 --- a/pkg/controllers/dynakube/oneagent/daemonset/volumes.go +++ b/pkg/controllers/dynakube/oneagent/daemonset/volumes.go @@ -1,13 +1,14 @@ package daemonset import ( - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" dtcsi "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi" csivolumes "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi/driver/volumes" hostvolumes "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi/driver/volumes/host" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/connectioninfo" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/proxy" corev1 "k8s.io/api/core/v1" + "k8s.io/utils/ptr" ) func prepareVolumeMounts(dk *dynakube.DynaKube) []corev1.VolumeMount { @@ -15,22 +16,32 @@ func prepareVolumeMounts(dk *dynakube.DynaKube) []corev1.VolumeMount { volumeMounts = append(volumeMounts, getOneAgentSecretVolumeMount()) - if dk != nil && dk.UseReadOnlyOneAgents() { + if dk == nil { + volumeMounts = append(volumeMounts, getRootMount()) + + return volumeMounts + } + + if dk.OneAgent().IsReadOnlyFSSupported() { volumeMounts = append(volumeMounts, getReadOnlyRootMount()) - volumeMounts = append(volumeMounts, getCSIStorageMount()) + if dk.OneAgent().IsCSIAvailable() { + volumeMounts = append(volumeMounts, getCSIStorageMount()) + } else { + volumeMounts = append(volumeMounts, getStorageVolumeMount(dk)) + } } else { volumeMounts = append(volumeMounts, getRootMount()) } - if dk != nil && dk.Spec.TrustedCAs != "" { + if dk.Spec.TrustedCAs != "" { volumeMounts = append(volumeMounts, getClusterCaCertVolumeMount()) } - if dk != nil && dk.ActiveGate().HasCaCert() { + if dk.ActiveGate().HasCaCert() { volumeMounts = append(volumeMounts, getActiveGateCaCertVolumeMount()) } - if dk != nil && dk.NeedsOneAgentProxy() { + if dk.NeedsOneAgentProxy() { volumeMounts = append(volumeMounts, getHttpProxyMount()) } @@ -81,6 +92,17 @@ func getCSIStorageMount() corev1.VolumeMount { } } +func getStorageVolumeMount(dk *dynakube.DynaKube) corev1.VolumeMount { + // the TenantUUID is already set + tenant, _ := dk.TenantUUID() + + return corev1.VolumeMount{ + Name: storageVolumeName, + SubPath: tenant, + MountPath: csiStorageVolumeMount, + } +} + func getHttpProxyMount() corev1.VolumeMount { return proxy.BuildVolumeMount() } @@ -94,8 +116,12 @@ func prepareVolumes(dk *dynakube.DynaKube) []corev1.Volume { volumes = append(volumes, getOneAgentSecretVolume(dk)) - if dk.UseReadOnlyOneAgents() { - volumes = append(volumes, getCSIStorageVolume(dk)) + if dk.OneAgent().IsReadOnlyFSSupported() { + if dk.OneAgent().IsCSIAvailable() { + volumes = append(volumes, getCSIStorageVolume(dk)) + } else { + volumes = append(volumes, getStorageVolume(dk)) + } } if dk.Spec.TrustedCAs != "" { @@ -141,19 +167,30 @@ func getCSIStorageVolume(dk *dynakube.DynaKube) corev1.Volume { VolumeAttributes: map[string]string{ csivolumes.CSIVolumeAttributeModeField: hostvolumes.Mode, csivolumes.CSIVolumeAttributeDynakubeField: dk.Name, - csivolumes.CSIVolumeAttributeRetryTimeout: dk.FeatureMaxCSIRetryTimeout().String(), }, }, }, } } +func getStorageVolume(dk *dynakube.DynaKube) corev1.Volume { + return corev1.Volume{ + Name: storageVolumeName, + VolumeSource: corev1.VolumeSource{ + HostPath: &corev1.HostPathVolumeSource{ + Path: dk.OneAgent().GetHostPath(), + Type: ptr.To(corev1.HostPathDirectoryOrCreate), + }, + }, + } +} + func getActiveGateCaCertVolume(dk *dynakube.DynaKube) corev1.Volume { return corev1.Volume{ Name: activeGateCaCertVolumeName, VolumeSource: corev1.VolumeSource{ Secret: &corev1.SecretVolumeSource{ - SecretName: dk.Spec.ActiveGate.TlsSecretName, + SecretName: dk.Spec.ActiveGate.GetTLSSecretName(), Items: []corev1.KeyToPath{ { Key: "server.crt", @@ -181,7 +218,7 @@ func getOneAgentSecretVolume(dk *dynakube.DynaKube) corev1.Volume { Name: connectioninfo.TenantSecretVolumeName, VolumeSource: corev1.VolumeSource{ Secret: &corev1.SecretVolumeSource{ - SecretName: dk.OneagentTenantSecret(), + SecretName: dk.OneAgent().GetTenantSecret(), }, }, } diff --git a/pkg/controllers/dynakube/oneagent/daemonset/volumes_test.go b/pkg/controllers/dynakube/oneagent/daemonset/volumes_test.go index 9204bf86ec..9b98cd5f6c 100644 --- a/pkg/controllers/dynakube/oneagent/daemonset/volumes_test.go +++ b/pkg/controllers/dynakube/oneagent/daemonset/volumes_test.go @@ -4,9 +4,11 @@ import ( "testing" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/value" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/proxy" + "github.com/Dynatrace/dynatrace-operator/pkg/util/installconfig" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -21,8 +23,8 @@ func TestPrepareVolumes(t *testing.T) { t.Run(`has root volume`, func(t *testing.T) { dk := &dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - HostMonitoring: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + HostMonitoring: &oneagent.HostInjectSpec{}, }, }, } @@ -45,8 +47,8 @@ func TestPrepareVolumes(t *testing.T) { dk := &dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ TrustedCAs: testName, - OneAgent: dynakube.OneAgentSpec{ - HostMonitoring: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + HostMonitoring: &oneagent.HostInjectSpec{}, }, }, } @@ -77,8 +79,28 @@ func TestPrepareVolumes(t *testing.T) { }, TlsSecretName: "testing", }, - OneAgent: dynakube.OneAgentSpec{ - HostMonitoring: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + HostMonitoring: &oneagent.HostInjectSpec{}, + }, + }, + } + volumes := prepareVolumes(dk) + assert.Contains(t, volumes, getActiveGateCaCertVolume(dk)) + }) + t.Run(`has automatically created AG tls volume`, func(t *testing.T) { + dk := &dynakube.DynaKube{ + ObjectMeta: corev1.ObjectMeta{ + Name: "dynakube", + }, + Spec: dynakube.DynaKubeSpec{ + ActiveGate: activegate.Spec{ + Capabilities: []activegate.CapabilityDisplayName{ + activegate.KubeMonCapability.DisplayName, + }, + }, + TrustedCAs: testName, + OneAgent: oneagent.Spec{ + HostMonitoring: &oneagent.HostInjectSpec{}, }, }, } @@ -88,8 +110,8 @@ func TestPrepareVolumes(t *testing.T) { t.Run(`csi volume not supported on classicFullStack`, func(t *testing.T) { dk := &dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - ClassicFullStack: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + ClassicFullStack: &oneagent.HostInjectSpec{}, }, }, } @@ -100,8 +122,8 @@ func TestPrepareVolumes(t *testing.T) { dk := &dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ TrustedCAs: testName, - OneAgent: dynakube.OneAgentSpec{ - HostMonitoring: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + HostMonitoring: &oneagent.HostInjectSpec{}, }, ActiveGate: activegate.Spec{ Capabilities: []activegate.CapabilityDisplayName{ @@ -139,21 +161,22 @@ func TestPrepareVolumeMounts(t *testing.T) { t.Run(`has root volume mount`, func(t *testing.T) { dk := &dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - HostMonitoring: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + HostMonitoring: &oneagent.HostInjectSpec{}, }, }, } volumeMounts := prepareVolumeMounts(dk) assert.Contains(t, volumeMounts, getReadOnlyRootMount()) + assert.NotContains(t, volumeMounts, getClusterCaCertVolumeMount()) assert.NotContains(t, volumeMounts, getActiveGateCaCertVolumeMount()) }) t.Run(`has cluster certificate volume mount`, func(t *testing.T) { dk := &dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - HostMonitoring: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + HostMonitoring: &oneagent.HostInjectSpec{}, }, TrustedCAs: testName, }, @@ -167,10 +190,9 @@ func TestPrepareVolumeMounts(t *testing.T) { t.Run(`has ActiveGate CA volume mount`, func(t *testing.T) { dk := &dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - HostMonitoring: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + HostMonitoring: &oneagent.HostInjectSpec{}, }, - TrustedCAs: testName, ActiveGate: activegate.Spec{ Capabilities: []activegate.CapabilityDisplayName{ activegate.KubeMonCapability.DisplayName, @@ -183,13 +205,38 @@ func TestPrepareVolumeMounts(t *testing.T) { volumeMounts := prepareVolumeMounts(dk) assert.Contains(t, volumeMounts, getReadOnlyRootMount()) + assert.NotContains(t, volumeMounts, getClusterCaCertVolumeMount()) + assert.Contains(t, volumeMounts, getActiveGateCaCertVolumeMount()) + }) + t.Run(`has automatically created ActiveGate CA volume mount`, func(t *testing.T) { + dk := &dynakube.DynaKube{ + ObjectMeta: corev1.ObjectMeta{ + Name: "dynakube", + }, + Spec: dynakube.DynaKubeSpec{ + OneAgent: oneagent.Spec{ + HostMonitoring: &oneagent.HostInjectSpec{}, + }, + TrustedCAs: testName, + ActiveGate: activegate.Spec{ + Capabilities: []activegate.CapabilityDisplayName{ + activegate.KubeMonCapability.DisplayName, + }, + }, + }, + } + + volumeMounts := prepareVolumeMounts(dk) + + assert.Contains(t, volumeMounts, getReadOnlyRootMount()) + assert.Contains(t, volumeMounts, getClusterCaCertVolumeMount()) assert.Contains(t, volumeMounts, getActiveGateCaCertVolumeMount()) }) t.Run(`readonly volume not supported on classicFullStack`, func(t *testing.T) { dk := &dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - ClassicFullStack: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + ClassicFullStack: &oneagent.HostInjectSpec{}, }, }, } @@ -202,8 +249,8 @@ func TestPrepareVolumeMounts(t *testing.T) { dk := &dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ TrustedCAs: testName, - OneAgent: dynakube.OneAgentSpec{ - HostMonitoring: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + HostMonitoring: &oneagent.HostInjectSpec{}, }, ActiveGate: activegate.Spec{ Capabilities: []activegate.CapabilityDisplayName{ @@ -240,8 +287,8 @@ func TestPrepareVolumeMounts(t *testing.T) { }, Spec: dynakube.DynaKubeSpec{ Proxy: &value.Source{ValueFrom: proxy.BuildSecretName("Dynakube")}, - OneAgent: dynakube.OneAgentSpec{ - HostMonitoring: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + HostMonitoring: &oneagent.HostInjectSpec{}, }, }, } @@ -253,3 +300,141 @@ func TestPrepareVolumeMounts(t *testing.T) { assert.NotContains(t, mounts, getHttpProxyMount()) }) } + +func TestVolumesAndVolumeMountsVsCSIDriver(t *testing.T) { + dkHostMonitoring := &dynakube.DynaKube{ + Spec: dynakube.DynaKubeSpec{ + OneAgent: oneagent.Spec{ + HostMonitoring: &oneagent.HostInjectSpec{}, + }, + }, + } + dkCloudNativeFullStack := &dynakube.DynaKube{ + Spec: dynakube.DynaKubeSpec{ + OneAgent: oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{}, + }, + }, + } + dkClassicFullStack := &dynakube.DynaKube{ + Spec: dynakube.DynaKubeSpec{ + OneAgent: oneagent.Spec{ + ClassicFullStack: &oneagent.HostInjectSpec{}, + }, + }, + } + + type oneAgentVolumeTest struct { + testName string + dk *dynakube.DynaKube + csi bool + csiVolume bool + storageVolume bool + rootReadOnlyVolumeMount bool + } + + testCases := []oneAgentVolumeTest{ + { + testName: "hostMonitoring w/ CSI driver", + dk: dkHostMonitoring, + csi: true, + csiVolume: true, + storageVolume: false, + rootReadOnlyVolumeMount: true, + }, + { + testName: "hostMonitoring w/o CSI driver", + dk: dkHostMonitoring, + csi: false, + csiVolume: false, + storageVolume: true, + rootReadOnlyVolumeMount: true, + }, + + { + testName: "cloudNativeFullStack w/ CSI driver", + dk: dkCloudNativeFullStack, + csi: true, + csiVolume: true, + storageVolume: false, + rootReadOnlyVolumeMount: true, + }, + { + testName: "cloudNativeFullStack w/o CSI driver", + dk: dkCloudNativeFullStack, + csi: false, + csiVolume: false, + storageVolume: true, + rootReadOnlyVolumeMount: true, + }, + + { + testName: "classicFullStack w/o CSI driver", + dk: dkClassicFullStack, + csi: false, + csiVolume: false, + storageVolume: false, + rootReadOnlyVolumeMount: false, + }, + } + + for _, tc := range testCases { + t.Run("Volumes:"+tc.testName, func(t *testing.T) { + testVolumesVsCSIDriver(t, tc.dk, tc.csi, tc.csiVolume, tc.storageVolume) + }) + + t.Run("VolumeMounts:"+tc.testName, func(t *testing.T) { + testVolumeMountsVsCSIDriver(t, tc.dk, tc.csi, tc.csiVolume, tc.storageVolume, tc.rootReadOnlyVolumeMount) + }) + } +} + +func testVolumesVsCSIDriver(t *testing.T, dk *dynakube.DynaKube, csi bool, csiVolume bool, storageVolume bool) { + if csi { + installconfig.SetModulesOverride(t, installconfig.Modules{CSIDriver: true}) + } else { + installconfig.SetModulesOverride(t, installconfig.Modules{CSIDriver: false}) + } + + volumes := prepareVolumes(dk) + + if csiVolume { + assert.Contains(t, volumes, getCSIStorageVolume(dk)) + } else { + assert.NotContains(t, volumes, getCSIStorageVolume(dk)) + } + + if storageVolume { + assert.Contains(t, volumes, getStorageVolume(dk)) + } else { + assert.NotContains(t, volumes, getStorageVolume(dk)) + } +} + +func testVolumeMountsVsCSIDriver(t *testing.T, dk *dynakube.DynaKube, csi bool, csiVolume bool, storageVolume bool, rootReadOnlyVolumeMount bool) { //nolint:revive + if csi { + installconfig.SetModulesOverride(t, installconfig.Modules{CSIDriver: true}) + } else { + installconfig.SetModulesOverride(t, installconfig.Modules{CSIDriver: false}) + } + + volumeMounts := prepareVolumeMounts(dk) + + if csiVolume { + assert.Contains(t, volumeMounts, getCSIStorageMount()) + } else { + assert.NotContains(t, volumeMounts, getCSIStorageMount()) + } + + if storageVolume { + assert.Contains(t, volumeMounts, getStorageVolumeMount(dk)) + } else { + assert.NotContains(t, volumeMounts, getStorageVolumeMount(dk)) + } + + if rootReadOnlyVolumeMount { + assert.Contains(t, volumeMounts, getReadOnlyRootMount()) + } else { + assert.Contains(t, volumeMounts, getRootMount()) + } +} diff --git a/pkg/controllers/dynakube/oneagent/oneagent_reconciler.go b/pkg/controllers/dynakube/oneagent/oneagent_reconciler.go index 45a5665849..bbf429d408 100644 --- a/pkg/controllers/dynakube/oneagent/oneagent_reconciler.go +++ b/pkg/controllers/dynakube/oneagent/oneagent_reconciler.go @@ -9,7 +9,8 @@ import ( "time" "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/controllers" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/connectioninfo" @@ -110,7 +111,7 @@ func (r *Reconciler) Reconcile(ctx context.Context) error { return err } - if !r.dk.NeedsOneAgent() { + if !r.dk.OneAgent().IsDaemonsetRequired() { return r.cleanUp(ctx) } @@ -194,7 +195,7 @@ func (r *Reconciler) createOneAgentTenantConnectionInfoConfigMap(ctx context.Con configMapData := extractPublicData(r.dk) configMap, err := configmap.Build(r.dk, - r.dk.OneAgentConnectionInfoConfigMapName(), + r.dk.OneAgent().GetConnectionInfoConfigMapName(), configMapData, ) if err != nil { @@ -216,7 +217,7 @@ func (r *Reconciler) createOneAgentTenantConnectionInfoConfigMap(ctx context.Con func (r *Reconciler) deleteOneAgentTenantConnectionInfoConfigMap(ctx context.Context) error { cm, _ := configmap.Build(r.dk, - r.dk.OneAgentConnectionInfoConfigMapName(), + r.dk.OneAgent().GetConnectionInfoConfigMapName(), nil, ) query := configmap.Query(r.client, r.apiReader, log) @@ -288,7 +289,7 @@ func (r *Reconciler) reconcileRollout(ctx context.Context) error { } func (r *Reconciler) getOneagentPods(ctx context.Context, dk *dynakube.DynaKube, feature string) ([]corev1.Pod, []client.ListOption, error) { - agentVersion := dk.OneAgentVersion() + agentVersion := dk.OneAgent().GetVersion() appLabels := labels.NewAppLabels(labels.OneAgentComponentLabel, dk.Name, feature, agentVersion) podList := &corev1.PodList{} @@ -307,11 +308,11 @@ func (r *Reconciler) buildDesiredDaemonSet(dk *dynakube.DynaKube) (*appsv1.Daemo var err error switch { - case dk.ClassicFullStackMode(): + case dk.OneAgent().IsClassicFullStackMode(): ds, err = daemonset.NewClassicFullStack(dk, r.clusterID).BuildDaemonSet() - case dk.HostMonitoringMode(): + case dk.OneAgent().IsHostMonitoringMode(): ds, err = daemonset.NewHostMonitoring(dk, r.clusterID).BuildDaemonSet() - case dk.CloudNativeFullstackMode(): + case dk.OneAgent().IsCloudNativeFullstackMode(): ds, err = daemonset.NewCloudNativeFullStack(dk, r.clusterID).BuildDaemonSet() } @@ -352,16 +353,16 @@ func (r *Reconciler) reconcileInstanceStatuses(ctx context.Context, dk *dynakube } func (r *Reconciler) removeOneAgentDaemonSet(ctx context.Context, dk *dynakube.DynaKube) error { - oneAgentDaemonSet := appsv1.DaemonSet{ObjectMeta: metav1.ObjectMeta{Name: dk.OneAgentDaemonsetName(), Namespace: dk.Namespace}} + oneAgentDaemonSet := appsv1.DaemonSet{ObjectMeta: metav1.ObjectMeta{Name: dk.OneAgent().GetDaemonsetName(), Namespace: dk.Namespace}} return client.IgnoreNotFound(r.client.Delete(ctx, &oneAgentDaemonSet)) } -func getInstanceStatuses(pods []corev1.Pod) map[string]dynakube.OneAgentInstance { - instanceStatuses := make(map[string]dynakube.OneAgentInstance) +func getInstanceStatuses(pods []corev1.Pod) map[string]oneagent.Instance { + instanceStatuses := make(map[string]oneagent.Instance) for _, pod := range pods { - instanceStatuses[pod.Spec.NodeName] = dynakube.OneAgentInstance{ + instanceStatuses[pod.Spec.NodeName] = oneagent.Instance{ PodName: pod.Name, IPAddress: pod.Status.HostIP, } diff --git a/pkg/controllers/dynakube/oneagent/oneagent_reconciler_test.go b/pkg/controllers/dynakube/oneagent/oneagent_reconciler_test.go index f388a77dea..85488261a3 100644 --- a/pkg/controllers/dynakube/oneagent/oneagent_reconciler_test.go +++ b/pkg/controllers/dynakube/oneagent/oneagent_reconciler_test.go @@ -6,7 +6,8 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/communication" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/controllers" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/connectioninfo" @@ -48,8 +49,8 @@ func TestReconcile(t *testing.T) { dk := &dynakube.DynaKube{ ObjectMeta: metav1.ObjectMeta{Name: dkName, Namespace: namespace}, Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{}, + OneAgent: oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{}, }, }, } @@ -69,10 +70,10 @@ func TestReconcile(t *testing.T) { require.NoError(t, err) dsActual := &appsv1.DaemonSet{} - err = fakeClient.Get(ctx, types.NamespacedName{Name: dk.OneAgentDaemonsetName(), Namespace: namespace}, dsActual) + err = fakeClient.Get(ctx, types.NamespacedName{Name: dk.OneAgent().GetDaemonsetName(), Namespace: namespace}, dsActual) require.NoError(t, err, "failed to get DaemonSet") assert.Equal(t, namespace, dsActual.Namespace, "wrong namespace") - assert.Equal(t, dk.OneAgentDaemonsetName(), dsActual.GetObjectMeta().GetName(), "wrong name") + assert.Equal(t, dk.OneAgent().GetDaemonsetName(), dsActual.GetObjectMeta().GetName(), "wrong name") assert.NotNil(t, dsActual.Spec.Template.Spec.Affinity) }) @@ -80,7 +81,7 @@ func TestReconcile(t *testing.T) { t.Run("remove DaemonSet in case OneAgent is not needed + remove condition", func(t *testing.T) { dk := &dynakube.DynaKube{ObjectMeta: metav1.ObjectMeta{Name: dkName, Namespace: namespace}} setDaemonSetCreatedCondition(dk.Conditions()) - fakeClient := fake.NewClient(dk, &appsv1.DaemonSet{ObjectMeta: metav1.ObjectMeta{Name: dk.OneAgentDaemonsetName(), Namespace: dk.Namespace}}) + fakeClient := fake.NewClient(dk, &appsv1.DaemonSet{ObjectMeta: metav1.ObjectMeta{Name: dk.OneAgent().GetDaemonsetName(), Namespace: dk.Namespace}}) reconciler := &Reconciler{ client: fakeClient, @@ -94,7 +95,7 @@ func TestReconcile(t *testing.T) { require.NoError(t, err) dsActual := &appsv1.DaemonSet{} - err = fakeClient.Get(ctx, types.NamespacedName{Name: dk.OneAgentDaemonsetName(), Namespace: namespace}, dsActual) + err = fakeClient.Get(ctx, types.NamespacedName{Name: dk.OneAgent().GetDaemonsetName(), Namespace: namespace}, dsActual) require.Error(t, err) assert.True(t, k8serrors.IsNotFound(err)) @@ -123,8 +124,8 @@ func TestReconcile(t *testing.T) { Spec: dynakube.DynaKubeSpec{ APIURL: "https://ENVIRONMENTID.live.dynatrace.com/api", NetworkZone: "test", - OneAgent: dynakube.OneAgentSpec{ - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{}, + OneAgent: oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{}, }, }, } @@ -151,8 +152,8 @@ func TestReconcile(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Name: dkName, Namespace: namespace}, Spec: dynakube.DynaKubeSpec{ APIURL: "https://ENVIRONMENTID.live.dynatrace.com/api", - OneAgent: dynakube.OneAgentSpec{ - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{}, + OneAgent: oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{}, }, }, } @@ -184,8 +185,8 @@ func TestReconcileOneAgent_ReconcileOnEmptyEnvironmentAndDNSPolicy(t *testing.T) dkSpec := dynakube.DynaKubeSpec{ APIURL: "https://ENVIRONMENTID.live.dynatrace.com/api", Tokens: dkName, - OneAgent: dynakube.OneAgentSpec{ - ClassicFullStack: &dynakube.HostInjectSpec{ + OneAgent: oneagent.Spec{ + ClassicFullStack: &oneagent.HostInjectSpec{ DNSPolicy: corev1.DNSClusterFirstWithHostNet, Labels: map[string]string{ "label_key": "label_value", @@ -201,7 +202,7 @@ func TestReconcileOneAgent_ReconcileOnEmptyEnvironmentAndDNSPolicy(t *testing.T) } dk.Status.OneAgent.ConnectionInfoStatus.ConnectionInfo.TenantUUID = "test-tenant" - dk.Status.OneAgent.ConnectionInfoStatus.CommunicationHosts = []dynakube.CommunicationHostStatus{ + dk.Status.OneAgent.ConnectionInfoStatus.CommunicationHosts = []oneagent.CommunicationHostStatus{ { Protocol: "http", Host: "dummyhost", @@ -225,10 +226,10 @@ func TestReconcileOneAgent_ReconcileOnEmptyEnvironmentAndDNSPolicy(t *testing.T) require.NoError(t, err) dsActual := &appsv1.DaemonSet{} - err = fakeClient.Get(ctx, types.NamespacedName{Name: dk.OneAgentDaemonsetName(), Namespace: namespace}, dsActual) + err = fakeClient.Get(ctx, types.NamespacedName{Name: dk.OneAgent().GetDaemonsetName(), Namespace: namespace}, dsActual) require.NoError(t, err, "failed to get DaemonSet") assert.Equal(t, namespace, dsActual.Namespace, "wrong namespace") - assert.Equal(t, dk.OneAgentDaemonsetName(), dsActual.GetObjectMeta().GetName(), "wrong name") + assert.Equal(t, dk.OneAgent().GetDaemonsetName(), dsActual.GetObjectMeta().GetName(), "wrong name") assert.Equal(t, corev1.DNSClusterFirstWithHostNet, dsActual.Spec.Template.Spec.DNSPolicy, "wrong policy") mock.AssertExpectationsForObjects(t, dtClient) @@ -251,13 +252,13 @@ func TestReconcile_InstancesSet(t *testing.T) { Spec: dynakube.DynaKubeSpec{ APIURL: "https://ENVIRONMENTID.live.dynatrace.com/api", Tokens: name, - OneAgent: dynakube.OneAgentSpec{ - ClassicFullStack: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + ClassicFullStack: &oneagent.HostInjectSpec{}, }, }, } base.Status.OneAgent.ConnectionInfoStatus.TenantUUID = "test-tenant" - base.Status.OneAgent.ConnectionInfoStatus.CommunicationHosts = []dynakube.CommunicationHostStatus{ + base.Status.OneAgent.ConnectionInfoStatus.CommunicationHosts = []oneagent.CommunicationHostStatus{ { Protocol: "http", Host: "dummyhost", @@ -357,8 +358,8 @@ func TestMigrationForDaemonSetWithoutAnnotation(t *testing.T) { dk := &dynakube.DynaKube{ ObjectMeta: dkKey, Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - HostMonitoring: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + HostMonitoring: &oneagent.HostInjectSpec{}, }, }, } @@ -507,16 +508,16 @@ func TestHasSpecChanged(t *testing.T) { oldInstance := dynakube.DynaKube{ ObjectMeta: key, Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - HostMonitoring: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + HostMonitoring: &oneagent.HostInjectSpec{}, }, }, } newInstance := dynakube.DynaKube{ ObjectMeta: key, Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - HostMonitoring: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + HostMonitoring: &oneagent.HostInjectSpec{}, }, }, } @@ -608,8 +609,8 @@ func newDynaKube() *dynakube.DynaKube { UID: "69e98f18-805a-42de-84b5-3eae66534f75", }, Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - HostMonitoring: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + HostMonitoring: &oneagent.HostInjectSpec{}, }, }, } @@ -624,8 +625,8 @@ func TestInstanceStatus(t *testing.T) { Spec: dynakube.DynaKubeSpec{ APIURL: "https://ENVIRONMENTID.live.dynatrace.com/api", Tokens: dkName, - OneAgent: dynakube.OneAgentSpec{ - HostMonitoring: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + HostMonitoring: &oneagent.HostInjectSpec{}, }, }, } @@ -678,8 +679,8 @@ func TestEmptyInstancesWithWrongLabels(t *testing.T) { Spec: dynakube.DynaKubeSpec{ APIURL: "https://ENVIRONMENTID.live.dynatrace.com/api", Tokens: dkName, - OneAgent: dynakube.OneAgentSpec{ - HostMonitoring: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + HostMonitoring: &oneagent.HostInjectSpec{}, }, }, } @@ -723,13 +724,13 @@ func TestReconcile_OneAgentConfigMap(t *testing.T) { ctx := context.Background() dk := newDynaKube() dk.Status = dynakube.DynaKubeStatus{ - OneAgent: dynakube.OneAgentStatus{ - ConnectionInfoStatus: dynakube.OneAgentConnectionInfoStatus{ + OneAgent: oneagent.Status{ + ConnectionInfoStatus: oneagent.ConnectionInfoStatus{ ConnectionInfo: communication.ConnectionInfo{ TenantUUID: testTenantUUID, Endpoints: testTenantEndpoints, }, - CommunicationHosts: []dynakube.CommunicationHostStatus{ + CommunicationHosts: []oneagent.CommunicationHostStatus{ { Protocol: "http", Host: "dummyhost", @@ -757,7 +758,7 @@ func TestReconcile_OneAgentConfigMap(t *testing.T) { require.NoError(t, err) var actual corev1.ConfigMap - err = fakeClient.Get(ctx, client.ObjectKey{Name: dk.OneAgentConnectionInfoConfigMapName(), Namespace: dk.Namespace}, &actual) + err = fakeClient.Get(ctx, client.ObjectKey{Name: dk.OneAgent().GetConnectionInfoConfigMapName(), Namespace: dk.Namespace}, &actual) require.NoError(t, err) assert.Equal(t, testTenantUUID, actual.Data[connectioninfo.TenantUUIDKey]) assert.Equal(t, testTenantEndpoints, actual.Data[connectioninfo.CommunicationEndpointsKey]) diff --git a/pkg/controllers/dynakube/otelc/config.go b/pkg/controllers/dynakube/otelc/config.go new file mode 100644 index 0000000000..0287cccc36 --- /dev/null +++ b/pkg/controllers/dynakube/otelc/config.go @@ -0,0 +1,9 @@ +package otelc + +import ( + "github.com/Dynatrace/dynatrace-operator/pkg/logd" +) + +var ( + log = logd.Get().WithName("otel-collector") +) diff --git a/pkg/controllers/dynakube/otelc/configuration/conditions.go b/pkg/controllers/dynakube/otelc/configuration/conditions.go new file mode 100644 index 0000000000..853bd5a929 --- /dev/null +++ b/pkg/controllers/dynakube/otelc/configuration/conditions.go @@ -0,0 +1,3 @@ +package configuration + +const conditionType string = "OTELCConfigurationConfigMap" diff --git a/pkg/controllers/dynakube/otelc/configuration/config.go b/pkg/controllers/dynakube/otelc/configuration/config.go new file mode 100644 index 0000000000..00fe75dbf9 --- /dev/null +++ b/pkg/controllers/dynakube/otelc/configuration/config.go @@ -0,0 +1,7 @@ +package configuration + +import "github.com/Dynatrace/dynatrace-operator/pkg/logd" + +var ( + log = logd.Get().WithName("otelc-config") +) diff --git a/pkg/controllers/dynakube/otelc/configuration/reconciler.go b/pkg/controllers/dynakube/otelc/configuration/reconciler.go new file mode 100644 index 0000000000..9e56452a29 --- /dev/null +++ b/pkg/controllers/dynakube/otelc/configuration/reconciler.go @@ -0,0 +1,147 @@ +package configuration + +import ( + "path/filepath" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/consts" + otelcconsts "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/otelc/consts" + "github.com/Dynatrace/dynatrace-operator/pkg/otelcgen" + "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" + k8sconfigmap "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/configmap" + k8slabels "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/labels" + "golang.org/x/net/context" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type Reconciler struct { + client client.Client + apiReader client.Reader + dk *dynakube.DynaKube +} + +func NewReconciler(clt client.Client, + apiReader client.Reader, + dk *dynakube.DynaKube) *Reconciler { + return &Reconciler{ + client: clt, + apiReader: apiReader, + dk: dk, + } +} + +func (r *Reconciler) Reconcile(ctx context.Context) error { + if !r.dk.TelemetryIngest().IsEnabled() { + if meta.FindStatusCondition(*r.dk.Conditions(), conditionType) == nil { + return nil // no condition == nothing is there to clean up + } + + query := k8sconfigmap.Query(r.client, r.apiReader, log) + err := query.Delete(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: GetConfigMapName(r.dk.Name), Namespace: r.dk.Namespace}}) + + if err != nil { + log.Error(err, "failed to clean-up OTELC configuration configmap") + } + + meta.RemoveStatusCondition(r.dk.Conditions(), conditionType) + + return nil + } + + return r.reconcileConfigMap(ctx) +} + +func (r *Reconciler) reconcileConfigMap(ctx context.Context) error { + query := k8sconfigmap.Query(r.client, r.apiReader, log) + + newConfigMap, err := r.prepareConfigMap() + if err != nil { + return err + } + + changed, err := query.CreateOrUpdate(ctx, newConfigMap) + if err != nil { + conditions.SetKubeApiError(r.dk.Conditions(), conditionType, err) + + return err + } else if changed { + conditions.SetConfigMapOutdated(r.dk.Conditions(), conditionType, newConfigMap.Name) // needed so the timestamp updates, will never actually show up in the status + } + + conditions.SetConfigMapCreatedOrUpdated(r.dk.Conditions(), conditionType, newConfigMap.Name) + + return nil +} + +func (r *Reconciler) prepareConfigMap() (*corev1.ConfigMap, error) { + data, err := r.getData() + if err != nil { + return nil, err + } + + coreLabels := k8slabels.NewCoreLabels(r.dk.Name, k8slabels.OtelCComponentLabel).BuildLabels() + + newSecret, err := k8sconfigmap.Build(r.dk, + GetConfigMapName(r.dk.Name), + data, + k8sconfigmap.SetLabels(coreLabels), + ) + if err != nil { + conditions.SetConfigMapGenFailed(r.dk.Conditions(), conditionType, err) + + return nil, err + } + + return newSecret, err +} + +func (r *Reconciler) getData() (map[string]string, error) { + myPodIp := "${env:MY_POD_IP}" + + options := []otelcgen.Option{ + otelcgen.WithApiToken("${env:" + otelcconsts.EnvDataIngestToken + "}"), + otelcgen.WithExportersEndpoint("${env:DT_ENDPOINT}"), + } + + if r.dk.IsAGCertificateNeeded() { + options = append(options, otelcgen.WithCA(otelcconsts.ActiveGateTLSCertVolumePath)) + } else if r.dk.IsCACertificateNeeded() { + options = append(options, otelcgen.WithCA(otelcconsts.TrustedCAVolumePath)) + options = append(options, otelcgen.WithSystemCAs(true)) + } + + if r.dk.TelemetryIngest().IsEnabled() && r.dk.TelemetryIngest().Spec.TlsRefName != "" { + options = append(options, otelcgen.WithTLS(filepath.Join(otelcconsts.CustomTlsCertMountPath, consts.TLSCrtDataName), filepath.Join(otelcconsts.CustomTlsCertMountPath, consts.TLSKeyDataName))) + } + + options = append(options, + otelcgen.WithExporters(), + otelcgen.WithProcessors(), + otelcgen.WithReceivers(), + otelcgen.WithExtensions(), + otelcgen.WithServices(), + ) + + config, err := otelcgen.NewConfig(myPodIp, r.dk.TelemetryIngest().GetProtocols(), options...) + if err != nil { + return nil, err + } + + configBytes, err := config.Marshal() + if err != nil { + return nil, err + } + + configMap := map[string]string{ + otelcconsts.ConfigFieldName: string(configBytes), + } + + return configMap, nil +} + +func GetConfigMapName(dkName string) string { + return dkName + otelcconsts.TelemetryCollectorConfigmapSuffix +} diff --git a/pkg/controllers/dynakube/otelc/configuration/reconciler_test.go b/pkg/controllers/dynakube/otelc/configuration/reconciler_test.go new file mode 100644 index 0000000000..d69315e765 --- /dev/null +++ b/pkg/controllers/dynakube/otelc/configuration/reconciler_test.go @@ -0,0 +1,56 @@ +package configuration + +import ( + "context" + "testing" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/telemetryingest" + "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/otelc/consts" + "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +const ( + testDynakubeName = "dynakube" + testNamespaceName = "dynatrace" +) + +func getTestDynakube(telemetryIngestSpec *telemetryingest.Spec) *dynakube.DynaKube { + return &dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Name: testDynakubeName, + Namespace: testNamespaceName, + }, + Spec: dynakube.DynaKubeSpec{ + TelemetryIngest: telemetryIngestSpec, + }, + Status: dynakube.DynaKubeStatus{}, + } +} + +func TestConfigurationConfigMap(t *testing.T) { + t.Run("create configmap if it does not exist", func(t *testing.T) { + mockK8sClient := fake.NewFakeClient() + dk := getTestDynakube(&telemetryingest.Spec{}) + err := NewReconciler(mockK8sClient, mockK8sClient, dk).Reconcile(context.Background()) + require.NoError(t, err) + + configMap := &corev1.ConfigMap{} + err = mockK8sClient.Get(context.Background(), client.ObjectKey{Name: GetConfigMapName(dk.Name), Namespace: dk.Namespace}, configMap) + require.NoError(t, err) + + _, ok := configMap.Data[consts.ConfigFieldName] + assert.True(t, ok) + + require.Len(t, dk.Status.Conditions, 1) + assert.Equal(t, conditionType, dk.Status.Conditions[0].Type) + assert.Equal(t, conditions.ConfigMapCreatedOrUpdatedReason, dk.Status.Conditions[0].Reason) + assert.Equal(t, metav1.ConditionTrue, dk.Status.Conditions[0].Status) + }) +} diff --git a/pkg/controllers/dynakube/otelc/consts/consts.go b/pkg/controllers/dynakube/otelc/consts/consts.go new file mode 100644 index 0000000000..d781099742 --- /dev/null +++ b/pkg/controllers/dynakube/otelc/consts/consts.go @@ -0,0 +1,20 @@ +package consts + +const ( + OtlpApiEndpointConfigMapName = "dynatrace-otlp-api-endpoint" + + ConfigFieldName = "telemetry.yaml" + TelemetryCollectorConfigmapSuffix = "-telemetry-collector-config" + + CustomTlsCertMountPath = "/tls/custom/telemetry" + + TrustedCAsFile = "rootca.pem" + TrustedCAVolumeMountPath = "/tls/custom/cacerts" + TrustedCAVolumePath = TrustedCAVolumeMountPath + "/" + TrustedCAsFile + + ActiveGateCertFile = "cert.pem" + ActiveGateTLSCertCAVolumeMountPath = "/tls/custom/activegate" + ActiveGateTLSCertVolumePath = ActiveGateTLSCertCAVolumeMountPath + "/" + ActiveGateCertFile + + EnvDataIngestToken = "DT_DATA_INGEST_TOKEN" +) diff --git a/pkg/controllers/dynakube/otelc/endpoint/conditions.go b/pkg/controllers/dynakube/otelc/endpoint/conditions.go new file mode 100644 index 0000000000..9419975d45 --- /dev/null +++ b/pkg/controllers/dynakube/otelc/endpoint/conditions.go @@ -0,0 +1,5 @@ +package endpoint + +const ( + configMapConditionType = "OtelpApiEndpointConfigMap" +) diff --git a/pkg/controllers/dynakube/otelc/endpoint/config.go b/pkg/controllers/dynakube/otelc/endpoint/config.go new file mode 100644 index 0000000000..70bf7ad36a --- /dev/null +++ b/pkg/controllers/dynakube/otelc/endpoint/config.go @@ -0,0 +1,7 @@ +package endpoint + +import "github.com/Dynatrace/dynatrace-operator/pkg/logd" + +var ( + log = logd.Get().WithName("telemetry-ingest-api-credentials-secret") +) diff --git a/pkg/controllers/dynakube/otelc/endpoint/reconciler.go b/pkg/controllers/dynakube/otelc/endpoint/reconciler.go new file mode 100644 index 0000000000..8b98c5b9f7 --- /dev/null +++ b/pkg/controllers/dynakube/otelc/endpoint/reconciler.go @@ -0,0 +1,136 @@ +package endpoint + +import ( + "context" + "fmt" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/otelc/consts" + "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" + "github.com/Dynatrace/dynatrace-operator/pkg/util/hasher" + k8sconfigmap "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/configmap" + k8slabels "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/labels" + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type Reconciler struct { + client client.Client + apiReader client.Reader + dk *dynakube.DynaKube +} + +type ReconcilerBuilder func(client client.Client, apiReader client.Reader, dk *dynakube.DynaKube) *Reconciler + +func NewReconciler(client client.Client, apiReader client.Reader, dk *dynakube.DynaKube) *Reconciler { + return &Reconciler{ + client: client, + dk: dk, + apiReader: apiReader, + } +} + +func (r *Reconciler) Reconcile(ctx context.Context) error { + if r.dk.TelemetryIngest().IsEnabled() { + return r.ensureOtlpApiEndpointConfigMap(ctx) + } + + return r.removeOtlpApiEndpointConfigMap(ctx) +} + +func (r *Reconciler) ensureOtlpApiEndpointConfigMap(ctx context.Context) error { + query := k8sconfigmap.Query(r.client, r.apiReader, log) + _, err := query.Get(ctx, types.NamespacedName{Name: consts.OtlpApiEndpointConfigMapName, Namespace: r.dk.Namespace}) + + if err != nil && k8serrors.IsNotFound(err) { + log.Info("creating new config map for telemetry api endpoint") + + configMap, err := r.generateOtlpApiEndpointConfigMap(consts.OtlpApiEndpointConfigMapName) + + if err != nil { + conditions.SetConfigMapGenFailed(r.dk.Conditions(), configMapConditionType, err) + + return err + } + + _, err = hasher.GenerateHash(configMap.Data) + if err != nil { + conditions.SetConfigMapGenFailed(r.dk.Conditions(), configMapConditionType, err) + + return err + } + + err = query.Create(ctx, configMap) + if err != nil { + log.Info("could not create secret for telemetry api credentials", "name", configMap.Name) + conditions.SetKubeApiError(r.dk.Conditions(), configMapConditionType, err) + + return err + } + + conditions.SetConfigMapCreatedOrUpdated(r.dk.Conditions(), configMapConditionType, consts.OtlpApiEndpointConfigMapName) + } else if err != nil { + conditions.SetKubeApiError(r.dk.Conditions(), configMapConditionType, err) + + return err + } + + return nil +} + +func (r *Reconciler) getDtEndpoint() (string, error) { + if r.dk.ActiveGate().IsEnabled() { + tenantUUID, err := r.dk.TenantUUID() + if err != nil { + return "", err + } + + return fmt.Sprintf("https://%s-activegate.dynatrace.svc/e/%s/api/v2/otlp", r.dk.Name, tenantUUID), nil + } + + return r.dk.ApiUrl() + "/v2/otlp", nil +} + +func (r *Reconciler) generateOtlpApiEndpointConfigMap(name string) (secret *corev1.ConfigMap, err error) { + data := make(map[string]string) + + dtEndpoint, err := r.getDtEndpoint() + if err != nil { + return nil, err + } + + data["DT_ENDPOINT"] = dtEndpoint + + configMap, err := k8sconfigmap.Build(r.dk, + name, + data, + k8sconfigmap.SetLabels(k8slabels.NewCoreLabels(r.dk.Name, k8slabels.OtelCComponentLabel).BuildLabels()), + ) + + if err != nil { + return nil, err + } + + return configMap, nil +} + +func (r *Reconciler) removeOtlpApiEndpointConfigMap(ctx context.Context) error { + if meta.FindStatusCondition(*r.dk.Conditions(), configMapConditionType) == nil { + return nil // no condition == nothing is there to clean up + } + + query := k8sconfigmap.Query(r.client, r.apiReader, log) + err := query.Delete(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: consts.OtlpApiEndpointConfigMapName, Namespace: r.dk.Namespace}}) + + if err != nil { + log.Error(err, "could not delete apiEndpoint config map", "name", consts.OtlpApiEndpointConfigMapName) + } + + meta.RemoveStatusCondition(r.dk.Conditions(), configMapConditionType) + + return nil +} diff --git a/pkg/controllers/dynakube/otelc/endpoint/reconciler_test.go b/pkg/controllers/dynakube/otelc/endpoint/reconciler_test.go new file mode 100644 index 0000000000..913d047eb9 --- /dev/null +++ b/pkg/controllers/dynakube/otelc/endpoint/reconciler_test.go @@ -0,0 +1,168 @@ +package endpoint + +import ( + "context" + "fmt" + "testing" + + schemeFake "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" + "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/communication" + "github.com/Dynatrace/dynatrace-operator/pkg/api/status" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/telemetryingest" + dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" + "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/otelc/consts" + "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/configmap" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +const ( + testApiToken = "apiTokenValue" + testTenantUUID = "abc12345" + testKubeSystemUUID = "12345" +) + +func TestConfigMapCreation(t *testing.T) { + ctx := context.Background() + + t.Run("creates config map if it does not exist", func(t *testing.T) { + dk := createDynaKube(true) + + testConfigMap, err := configmap.Build(&dk, dk.Name, map[string]string{ + dtclient.ApiToken: testApiToken, + }) + require.NoError(t, err) + + clt := fake.NewFakeClient(testConfigMap) + + r := NewReconciler(clt, clt, &dk) + + err = r.ensureOtlpApiEndpointConfigMap(ctx) + require.NoError(t, err) + + var apiEndpointConfigMap corev1.ConfigMap + err = clt.Get(ctx, types.NamespacedName{Name: consts.OtlpApiEndpointConfigMapName, Namespace: dk.Namespace}, &apiEndpointConfigMap) + require.NoError(t, err) + assert.NotEmpty(t, apiEndpointConfigMap) + require.NotNil(t, meta.FindStatusCondition(*dk.Conditions(), configMapConditionType)) + assert.Equal(t, conditions.ConfigMapCreatedOrUpdatedReason, meta.FindStatusCondition(*dk.Conditions(), configMapConditionType).Reason) + }) + + t.Run("removes secret if exists but we don't need it", func(t *testing.T) { + dk := createDynaKube(false) + conditions.SetConfigMapCreatedOrUpdated(dk.Conditions(), configMapConditionType, consts.OtlpApiEndpointConfigMapName) + + objs := []client.Object{ + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: consts.OtlpApiEndpointConfigMapName, + Namespace: dk.Namespace, + }, + }, + } + + clt := schemeFake.NewClient(objs...) + r := NewReconciler(clt, clt, &dk) + + err := r.Reconcile(ctx) + require.NoError(t, err) + + var apiEndpointConfigmap corev1.ConfigMap + err = clt.Get(ctx, types.NamespacedName{Name: consts.OtlpApiEndpointConfigMapName, Namespace: dk.Namespace}, &apiEndpointConfigmap) + + require.Error(t, err) + assert.Empty(t, apiEndpointConfigmap) + }) +} + +func TestEndpoint(t *testing.T) { + tests := []struct { + name string + apiUrl string + expectedEndpoint string + inClusterAg bool + }{ + { + name: "in-cluster ActiveGate", + apiUrl: fmt.Sprintf("https://%s.dev.dynatracelabs.com/api", testTenantUUID), + inClusterAg: true, + expectedEndpoint: fmt.Sprintf("https://test-dk-activegate.dynatrace.svc/e/%s/api/v2/otlp", testTenantUUID), + }, + { + name: "public ActiveGate", + apiUrl: fmt.Sprintf("https://%s.dev.dynatracelabs.com/api", testTenantUUID), + inClusterAg: false, + expectedEndpoint: fmt.Sprintf("https://%s.dev.dynatracelabs.com/api/v2/otlp", testTenantUUID), + }, + { + name: "managed ActiveGate", + apiUrl: "https://dynatrace.foobar.com/e/abcdefgh-1234-5678-9abc-deadbeef/api", + inClusterAg: false, + expectedEndpoint: "https://dynatrace.foobar.com/e/abcdefgh-1234-5678-9abc-deadbeef/api/v2/otlp", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dk := createDynaKube(true) + dk.Spec.APIURL = tt.apiUrl + + if tt.inClusterAg { + dk.Spec.ActiveGate = activegate.Spec{ + Capabilities: []activegate.CapabilityDisplayName{"dynatrace-api"}, + } + } + + objs := []client.Object{ + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: consts.OtlpApiEndpointConfigMapName, + Namespace: dk.Namespace, + }, + }, + } + + clt := schemeFake.NewClient(objs...) + r := NewReconciler(clt, clt, &dk) + + endpoint, err := r.getDtEndpoint() + require.NoError(t, err) + assert.Equal(t, tt.expectedEndpoint, endpoint) + }) + } +} + +func createDynaKube(telemetryIngestEnabled bool) dynakube.DynaKube { + dk := dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-dk", + }, + Spec: dynakube.DynaKubeSpec{}, + Status: dynakube.DynaKubeStatus{ + ActiveGate: activegate.Status{ + ConnectionInfo: communication.ConnectionInfo{ + TenantUUID: testTenantUUID, + }, + VersionStatus: status.VersionStatus{}, + }, + KubeSystemUUID: testKubeSystemUUID, + }, + } + + if telemetryIngestEnabled { + dk.TelemetryIngest().Spec = &telemetryingest.Spec{} + } else { + dk.TelemetryIngest().Spec = nil + } + + return dk +} diff --git a/pkg/controllers/dynakube/otelc/reconciler.go b/pkg/controllers/dynakube/otelc/reconciler.go new file mode 100644 index 0000000000..15dd5ad277 --- /dev/null +++ b/pkg/controllers/dynakube/otelc/reconciler.go @@ -0,0 +1,63 @@ +package otelc + +import ( + "context" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/controllers" + "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/otelc/configuration" + "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/otelc/endpoint" + "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/otelc/service" + "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/otelc/statefulset" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type Reconciler struct { + client client.Client + apiReader client.Reader + dk *dynakube.DynaKube + statefulsetReconciler controllers.Reconciler + serviceReconciler *service.Reconciler + endpointReconciler *endpoint.Reconciler + configurationReconciler *configuration.Reconciler +} + +type ReconcilerBuilder func(client client.Client, apiReader client.Reader, dk *dynakube.DynaKube) controllers.Reconciler + +func NewReconciler(client client.Client, apiReader client.Reader, dk *dynakube.DynaKube) controllers.Reconciler { //nolint + return &Reconciler{ + client: client, + apiReader: apiReader, + dk: dk, + statefulsetReconciler: statefulset.NewReconciler(client, apiReader, dk), + serviceReconciler: service.NewReconciler(client, apiReader, dk), + endpointReconciler: endpoint.NewReconciler(client, apiReader, dk), + configurationReconciler: configuration.NewReconciler(client, apiReader, dk), + } +} + +func (r *Reconciler) Reconcile(ctx context.Context) error { + err := r.serviceReconciler.Reconcile(ctx) + if err != nil { + return err + } + + err = r.endpointReconciler.Reconcile(ctx) + if err != nil { + return err + } + + err = r.configurationReconciler.Reconcile(ctx) + if err != nil { + return err + } + + err = r.statefulsetReconciler.Reconcile(ctx) + if err != nil { + log.Info("failed to reconcile Dynatrace OTELc statefulset") + + return err + } + + return nil +} diff --git a/pkg/controllers/dynakube/otelc/service/conditions.go b/pkg/controllers/dynakube/otelc/service/conditions.go new file mode 100644 index 0000000000..c6a49c2d7a --- /dev/null +++ b/pkg/controllers/dynakube/otelc/service/conditions.go @@ -0,0 +1,5 @@ +package service + +const ( + serviceConditionType = "OTELCService" +) diff --git a/pkg/controllers/dynakube/otelc/service/config.go b/pkg/controllers/dynakube/otelc/service/config.go new file mode 100644 index 0000000000..16130d363b --- /dev/null +++ b/pkg/controllers/dynakube/otelc/service/config.go @@ -0,0 +1,7 @@ +package service + +import "github.com/Dynatrace/dynatrace-operator/pkg/logd" + +var ( + log = logd.Get().WithName("otelc-service") +) diff --git a/pkg/controllers/dynakube/otelc/service/reconciler.go b/pkg/controllers/dynakube/otelc/service/reconciler.go new file mode 100644 index 0000000000..7ee81e22cd --- /dev/null +++ b/pkg/controllers/dynakube/otelc/service/reconciler.go @@ -0,0 +1,205 @@ +package service + +import ( + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/otelcgen" + "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/labels" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/service" + "golang.org/x/net/context" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/util/intstr" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + zipkinPortName = "zipkin" + zipkinPort = 9411 + otlpGrpcPortName = "otlp-grpc" + otlpGrpcPort = 4317 + otlpHttpPortName = "otlp-http" + otlpHttpPort = 4318 + jaegerGrpcPortName = "jaeger-grpc" + jaegerGrpcPort = 14250 + jaegerThriftBinaryPortName = "jaeger-thrift-binary" + jaegerThriftBinaryPort = 6832 + jaegerThriftCompactPortName = "jaeger-thrift-compact" + jaegerThriftCompactPort = 6831 + jaegerThriftHttpPortName = "jaeger-thrift-http" + jaegerThriftHttpPort = 14268 + statsdPortName = "statsd" + statsdPort = 8125 +) + +type Reconciler struct { + client client.Client + apiReader client.Reader + dk *dynakube.DynaKube +} + +type ReconcilerBuilder func(client client.Client, apiReader client.Reader, dk *dynakube.DynaKube) *Reconciler + +func NewReconciler(client client.Client, apiReader client.Reader, dk *dynakube.DynaKube) *Reconciler { + return &Reconciler{ + client: client, + dk: dk, + apiReader: apiReader, + } +} + +func (r *Reconciler) Reconcile(ctx context.Context) error { + if !r.dk.TelemetryIngest().IsEnabled() { + r.removeServiceOnce(ctx) + + return nil + } + + r.removeAllServicesExcept(ctx, r.dk.TelemetryIngest().GetServiceName()) + + return r.createOrUpdateService(ctx) +} + +func (r *Reconciler) removeServiceOnce(ctx context.Context) { + if meta.FindStatusCondition(*r.dk.Conditions(), serviceConditionType) == nil { + return + } + defer meta.RemoveStatusCondition(r.dk.Conditions(), serviceConditionType) + + r.removeAllServicesExcept(ctx, "") +} + +func (r *Reconciler) removeAllServicesExcept(ctx context.Context, actualServiceName string) { + telemetryServiceList := &corev1.ServiceList{} + + listOps := []client.ListOption{ + client.InNamespace(r.dk.Namespace), + client.MatchingLabels{ + labels.AppComponentLabel: labels.OtelCComponentLabel, + labels.AppCreatedByLabel: r.dk.Name, + }, + } + + if err := r.apiReader.List(ctx, telemetryServiceList, listOps...); err != nil { + log.Info("failed to list telemetry services, skipping cleanup", "error", err) + + return + } + + for _, service := range telemetryServiceList.Items { + if service.Name != actualServiceName { + if err := r.client.Delete(ctx, &service); err != nil { + log.Info("failed to clean up telemetry service", "service name", service.Name, "namespace", service.Namespace, "error", err) + } else { + log.Info("removed unused telemetry service", "service name", service.Name, "namespace", service.Namespace) + } + } + } +} + +func (r *Reconciler) createOrUpdateService(ctx context.Context) error { + newService, err := r.buildService() + if err != nil { + conditions.SetServiceGenFailed(r.dk.Conditions(), serviceConditionType, err) + + return err + } + + _, err = service.Query(r.client, r.apiReader, log).CreateOrUpdate(ctx, newService) + if err != nil { + log.Info("failed to create/update telemetry service") + conditions.SetKubeApiError(r.dk.Conditions(), serviceConditionType, err) + + return err + } + + conditions.SetServiceCreated(r.dk.Conditions(), serviceConditionType, r.dk.TelemetryIngest().GetServiceName()) + + return nil +} + +func (r *Reconciler) buildService() (*corev1.Service, error) { + coreLabels := labels.NewCoreLabels(r.dk.Name, labels.OtelCComponentLabel) + // TODO: add proper version later on + appLabels := labels.NewAppLabels(labels.OtelCComponentLabel, r.dk.Name, labels.OtelCComponentLabel, "") + + return service.Build(r.dk, + r.dk.TelemetryIngest().GetServiceName(), + appLabels.BuildMatchLabels(), + buildServicePortList(r.dk.TelemetryIngest().GetProtocols()), + service.SetLabels(coreLabels.BuildLabels()), + service.SetType(corev1.ServiceTypeClusterIP), + ) +} + +func buildServicePortList(protocols []otelcgen.Protocol) []corev1.ServicePort { + if len(protocols) == 0 { + return nil + } + + svcPorts := make([]corev1.ServicePort, 0) + + for _, protocol := range protocols { + switch protocol { + case otelcgen.ZipkinProtocol: + svcPorts = append(svcPorts, corev1.ServicePort{ + Name: zipkinPortName, + Port: zipkinPort, + Protocol: corev1.ProtocolTCP, + TargetPort: intstr.FromInt32(zipkinPort), + }) + case otelcgen.OtlpProtocol: + svcPorts = append(svcPorts, + corev1.ServicePort{ + Name: otlpGrpcPortName, + Port: otlpGrpcPort, + Protocol: corev1.ProtocolTCP, + TargetPort: intstr.FromInt32(otlpGrpcPort), + }, + corev1.ServicePort{ + Name: otlpHttpPortName, + Port: otlpHttpPort, + Protocol: corev1.ProtocolTCP, + TargetPort: intstr.FromInt32(otlpHttpPort), + }) + case otelcgen.JaegerProtocol: + svcPorts = append(svcPorts, + corev1.ServicePort{ + Name: jaegerGrpcPortName, + Port: jaegerGrpcPort, + Protocol: corev1.ProtocolTCP, + TargetPort: intstr.FromInt32(jaegerGrpcPort), + }, + corev1.ServicePort{ + Name: jaegerThriftBinaryPortName, + Port: jaegerThriftBinaryPort, + Protocol: corev1.ProtocolUDP, + TargetPort: intstr.FromInt32(jaegerThriftBinaryPort), + }, + corev1.ServicePort{ + Name: jaegerThriftCompactPortName, + Port: jaegerThriftCompactPort, + Protocol: corev1.ProtocolUDP, + TargetPort: intstr.FromInt32(jaegerThriftCompactPort), + }, + corev1.ServicePort{ + Name: jaegerThriftHttpPortName, + Port: jaegerThriftHttpPort, + Protocol: corev1.ProtocolTCP, + TargetPort: intstr.FromInt32(jaegerThriftHttpPort), + }) + case otelcgen.StatsdProtocol: + svcPorts = append(svcPorts, + corev1.ServicePort{ + Name: statsdPortName, + Port: statsdPort, + Protocol: corev1.ProtocolUDP, + TargetPort: intstr.FromInt32(statsdPort), + }) + default: + log.Info("unknown telemetry service protocol ignored", "protocol", protocol) + } + } + + return svcPorts +} diff --git a/pkg/controllers/dynakube/otelc/service/reconciler_test.go b/pkg/controllers/dynakube/otelc/service/reconciler_test.go new file mode 100644 index 0000000000..615e0cafb1 --- /dev/null +++ b/pkg/controllers/dynakube/otelc/service/reconciler_test.go @@ -0,0 +1,241 @@ +package service + +import ( + "context" + "testing" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/telemetryingest" + "github.com/Dynatrace/dynatrace-operator/pkg/otelcgen" + "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/labels" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +const ( + testDynakubeName = "dynakube" + testNamespaceName = "dynatrace" + testServiceName = "test-service-name" +) + +func getTestDynakube(telemetryIngestSpec *telemetryingest.Spec) *dynakube.DynaKube { + return &dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Name: testDynakubeName, + Namespace: testNamespaceName, + }, + Spec: dynakube.DynaKubeSpec{ + TelemetryIngest: telemetryIngestSpec, + }, + Status: dynakube.DynaKubeStatus{}, + } +} + +func TestService(t *testing.T) { + t.Run("create service if it does not exist", func(t *testing.T) { + mockK8sClient := fake.NewFakeClient() + dk := getTestDynakube(&telemetryingest.Spec{}) + err := NewReconciler(mockK8sClient, mockK8sClient, dk).Reconcile(context.Background()) + require.NoError(t, err) + + service := &corev1.Service{} + err = mockK8sClient.Get(context.Background(), client.ObjectKey{Name: dk.TelemetryIngest().GetDefaultServiceName(), Namespace: dk.Namespace}, service) + require.NoError(t, err) + + require.Len(t, service.Spec.Ports, 8) + assert.Equal(t, otlpGrpcPortName, service.Spec.Ports[0].Name) + assert.Equal(t, int32(4317), service.Spec.Ports[0].Port) + assert.Equal(t, int32(4317), service.Spec.Ports[0].TargetPort.IntVal) + assert.Equal(t, corev1.ProtocolTCP, service.Spec.Ports[0].Protocol) + + assert.Equal(t, otlpHttpPortName, service.Spec.Ports[1].Name) + assert.Equal(t, int32(4318), service.Spec.Ports[1].Port) + assert.Equal(t, int32(4318), service.Spec.Ports[1].TargetPort.IntVal) + assert.Equal(t, corev1.ProtocolTCP, service.Spec.Ports[1].Protocol) + + assert.Equal(t, jaegerGrpcPortName, service.Spec.Ports[2].Name) + assert.Equal(t, int32(14250), service.Spec.Ports[2].Port) + assert.Equal(t, int32(14250), service.Spec.Ports[2].TargetPort.IntVal) + assert.Equal(t, corev1.ProtocolTCP, service.Spec.Ports[2].Protocol) + + assert.Equal(t, jaegerThriftBinaryPortName, service.Spec.Ports[3].Name) + assert.Equal(t, int32(6832), service.Spec.Ports[3].Port) + assert.Equal(t, int32(6832), service.Spec.Ports[3].TargetPort.IntVal) + assert.Equal(t, corev1.ProtocolUDP, service.Spec.Ports[3].Protocol) + + assert.Equal(t, jaegerThriftCompactPortName, service.Spec.Ports[4].Name) + assert.Equal(t, int32(6831), service.Spec.Ports[4].Port) + assert.Equal(t, int32(6831), service.Spec.Ports[4].TargetPort.IntVal) + assert.Equal(t, corev1.ProtocolUDP, service.Spec.Ports[4].Protocol) + + assert.Equal(t, jaegerThriftHttpPortName, service.Spec.Ports[5].Name) + assert.Equal(t, int32(14268), service.Spec.Ports[5].Port) + assert.Equal(t, int32(14268), service.Spec.Ports[5].TargetPort.IntVal) + assert.Equal(t, corev1.ProtocolTCP, service.Spec.Ports[5].Protocol) + + assert.Equal(t, statsdPortName, service.Spec.Ports[6].Name) + assert.Equal(t, int32(8125), service.Spec.Ports[6].Port) + assert.Equal(t, int32(8125), service.Spec.Ports[6].TargetPort.IntVal) + assert.Equal(t, corev1.ProtocolUDP, service.Spec.Ports[6].Protocol) + + assert.Equal(t, zipkinPortName, service.Spec.Ports[7].Name) + assert.Equal(t, int32(9411), service.Spec.Ports[7].Port) + assert.Equal(t, int32(9411), service.Spec.Ports[7].TargetPort.IntVal) + assert.Equal(t, corev1.ProtocolTCP, service.Spec.Ports[7].Protocol) + + require.Len(t, dk.Status.Conditions, 1) + assert.Equal(t, serviceConditionType, dk.Status.Conditions[0].Type) + assert.Equal(t, conditions.ServiceCreatedReason, dk.Status.Conditions[0].Reason) + assert.Equal(t, metav1.ConditionTrue, dk.Status.Conditions[0].Status) + }) + t.Run("create service for specified protocols", func(t *testing.T) { + mockK8sClient := fake.NewFakeClient() + dk := getTestDynakube(&telemetryingest.Spec{ + Protocols: []string{ + string(otelcgen.ZipkinProtocol), + string(otelcgen.StatsdProtocol), + }, + }) + err := NewReconciler(mockK8sClient, mockK8sClient, dk).Reconcile(context.Background()) + require.NoError(t, err) + + service := &corev1.Service{} + err = mockK8sClient.Get(context.Background(), client.ObjectKey{Name: dk.TelemetryIngest().GetDefaultServiceName(), Namespace: dk.Namespace}, service) + require.NoError(t, err) + + require.Len(t, service.Spec.Ports, 2) + assert.Equal(t, zipkinPortName, service.Spec.Ports[0].Name) + assert.Equal(t, statsdPortName, service.Spec.Ports[1].Name) + + require.Len(t, dk.Status.Conditions, 1) + assert.Equal(t, serviceConditionType, dk.Status.Conditions[0].Type) + assert.Equal(t, conditions.ServiceCreatedReason, dk.Status.Conditions[0].Reason) + assert.Equal(t, metav1.ConditionTrue, dk.Status.Conditions[0].Status) + }) + t.Run("default service name, remove service if it is not needed", func(t *testing.T) { + dk := getTestDynakube(nil) + dk.Status.Conditions = []metav1.Condition{ + { + Type: serviceConditionType, + }, + } + + mockK8sClient := fake.NewFakeClient() + err := mockK8sClient.Create(context.Background(), &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: dk.TelemetryIngest().GetDefaultServiceName(), + Namespace: dk.Namespace, + Labels: map[string]string{ + labels.AppComponentLabel: labels.OtelCComponentLabel, + labels.AppCreatedByLabel: dk.Name, + }, + }, + }) + require.NoError(t, err) + + err = NewReconciler(mockK8sClient, mockK8sClient, dk).Reconcile(context.Background()) + require.NoError(t, err) + + service := &corev1.Service{} + err = mockK8sClient.Get(context.Background(), client.ObjectKey{Name: dk.TelemetryIngest().GetDefaultServiceName(), Namespace: dk.Namespace}, service) + require.Error(t, err) + assert.True(t, k8serrors.IsNotFound(err)) + + require.Empty(t, dk.Status.Conditions) + }) + t.Run("custom service name, remove service if it is not needed", func(t *testing.T) { + dk := getTestDynakube(nil) + dk.Status.Conditions = []metav1.Condition{ + { + Type: serviceConditionType, + }, + } + + mockK8sClient := fake.NewFakeClient() + err := mockK8sClient.Create(context.Background(), &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: testServiceName, + Namespace: dk.Namespace, + Labels: map[string]string{ + labels.AppComponentLabel: labels.OtelCComponentLabel, + labels.AppCreatedByLabel: dk.Name, + }, + }, + }) + require.NoError(t, err) + + err = NewReconciler(mockK8sClient, mockK8sClient, dk).Reconcile(context.Background()) + require.NoError(t, err) + + service := &corev1.Service{} + err = mockK8sClient.Get(context.Background(), client.ObjectKey{Name: dk.TelemetryIngest().GetDefaultServiceName(), Namespace: dk.Namespace}, service) + require.Error(t, err) + assert.True(t, k8serrors.IsNotFound(err)) + + require.Empty(t, dk.Status.Conditions) + }) + t.Run("update from default service to custom service", func(t *testing.T) { + mockK8sClient := fake.NewFakeClient() + dk := getTestDynakube(&telemetryingest.Spec{}) + err := NewReconciler(mockK8sClient, mockK8sClient, dk).Reconcile(context.Background()) + require.NoError(t, err) + + service := &corev1.Service{} + err = mockK8sClient.Get(context.Background(), client.ObjectKey{Name: dk.TelemetryIngest().GetDefaultServiceName(), Namespace: dk.Namespace}, service) + require.NoError(t, err) + + require.Len(t, dk.Status.Conditions, 1) + assert.Equal(t, serviceConditionType, dk.Status.Conditions[0].Type) + assert.Equal(t, conditions.ServiceCreatedReason, dk.Status.Conditions[0].Reason) + assert.Equal(t, metav1.ConditionTrue, dk.Status.Conditions[0].Status) + + // update + dk.Spec.TelemetryIngest = &telemetryingest.Spec{ + ServiceName: testServiceName, + } + err = NewReconciler(mockK8sClient, mockK8sClient, dk).Reconcile(context.Background()) + require.NoError(t, err) + + service = &corev1.Service{} + err = mockK8sClient.Get(context.Background(), client.ObjectKey{Name: dk.TelemetryIngest().GetDefaultServiceName(), Namespace: dk.Namespace}, service) + require.Error(t, err) + assert.True(t, k8serrors.IsNotFound(err)) + + assert.NotEmpty(t, dk.Status.Conditions) + }) + t.Run("update from custom service to default service", func(t *testing.T) { + mockK8sClient := fake.NewFakeClient() + dk := getTestDynakube(&telemetryingest.Spec{ + ServiceName: testServiceName, + }) + err := NewReconciler(mockK8sClient, mockK8sClient, dk).Reconcile(context.Background()) + require.NoError(t, err) + + service := &corev1.Service{} + err = mockK8sClient.Get(context.Background(), client.ObjectKey{Name: testServiceName, Namespace: dk.Namespace}, service) + require.NoError(t, err) + + require.Len(t, dk.Status.Conditions, 1) + assert.Equal(t, serviceConditionType, dk.Status.Conditions[0].Type) + assert.Equal(t, conditions.ServiceCreatedReason, dk.Status.Conditions[0].Reason) + assert.Equal(t, metav1.ConditionTrue, dk.Status.Conditions[0].Status) + + // update + dk.Spec.TelemetryIngest = &telemetryingest.Spec{} + err = NewReconciler(mockK8sClient, mockK8sClient, dk).Reconcile(context.Background()) + require.NoError(t, err) + + service = &corev1.Service{} + err = mockK8sClient.Get(context.Background(), client.ObjectKey{Name: testServiceName, Namespace: dk.Namespace}, service) + require.Error(t, err) + assert.True(t, k8serrors.IsNotFound(err)) + + assert.NotEmpty(t, dk.Status.Conditions) + }) +} diff --git a/pkg/controllers/dynakube/otelc/statefulset/conditions.go b/pkg/controllers/dynakube/otelc/statefulset/conditions.go new file mode 100644 index 0000000000..c5e6baa29d --- /dev/null +++ b/pkg/controllers/dynakube/otelc/statefulset/conditions.go @@ -0,0 +1,3 @@ +package statefulset + +const conditionType string = "OtelStatefulSet" diff --git a/pkg/controllers/dynakube/otelc/statefulset/config.go b/pkg/controllers/dynakube/otelc/statefulset/config.go new file mode 100644 index 0000000000..ae77de6536 --- /dev/null +++ b/pkg/controllers/dynakube/otelc/statefulset/config.go @@ -0,0 +1,9 @@ +package statefulset + +import ( + "github.com/Dynatrace/dynatrace-operator/pkg/logd" +) + +var ( + log = logd.Get().WithName("otelc-statefulset") +) diff --git a/pkg/controllers/dynakube/otelc/statefulset/container.go b/pkg/controllers/dynakube/otelc/statefulset/container.go new file mode 100644 index 0000000000..cb44fcae7c --- /dev/null +++ b/pkg/controllers/dynakube/otelc/statefulset/container.go @@ -0,0 +1,56 @@ +package statefulset + +import ( + "fmt" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/consts" + corev1 "k8s.io/api/core/v1" +) + +const ( + // default values + defaultImageRepo = "public.ecr.aws/dynatrace/dynatrace-otel-collector" + defaultImageTag = "latest" + containerName = "collector" + secretsTokensPath = "/secrets/tokens" + otelcSecretTokenFilePath = secretsTokensPath + "/" + consts.OtelcTokenSecretKey +) + +func getContainer(dk *dynakube.DynaKube) corev1.Container { + imageRepo := dk.Spec.Templates.OpenTelemetryCollector.ImageRef.Repository + imageTag := dk.Spec.Templates.OpenTelemetryCollector.ImageRef.Tag + + if imageRepo == "" { + imageRepo = defaultImageRepo + } + + if imageTag == "" { + imageTag = defaultImageTag + } + + return corev1.Container{ + Name: containerName, + Image: imageRepo + ":" + imageTag, + ImagePullPolicy: corev1.PullAlways, + SecurityContext: buildSecurityContext(), + Env: getEnvs(dk), + Resources: dk.Spec.Templates.OpenTelemetryCollector.Resources, + Args: buildArgs(dk), + VolumeMounts: buildContainerVolumeMounts(dk), + } +} + +func buildArgs(dk *dynakube.DynaKube) []string { + args := []string{} + + if dk.IsExtensionsEnabled() { + args = append(args, fmt.Sprintf("--config=eec://%s:%d/otcconfig/prometheusMetrics#refresh-interval=5s&auth-file=%s", dk.ExtensionsServiceNameFQDN(), consts.OtelCollectorComPort, otelcSecretTokenFilePath)) + } + + if dk.TelemetryIngest().IsEnabled() { + args = append(args, "--config=file:///config/telemetry.yaml") + } + + return args +} diff --git a/pkg/controllers/dynakube/otelc/statefulset/container_test.go b/pkg/controllers/dynakube/otelc/statefulset/container_test.go new file mode 100644 index 0000000000..7b71d68351 --- /dev/null +++ b/pkg/controllers/dynakube/otelc/statefulset/container_test.go @@ -0,0 +1,40 @@ +package statefulset + +import ( + "testing" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/telemetryingest" + "github.com/stretchr/testify/assert" +) + +func TestContainer(t *testing.T) { + t.Run("only TelemetryIngest enabled", func(t *testing.T) { + dk := getTestDynakube() + dk.Spec.TelemetryIngest = &telemetryingest.Spec{} + + assert.Equal(t, []string{"--config=file:///config/telemetry.yaml"}, buildArgs(dk)) + }) + + t.Run("only EEC enabled", func(t *testing.T) { + dk := getTestDynakubeWithExtensions() + assert.Equal( + t, + []string{"--config=eec://dynakube-extensions-controller.dynatrace:14599/otcconfig/prometheusMetrics#refresh-interval=5s&auth-file=/secrets/tokens/otelc.token"}, + buildArgs(dk), + ) + }) + + t.Run("TelemetryIngest and EEC enabled", func(t *testing.T) { + dk := getTestDynakubeWithExtensions() + dk.Spec.TelemetryIngest = &telemetryingest.Spec{} + + assert.Equal( + t, + []string{ + "--config=eec://dynakube-extensions-controller.dynatrace:14599/otcconfig/prometheusMetrics#refresh-interval=5s&auth-file=/secrets/tokens/otelc.token", + "--config=file:///config/telemetry.yaml", + }, + buildArgs(dk), + ) + }) +} diff --git a/pkg/controllers/dynakube/otelc/statefulset/env.go b/pkg/controllers/dynakube/otelc/statefulset/env.go new file mode 100644 index 0000000000..db99b55dbd --- /dev/null +++ b/pkg/controllers/dynakube/otelc/statefulset/env.go @@ -0,0 +1,152 @@ +package statefulset + +import ( + "strconv" + "strings" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/value" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" + "github.com/Dynatrace/dynatrace-operator/pkg/consts" + "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/capability" + agconsts "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/consts" + otelcConsts "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/otelc/consts" + corev1 "k8s.io/api/core/v1" +) + +const ( + // default values + defaultOLTPgrpcPort = "10001" + defaultOLTPhttpPort = "10002" + defaultReplicas = 1 + + // env variables + envShards = "SHARDS" + envShardId = "SHARD_ID" + envPodNamePrefix = "POD_NAME_PREFIX" + envPodName = "POD_NAME" + envMyPodIP = "MY_POD_IP" + envOTLPgrpcPort = "OTLP_GRPC_PORT" + envOTLPhttpPort = "OTLP_HTTP_PORT" + envEECDStoken = "EEC_DS_TOKEN" + envTrustedCAs = "TRUSTED_CAS" + envK8sClusterName = "K8S_CLUSTER_NAME" + envK8sClusterUid = "K8S_CLUSTER_UID" + envDTentityK8sCluster = "DT_ENTITY_KUBERNETES_CLUSTER" + envDTendpoint = "DT_ENDPOINT" + // certDirEnv is the environment variable that identifies which directory + // to check for SSL certificate files. If set, this overrides the system default. + // It is a colon separated list of directories. + // See https://www.openssl.org/docs/man1.0.2/man1/c_rehash.html. + envCertDir = "SSL_CERT_DIR" + envEECcontrollerTLS = "EXTENSIONS_CONTROLLER_TLS" + envHttpProxy = "HTTP_PROXY" + envHttpsProxy = "HTTPS_PROXY" + envNoProxy = "NO_PROXY" + + // Volume names and paths + customEecTLSCertificatePath = "/tls/custom/eec" + customEecTLSCertificateFullPath = customEecTLSCertificatePath + "/" + consts.TLSCrtDataName +) + +func getEnvs(dk *dynakube.DynaKube) []corev1.EnvVar { + envs := []corev1.EnvVar{ + {Name: envShards, Value: strconv.Itoa(int(getReplicas(dk)))}, + {Name: envPodNamePrefix, Value: dk.OtelCollectorStatefulsetName()}, + {Name: envPodName, ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.labels['statefulset.kubernetes.io/pod-name']", + }, + }, + }, + {Name: envShardId, ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.labels['apps.kubernetes.io/pod-index']", + }, + }, + }, + {Name: envOTLPgrpcPort, Value: defaultOLTPgrpcPort}, + {Name: envOTLPhttpPort, Value: defaultOLTPhttpPort}, + {Name: envK8sClusterName, Value: dk.Name}, + {Name: envK8sClusterUid, Value: dk.Status.KubeSystemUUID}, + {Name: envDTentityK8sCluster, Value: dk.Status.KubernetesClusterMEID}, + } + + if dk.HasProxy() { + envs = append(envs, getDynakubeProxyEnvValue(envHttpsProxy, dk.Spec.Proxy)) + envs = append(envs, getDynakubeProxyEnvValue(envHttpProxy, dk.Spec.Proxy)) + envs = append(envs, corev1.EnvVar{Name: envNoProxy, Value: getDynakubeNoProxyEnvValue(dk)}) + } + + if dk.IsExtensionsEnabled() { + envs = append( + envs, + corev1.EnvVar{Name: envEECDStoken, ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: dk.ExtensionsTokenSecretName()}, + Key: consts.OtelcTokenSecretKey, + }}, + }, + corev1.EnvVar{Name: envCertDir, Value: customEecTLSCertificatePath}, + corev1.EnvVar{Name: envEECcontrollerTLS, Value: customEecTLSCertificateFullPath}, + ) + } + + if dk.TelemetryIngest().IsEnabled() { + envs = append(envs, + corev1.EnvVar{Name: envDTendpoint, ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: otelcConsts.OtlpApiEndpointConfigMapName}, + Key: envDTendpoint, + }, + }}, + corev1.EnvVar{Name: envMyPodIP, ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "status.podIP", + }, + }}, + corev1.EnvVar{Name: otelcConsts.EnvDataIngestToken, ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: dk.Tokens()}, + Key: dynatrace.DataIngestToken, + }, + }}, + ) + } + + if dk.IsExtensionsEnabled() && dk.Spec.TrustedCAs != "" { + envs = append(envs, corev1.EnvVar{Name: envTrustedCAs, Value: otelcConsts.TrustedCAVolumePath}) + } + + return envs +} + +func getDynakubeProxyEnvValue(envVar string, src *value.Source) corev1.EnvVar { + if src.ValueFrom != "" { + return corev1.EnvVar{ + Name: envVar, + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: src.ValueFrom}, + Key: dynakube.ProxyKey, + }, + }, + } + } + + return corev1.EnvVar{Name: envVar, Value: src.Value} +} + +func getDynakubeNoProxyEnvValue(dk *dynakube.DynaKube) string { + noProxyValues := []string{} + + if dk.IsExtensionsEnabled() { + noProxyValues = append(noProxyValues, dk.ExtensionsServiceNameFQDN()) + } + + if dk.ActiveGate().IsEnabled() { + noProxyValues = append(noProxyValues, capability.BuildServiceName(dk.Name, agconsts.MultiActiveGateName)+"."+dk.Namespace) + } + + return strings.Join(noProxyValues, ",") +} diff --git a/pkg/controllers/dynakube/otelc/statefulset/env_test.go b/pkg/controllers/dynakube/otelc/statefulset/env_test.go new file mode 100644 index 0000000000..a90e6bc274 --- /dev/null +++ b/pkg/controllers/dynakube/otelc/statefulset/env_test.go @@ -0,0 +1,346 @@ +package statefulset + +import ( + "fmt" + "testing" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/value" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/telemetryingest" + "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" + "github.com/Dynatrace/dynatrace-operator/pkg/consts" + otelcConsts "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/otelc/consts" + "github.com/stretchr/testify/assert" + "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" +) + +func TestEnvironmentVariables(t *testing.T) { + t.Run("environment variables with Extensions enabled", func(t *testing.T) { + dk := getTestDynakubeWithExtensions() + + statefulSet := getStatefulset(t, dk) + + assert.Equal(t, corev1.EnvVar{Name: envShards, Value: fmt.Sprintf("%d", getReplicas(dk))}, statefulSet.Spec.Template.Spec.Containers[0].Env[0]) + assert.Equal(t, corev1.EnvVar{Name: envPodNamePrefix, Value: dk.Name + "-otel-collector"}, statefulSet.Spec.Template.Spec.Containers[0].Env[1]) + assert.Equal(t, corev1.EnvVar{Name: envPodName, ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.labels['statefulset.kubernetes.io/pod-name']", + }, + }}, statefulSet.Spec.Template.Spec.Containers[0].Env[2]) + assert.Equal(t, corev1.EnvVar{Name: envShardId, ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.labels['apps.kubernetes.io/pod-index']", + }, + }}, statefulSet.Spec.Template.Spec.Containers[0].Env[3]) + assert.Equal(t, corev1.EnvVar{Name: envOTLPgrpcPort, Value: defaultOLTPgrpcPort}, statefulSet.Spec.Template.Spec.Containers[0].Env[4]) + assert.Equal(t, corev1.EnvVar{Name: envOTLPhttpPort, Value: defaultOLTPhttpPort}, statefulSet.Spec.Template.Spec.Containers[0].Env[5]) + assert.Equal(t, corev1.EnvVar{Name: envK8sClusterName, Value: dk.Name}, statefulSet.Spec.Template.Spec.Containers[0].Env[6]) + assert.Equal(t, corev1.EnvVar{Name: envK8sClusterUid, Value: dk.Status.KubeSystemUUID}, statefulSet.Spec.Template.Spec.Containers[0].Env[7]) + assert.Equal(t, corev1.EnvVar{Name: envDTentityK8sCluster, Value: dk.Status.KubernetesClusterMEID}, statefulSet.Spec.Template.Spec.Containers[0].Env[8]) + assert.Equal(t, corev1.EnvVar{Name: envEECDStoken, ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: dk.ExtensionsTokenSecretName()}, + Key: consts.OtelcTokenSecretKey, + }, + }}, statefulSet.Spec.Template.Spec.Containers[0].Env[9]) + assert.Equal(t, corev1.EnvVar{Name: envCertDir, Value: customEecTLSCertificatePath}, statefulSet.Spec.Template.Spec.Containers[0].Env[10]) + }) + t.Run("environment variables with trustedCA", func(t *testing.T) { + dk := getTestDynakubeWithExtensions() + dk.Spec.TrustedCAs = "test-trusted-ca" + + statefulSet := getStatefulset(t, dk) + + assert.Contains(t, statefulSet.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{Name: envTrustedCAs, Value: otelcConsts.TrustedCAVolumePath}) + }) + t.Run("environment variables with custom EEC TLS certificate", func(t *testing.T) { + dk := getTestDynakubeWithExtensions() + dk.Spec.Templates.ExtensionExecutionController.TlsRefName = "test-tls-ca" + + statefulSet := getStatefulset(t, dk) + + assert.Contains(t, statefulSet.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{Name: envEECcontrollerTLS, Value: customEecTLSCertificateFullPath}) + }) + t.Run("environment variables for open signal configuration", func(t *testing.T) { + dk := getTestDynakube() + dk.Spec.TelemetryIngest = &telemetryingest.Spec{} + + statefulSet := getWorkload(t, dk) + + assert.Len(t, statefulSet.Spec.Template.Spec.Containers[0].Env, 12) + + assert.Equal(t, corev1.EnvVar{Name: envShards, Value: fmt.Sprintf("%d", getReplicas(dk))}, statefulSet.Spec.Template.Spec.Containers[0].Env[0]) + assert.Equal(t, corev1.EnvVar{Name: envPodNamePrefix, Value: dk.Name + "-otel-collector"}, statefulSet.Spec.Template.Spec.Containers[0].Env[1]) + assert.Equal(t, corev1.EnvVar{Name: envPodName, ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.labels['statefulset.kubernetes.io/pod-name']", + }, + }}, statefulSet.Spec.Template.Spec.Containers[0].Env[2]) + assert.Equal(t, corev1.EnvVar{Name: envShardId, ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.labels['apps.kubernetes.io/pod-index']", + }, + }}, statefulSet.Spec.Template.Spec.Containers[0].Env[3]) + assert.Equal(t, corev1.EnvVar{Name: envOTLPgrpcPort, Value: defaultOLTPgrpcPort}, statefulSet.Spec.Template.Spec.Containers[0].Env[4]) + assert.Equal(t, corev1.EnvVar{Name: envOTLPhttpPort, Value: defaultOLTPhttpPort}, statefulSet.Spec.Template.Spec.Containers[0].Env[5]) + assert.Equal(t, corev1.EnvVar{Name: envK8sClusterName, Value: dk.Name}, statefulSet.Spec.Template.Spec.Containers[0].Env[6]) + assert.Equal(t, corev1.EnvVar{Name: envK8sClusterUid, Value: dk.Status.KubeSystemUUID}, statefulSet.Spec.Template.Spec.Containers[0].Env[7]) + assert.Equal(t, corev1.EnvVar{Name: envDTentityK8sCluster, Value: dk.Status.KubernetesClusterMEID}, statefulSet.Spec.Template.Spec.Containers[0].Env[8]) + + assert.Equal(t, corev1.EnvVar{Name: envDTendpoint, ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: otelcConsts.OtlpApiEndpointConfigMapName}, + Key: envDTendpoint, + }, + }}, statefulSet.Spec.Template.Spec.Containers[0].Env[9]) + assert.Equal(t, corev1.EnvVar{Name: envMyPodIP, ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "status.podIP", + }, + }}, statefulSet.Spec.Template.Spec.Containers[0].Env[10]) + + assert.Equal(t, corev1.EnvVar{Name: otelcConsts.EnvDataIngestToken, ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: dk.Tokens()}, + Key: dynatrace.DataIngestToken, + }, + }}, statefulSet.Spec.Template.Spec.Containers[0].Env[11]) + }) +} + +func TestProxyEnvsNoProxy(t *testing.T) { + tests := []struct { + name string + extensions *dynakube.ExtensionsSpec + telemetryIngest *telemetryingest.Spec + activeGate *activegate.Spec + }{ + { + name: "extensions without proxy", + extensions: &dynakube.ExtensionsSpec{}, + telemetryIngest: nil, + }, + { + name: "telemetryIngest, public AG, without proxy", + extensions: nil, + telemetryIngest: &telemetryingest.Spec{}, + activeGate: nil, + }, + { + name: "telemetryIngest, local AG, without proxy", + extensions: nil, + telemetryIngest: &telemetryingest.Spec{}, + activeGate: getProxyTestActiveGate(), + }, + { + name: "telemetryIngest, extensions, local AG, without proxy", + extensions: &dynakube.ExtensionsSpec{}, + telemetryIngest: &telemetryingest.Spec{}, + activeGate: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dk := getTestDynakube() + dk.Spec.Extensions = tt.extensions + dk.Spec.TelemetryIngest = tt.telemetryIngest + dk.Spec.Proxy = nil + + if tt.activeGate != nil { + dk.Spec.ActiveGate = *tt.activeGate + } + + statefulSet := getWorkload(t, dk) + + assert.NotContains(t, statefulSet.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{Name: envHttpsProxy}) + assert.NotContains(t, statefulSet.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{Name: envHttpProxy}) + assert.NotContains(t, statefulSet.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{Name: envNoProxy}) + }) + } +} + +func TestProxyEnvsProxySecret(t *testing.T) { + const testProxySecretName = "test-proxy-secret" + + tests := []struct { + name string + extensions *dynakube.ExtensionsSpec + telemetryIngest *telemetryingest.Spec + activeGate *activegate.Spec + proxy *value.Source + + expectedNoProxy string + }{ + { + name: "extensions with proxy secret", + extensions: &dynakube.ExtensionsSpec{}, + telemetryIngest: nil, + proxy: &value.Source{ + ValueFrom: testProxySecretName, + }, + expectedNoProxy: "dynakube-extensions-controller.dynatrace,dynakube-activegate.dynatrace", + }, + { + name: "telemetryIngest, public AG, with proxy secret", + extensions: nil, + telemetryIngest: &telemetryingest.Spec{}, + activeGate: nil, + proxy: &value.Source{ + ValueFrom: testProxySecretName, + }, + expectedNoProxy: "", + }, + { + name: "telemetryIngest, local AG, with proxy secret", + extensions: nil, + telemetryIngest: &telemetryingest.Spec{}, + activeGate: getProxyTestActiveGate(), + proxy: &value.Source{ + ValueFrom: testProxySecretName, + }, + expectedNoProxy: "dynakube-activegate.dynatrace", + }, + { + name: "telemetryIngest, extensions, local AG, with proxy secret", + extensions: &dynakube.ExtensionsSpec{}, + telemetryIngest: &telemetryingest.Spec{}, + activeGate: nil, + proxy: &value.Source{ + ValueFrom: testProxySecretName, + }, + expectedNoProxy: "dynakube-extensions-controller.dynatrace,dynakube-activegate.dynatrace", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dk := getTestDynakube() + dk.Spec.Extensions = tt.extensions + dk.Spec.TelemetryIngest = tt.telemetryIngest + dk.Spec.Proxy = tt.proxy + + if tt.activeGate != nil { + dk.Spec.ActiveGate = *tt.activeGate + } + + statefulSet := getWorkload(t, dk) + + assert.Contains(t, statefulSet.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{ + Name: envHttpsProxy, + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: tt.proxy.ValueFrom}, + Key: dynakube.ProxyKey, + }, + }, + }) + assert.Contains(t, statefulSet.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{ + Name: envHttpProxy, + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: tt.proxy.ValueFrom}, + Key: dynakube.ProxyKey, + }, + }, + }) + assert.Contains(t, statefulSet.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{Name: envNoProxy, Value: tt.expectedNoProxy}) + }) + } +} + +func TestProxyEnvsProxyValue(t *testing.T) { + const testProxyValue = "http://test.proxy.com:8888" + + tests := []struct { + name string + extensions *dynakube.ExtensionsSpec + telemetryIngest *telemetryingest.Spec + activeGate *activegate.Spec + proxy *value.Source + + expectedNoProxy string + }{ + { + name: "extensions with proxy value", + extensions: &dynakube.ExtensionsSpec{}, + telemetryIngest: nil, + proxy: &value.Source{ + Value: testProxyValue, + }, + expectedNoProxy: "dynakube-extensions-controller.dynatrace,dynakube-activegate.dynatrace", + }, + { + name: "telemetryIngest, public AG, with proxy value", + extensions: nil, + telemetryIngest: &telemetryingest.Spec{}, + activeGate: nil, + proxy: &value.Source{ + Value: testProxyValue, + }, + expectedNoProxy: "", + }, + { + name: "telemetryIngest, local AG, with proxy value", + extensions: nil, + telemetryIngest: &telemetryingest.Spec{}, + activeGate: getProxyTestActiveGate(), + proxy: &value.Source{ + Value: testProxyValue, + }, + expectedNoProxy: "dynakube-activegate.dynatrace", + }, + { + name: "telemetryIngest, extensions, local AG, with proxy value", + extensions: &dynakube.ExtensionsSpec{}, + telemetryIngest: &telemetryingest.Spec{}, + activeGate: nil, + proxy: &value.Source{ + Value: testProxyValue, + }, + expectedNoProxy: "dynakube-extensions-controller.dynatrace,dynakube-activegate.dynatrace", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dk := getTestDynakube() + dk.Spec.Extensions = tt.extensions + dk.Spec.TelemetryIngest = tt.telemetryIngest + dk.Spec.Proxy = tt.proxy + + if tt.activeGate != nil { + dk.Spec.ActiveGate = *tt.activeGate + } + + statefulSet := getWorkload(t, dk) + + assert.Contains(t, statefulSet.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{ + Name: envHttpsProxy, + Value: tt.proxy.Value, + }) + assert.Contains(t, statefulSet.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{ + Name: envHttpProxy, + Value: tt.proxy.Value, + }) + + assert.Contains(t, statefulSet.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{Name: envNoProxy, Value: tt.expectedNoProxy}) + }) + } +} + +func getWorkload(t *testing.T, dk *dynakube.DynaKube) *v1.StatefulSet { + dataIngestToken := getTokens(dk.Name, dk.Namespace) + configMap := getConfigConfigMap(dk.Name, dk.Namespace) + + return getStatefulset(t, dk, &dataIngestToken, &configMap) +} + +func getProxyTestActiveGate() *activegate.Spec { + return &activegate.Spec{ + CapabilityProperties: activegate.CapabilityProperties{}, + Capabilities: []activegate.CapabilityDisplayName{"otlp-ingest"}, + UseEphemeralVolume: false, + } +} diff --git a/pkg/controllers/dynakube/otelc/statefulset/reconciler.go b/pkg/controllers/dynakube/otelc/statefulset/reconciler.go new file mode 100644 index 0000000000..385053e9f7 --- /dev/null +++ b/pkg/controllers/dynakube/otelc/statefulset/reconciler.go @@ -0,0 +1,265 @@ +package statefulset + +import ( + "context" + + "github.com/Dynatrace/dynatrace-operator/pkg/api" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/otelc/configuration" + "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/token" + "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" + "github.com/Dynatrace/dynatrace-operator/pkg/util/hasher" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/configmap" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/labels" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/node" + k8ssecret "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/secret" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/statefulset" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/topology" + "github.com/pkg/errors" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + serviceAccountName = "dynatrace-opentelemetry-collector" + annotationTelemetryIngestSecretHash = api.InternalFlagPrefix + "telemetry-ingest-secret-hash" + annotationTelemetryIngestConfigurationConfigMapHash = api.InternalFlagPrefix + "telemetry-ingest-config-hash" +) + +type Reconciler struct { + client client.Client + apiReader client.Reader + dk *dynakube.DynaKube +} + +func NewReconciler(clt client.Client, + apiReader client.Reader, + dk *dynakube.DynaKube) *Reconciler { + return &Reconciler{ + client: clt, + apiReader: apiReader, + dk: dk, + } +} + +func (r *Reconciler) Reconcile(ctx context.Context) error { + if r.dk.IsExtensionsEnabled() || r.dk.TelemetryIngest().IsEnabled() { + return r.createOrUpdateStatefulset(ctx) + } else { // do cleanup or + if meta.FindStatusCondition(*r.dk.Conditions(), conditionType) == nil { + return nil + } + defer meta.RemoveStatusCondition(r.dk.Conditions(), conditionType) + + sts, err := statefulset.Build(r.dk, r.dk.OtelCollectorStatefulsetName(), corev1.Container{}) + if err != nil { + log.Error(err, "could not build "+r.dk.OtelCollectorStatefulsetName()+" during cleanup") + + return err + } + + err = statefulset.Query(r.client, r.apiReader, log).Delete(ctx, sts) + + if err != nil { + log.Error(err, "failed to clean up "+r.dk.OtelCollectorStatefulsetName()+" statufulset") + + return nil + } + + return nil + } +} + +func (r *Reconciler) createOrUpdateStatefulset(ctx context.Context) error { + if r.dk.TelemetryIngest().IsEnabled() { + if !r.checkDataIngestTokenExists(ctx) { + msg := "data ingest token is missing, but it's required for telemetery ingest" + conditions.SetDataIngestTokenMissing(r.dk.Conditions(), dynakube.TokenConditionType, msg) + + log.Error(errors.New(msg), "could not create or update statefulset") + + return nil + } + } + + appLabels := buildAppLabels(r.dk.Name) + + templateAnnotations, err := r.buildTemplateAnnotations(ctx) + if err != nil { + return err + } + + topologySpreadConstraints := topology.MaxOnePerNode(appLabels) + if len(r.dk.Spec.Templates.OpenTelemetryCollector.TopologySpreadConstraints) > 0 { + topologySpreadConstraints = r.dk.Spec.Templates.OpenTelemetryCollector.TopologySpreadConstraints + } + + sts, err := statefulset.Build(r.dk, r.dk.OtelCollectorStatefulsetName(), getContainer(r.dk), + statefulset.SetReplicas(getReplicas(r.dk)), + statefulset.SetPodManagementPolicy(appsv1.ParallelPodManagement), + statefulset.SetAllLabels(appLabels.BuildLabels(), appLabels.BuildMatchLabels(), appLabels.BuildLabels(), r.dk.Spec.Templates.OpenTelemetryCollector.Labels), + statefulset.SetAllAnnotations(nil, templateAnnotations), + statefulset.SetAffinity(buildAffinity()), + statefulset.SetServiceAccount(serviceAccountName), + statefulset.SetTolerations(r.dk.Spec.Templates.OpenTelemetryCollector.Tolerations), + statefulset.SetTopologySpreadConstraints(topologySpreadConstraints), + statefulset.SetSecurityContext(buildPodSecurityContext()), + statefulset.SetRollingUpdateStrategyType(), + setImagePullSecrets(r.dk.ImagePullSecretReferences()), + setVolumes(r.dk), + ) + + if err != nil { + conditions.SetKubeApiError(r.dk.Conditions(), conditionType, err) + + return err + } + + if err := hasher.AddAnnotation(sts); err != nil { + conditions.SetKubeApiError(r.dk.Conditions(), conditionType, err) + + return err + } + + _, err = statefulset.Query(r.client, r.apiReader, log).WithOwner(r.dk).CreateOrUpdate(ctx, sts) + if err != nil { + log.Info("failed to create/update " + r.dk.OtelCollectorStatefulsetName() + " statefulset") + conditions.SetKubeApiError(r.dk.Conditions(), conditionType, err) + + return err + } + + conditions.SetStatefulSetCreated(r.dk.Conditions(), conditionType, sts.Name) + + return nil +} + +func (r *Reconciler) buildTemplateAnnotations(ctx context.Context) (map[string]string, error) { + templateAnnotations := map[string]string{} + + if r.dk.IsExtensionsEnabled() { + if r.dk.Spec.Templates.OpenTelemetryCollector.Annotations != nil { + templateAnnotations = r.dk.Spec.Templates.OpenTelemetryCollector.Annotations + } + + tlsSecretHash, err := r.calculateSecretHash(ctx, r.dk.ExtensionsTLSSecretName()) + if err != nil { + return nil, err + } + + templateAnnotations[api.AnnotationExtensionsSecretHash] = tlsSecretHash + } + + if r.dk.TelemetryIngest().IsEnabled() && r.dk.TelemetryIngest().Spec.TlsRefName != "" { + tlsSecretHash, err := r.calculateSecretHash(ctx, r.dk.TelemetryIngest().Spec.TlsRefName) + if err != nil { + return nil, err + } + + templateAnnotations[annotationTelemetryIngestSecretHash] = tlsSecretHash + } + + if r.dk.TelemetryIngest().IsEnabled() { + configConfigMapHash, err := r.calculateConfigMapHash(ctx, configuration.GetConfigMapName(r.dk.Name)) + if err != nil { + return nil, err + } + + templateAnnotations[annotationTelemetryIngestConfigurationConfigMapHash] = configConfigMapHash + } + + return templateAnnotations, nil +} + +func (r *Reconciler) calculateSecretHash(ctx context.Context, secretName string) (string, error) { + query := k8ssecret.Query(r.client, r.client, log) + + tlsSecret, err := query.Get(ctx, types.NamespacedName{ + Name: secretName, + Namespace: r.dk.Namespace, + }) + if err != nil { + return "", err + } + + tlsSecretHash, err := hasher.GenerateHash(tlsSecret.Data) + if err != nil { + return "", err + } + + return tlsSecretHash, nil +} + +func (r *Reconciler) calculateConfigMapHash(ctx context.Context, configMapName string) (string, error) { + query := configmap.Query(r.client, r.client, log) + + configConfigMap, err := query.Get(ctx, types.NamespacedName{ + Name: configMapName, + Namespace: r.dk.Namespace, + }) + if err != nil { + return "", err + } + + configConfigMaptHash, err := hasher.GenerateHash(configConfigMap.Data) + if err != nil { + return "", err + } + + return configConfigMaptHash, nil +} + +func (r *Reconciler) checkDataIngestTokenExists(ctx context.Context) bool { + tokenReader := token.NewReader(r.apiReader, r.dk) + + tokens, err := tokenReader.ReadTokens(ctx) + if err != nil { + return false + } + + return token.CheckForDataIngestToken(tokens) +} + +func getReplicas(dk *dynakube.DynaKube) int32 { + if dk.Spec.Templates.OpenTelemetryCollector.Replicas != nil { + return *dk.Spec.Templates.OpenTelemetryCollector.Replicas + } + + return defaultReplicas +} + +func buildSecurityContext() *corev1.SecurityContext { + return &corev1.SecurityContext{ + SeccompProfile: &corev1.SeccompProfile{ + Type: corev1.SeccompProfileTypeRuntimeDefault, + }, + } +} + +func buildPodSecurityContext() *corev1.PodSecurityContext { + return &corev1.PodSecurityContext{ + SeccompProfile: &corev1.SeccompProfile{ + Type: corev1.SeccompProfileTypeRuntimeDefault, + }, + } +} + +func buildAppLabels(dkName string) *labels.AppLabels { + // TODO: when version is available + version := "0.0.0" + + return labels.NewAppLabels(labels.OtelCComponentLabel, dkName, labels.OtelCComponentLabel, version) +} + +func buildAffinity() corev1.Affinity { + return node.Affinity() +} + +func setImagePullSecrets(imagePullSecrets []corev1.LocalObjectReference) func(o *appsv1.StatefulSet) { + return func(o *appsv1.StatefulSet) { + o.Spec.Template.Spec.ImagePullSecrets = imagePullSecrets + } +} diff --git a/pkg/controllers/dynakube/otelc/statefulset/reconciler_test.go b/pkg/controllers/dynakube/otelc/statefulset/reconciler_test.go new file mode 100644 index 0000000000..f526c1c39a --- /dev/null +++ b/pkg/controllers/dynakube/otelc/statefulset/reconciler_test.go @@ -0,0 +1,445 @@ +package statefulset + +import ( + "context" + "testing" + + "github.com/Dynatrace/dynatrace-operator/pkg/api" + "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" + "github.com/Dynatrace/dynatrace-operator/pkg/consts" + otelcconsts "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/otelc/consts" + "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/node" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/topology" + maputils "github.com/Dynatrace/dynatrace-operator/pkg/util/map" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + testDynakubeName = "dynakube" + testNamespaceName = "dynatrace" + testOtelPullSecret = "otelc-pull-secret" + testTelemetryIngestSecret = "test-ts-secret" +) + +func TestReconcile(t *testing.T) { + ctx := context.Background() + + t.Run("Create and update works with minimal setup", func(t *testing.T) { + dk := getTestDynakubeWithExtensions() + + mockK8sClient := fake.NewClient() + mockK8sClient = mockTLSSecret(t, mockK8sClient, dk) + + reconciler := NewReconciler(mockK8sClient, + mockK8sClient, dk) + err := reconciler.Reconcile(ctx) + require.NoError(t, err) + + condition := meta.FindStatusCondition(*dk.Conditions(), conditionType) + oldTransitionTime := condition.LastTransitionTime + require.NotNil(t, condition) + require.NotEmpty(t, oldTransitionTime) + assert.Equal(t, conditions.StatefulSetCreatedReason, condition.Reason) + assert.Equal(t, metav1.ConditionTrue, condition.Status) + + err = reconciler.Reconcile(context.Background()) + require.NoError(t, err) + + var sts appsv1.StatefulSet + err = mockK8sClient.Get(ctx, types.NamespacedName{ + Name: dk.OtelCollectorStatefulsetName(), + Namespace: dk.Namespace, + }, &sts) + require.False(t, k8serrors.IsNotFound(err)) + assert.NotEmpty(t, sts) + }) + t.Run("Only runs when required, and cleans up condition + statefulset", func(t *testing.T) { + dk := getTestDynakubeWithExtensions() + dk.Spec.Extensions = nil + + previousSts := appsv1.StatefulSet{} + previousSts.Name = dk.OtelCollectorStatefulsetName() + previousSts.Namespace = dk.Namespace + mockK8sClient := fake.NewClient(&previousSts) + mockK8sClient = mockTLSSecret(t, mockK8sClient, dk) + + conditions.SetStatefulSetCreated(dk.Conditions(), conditionType, "this is a test") + + reconciler := NewReconciler(mockK8sClient, mockK8sClient, dk) + err := reconciler.Reconcile(ctx) + + require.NoError(t, err) + assert.Empty(t, *dk.Conditions()) + + var sts appsv1.StatefulSet + err = mockK8sClient.Get(ctx, types.NamespacedName{ + Name: dk.OtelCollectorStatefulsetName(), + Namespace: dk.Namespace, + }, &sts) + require.True(t, k8serrors.IsNotFound(err)) + }) +} + +func TestSecretHashAnnotation(t *testing.T) { + t.Run("annotation is set with self-signed tls secret", func(t *testing.T) { + dk := getTestDynakubeWithExtensions() + dk.Spec.Templates.ExtensionExecutionController.TlsRefName = "" + statefulSet := getStatefulset(t, dk) + + require.Len(t, statefulSet.Spec.Template.Annotations, 1) + assert.NotEmpty(t, statefulSet.Spec.Template.Annotations[api.AnnotationExtensionsSecretHash]) + }) + t.Run("annotation is set with tlsRefName", func(t *testing.T) { + dk := getTestDynakubeWithExtensions() + dk.Spec.Templates.ExtensionExecutionController.TlsRefName = "dummy-secret" + statefulSet := getStatefulset(t, dk) + + require.Len(t, statefulSet.Spec.Template.Annotations, 1) + assert.NotEmpty(t, statefulSet.Spec.Template.Annotations[api.AnnotationExtensionsSecretHash]) + }) + t.Run("annotation is updated when TLS Secret gets updated", func(t *testing.T) { + statefulSet := &appsv1.StatefulSet{} + dk := getTestDynakubeWithExtensions() + + // first reconcile a basic setup - TLS Secret gets created + mockK8sClient := fake.NewClient(dk) + mockK8sClient = mockTLSSecret(t, mockK8sClient, dk) + + reconciler := NewReconciler(mockK8sClient, mockK8sClient, dk) + err := reconciler.Reconcile(context.Background()) + require.NoError(t, err) + + err = mockK8sClient.Get(context.Background(), client.ObjectKey{Name: dk.OtelCollectorStatefulsetName(), Namespace: dk.Namespace}, statefulSet) + require.NoError(t, err) + + originalSecretHash := statefulSet.Spec.Template.Annotations[api.AnnotationExtensionsSecretHash] + + // then update the TLS Secret and call reconcile again + updatedTLSSecret := getTLSSecret(dk.ExtensionsTLSSecretName(), dk.Namespace, "updated-cert", "updated-key") + err = mockK8sClient.Update(context.Background(), &updatedTLSSecret) + require.NoError(t, err) + + err = reconciler.Reconcile(context.Background()) + require.NoError(t, err) + err = mockK8sClient.Get(context.Background(), client.ObjectKey{Name: dk.OtelCollectorStatefulsetName(), Namespace: dk.Namespace}, statefulSet) + require.NoError(t, err) + + resultingSecretHash := statefulSet.Spec.Template.Annotations[api.AnnotationExtensionsSecretHash] + + // original hash and resulting hash should be different, value got updated on reconcile + assert.NotEqual(t, originalSecretHash, resultingSecretHash) + }) +} + +func TestStatefulsetBase(t *testing.T) { + t.Run("replicas", func(t *testing.T) { + statefulSet := getStatefulset(t, getTestDynakubeWithExtensions()) + + assert.Equal(t, int32(1), *statefulSet.Spec.Replicas) + }) + + t.Run("pod management policy", func(t *testing.T) { + statefulSet := getStatefulset(t, getTestDynakubeWithExtensions()) + + assert.Equal(t, appsv1.ParallelPodManagement, statefulSet.Spec.PodManagementPolicy) + }) +} + +func TestServiceAccountName(t *testing.T) { + t.Run("serviceAccountName is set", func(t *testing.T) { + statefulSet := getStatefulset(t, getTestDynakubeWithExtensions()) + + assert.Equal(t, serviceAccountName, statefulSet.Spec.Template.Spec.ServiceAccountName) + assert.Equal(t, serviceAccountName, statefulSet.Spec.Template.Spec.DeprecatedServiceAccount) + }) +} + +func TestTopologySpreadConstraints(t *testing.T) { + t.Run("the default TopologySpreadConstraints", func(t *testing.T) { + dk := getTestDynakubeWithExtensions() + statefulSet := getStatefulset(t, dk) + appLabels := buildAppLabels(dk.Name) + assert.Equal(t, topology.MaxOnePerNode(appLabels), statefulSet.Spec.Template.Spec.TopologySpreadConstraints) + }) + + t.Run("custom TopologySpreadConstraints", func(t *testing.T) { + dk := getTestDynakubeWithExtensions() + + customTopologySpreadConstraints := []corev1.TopologySpreadConstraint{ + { + MaxSkew: 2, + TopologyKey: "kubernetes.io/hostname", + WhenUnsatisfiable: "DoNotSchedule", + LabelSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "a": "b", + }, + }, + }, + } + + dk.Spec.Templates.OpenTelemetryCollector.TopologySpreadConstraints = customTopologySpreadConstraints + + statefulSet := getStatefulset(t, dk) + + assert.Equal(t, customTopologySpreadConstraints, statefulSet.Spec.Template.Spec.TopologySpreadConstraints) + }) +} + +func TestAffinity(t *testing.T) { + t.Run("affinity", func(t *testing.T) { + dk := getTestDynakubeWithExtensions() + statefulSet := getStatefulset(t, dk) + + expectedAffinity := node.Affinity() + + assert.Equal(t, expectedAffinity, *statefulSet.Spec.Template.Spec.Affinity) + }) +} + +func TestImagePullSecrets(t *testing.T) { + t.Run("the default image pull secret only", func(t *testing.T) { + statefulSet := getStatefulset(t, getTestDynakubeWithExtensions()) + + assert.Len(t, statefulSet.Spec.Template.Spec.ImagePullSecrets, 1) + }) + + t.Run("custom pull secret", func(t *testing.T) { + dk := getTestDynakubeWithExtensions() + dk.Spec.CustomPullSecret = testOtelPullSecret + + statefulSet := getStatefulset(t, dk) + + require.Len(t, statefulSet.Spec.Template.Spec.ImagePullSecrets, 2) + assert.Equal(t, dk.Name+dynakube.PullSecretSuffix, statefulSet.Spec.Template.Spec.ImagePullSecrets[0].Name) + assert.Equal(t, dk.Spec.CustomPullSecret, statefulSet.Spec.Template.Spec.ImagePullSecrets[1].Name) + }) +} + +func TestResources(t *testing.T) { + t.Run("no resources", func(t *testing.T) { + dk := getTestDynakubeWithExtensions() + statefulSet := getStatefulset(t, dk) + + assert.Empty(t, statefulSet.Spec.Template.Spec.Containers[0].Resources) + }) + + t.Run("custom resources", func(t *testing.T) { + dk := getTestDynakubeWithExtensions() + dk.Spec.Templates.ExtensionExecutionController.Resources = corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("1Gi"), + }, + } + + statefulSet := getStatefulset(t, dk) + + assert.Equal(t, dk.Spec.Templates.OpenTelemetryCollector.Resources, statefulSet.Spec.Template.Spec.Containers[0].Resources) + }) +} + +func TestLabels(t *testing.T) { + t.Run("the default labels", func(t *testing.T) { + dk := getTestDynakubeWithExtensions() + + statefulSet := getStatefulset(t, dk) + + appLabels := buildAppLabels(dk.Name) + + assert.Equal(t, appLabels.BuildLabels(), statefulSet.ObjectMeta.Labels) + assert.Equal(t, &metav1.LabelSelector{MatchLabels: appLabels.BuildMatchLabels()}, statefulSet.Spec.Selector) + assert.Equal(t, appLabels.BuildLabels(), statefulSet.Spec.Template.ObjectMeta.Labels) + }) + + t.Run("custom labels", func(t *testing.T) { + dk := getTestDynakubeWithExtensions() + customLabels := map[string]string{ + "a": "b", + } + dk.Spec.Templates.OpenTelemetryCollector.Labels = customLabels + + statefulSet := getStatefulset(t, dk) + + appLabels := buildAppLabels(dk.Name) + podLabels := maputils.MergeMap(customLabels, appLabels.BuildLabels()) + + assert.Equal(t, appLabels.BuildLabels(), statefulSet.ObjectMeta.Labels) + assert.Equal(t, &metav1.LabelSelector{MatchLabels: appLabels.BuildMatchLabels()}, statefulSet.Spec.Selector) + assert.Equal(t, podLabels, statefulSet.Spec.Template.ObjectMeta.Labels) + }) +} + +func TestAnnotations(t *testing.T) { + t.Run("the default annotations", func(t *testing.T) { + statefulSet := getStatefulset(t, getTestDynakubeWithExtensions()) + + assert.Len(t, statefulSet.ObjectMeta.Annotations, 1) + require.Len(t, statefulSet.Spec.Template.ObjectMeta.Annotations, 1) + assert.NotEmpty(t, statefulSet.Spec.Template.ObjectMeta.Annotations[api.AnnotationExtensionsSecretHash]) + }) + + t.Run("custom annotations", func(t *testing.T) { + dk := getTestDynakubeWithExtensions() + customAnnotations := map[string]string{ + "a": "b", + } + dk.Spec.Templates.OpenTelemetryCollector.Annotations = customAnnotations + + statefulSet := getStatefulset(t, dk) + + require.Len(t, statefulSet.ObjectMeta.Annotations, 1) + assert.Empty(t, statefulSet.ObjectMeta.Annotations["a"]) + require.Len(t, statefulSet.Spec.Template.ObjectMeta.Annotations, 2) + assert.Equal(t, "b", statefulSet.Spec.Template.ObjectMeta.Annotations["a"]) + assert.NotEmpty(t, statefulSet.Spec.Template.ObjectMeta.Annotations[api.AnnotationExtensionsSecretHash]) + }) +} + +func TestTolerations(t *testing.T) { + t.Run("the default tolerations", func(t *testing.T) { + statefulSet := getStatefulset(t, getTestDynakubeWithExtensions()) + + assert.Empty(t, statefulSet.Spec.Template.Spec.Tolerations) + }) + + t.Run("custom tolerations", func(t *testing.T) { + dk := getTestDynakubeWithExtensions() + + customTolerations := []corev1.Toleration{ + { + Key: "a", + Operator: corev1.TolerationOpEqual, + Value: "b", + Effect: corev1.TaintEffectNoSchedule, + }, + } + dk.Spec.Templates.OpenTelemetryCollector.Tolerations = customTolerations + + statefulSet := getStatefulset(t, dk) + + assert.Equal(t, customTolerations, statefulSet.Spec.Template.Spec.Tolerations) + }) +} + +func TestSecurityContext(t *testing.T) { + t.Run("the default securityContext is set", func(t *testing.T) { + statefulSet := getStatefulset(t, getTestDynakubeWithExtensions()) + + assert.NotNil(t, statefulSet.Spec.Template.Spec.SecurityContext) + assert.NotNil(t, statefulSet.Spec.Template.Spec.Containers[0].SecurityContext) + }) +} + +func TestUpdateStrategy(t *testing.T) { + t.Run("the default update strategy is set", func(t *testing.T) { + statefulSet := getStatefulset(t, getTestDynakubeWithExtensions()) + + assert.NotNil(t, statefulSet.Spec.UpdateStrategy.RollingUpdate.Partition) + assert.NotEmpty(t, statefulSet.Spec.UpdateStrategy.Type) + }) +} + +func getTestDynakubeWithExtensions() *dynakube.DynaKube { + return &dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Name: testDynakubeName, + Namespace: testNamespaceName, + Annotations: map[string]string{}, + }, + Spec: dynakube.DynaKubeSpec{ + Extensions: &dynakube.ExtensionsSpec{}, + Templates: dynakube.TemplatesSpec{OpenTelemetryCollector: dynakube.OpenTelemetryCollectorSpec{}}, + }, + } +} + +func getTestDynakube() *dynakube.DynaKube { + dk := &dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Name: testDynakubeName, + Namespace: testNamespaceName, + Annotations: map[string]string{}, + }, + Spec: dynakube.DynaKubeSpec{}, + } + + return dk +} + +func getStatefulset(t *testing.T, dk *dynakube.DynaKube, objs ...client.Object) *appsv1.StatefulSet { + mockK8sClient := fake.NewClient(dk) + mockK8sClient = mockTLSSecret(t, mockK8sClient, dk) + + for _, obj := range objs { + err := mockK8sClient.Create(context.Background(), obj) + require.NoError(t, err) + } + + err := NewReconciler(mockK8sClient, mockK8sClient, dk).Reconcile(context.Background()) + require.NoError(t, err) + + statefulSet := &appsv1.StatefulSet{} + err = mockK8sClient.Get(context.Background(), client.ObjectKey{Name: dk.OtelCollectorStatefulsetName(), Namespace: dk.Namespace}, statefulSet) + require.NoError(t, err) + + return statefulSet +} + +func mockTLSSecret(t *testing.T, client client.Client, dk *dynakube.DynaKube) client.Client { + tlsSecret := getTLSSecret(dk.ExtensionsTLSSecretName(), dk.Namespace, "super-cert", "super-key") + + err := client.Create(context.Background(), &tlsSecret) + require.NoError(t, err) + + return client +} + +func getTokens(name string, namespace string) corev1.Secret { + return corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Data: map[string][]byte{ + dtclient.ApiToken: []byte("test"), + dtclient.DataIngestToken: []byte("test"), + }, + } +} + +func getTLSSecret(name string, namespace string, crt string, key string) corev1.Secret { + return corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Data: map[string][]byte{ + consts.TLSCrtDataName: []byte(crt), + consts.TLSKeyDataName: []byte(key), + }, + } +} + +func getConfigConfigMap(name string, namespace string) corev1.ConfigMap { + return corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: name + otelcconsts.TelemetryCollectorConfigmapSuffix, + Namespace: namespace, + }, + Data: map[string]string{ + otelcconsts.ConfigFieldName: "test", + }, + } +} diff --git a/pkg/controllers/dynakube/otelc/statefulset/volumes.go b/pkg/controllers/dynakube/otelc/statefulset/volumes.go new file mode 100644 index 0000000000..a145905c0a --- /dev/null +++ b/pkg/controllers/dynakube/otelc/statefulset/volumes.go @@ -0,0 +1,191 @@ +package statefulset + +import ( + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/consts" + "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/otelc/configuration" + otelcconsts "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/otelc/consts" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/utils/ptr" +) + +const ( + // Volume names and paths + caCertsVolumeName = "cacerts" + agCertVolumeName = "agcert" + + customTlsCertVolumeName = "telemetry-ingest-custom-tls" + extensionsControllerTLSVolumeName = "extensions-controller-tls" + telemetryCollectorConfigVolumeName = "telemetry-collector-config" + telemetryCollectorConfigPath = "/config" +) + +func setVolumes(dk *dynakube.DynaKube) func(o *appsv1.StatefulSet) { + var volumes []corev1.Volume + + if dk.IsExtensionsEnabled() { + volumes = append( + volumes, + corev1.Volume{ + Name: consts.ExtensionsTokensVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: dk.ExtensionsTokenSecretName(), + Items: []corev1.KeyToPath{ + { + Key: consts.OtelcTokenSecretKey, + Path: consts.OtelcTokenSecretKey, + }, + }, + DefaultMode: ptr.To(int32(420)), + }, + }, + }, + corev1.Volume{ + Name: extensionsControllerTLSVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: dk.ExtensionsTLSSecretName(), + Items: []corev1.KeyToPath{ + { + Key: consts.TLSCrtDataName, + Path: consts.TLSCrtDataName, + }, + }, + }, + }, + }, + ) + } + + if isTrustedCAsVolumeNeeded(dk) { + volumes = append(volumes, corev1.Volume{ + Name: caCertsVolumeName, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: dk.Spec.TrustedCAs, + }, + Items: []corev1.KeyToPath{ + { + Key: "certs", + Path: otelcconsts.TrustedCAsFile, + }, + }, + }, + }, + }) + } + + if dk.TelemetryIngest().IsEnabled() { + if dk.IsAGCertificateNeeded() { + volumes = append(volumes, corev1.Volume{ + Name: agCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: dk.ActiveGate().GetTLSSecretName(), + Items: []corev1.KeyToPath{ + { + Key: dynakube.TLSCertKey, + Path: otelcconsts.ActiveGateCertFile, + }, + }, + }, + }, + }) + } + + if dk.TelemetryIngest().Spec.TlsRefName != "" { + volumes = append(volumes, corev1.Volume{ + Name: customTlsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: dk.TelemetryIngest().Spec.TlsRefName, + Items: []corev1.KeyToPath{ + { + Key: consts.TLSCrtDataName, + Path: consts.TLSCrtDataName, + }, + { + Key: consts.TLSKeyDataName, + Path: consts.TLSKeyDataName, + }, + }, + }, + }, + }) + } + + volumes = append(volumes, corev1.Volume{ + Name: telemetryCollectorConfigVolumeName, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: configuration.GetConfigMapName(dk.Name), + }, + }, + }, + }) + } + + return func(o *appsv1.StatefulSet) { + o.Spec.Template.Spec.Volumes = volumes + } +} + +func buildContainerVolumeMounts(dk *dynakube.DynaKube) []corev1.VolumeMount { + var vm []corev1.VolumeMount + + if dk.IsExtensionsEnabled() { + vm = append( + vm, + corev1.VolumeMount{ + Name: consts.ExtensionsTokensVolumeName, ReadOnly: true, MountPath: secretsTokensPath, + }, + corev1.VolumeMount{ + Name: extensionsControllerTLSVolumeName, + MountPath: customEecTLSCertificatePath, + ReadOnly: true, + }, + ) + } + + if isTrustedCAsVolumeNeeded(dk) { + vm = append(vm, corev1.VolumeMount{ + Name: caCertsVolumeName, + MountPath: otelcconsts.TrustedCAVolumeMountPath, + ReadOnly: true, + }) + } + + if dk.TelemetryIngest().IsEnabled() { + if dk.IsAGCertificateNeeded() { + vm = append(vm, corev1.VolumeMount{ + Name: agCertVolumeName, + MountPath: otelcconsts.ActiveGateTLSCertCAVolumeMountPath, + ReadOnly: true, + }) + } + + if dk.TelemetryIngest().Spec.TlsRefName != "" { + vm = append(vm, corev1.VolumeMount{ + Name: customTlsCertVolumeName, + MountPath: otelcconsts.CustomTlsCertMountPath, + ReadOnly: true, + }) + } + + vm = append(vm, corev1.VolumeMount{ + Name: telemetryCollectorConfigVolumeName, + MountPath: telemetryCollectorConfigPath, + ReadOnly: true, + }) + } + + return vm +} + +func isTrustedCAsVolumeNeeded(dk *dynakube.DynaKube) bool { + return dk.IsExtensionsEnabled() && dk.Spec.TrustedCAs != "" || dk.TelemetryIngest().IsEnabled() && dk.IsCACertificateNeeded() +} diff --git a/pkg/controllers/dynakube/otelc/statefulset/volumes_test.go b/pkg/controllers/dynakube/otelc/statefulset/volumes_test.go new file mode 100644 index 0000000000..82c9a5284c --- /dev/null +++ b/pkg/controllers/dynakube/otelc/statefulset/volumes_test.go @@ -0,0 +1,407 @@ +package statefulset + +import ( + "testing" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/telemetryingest" + "github.com/Dynatrace/dynatrace-operator/pkg/consts" + otelcconsts "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/otelc/consts" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" +) + +func TestVolumes(t *testing.T) { + t.Run("volume mounts with trusted CAs", func(t *testing.T) { + dk := getTestDynakubeWithExtensions() + dk.Spec.TrustedCAs = "test-trusted-cas" + statefulSet := getStatefulset(t, dk) + + assert.Contains(t, statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts, trustedCAsVolumeMount()) + }) + t.Run("volume mounts without trusted CAs", func(t *testing.T) { + dk := getTestDynakubeWithExtensions() + statefulSet := getStatefulset(t, dk) + + assert.NotContains(t, statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts, trustedCAsVolumeMount()) + }) + t.Run("volumes and volume mounts with custom EEC TLS certificate", func(t *testing.T) { + dk := getTestDynakubeWithExtensions() + dk.Spec.Templates.ExtensionExecutionController.TlsRefName = "test-tls-name" + statefulSet := getStatefulset(t, dk) + + expectedVolumeMount := corev1.VolumeMount{ + Name: extensionsControllerTLSVolumeName, + MountPath: customEecTLSCertificatePath, + ReadOnly: true, + } + + expectedVolumes := []corev1.Volume{ + { + Name: consts.ExtensionsTokensVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: dk.ExtensionsTokenSecretName(), + Items: []corev1.KeyToPath{ + { + Key: consts.OtelcTokenSecretKey, + Path: consts.OtelcTokenSecretKey, + }, + }, + DefaultMode: ptr.To(int32(420)), + }, + }, + }, + { + Name: extensionsControllerTLSVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "test-tls-name", + Items: []corev1.KeyToPath{ + { + Key: consts.TLSCrtDataName, + Path: consts.TLSCrtDataName, + }, + }, + }, + }, + }, + } + + assert.Contains(t, statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts, expectedVolumeMount) + assert.Equal(t, expectedVolumes, statefulSet.Spec.Template.Spec.Volumes) + }) + t.Run("volumes with trusted CAs", func(t *testing.T) { + dk := getTestDynakubeWithExtensions() + dk.Spec.TrustedCAs = "test-trusted-cas" + statefulSet := getStatefulset(t, dk) + + assert.Contains(t, statefulSet.Spec.Template.Spec.Volumes, trustedCAsVolume(dk)) + }) + t.Run("volumes with otelc token", func(t *testing.T) { + dk := getTestDynakubeWithExtensions() + statefulSet := getStatefulset(t, dk) + + expectedVolume := corev1.Volume{ + Name: consts.ExtensionsTokensVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: dk.ExtensionsTokenSecretName(), + Items: []corev1.KeyToPath{ + { + Key: consts.OtelcTokenSecretKey, + Path: consts.OtelcTokenSecretKey, + }, + }, + DefaultMode: ptr.To(int32(420)), + }, + }, + } + + assert.Contains(t, statefulSet.Spec.Template.Spec.Volumes, expectedVolume) + }) + + t.Run("volumes and volume mounts with telemetry service custom TLS certificate", func(t *testing.T) { + dk := getTestDynakubeWithExtensions() + dk.Spec.TelemetryIngest = &telemetryingest.Spec{ + TlsRefName: testTelemetryIngestSecret, + } + + tlsSecret := getTLSSecret(dk.TelemetryIngest().Spec.TlsRefName, dk.Namespace, "crt", "key") + dataIngestToken := getTokens(dk.Name, dk.Namespace) + configMap := getConfigConfigMap(dk.Name, dk.Namespace) + + statefulSet := getStatefulset(t, dk, &tlsSecret, &dataIngestToken, &configMap) + + expectedVolume := corev1.Volume{ + Name: customTlsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: dk.TelemetryIngest().Spec.TlsRefName, + Items: []corev1.KeyToPath{ + { + Key: consts.TLSCrtDataName, + Path: consts.TLSCrtDataName, + }, + { + Key: consts.TLSKeyDataName, + Path: consts.TLSKeyDataName, + }, + }, + }, + }, + } + + assert.Contains(t, statefulSet.Spec.Template.Spec.Volumes, expectedVolume) + + expectedVolumeMount := corev1.VolumeMount{ + Name: customTlsCertVolumeName, + MountPath: otelcconsts.CustomTlsCertMountPath, + ReadOnly: true, + } + + assert.Contains(t, statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts, expectedVolumeMount) + }) +} + +func TestVolumesWithTelemetryIngestAndRemoteActiveGate(t *testing.T) { + t.Run("volumes without trusted CAs", func(t *testing.T) { + dk := getTestDynakubeWithTelemetryIngest() + tokensSecret := getTokens(dk.Name, dk.Namespace) + configMap := getConfigConfigMap(dk.Name, dk.Namespace) + statefulSet := getStatefulset(t, dk, &tokensSecret, &configMap) + + assert.NotContains(t, statefulSet.Spec.Template.Spec.Volumes, trustedCAsVolume(dk)) + }) + t.Run("volume mounts without trusted CAs", func(t *testing.T) { + dk := getTestDynakubeWithTelemetryIngest() + tokensSecret := getTokens(dk.Name, dk.Namespace) + configMap := getConfigConfigMap(dk.Name, dk.Namespace) + statefulSet := getStatefulset(t, dk, &tokensSecret, &configMap) + + assert.NotContains(t, statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts, trustedCAsVolumeMount()) + }) + + t.Run("volumes with trusted CAs", func(t *testing.T) { + dk := getTestDynakubeWithTelemetryIngest() + dk.Spec.TrustedCAs = "test-trusted-cas" + tokensSecret := getTokens(dk.Name, dk.Namespace) + configMap := getConfigConfigMap(dk.Name, dk.Namespace) + statefulSet := getStatefulset(t, dk, &tokensSecret, &configMap) + + assert.Contains(t, statefulSet.Spec.Template.Spec.Volumes, trustedCAsVolume(dk)) + }) + t.Run("volume mounts with trusted CAs", func(t *testing.T) { + dk := getTestDynakubeWithTelemetryIngest() + dk.Spec.TrustedCAs = "test-trusted-cas" + tokensSecret := getTokens(dk.Name, dk.Namespace) + configMap := getConfigConfigMap(dk.Name, dk.Namespace) + statefulSet := getStatefulset(t, dk, &tokensSecret, &configMap) + + assert.Contains(t, statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts, trustedCAsVolumeMount()) + }) +} + +func TestVolumesWithTelemetryIngestAndInClusterActiveGate(t *testing.T) { + t.Run("volumes without trusted CAs", func(t *testing.T) { + dk := getTestDynakubeWithTelemetryIngest() + dk.Spec.ActiveGate = activegate.Spec{ + TlsSecretName: "test-ag-cert", + Capabilities: []activegate.CapabilityDisplayName{ + activegate.DynatraceApiCapability.DisplayName, + }, + } + tokensSecret := getTokens(dk.Name, dk.Namespace) + configMap := getConfigConfigMap(dk.Name, dk.Namespace) + statefulSet := getStatefulset(t, dk, &tokensSecret, &configMap) + + assert.NotContains(t, statefulSet.Spec.Template.Spec.Volumes, trustedCAsVolume(dk)) + + assert.Contains(t, statefulSet.Spec.Template.Spec.Volumes, agCertVolume(dk)) + }) + t.Run("volume mounts without trusted CAs", func(t *testing.T) { + dk := getTestDynakubeWithTelemetryIngest() + dk.Spec.ActiveGate = activegate.Spec{ + TlsSecretName: "test-ag-cert", + Capabilities: []activegate.CapabilityDisplayName{ + activegate.DynatraceApiCapability.DisplayName, + }, + } + tokensSecret := getTokens(dk.Name, dk.Namespace) + configMap := getConfigConfigMap(dk.Name, dk.Namespace) + statefulSet := getStatefulset(t, dk, &tokensSecret, &configMap) + + assert.NotContains(t, statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts, trustedCAsVolumeMount()) + + assert.Contains(t, statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts, agCertVolumeMount()) + }) + + t.Run("volumes with trusted CAs", func(t *testing.T) { + dk := getTestDynakubeWithTelemetryIngest() + dk.Spec.ActiveGate = activegate.Spec{ + TlsSecretName: "test-ag-cert", + Capabilities: []activegate.CapabilityDisplayName{ + activegate.DynatraceApiCapability.DisplayName, + }, + } + dk.Spec.TrustedCAs = "test-trusted-cas" + tokensSecret := getTokens(dk.Name, dk.Namespace) + configMap := getConfigConfigMap(dk.Name, dk.Namespace) + statefulSet := getStatefulset(t, dk, &tokensSecret, &configMap) + + assert.NotContains(t, statefulSet.Spec.Template.Spec.Volumes, trustedCAsVolume(dk)) + + assert.Contains(t, statefulSet.Spec.Template.Spec.Volumes, agCertVolume(dk)) + }) + t.Run("volume mounts with trusted CAs", func(t *testing.T) { + dk := getTestDynakubeWithTelemetryIngest() + dk.Spec.ActiveGate = activegate.Spec{ + TlsSecretName: "test-ag-cert", + Capabilities: []activegate.CapabilityDisplayName{ + activegate.DynatraceApiCapability.DisplayName, + }, + } + dk.Spec.TrustedCAs = "test-trusted-cas" + tokensSecret := getTokens(dk.Name, dk.Namespace) + configMap := getConfigConfigMap(dk.Name, dk.Namespace) + statefulSet := getStatefulset(t, dk, &tokensSecret, &configMap) + + assert.NotContains(t, statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts, trustedCAsVolumeMount()) + + assert.Contains(t, statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts, agCertVolumeMount()) + }) +} + +func TestVolumesWithTelemetryIngestAndExtensionsAndInClusterActiveGate(t *testing.T) { + t.Run("volumes without trusted CAs", func(t *testing.T) { + dk := getTestDynakubeWithExtensionsAndTelemetryIngest() + dk.Spec.ActiveGate = activegate.Spec{ + TlsSecretName: "test-ag-cert", + Capabilities: []activegate.CapabilityDisplayName{ + activegate.DynatraceApiCapability.DisplayName, + }, + } + tokensSecret := getTokens(dk.Name, dk.Namespace) + configMap := getConfigConfigMap(dk.Name, dk.Namespace) + statefulSet := getStatefulset(t, dk, &tokensSecret, &configMap) + + assert.NotContains(t, statefulSet.Spec.Template.Spec.Volumes, trustedCAsVolume(dk)) + + assert.Contains(t, statefulSet.Spec.Template.Spec.Volumes, agCertVolume(dk)) + }) + t.Run("volume mounts without trusted CAs", func(t *testing.T) { + dk := getTestDynakubeWithExtensionsAndTelemetryIngest() + dk.Spec.ActiveGate = activegate.Spec{ + TlsSecretName: "test-ag-cert", + Capabilities: []activegate.CapabilityDisplayName{ + activegate.DynatraceApiCapability.DisplayName, + }, + } + tokensSecret := getTokens(dk.Name, dk.Namespace) + configMap := getConfigConfigMap(dk.Name, dk.Namespace) + statefulSet := getStatefulset(t, dk, &tokensSecret, &configMap) + + assert.NotContains(t, statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts, trustedCAsVolumeMount()) + + assert.Contains(t, statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts, agCertVolumeMount()) + }) + + t.Run("volumes with trusted CAs", func(t *testing.T) { + dk := getTestDynakubeWithExtensionsAndTelemetryIngest() + dk.Spec.ActiveGate = activegate.Spec{ + TlsSecretName: "test-ag-cert", + Capabilities: []activegate.CapabilityDisplayName{ + activegate.DynatraceApiCapability.DisplayName, + }, + } + dk.Spec.TrustedCAs = "test-trusted-cas" + tokensSecret := getTokens(dk.Name, dk.Namespace) + configMap := getConfigConfigMap(dk.Name, dk.Namespace) + statefulSet := getStatefulset(t, dk, &tokensSecret, &configMap) + + assert.Contains(t, statefulSet.Spec.Template.Spec.Volumes, trustedCAsVolume(dk)) + + assert.Contains(t, statefulSet.Spec.Template.Spec.Volumes, agCertVolume(dk)) + }) + t.Run("volume mounts with trusted CAs", func(t *testing.T) { + dk := getTestDynakubeWithExtensionsAndTelemetryIngest() + dk.Spec.ActiveGate = activegate.Spec{ + TlsSecretName: "test-ag-cert", + Capabilities: []activegate.CapabilityDisplayName{ + activegate.DynatraceApiCapability.DisplayName, + }, + } + dk.Spec.TrustedCAs = "test-trusted-cas" + tokensSecret := getTokens(dk.Name, dk.Namespace) + configMap := getConfigConfigMap(dk.Name, dk.Namespace) + statefulSet := getStatefulset(t, dk, &tokensSecret, &configMap) + + assert.Contains(t, statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts, trustedCAsVolumeMount()) + + assert.Contains(t, statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts, agCertVolumeMount()) + }) +} + +func trustedCAsVolume(dk *dynakube.DynaKube) corev1.Volume { + return corev1.Volume{ + Name: caCertsVolumeName, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: dk.Spec.TrustedCAs, + }, + Items: []corev1.KeyToPath{ + { + Key: "certs", + Path: otelcconsts.TrustedCAsFile, + }, + }, + }, + }, + } +} + +func trustedCAsVolumeMount() corev1.VolumeMount { + return corev1.VolumeMount{ + Name: caCertsVolumeName, + MountPath: otelcconsts.TrustedCAVolumeMountPath, + ReadOnly: true, + } +} + +func agCertVolume(dk *dynakube.DynaKube) corev1.Volume { + return corev1.Volume{ + Name: agCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: dk.ActiveGate().GetTLSSecretName(), + Items: []corev1.KeyToPath{ + { + Key: dynakube.TLSCertKey, + Path: otelcconsts.ActiveGateCertFile, + }, + }, + }, + }, + } +} + +func agCertVolumeMount() corev1.VolumeMount { + return corev1.VolumeMount{ + Name: agCertVolumeName, + MountPath: otelcconsts.ActiveGateTLSCertCAVolumeMountPath, + ReadOnly: true, + } +} + +func getTestDynakubeWithTelemetryIngest() *dynakube.DynaKube { + return &dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Name: testDynakubeName, + Namespace: testNamespaceName, + Annotations: map[string]string{}, + }, + Spec: dynakube.DynaKubeSpec{ + TelemetryIngest: &telemetryingest.Spec{}, + Templates: dynakube.TemplatesSpec{OpenTelemetryCollector: dynakube.OpenTelemetryCollectorSpec{}}, + }, + } +} + +func getTestDynakubeWithExtensionsAndTelemetryIngest() *dynakube.DynaKube { + return &dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Name: testDynakubeName, + Namespace: testNamespaceName, + Annotations: map[string]string{}, + }, + Spec: dynakube.DynaKubeSpec{ + Extensions: &dynakube.ExtensionsSpec{}, + TelemetryIngest: &telemetryingest.Spec{}, + Templates: dynakube.TemplatesSpec{OpenTelemetryCollector: dynakube.OpenTelemetryCollectorSpec{}}, + }, + } +} diff --git a/pkg/controllers/dynakube/phase.go b/pkg/controllers/dynakube/phase.go index 67409f43ab..3845883150 100644 --- a/pkg/controllers/dynakube/phase.go +++ b/pkg/controllers/dynakube/phase.go @@ -4,7 +4,7 @@ import ( "context" "github.com/Dynatrace/dynatrace-operator/pkg/api/status" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/capability" appsv1 "k8s.io/api/apps/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" @@ -17,6 +17,8 @@ func (controller *Controller) determineDynaKubePhase(dk *dynakube.DynaKube) stat controller.determineExtensionsExecutionControllerPhase, controller.determineExtensionsCollectorPhase, controller.determineOneAgentPhase, + controller.determineLogAgentPhase, + controller.determineKSPMPhase, } for _, component := range components { if phase := component(dk); phase != status.Running { @@ -57,7 +59,7 @@ func (controller *Controller) determineExtensionsExecutionControllerPhase(dk *dy } func (controller *Controller) determineExtensionsCollectorPhase(dk *dynakube.DynaKube) status.DeploymentPhase { - return controller.determinePrometheusStatefulsetPhase(dk, dk.ExtensionsCollectorStatefulsetName()) + return controller.determinePrometheusStatefulsetPhase(dk, dk.OtelCollectorStatefulsetName()) } func (controller *Controller) determinePrometheusStatefulsetPhase(dk *dynakube.DynaKube, statefulsetName string) status.DeploymentPhase { @@ -93,8 +95,8 @@ func (controller *Controller) determinePrometheusStatefulsetPhase(dk *dynakube.D } func (controller *Controller) determineOneAgentPhase(dk *dynakube.DynaKube) status.DeploymentPhase { - if dk.CloudNativeFullstackMode() || dk.ClassicFullStackMode() || dk.HostMonitoringMode() { - oneAgentPods, err := controller.numberOfMissingOneagentPods(dk) + if dk.OneAgent().IsCloudNativeFullstackMode() || dk.OneAgent().IsClassicFullStackMode() || dk.OneAgent().IsHostMonitoringMode() { + oneAgentPods, err := controller.numberOfMissingDaemonSetPods(dk, dk.OneAgent().GetDaemonsetName()) if k8serrors.IsNotFound(err) { log.Info("oneagent daemonset not yet available", "dynakube", dk.Name) @@ -117,16 +119,66 @@ func (controller *Controller) determineOneAgentPhase(dk *dynakube.DynaKube) stat return status.Running } -func (controller *Controller) numberOfMissingOneagentPods(dk *dynakube.DynaKube) (int32, error) { - oneAgentDaemonSet := &appsv1.DaemonSet{} - instanceName := dk.OneAgentDaemonsetName() +func (controller *Controller) determineLogAgentPhase(dk *dynakube.DynaKube) status.DeploymentPhase { + if dk.LogMonitoring().IsStandalone() { + logAgentPods, err := controller.numberOfMissingDaemonSetPods(dk, dk.LogMonitoring().GetDaemonSetName()) + if k8serrors.IsNotFound(err) { + log.Info("logagent daemonset not yet available", "dynakube", dk.Name) + + return status.Deploying + } + + if err != nil { + log.Error(err, "logagent daemonset could not be accessed", "dynakube", dk.Name) + + return status.Error + } + + if logAgentPods > 0 { + log.Info("logagent daemonset is still deploying", "dynakube", dk.Name) + + return status.Deploying + } + } + + return status.Running +} + +func (controller *Controller) determineKSPMPhase(dk *dynakube.DynaKube) status.DeploymentPhase { + if dk.KSPM().IsEnabled() { + kspmPods, err := controller.numberOfMissingDaemonSetPods(dk, dk.KSPM().GetDaemonSetName()) + if k8serrors.IsNotFound(err) { + log.Info("kspm daemonset not yet available", "dynakube", dk.Name) + + return status.Deploying + } + + if err != nil { + log.Error(err, "kspm daemonset could not be accessed", "dynakube", dk.Name) + + return status.Error + } + + if kspmPods > 0 { + log.Info("kspm daemonset is still deploying", "dynakube", dk.Name) + + return status.Deploying + } + } + + return status.Running +} + +func (controller *Controller) numberOfMissingDaemonSetPods(dk *dynakube.DynaKube, dsName string) (int32, error) { + daemonSet := &appsv1.DaemonSet{} + instanceName := dsName - err := controller.client.Get(context.Background(), types.NamespacedName{Name: instanceName, Namespace: dk.Namespace}, oneAgentDaemonSet) + err := controller.client.Get(context.Background(), types.NamespacedName{Name: instanceName, Namespace: dk.Namespace}, daemonSet) if err != nil { return 0, err } - return oneAgentDaemonSet.Status.CurrentNumberScheduled - oneAgentDaemonSet.Status.NumberReady, nil + return daemonSet.Status.CurrentNumberScheduled - daemonSet.Status.NumberReady, nil } func (controller *Controller) numberOfMissingActiveGatePods(dk *dynakube.DynaKube) (int32, error) { diff --git a/pkg/controllers/dynakube/phase_test.go b/pkg/controllers/dynakube/phase_test.go index 46d60d1996..380a600ba7 100644 --- a/pkg/controllers/dynakube/phase_test.go +++ b/pkg/controllers/dynakube/phase_test.go @@ -4,9 +4,13 @@ import ( "testing" "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" + "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/image" "github.com/Dynatrace/dynatrace-operator/pkg/api/status" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/kspm" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/logmonitoring" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" "github.com/stretchr/testify/assert" appsv1 "k8s.io/api/apps/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -99,8 +103,8 @@ func TestOneAgentPhaseChanges(t *testing.T) { Namespace: testNamespace, }, Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - ClassicFullStack: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + ClassicFullStack: &oneagent.HostInjectSpec{}, }, }, } @@ -237,7 +241,7 @@ func TestExtensionsCollectorPhaseChanges(t *testing.T) { assert.Equal(t, status.Error, phase) }) t.Run("otelc pods not ready -> deploying", func(t *testing.T) { - fakeClient := fake.NewClient(createStatefulset(testNamespace, dk.ExtensionsCollectorStatefulsetName(), 2, 1)) + fakeClient := fake.NewClient(createStatefulset(testNamespace, dk.OtelCollectorStatefulsetName(), 2, 1)) controller := &Controller{ client: fakeClient, @@ -247,7 +251,7 @@ func TestExtensionsCollectorPhaseChanges(t *testing.T) { assert.Equal(t, status.Deploying, phase) }) t.Run("otelc deployed -> running", func(t *testing.T) { - fakeClient := fake.NewClient(createStatefulset(testNamespace, dk.ExtensionsCollectorStatefulsetName(), 2, 2)) + fakeClient := fake.NewClient(createStatefulset(testNamespace, dk.OtelCollectorStatefulsetName(), 2, 2)) controller := &Controller{ client: fakeClient, @@ -258,6 +262,120 @@ func TestExtensionsCollectorPhaseChanges(t *testing.T) { }) } +func TestLogAgentPhaseChanges(t *testing.T) { + dk := &dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Name: testName, + Namespace: testNamespace, + }, + Spec: dynakube.DynaKubeSpec{ + LogMonitoring: &logmonitoring.Spec{}, + Templates: dynakube.TemplatesSpec{ + LogMonitoring: &logmonitoring.TemplateSpec{ + ImageRef: image.Ref{ + Repository: "test", + Tag: "test-tag", + }, + }, + }, + }, + } + + t.Run("no LogAgent pods in cluster -> deploying", func(t *testing.T) { + fakeClient := fake.NewClient() + controller := &Controller{ + client: fakeClient, + apiReader: fakeClient, + } + phase := controller.determineDynaKubePhase(dk) + assert.Equal(t, status.Deploying, phase) + }) + t.Run("Error accessing k8s api", func(t *testing.T) { + fakeClient := errorClient{} + controller := &Controller{ + client: fakeClient, + apiReader: fakeClient, + } + phase := controller.determineDynaKubePhase(dk) + assert.Equal(t, status.Error, phase) + }) + t.Run("LogAgent daemonsets in cluster not all ready -> deploying", func(t *testing.T) { + fakeClient := fake.NewClient(createDaemonSet(testNamespace, dk.LogMonitoring().GetDaemonSetName(), 3, 2)) + controller := &Controller{ + client: fakeClient, + apiReader: fakeClient, + } + phase := controller.determineDynaKubePhase(dk) + assert.Equal(t, status.Deploying, phase) + }) + t.Run("LogAgent daemonsets in cluster all ready -> running", func(t *testing.T) { + fakeClient := fake.NewClient(createDaemonSet(testNamespace, dk.LogMonitoring().GetDaemonSetName(), 3, 3)) + controller := &Controller{ + client: fakeClient, + apiReader: fakeClient, + } + phase := controller.determineDynaKubePhase(dk) + assert.Equal(t, status.Running, phase) + }) +} + +func TestKSPMPhaseChanges(t *testing.T) { + dk := &dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Name: testName, + Namespace: testNamespace, + }, + Spec: dynakube.DynaKubeSpec{ + Kspm: &kspm.Spec{}, + Templates: dynakube.TemplatesSpec{ + KspmNodeConfigurationCollector: kspm.NodeConfigurationCollectorSpec{ + ImageRef: image.Ref{ + Repository: "test", + Tag: "test-tag", + }, + }, + }, + }, + } + + t.Run("no KSPM pods in cluster -> deploying", func(t *testing.T) { + fakeClient := fake.NewClient() + controller := &Controller{ + client: fakeClient, + apiReader: fakeClient, + } + phase := controller.determineDynaKubePhase(dk) + assert.Equal(t, status.Deploying, phase) + }) + t.Run("Error accessing k8s api", func(t *testing.T) { + fakeClient := errorClient{} + controller := &Controller{ + client: fakeClient, + apiReader: fakeClient, + } + phase := controller.determineDynaKubePhase(dk) + assert.Equal(t, status.Error, phase) + }) + t.Run("KSPM daemonsets in cluster not all ready -> deploying", func(t *testing.T) { + fakeClient := fake.NewClient(createDaemonSet(testNamespace, dk.KSPM().GetDaemonSetName(), 3, 2)) + controller := &Controller{ + client: fakeClient, + apiReader: fakeClient, + } + phase := controller.determineDynaKubePhase(dk) + assert.Equal(t, status.Deploying, phase) + }) + t.Run("KSPM daemonsets in cluster all ready -> running", func(t *testing.T) { + fakeClient := fake.NewClient(createDaemonSet(testNamespace, dk.KSPM().GetDaemonSetName(), 3, 3)) + controller := &Controller{ + client: fakeClient, + apiReader: fakeClient, + } + phase := controller.determineDynaKubePhase(dk) + assert.Equal(t, status.Running, phase) + }) +} + func TestDynakubePhaseChanges(t *testing.T) { dk := &dynakube.DynaKube{ ObjectMeta: metav1.ObjectMeta{ @@ -265,10 +383,14 @@ func TestDynakubePhaseChanges(t *testing.T) { Namespace: testNamespace, }, Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - ClassicFullStack: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + ClassicFullStack: &oneagent.HostInjectSpec{}, }, + LogMonitoring: &logmonitoring.Spec{}, + + Kspm: &kspm.Spec{}, + Extensions: &dynakube.ExtensionsSpec{}, }, } @@ -277,10 +399,14 @@ func TestDynakubePhaseChanges(t *testing.T) { agNotReady := createStatefulset(testNamespace, "test-name-activegate", 1, 0) eecReady := createStatefulset(testNamespace, dk.ExtensionsExecutionControllerStatefulsetName(), 1, 1) eecNotReady := createStatefulset(testNamespace, dk.ExtensionsExecutionControllerStatefulsetName(), 1, 0) - otelcReady := createStatefulset(testNamespace, dk.ExtensionsCollectorStatefulsetName(), 2, 2) - otelcNotReady := createStatefulset(testNamespace, dk.ExtensionsCollectorStatefulsetName(), 2, 1) + otelcReady := createStatefulset(testNamespace, dk.OtelCollectorStatefulsetName(), 2, 2) + otelcNotReady := createStatefulset(testNamespace, dk.OtelCollectorStatefulsetName(), 2, 1) oaReady := createDaemonSet(testNamespace, "test-name-oneagent", 3, 3) oaNotReady := createDaemonSet(testNamespace, "test-name-oneagent", 3, 2) + logAgentReady := createDaemonSet(testNamespace, dk.LogMonitoring().GetDaemonSetName(), 3, 3) + logAgentNotReady := createDaemonSet(testNamespace, dk.LogMonitoring().GetDaemonSetName(), 3, 2) + kspmReady := createDaemonSet(testNamespace, dk.KSPM().GetDaemonSetName(), 3, 3) + kspmNotReady := createDaemonSet(testNamespace, dk.KSPM().GetDaemonSetName(), 3, 2) tests := []struct { clt client.Client @@ -347,9 +473,17 @@ func TestDynakubePhaseChanges(t *testing.T) { phase: status.Deploying, }, { - clt: fake.NewClient(agReady, oaReady, eecReady, otelcReady), + clt: fake.NewClient(agReady, oaReady, eecReady, otelcReady, logAgentReady, kspmReady), phase: status.Running, }, + { + clt: fake.NewClient(agReady, oaNotReady, eecReady, otelcReady, logAgentNotReady, kspmReady), + phase: status.Deploying, + }, + { + clt: fake.NewClient(agReady, oaReady, eecReady, otelcReady, logAgentReady, kspmNotReady), + phase: status.Deploying, + }, } for i, test := range tests { diff --git a/pkg/controllers/dynakube/processmoduleconfigsecret/conditions.go b/pkg/controllers/dynakube/processmoduleconfigsecret/conditions.go index 8e7e9dba4e..69babff381 100644 --- a/pkg/controllers/dynakube/processmoduleconfigsecret/conditions.go +++ b/pkg/controllers/dynakube/processmoduleconfigsecret/conditions.go @@ -1,5 +1,5 @@ package processmoduleconfigsecret const ( - pmcConditionType = "ProcessModuleConfig" + ConditionType = "ProcessModuleConfig" ) diff --git a/pkg/controllers/dynakube/processmoduleconfigsecret/reconciler.go b/pkg/controllers/dynakube/processmoduleconfigsecret/reconciler.go index 859c2e7ef4..200d6cf2f7 100644 --- a/pkg/controllers/dynakube/processmoduleconfigsecret/reconciler.go +++ b/pkg/controllers/dynakube/processmoduleconfigsecret/reconciler.go @@ -4,7 +4,7 @@ import ( "context" "encoding/json" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/capability" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/connectioninfo" @@ -20,7 +20,7 @@ import ( ) const ( - PullSecretSuffix = "-pmc-secret" + SecretSuffix = "-pmc-secret" SecretKeyProcessModuleConfig = "ruxitagentproc.conf" ) @@ -49,12 +49,16 @@ func NewReconciler(clt client.Client, } func (r *Reconciler) Reconcile(ctx context.Context) error { - if !(r.dk.CloudNativeFullstackMode() || r.dk.ApplicationMonitoringMode()) { - if meta.FindStatusCondition(*r.dk.Conditions(), pmcConditionType) == nil { + isNeeded := r.dk.OneAgent().IsCSIAvailable() && + (r.dk.OneAgent().IsCloudNativeFullstackMode() || + r.dk.OneAgent().IsApplicationMonitoringMode()) + + if !(isNeeded) { + if meta.FindStatusCondition(*r.dk.Conditions(), ConditionType) == nil { return nil } - defer meta.RemoveStatusCondition(r.dk.Conditions(), pmcConditionType) + defer meta.RemoveStatusCondition(r.dk.Conditions(), ConditionType) err := r.deleteSecret(ctx, &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -79,12 +83,12 @@ func (r *Reconciler) Reconcile(ctx context.Context) error { } func (r *Reconciler) reconcileSecret(ctx context.Context) error { - if !conditions.IsOutdated(r.timeProvider, r.dk, pmcConditionType) { + if !conditions.IsOutdated(r.timeProvider, r.dk, ConditionType) { return nil } log.Info("processModuleConfig is outdated, updating") - conditions.SetSecretOutdated(r.dk.Conditions(), pmcConditionType, "secret is outdated, update in progress") + conditions.SetSecretOutdated(r.dk.Conditions(), ConditionType, "secret is outdated, update in progress") secret, err := r.prepareSecret(ctx) if err != nil { @@ -97,19 +101,19 @@ func (r *Reconciler) reconcileSecret(ctx context.Context) error { func (r *Reconciler) createOrUpdateSecret(ctx context.Context, secret *corev1.Secret) error { _, err := k8ssecret.Query(r.client, r.apiReader, log).WithOwner(r.dk).CreateOrUpdate(ctx, secret) if err != nil { - conditions.SetKubeApiError(r.dk.Conditions(), pmcConditionType, err) + conditions.SetKubeApiError(r.dk.Conditions(), ConditionType, err) return errors.Errorf("failed to create or update secret '%s': %v", secret.Name, err) } - conditions.SetSecretCreatedOrUpdated(r.dk.Conditions(), pmcConditionType, secret.Name) + conditions.SetSecretCreatedOrUpdated(r.dk.Conditions(), ConditionType, secret.Name) return nil } func (r *Reconciler) deleteSecret(ctx context.Context, secret *corev1.Secret) error { if err := k8ssecret.Query(r.client, r.apiReader, log).Delete(ctx, secret); err != nil { - conditions.SetKubeApiError(r.dk.Conditions(), pmcConditionType, err) + conditions.SetKubeApiError(r.dk.Conditions(), ConditionType, err) return err } @@ -120,23 +124,23 @@ func (r *Reconciler) deleteSecret(ctx context.Context, secret *corev1.Secret) er func (r *Reconciler) prepareSecret(ctx context.Context) (*corev1.Secret, error) { pmc, err := r.dtClient.GetProcessModuleConfig(ctx, 0) if err != nil { - conditions.SetDynatraceApiError(r.dk.Conditions(), pmcConditionType, err) + conditions.SetDynatraceApiError(r.dk.Conditions(), ConditionType, err) return nil, err } tenantToken, err := k8ssecret.GetDataFromSecretName(ctx, r.apiReader, types.NamespacedName{ - Name: r.dk.OneagentTenantSecret(), + Name: r.dk.OneAgent().GetTenantSecret(), Namespace: r.dk.Namespace, }, connectioninfo.TenantTokenKey, log) if err != nil { - conditions.SetKubeApiError(r.dk.Conditions(), pmcConditionType, err) + conditions.SetKubeApiError(r.dk.Conditions(), ConditionType, err) return nil, err } pmc = pmc. - AddHostGroup(r.dk.HostGroup()). + AddHostGroup(r.dk.OneAgent().GetHostGroup()). AddConnectionInfo(r.dk.Status.OneAgent.ConnectionInfoStatus, tenantToken). // set proxy explicitly empty, so old proxy settings get deleted where necessary AddProxy("") @@ -144,7 +148,7 @@ func (r *Reconciler) prepareSecret(ctx context.Context) (*corev1.Secret, error) if r.dk.NeedsOneAgentProxy() { proxy, err := r.dk.Proxy(ctx, r.apiReader) if err != nil { - conditions.SetKubeApiError(r.dk.Conditions(), pmcConditionType, err) + conditions.SetKubeApiError(r.dk.Conditions(), ConditionType, err) return nil, err } @@ -175,7 +179,7 @@ func (r *Reconciler) prepareSecret(ctx context.Context) (*corev1.Secret, error) k8ssecret.SetType(corev1.SecretTypeOpaque) if err != nil { - conditions.SetKubeApiError(r.dk.Conditions(), pmcConditionType, err) + conditions.SetKubeApiError(r.dk.Conditions(), ConditionType, err) return nil, err } @@ -211,5 +215,5 @@ func unmarshal(secret *corev1.Secret) (*dtclient.ProcessModuleConfig, error) { } func extendWithSuffix(name string) string { - return name + PullSecretSuffix + return name + SecretSuffix } diff --git a/pkg/controllers/dynakube/processmoduleconfigsecret/reconciler_test.go b/pkg/controllers/dynakube/processmoduleconfigsecret/reconciler_test.go index 7a2f0d7b76..4e11f398de 100644 --- a/pkg/controllers/dynakube/processmoduleconfigsecret/reconciler_test.go +++ b/pkg/controllers/dynakube/processmoduleconfigsecret/reconciler_test.go @@ -8,7 +8,8 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/value" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/connectioninfo" "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" @@ -34,8 +35,8 @@ const ( func TestReconcile(t *testing.T) { t.Run("Create and update works with minimal setup", func(t *testing.T) { - dk := createDynakube(dynakube.OneAgentSpec{ - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{}}) + dk := createDynakube(oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{}}) mockK8sClient := createK8sClientWithOneAgentTenantSecret(testTokenValue) @@ -48,7 +49,7 @@ func TestReconcile(t *testing.T) { checkSecretForValue(t, mockK8sClient, "\"revision\":0") - condition := meta.FindStatusCondition(*dk.Conditions(), pmcConditionType) + condition := meta.FindStatusCondition(*dk.Conditions(), ConditionType) oldTransitionTime := condition.LastTransitionTime require.NotNil(t, condition) require.NotEmpty(t, oldTransitionTime) @@ -62,7 +63,7 @@ func TestReconcile(t *testing.T) { require.NoError(t, err) checkSecretForValue(t, mockK8sClient, "\"revision\":0") - condition = meta.FindStatusCondition(*dk.Conditions(), pmcConditionType) + condition = meta.FindStatusCondition(*dk.Conditions(), ConditionType) require.NotNil(t, condition) require.Equal(t, condition.LastTransitionTime, oldTransitionTime) @@ -73,16 +74,16 @@ func TestReconcile(t *testing.T) { require.NoError(t, err) checkSecretForValue(t, mockK8sClient, "\"revision\":1") - condition = meta.FindStatusCondition(*dk.Conditions(), pmcConditionType) + condition = meta.FindStatusCondition(*dk.Conditions(), ConditionType) require.NotNil(t, condition) require.Greater(t, condition.LastTransitionTime.Time, oldTransitionTime.Time) assert.Equal(t, conditions.SecretCreatedOrUpdatedReason, condition.Reason) assert.Equal(t, metav1.ConditionTrue, condition.Status) }) t.Run("Only runs when required, and cleans up condition and pmc secret", func(t *testing.T) { - dk := createDynakube(dynakube.OneAgentSpec{ - ClassicFullStack: &dynakube.HostInjectSpec{}}) - conditions.SetSecretCreated(dk.Conditions(), pmcConditionType, "this is a test") + dk := createDynakube(oneagent.Spec{ + ClassicFullStack: &oneagent.HostInjectSpec{}}) + conditions.SetSecretCreated(dk.Conditions(), ConditionType, "this is a test") secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -101,8 +102,8 @@ func TestReconcile(t *testing.T) { assert.Error(t, mockK8sClient.Get(context.Background(), client.ObjectKey{Name: extendWithSuffix(testName), Namespace: testNamespace}, &corev1.Secret{})) }) t.Run("No proxy is set when proxy enabled and custom no proxy set", func(t *testing.T) { - dk := createDynakube(dynakube.OneAgentSpec{ - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{}}) + dk := createDynakube(oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{}}) dk.Spec.Proxy = &value.Source{ Value: "myproxy.at", } @@ -119,8 +120,8 @@ func TestReconcile(t *testing.T) { }) t.Run("problem with k8s request => visible in conditions", func(t *testing.T) { - dk := createDynakube(dynakube.OneAgentSpec{ - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{}}) + dk := createDynakube(oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{}}) boomClient := createBOOMK8sClient() @@ -133,14 +134,14 @@ func TestReconcile(t *testing.T) { require.Error(t, err) require.Len(t, *dk.Conditions(), 1) - condition := meta.FindStatusCondition(*dk.Conditions(), pmcConditionType) + condition := meta.FindStatusCondition(*dk.Conditions(), ConditionType) assert.Equal(t, conditions.KubeApiErrorReason, condition.Reason) assert.Equal(t, metav1.ConditionFalse, condition.Status) }) t.Run("problem with dynatrace request => visible in conditions", func(t *testing.T) { - dk := createDynakube(dynakube.OneAgentSpec{ - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{}}) + dk := createDynakube(oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{}}) mockK8sClient := fake.NewClient(dk) _ = createK8sClientWithOneAgentTenantSecret(testTokenValue) @@ -154,7 +155,7 @@ func TestReconcile(t *testing.T) { require.Error(t, err) require.Len(t, *dk.Conditions(), 1) - condition := meta.FindStatusCondition(*dk.Conditions(), pmcConditionType) + condition := meta.FindStatusCondition(*dk.Conditions(), ConditionType) assert.Equal(t, conditions.DynatraceApiErrorReason, condition.Reason) assert.Equal(t, metav1.ConditionFalse, condition.Status) }) @@ -170,7 +171,7 @@ func checkSecretForValue(t *testing.T, k8sClient client.Client, shouldContain st require.Contains(t, string(processModuleConfig), shouldContain) } -func createDynakube(oneAgentSpec dynakube.OneAgentSpec) *dynakube.DynaKube { +func createDynakube(oneAgentSpec oneagent.Spec) *dynakube.DynaKube { return &dynakube.DynaKube{ ObjectMeta: metav1.ObjectMeta{ Namespace: testNamespace, @@ -235,8 +236,8 @@ func createK8sClientWithOneAgentTenantSecret(token string) client.Client { func TestGetSecretData(t *testing.T) { t.Run("unmarshal secret data into struct", func(t *testing.T) { // use Reconcile to automatically create the secret to test - dk := createDynakube(dynakube.OneAgentSpec{ - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{}}) + dk := createDynakube(oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{}}) mockK8sClient := createK8sClientWithOneAgentTenantSecret(testTokenValue) mockTime := timeprovider.New().Freeze() diff --git a/pkg/controllers/dynakube/proxy/reconciler.go b/pkg/controllers/dynakube/proxy/reconciler.go index 8f70f98dc0..e44c16b206 100644 --- a/pkg/controllers/dynakube/proxy/reconciler.go +++ b/pkg/controllers/dynakube/proxy/reconciler.go @@ -5,7 +5,7 @@ import ( "net/url" "strings" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/controllers" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/consts" k8ssecret "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/secret" diff --git a/pkg/controllers/dynakube/proxy/reconciler_test.go b/pkg/controllers/dynakube/proxy/reconciler_test.go index 1d9c005451..32a68dba51 100644 --- a/pkg/controllers/dynakube/proxy/reconciler_test.go +++ b/pkg/controllers/dynakube/proxy/reconciler_test.go @@ -6,8 +6,8 @@ import ( dtfake "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/value" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" "github.com/Dynatrace/dynatrace-operator/pkg/controllers" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/connectioninfo" "github.com/stretchr/testify/assert" diff --git a/pkg/controllers/dynakube/token/feature.go b/pkg/controllers/dynakube/token/feature.go index b2a22879e5..49588ee271 100644 --- a/pkg/controllers/dynakube/token/feature.go +++ b/pkg/controllers/dynakube/token/feature.go @@ -1,7 +1,7 @@ package token import ( - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "golang.org/x/exp/slices" ) diff --git a/pkg/controllers/dynakube/token/feature_test.go b/pkg/controllers/dynakube/token/feature_test.go index e56777e824..65c89a68f7 100644 --- a/pkg/controllers/dynakube/token/feature_test.go +++ b/pkg/controllers/dynakube/token/feature_test.go @@ -3,7 +3,7 @@ package token import ( "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/stretchr/testify/assert" ) diff --git a/pkg/controllers/dynakube/token/reader.go b/pkg/controllers/dynakube/token/reader.go index 9c69378a61..03dfa1a3b3 100644 --- a/pkg/controllers/dynakube/token/reader.go +++ b/pkg/controllers/dynakube/token/reader.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" diff --git a/pkg/controllers/dynakube/token/reader_test.go b/pkg/controllers/dynakube/token/reader_test.go index 25cc72dc59..a2a2b4d72f 100644 --- a/pkg/controllers/dynakube/token/reader_test.go +++ b/pkg/controllers/dynakube/token/reader_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/secret" "github.com/stretchr/testify/assert" diff --git a/pkg/controllers/dynakube/token/token.go b/pkg/controllers/dynakube/token/token.go index daafeb9367..bb5af68013 100644 --- a/pkg/controllers/dynakube/token/token.go +++ b/pkg/controllers/dynakube/token/token.go @@ -4,7 +4,7 @@ import ( "context" "strings" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/pkg/errors" ) diff --git a/pkg/controllers/dynakube/token/tokens.go b/pkg/controllers/dynakube/token/tokens.go index 87930c968e..1ee8333a52 100644 --- a/pkg/controllers/dynakube/token/tokens.go +++ b/pkg/controllers/dynakube/token/tokens.go @@ -4,7 +4,7 @@ import ( "context" "errors" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/dynatraceapi" ) diff --git a/pkg/controllers/dynakube/token/tokens_test.go b/pkg/controllers/dynakube/token/tokens_test.go index f0f8075bb8..758baaf397 100644 --- a/pkg/controllers/dynakube/token/tokens_test.go +++ b/pkg/controllers/dynakube/token/tokens_test.go @@ -5,8 +5,8 @@ import ( "net/http" "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" dtclientmock "github.com/Dynatrace/dynatrace-operator/test/mocks/pkg/clients/dynatrace" "github.com/pkg/errors" @@ -107,7 +107,7 @@ func TestTokens(t *testing.T) { assert.Len(t, tokens.ApiToken().Features, 4) assert.Empty(t, tokens.PaasToken().Features) assert.Empty(t, tokens.DataIngestToken().Features) - assert.NoError(t, err, "") + assert.NoError(t, err) }) t.Run("activegate enabled dynakube, no permissions in api token => fail", func(t *testing.T) { dk := dynakube.DynaKube{} diff --git a/pkg/controllers/dynakube/version/activegate.go b/pkg/controllers/dynakube/version/activegate.go index 808a0b447c..9662dea782 100644 --- a/pkg/controllers/dynakube/version/activegate.go +++ b/pkg/controllers/dynakube/version/activegate.go @@ -4,7 +4,7 @@ import ( "context" "github.com/Dynatrace/dynatrace-operator/pkg/api/status" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" "github.com/pkg/errors" @@ -71,7 +71,7 @@ func (updater activeGateUpdater) IsAutoUpdateEnabled() bool { } func (updater activeGateUpdater) IsPublicRegistryEnabled() bool { - isPublicRegistry := updater.dk.FeaturePublicRegistry() && !updater.dk.ClassicFullStackMode() + isPublicRegistry := updater.dk.FeaturePublicRegistry() && !updater.dk.OneAgent().IsClassicFullStackMode() if isPublicRegistry { setVerifiedCondition(updater.dk.Conditions(), activeGateVersionConditionType) // Bit hacky, as things can still go wrong, but if so we will just overwrite this is LatestImageInfo. } diff --git a/pkg/controllers/dynakube/version/activegate_test.go b/pkg/controllers/dynakube/version/activegate_test.go index 79d1bedc8f..45c7cb52f3 100644 --- a/pkg/controllers/dynakube/version/activegate_test.go +++ b/pkg/controllers/dynakube/version/activegate_test.go @@ -6,8 +6,8 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" "github.com/Dynatrace/dynatrace-operator/pkg/api/status" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" dtclientmock "github.com/Dynatrace/dynatrace-operator/test/mocks/pkg/clients/dynatrace" "github.com/stretchr/testify/assert" @@ -47,7 +47,7 @@ func TestActiveGateUpdater(t *testing.T) { assert.Equal(t, "activegate", updater.Name()) assert.True(t, updater.IsEnabled()) assert.Equal(t, dk.Spec.ActiveGate.Image, updater.CustomImage()) - assert.Equal(t, "", updater.CustomVersion()) + assert.Empty(t, updater.CustomVersion()) assert.False(t, updater.IsAutoUpdateEnabled()) imageInfo, err := updater.LatestImageInfo(ctx) require.NoError(t, err) diff --git a/pkg/controllers/dynakube/version/codemodules.go b/pkg/controllers/dynakube/version/codemodules.go index a12c8a02f1..464e623a07 100644 --- a/pkg/controllers/dynakube/version/codemodules.go +++ b/pkg/controllers/dynakube/version/codemodules.go @@ -4,7 +4,8 @@ import ( "context" "github.com/Dynatrace/dynatrace-operator/pkg/api/status" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" "k8s.io/apimachinery/pkg/api/meta" @@ -31,7 +32,7 @@ func (updater codeModulesUpdater) Name() string { } func (updater codeModulesUpdater) IsEnabled() bool { - if updater.dk.NeedAppInjection() { + if updater.dk.OneAgent().IsAppInjectionNeeded() { return true } @@ -46,7 +47,7 @@ func (updater *codeModulesUpdater) Target() *status.VersionStatus { } func (updater codeModulesUpdater) CustomImage() string { - customImage := updater.dk.CustomCodeModulesImage() + customImage := updater.dk.OneAgent().GetCustomCodeModulesImage() if customImage != "" { setVerificationSkippedReasonCondition(updater.dk.Conditions(), cmConditionType) } @@ -55,7 +56,7 @@ func (updater codeModulesUpdater) CustomImage() string { } func (updater codeModulesUpdater) CustomVersion() string { - return updater.dk.CustomCodeModulesVersion() + return updater.dk.OneAgent().GetCustomCodeModulesVersion() } func (updater codeModulesUpdater) IsAutoUpdateEnabled() bool { @@ -87,7 +88,7 @@ func (updater *codeModulesUpdater) CheckForDowngrade(_ string) (bool, error) { func (updater *codeModulesUpdater) UseTenantRegistry(ctx context.Context) error { customVersion := updater.CustomVersion() if customVersion != "" { - updater.dk.Status.CodeModules = dynakube.CodeModulesStatus{ + updater.dk.Status.CodeModules = oneagent.CodeModulesStatus{ VersionStatus: status.VersionStatus{ Version: customVersion, }, @@ -106,7 +107,7 @@ func (updater *codeModulesUpdater) UseTenantRegistry(ctx context.Context) error return err } - updater.dk.Status.CodeModules = dynakube.CodeModulesStatus{ + updater.dk.Status.CodeModules = oneagent.CodeModulesStatus{ VersionStatus: status.VersionStatus{ Version: latestAgentVersionUnixPaas, }, diff --git a/pkg/controllers/dynakube/version/codemodules_test.go b/pkg/controllers/dynakube/version/codemodules_test.go index 26990d47b7..7081df283b 100644 --- a/pkg/controllers/dynakube/version/codemodules_test.go +++ b/pkg/controllers/dynakube/version/codemodules_test.go @@ -5,7 +5,8 @@ import ( "testing" "github.com/Dynatrace/dynatrace-operator/pkg/api/status" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" dtclientmock "github.com/Dynatrace/dynatrace-operator/test/mocks/pkg/clients/dynatrace" @@ -25,10 +26,10 @@ func TestCodeModulesUpdater(t *testing.T) { t.Run("Getters work as expected", func(t *testing.T) { dk := &dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - ApplicationMonitoring: &dynakube.ApplicationMonitoringSpec{ + OneAgent: oneagent.Spec{ + ApplicationMonitoring: &oneagent.ApplicationMonitoringSpec{ Version: testImage.Tag, - AppInjectionSpec: dynakube.AppInjectionSpec{ + AppInjectionSpec: oneagent.AppInjectionSpec{ CodeModulesImage: testImage.String(), }, }, @@ -57,8 +58,8 @@ func TestCodeModulesUseDefault(t *testing.T) { t.Run("Set according to version field, unset previous status", func(t *testing.T) { dk := &dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - ApplicationMonitoring: &dynakube.ApplicationMonitoringSpec{ + OneAgent: oneagent.Spec{ + ApplicationMonitoring: &oneagent.ApplicationMonitoringSpec{ Version: testVersion, }, }, @@ -80,8 +81,8 @@ func TestCodeModulesUseDefault(t *testing.T) { t.Run("Set according to default, unset previous status", func(t *testing.T) { dk := &dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - ApplicationMonitoring: &dynakube.ApplicationMonitoringSpec{}, + OneAgent: oneagent.Spec{ + ApplicationMonitoring: &oneagent.ApplicationMonitoringSpec{}, }, }, Status: dynakube.DynaKubeStatus{ @@ -102,8 +103,8 @@ func TestCodeModulesUseDefault(t *testing.T) { t.Run("problem with Dynatrace request => visible in conditions", func(t *testing.T) { dk := &dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - ApplicationMonitoring: &dynakube.ApplicationMonitoringSpec{}, + OneAgent: oneagent.Spec{ + ApplicationMonitoring: &oneagent.ApplicationMonitoringSpec{}, }, }, Status: dynakube.DynaKubeStatus{ @@ -125,7 +126,7 @@ func TestCodeModulesIsEnabled(t *testing.T) { t.Run("cleans up condition if not enabled", func(t *testing.T) { dk := &dynakube.DynaKube{ Status: dynakube.DynaKubeStatus{ - CodeModules: dynakube.CodeModulesStatus{ + CodeModules: oneagent.CodeModulesStatus{ VersionStatus: status.VersionStatus{ Version: "prev", }, @@ -201,15 +202,15 @@ func TestCodeModulesLatestImageInfo(t *testing.T) { }) } -func oldCodeModulesStatus() dynakube.CodeModulesStatus { - return dynakube.CodeModulesStatus{ +func oldCodeModulesStatus() oneagent.CodeModulesStatus { + return oneagent.CodeModulesStatus{ VersionStatus: status.VersionStatus{ ImageID: "prev", }, } } -func assertDefaultCodeModulesStatus(t *testing.T, expectedVersion string, codeModulesStatus dynakube.CodeModulesStatus) { +func assertDefaultCodeModulesStatus(t *testing.T, expectedVersion string, codeModulesStatus oneagent.CodeModulesStatus) { assert.Equal(t, expectedVersion, codeModulesStatus.Version) assert.Empty(t, codeModulesStatus.ImageID) } diff --git a/pkg/controllers/dynakube/version/oneagent.go b/pkg/controllers/dynakube/version/oneagent.go index c4d5500f61..c556d3e0e7 100644 --- a/pkg/controllers/dynakube/version/oneagent.go +++ b/pkg/controllers/dynakube/version/oneagent.go @@ -4,7 +4,7 @@ import ( "context" "github.com/Dynatrace/dynatrace-operator/pkg/api/status" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" "github.com/pkg/errors" @@ -39,7 +39,7 @@ func (updater oneAgentUpdater) Name() string { } func (updater oneAgentUpdater) IsEnabled() bool { - if updater.dk.NeedsOneAgent() { + if updater.dk.OneAgent().IsDaemonsetRequired() { return true } @@ -47,7 +47,7 @@ func (updater oneAgentUpdater) IsEnabled() bool { updater.dk.Status.OneAgent.Healthcheck = nil _ = meta.RemoveStatusCondition(updater.dk.Conditions(), oaConditionType) - return updater.dk.NeedsOneAgent() + return updater.dk.OneAgent().IsDaemonsetRequired() } func (updater *oneAgentUpdater) Target() *status.VersionStatus { @@ -55,7 +55,7 @@ func (updater *oneAgentUpdater) Target() *status.VersionStatus { } func (updater oneAgentUpdater) CustomImage() string { - customImage := updater.dk.CustomOneAgentImage() + customImage := updater.dk.OneAgent().GetCustomImage() if customImage != "" { setVerificationSkippedReasonCondition(updater.dk.Conditions(), oaConditionType) } @@ -64,15 +64,15 @@ func (updater oneAgentUpdater) CustomImage() string { } func (updater oneAgentUpdater) CustomVersion() string { - return updater.dk.CustomOneAgentVersion() + return updater.dk.OneAgent().GetCustomVersion() } func (updater oneAgentUpdater) IsAutoUpdateEnabled() bool { - return updater.dk.ShouldAutoUpdateOneAgent() + return updater.dk.OneAgent().IsAutoUpdateEnabled() } func (updater oneAgentUpdater) IsPublicRegistryEnabled() bool { - isPublicRegistry := updater.dk.FeaturePublicRegistry() && !updater.dk.ClassicFullStackMode() + isPublicRegistry := updater.dk.FeaturePublicRegistry() && !updater.dk.OneAgent().IsClassicFullStackMode() if isPublicRegistry { setVerifiedCondition(updater.dk.Conditions(), oaConditionType) // Bit hacky, as things can still go wrong, but if so we will just overwrite this is LatestImageInfo. } @@ -110,7 +110,7 @@ func (updater oneAgentUpdater) UseTenantRegistry(ctx context.Context) error { return err } - defaultImage := updater.dk.DefaultOneAgentImage(latestVersion) + defaultImage := updater.dk.OneAgent().GetDefaultImage(latestVersion) err = updateVersionStatusForTenantRegistry(updater.Target(), defaultImage, latestVersion) if err != nil { @@ -165,7 +165,7 @@ func (updater oneAgentUpdater) ValidateStatus() error { } if imageType == status.ImmutableImageType { - if updater.dk.ClassicFullStackMode() { + if updater.dk.OneAgent().IsClassicFullStackMode() { return errors.New("immutable OneAgent image in combination with classicFullStack mode is not possible") } } diff --git a/pkg/controllers/dynakube/version/oneagent_test.go b/pkg/controllers/dynakube/version/oneagent_test.go index f362592c59..d9451d2800 100644 --- a/pkg/controllers/dynakube/version/oneagent_test.go +++ b/pkg/controllers/dynakube/version/oneagent_test.go @@ -6,15 +6,16 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" "github.com/Dynatrace/dynatrace-operator/pkg/api/status" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" - "github.com/Dynatrace/dynatrace-operator/pkg/util/address" "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" dtclientmock "github.com/Dynatrace/dynatrace-operator/test/mocks/pkg/clients/dynatrace" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" ) func TestOneAgentUpdater(t *testing.T) { @@ -27,9 +28,9 @@ func TestOneAgentUpdater(t *testing.T) { t.Run("Getters work as expected", func(t *testing.T) { dk := &dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - ClassicFullStack: &dynakube.HostInjectSpec{ - AutoUpdate: address.Of(false), + OneAgent: oneagent.Spec{ + ClassicFullStack: &oneagent.HostInjectSpec{ + AutoUpdate: ptr.To(false), Image: testImage.String(), Version: testImage.Tag, }, @@ -56,7 +57,7 @@ func TestOneAgentIsEnabled(t *testing.T) { t.Run("cleans up condition if not enabled", func(t *testing.T) { dk := &dynakube.DynaKube{ Status: dynakube.DynaKubeStatus{ - OneAgent: dynakube.OneAgentStatus{ + OneAgent: oneagent.Status{ VersionStatus: status.VersionStatus{ Version: "prev", }, @@ -141,14 +142,14 @@ func TestOneAgentUseDefault(t *testing.T) { dk := &dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{ - ClassicFullStack: &dynakube.HostInjectSpec{ + OneAgent: oneagent.Spec{ + ClassicFullStack: &oneagent.HostInjectSpec{ Version: testVersion, }, }, }, } - expectedImage := dk.DefaultOneAgentImage(testVersion) + expectedImage := dk.OneAgent().GetDefaultImage(testVersion) mockClient := dtclientmock.NewClient(t) @@ -166,12 +167,12 @@ func TestOneAgentUseDefault(t *testing.T) { dk := &dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{ - ClassicFullStack: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + ClassicFullStack: &oneagent.HostInjectSpec{}, }, }, } - expectedImage := dk.DefaultOneAgentImage(testVersion) + expectedImage := dk.OneAgent().GetDefaultImage(testVersion) mockClient := dtclientmock.NewClient(t) mockLatestAgentVersion(mockClient, testVersion) @@ -191,12 +192,12 @@ func TestOneAgentUseDefault(t *testing.T) { dk := &dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{ - ClassicFullStack: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + ClassicFullStack: &oneagent.HostInjectSpec{}, }, }, Status: dynakube.DynaKubeStatus{ - OneAgent: dynakube.OneAgentStatus{ + OneAgent: oneagent.Status{ VersionStatus: status.VersionStatus{ ImageID: "some.registry.com:" + previousVersion, Version: previousVersion, @@ -225,12 +226,12 @@ func TestOneAgentUseDefault(t *testing.T) { dk := &dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{ - ClassicFullStack: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + ClassicFullStack: &oneagent.HostInjectSpec{}, }, }, Status: dynakube.DynaKubeStatus{ - OneAgent: dynakube.OneAgentStatus{ + OneAgent: oneagent.Status{ VersionStatus: status.VersionStatus{ ImageID: "some.registry.com:" + previousVersion, Version: previousVersion, @@ -257,12 +258,12 @@ func TestOneAgentUseDefault(t *testing.T) { dk := &dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{ - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{}, + OneAgent: oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{}, }, }, Status: dynakube.DynaKubeStatus{ - OneAgent: dynakube.OneAgentStatus{ + OneAgent: oneagent.Status{ VersionStatus: status.VersionStatus{ ImageID: "BOOM", Source: status.PublicRegistryVersionSource, @@ -295,7 +296,7 @@ type CheckForDowngradeTestCase struct { func newDynakubeWithOneAgentStatus(status status.VersionStatus) *dynakube.DynaKube { return &dynakube.DynaKube{ Status: dynakube.DynaKubeStatus{ - OneAgent: dynakube.OneAgentStatus{ + OneAgent: oneagent.Status{ VersionStatus: status, }, }, @@ -378,10 +379,10 @@ func TestCheckForDowngrade(t *testing.T) { func newDynakubeForCheckLabelTest(versionStatus status.VersionStatus) *dynakube.DynaKube { return &dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{}, + OneAgent: oneagent.Spec{}, }, Status: dynakube.DynaKubeStatus{ - OneAgent: dynakube.OneAgentStatus{ + OneAgent: oneagent.Status{ VersionStatus: versionStatus, }, }, @@ -400,26 +401,26 @@ func TestCheckLabels(t *testing.T) { t.Run("Validate immutable oneAgent image with default cloudNative", func(t *testing.T) { dk := newDynakubeForCheckLabelTest(versionStatus) - dk.Spec.OneAgent.CloudNativeFullStack = &dynakube.CloudNativeFullStackSpec{} + dk.Spec.OneAgent.CloudNativeFullStack = &oneagent.CloudNativeFullStackSpec{} updater := newOneAgentUpdater(dk, fake.NewClient(), nil) require.NoError(t, updater.ValidateStatus()) }) t.Run("Validate immutable oneAgent image with classicFullStack", func(t *testing.T) { dk := newDynakubeForCheckLabelTest(versionStatus) - dk.Spec.OneAgent.ClassicFullStack = &dynakube.HostInjectSpec{} + dk.Spec.OneAgent.ClassicFullStack = &oneagent.HostInjectSpec{} updater := newOneAgentUpdater(dk, fake.NewClient(), nil) require.Error(t, updater.ValidateStatus()) }) t.Run("Validate immutable oneAgent image when image version is not set", func(t *testing.T) { dk := newDynakubeForCheckLabelTest(versionStatus) - dk.Spec.OneAgent.CloudNativeFullStack = &dynakube.CloudNativeFullStackSpec{} + dk.Spec.OneAgent.CloudNativeFullStack = &oneagent.CloudNativeFullStackSpec{} dk.Status.OneAgent.VersionStatus.Version = "" updater := newOneAgentUpdater(dk, fake.NewClient(), nil) require.Error(t, updater.ValidateStatus()) }) t.Run("Validate mutable oneAgent image with classicFullStack", func(t *testing.T) { dk := newDynakubeForCheckLabelTest(versionStatus) - dk.Spec.OneAgent.ClassicFullStack = &dynakube.HostInjectSpec{} + dk.Spec.OneAgent.ClassicFullStack = &oneagent.HostInjectSpec{} dk.Status.OneAgent.VersionStatus.Type = "mutable" updater := newOneAgentUpdater(dk, fake.NewClient(), nil) require.NoError(t, updater.ValidateStatus()) diff --git a/pkg/controllers/dynakube/version/reconciler.go b/pkg/controllers/dynakube/version/reconciler.go index a479f984d8..10726bbcd2 100644 --- a/pkg/controllers/dynakube/version/reconciler.go +++ b/pkg/controllers/dynakube/version/reconciler.go @@ -5,7 +5,7 @@ import ( "strings" "github.com/Dynatrace/dynatrace-operator/pkg/api/status" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/util/timeprovider" "sigs.k8s.io/controller-runtime/pkg/client" @@ -79,7 +79,7 @@ func (r *reconciler) updateVersionStatuses(ctx context.Context, updater StatusUp _, ok := updater.(*oneAgentUpdater) if ok { - healthConfig, err := getOneAgentHealthConfig(dk.OneAgentVersion()) + healthConfig, err := getOneAgentHealthConfig(dk.OneAgent().GetVersion()) if err != nil { log.Error(err, "could not set OneAgent healthcheck") } else { diff --git a/pkg/controllers/dynakube/version/reconciler_test.go b/pkg/controllers/dynakube/version/reconciler_test.go index a72b0ad625..79c606647a 100644 --- a/pkg/controllers/dynakube/version/reconciler_test.go +++ b/pkg/controllers/dynakube/version/reconciler_test.go @@ -8,8 +8,9 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" "github.com/Dynatrace/dynatrace-operator/pkg/api/status" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/dtpullsecret" "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" @@ -39,8 +40,8 @@ func TestReconcile(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace}, Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{ - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{}, + OneAgent: oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{}, }, ActiveGate: activegate.Spec{ Capabilities: []activegate.CapabilityDisplayName{ @@ -100,7 +101,7 @@ func TestReconcile(t *testing.T) { assert.Equal(t, "Version verified for component.", condition.Message) assertStatusBasedOnTenantRegistry(t, dk.ActiveGate().GetDefaultImage(testActiveGateImage.Tag), testActiveGateImage.Tag, dkStatus.ActiveGate.VersionStatus) - assertStatusBasedOnTenantRegistry(t, dk.DefaultOneAgentImage(testOneAgentImage.Tag), testOneAgentImage.Tag, dkStatus.OneAgent.VersionStatus) + assertStatusBasedOnTenantRegistry(t, dk.OneAgent().GetDefaultImage(testOneAgentImage.Tag), testOneAgentImage.Tag, dkStatus.OneAgent.VersionStatus) assert.Equal(t, latestAgentVersion, dkStatus.CodeModules.VersionStatus.Version) // no change if probe not old enough @@ -203,12 +204,12 @@ func TestNeedsUpdate(t *testing.T) { dk := dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - ClassicFullStack: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + ClassicFullStack: &oneagent.HostInjectSpec{}, }, }, Status: dynakube.DynaKubeStatus{ - OneAgent: dynakube.OneAgentStatus{ + OneAgent: oneagent.Status{ VersionStatus: status.VersionStatus{ Source: status.TenantRegistryVersionSource, }, @@ -282,8 +283,8 @@ func TestNeedsUpdate(t *testing.T) { func TestHasCustomFieldChanged(t *testing.T) { dk := dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - ClassicFullStack: &dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + ClassicFullStack: &oneagent.HostInjectSpec{}, }, }, } diff --git a/pkg/controllers/dynakube/version/updater.go b/pkg/controllers/dynakube/version/updater.go index 68804c5c6e..6b674ed690 100644 --- a/pkg/controllers/dynakube/version/updater.go +++ b/pkg/controllers/dynakube/version/updater.go @@ -40,10 +40,9 @@ func (r *reconciler) run(ctx context.Context, updater StatusUpdater) error { } }() - customImage := updater.CustomImage() - if customImage != "" { + if currentSource == status.CustomImageVersionSource { log.Info("updating version status according to custom image", "updater", updater.Name()) - setImageIDToCustomImage(updater.Target(), customImage) + setImageIDToCustomImage(updater.Target(), updater.CustomImage()) return nil } diff --git a/pkg/controllers/dynakube/version/updater_test.go b/pkg/controllers/dynakube/version/updater_test.go index eff001a5bd..2c0f8ef46c 100644 --- a/pkg/controllers/dynakube/version/updater_test.go +++ b/pkg/controllers/dynakube/version/updater_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/Dynatrace/dynatrace-operator/pkg/api/status" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/util/timeprovider" versionmock "github.com/Dynatrace/dynatrace-operator/test/mocks/pkg/controllers/dynakube/version" diff --git a/pkg/controllers/edgeconnect/controller.go b/pkg/controllers/edgeconnect/controller.go index 9ae6d91728..9c8a8d595f 100644 --- a/pkg/controllers/edgeconnect/controller.go +++ b/pkg/controllers/edgeconnect/controller.go @@ -377,7 +377,11 @@ func (controller *Controller) reconcileEdgeConnectRegular(ctx context.Context, e return err } - desiredDeployment.Spec.Template.Annotations = map[string]string{consts.EdgeConnectAnnotationSecretHash: secretHash} + if desiredDeployment.Spec.Template.Annotations == nil { + desiredDeployment.Spec.Template.Annotations = map[string]string{} + } + + desiredDeployment.Spec.Template.Annotations[consts.EdgeConnectAnnotationSecretHash] = secretHash _, err = k8sdeployment.Query(controller.client, controller.apiReader, log).WithOwner(ec).CreateOrUpdate(ctx, desiredDeployment) if err != nil { @@ -674,7 +678,11 @@ func (controller *Controller) createOrUpdateEdgeConnectDeploymentAndSettings(ctx desiredDeployment := deployment.New(ec) - desiredDeployment.Spec.Template.Annotations = map[string]string{consts.EdgeConnectAnnotationSecretHash: secretHash} + if desiredDeployment.Spec.Template.Annotations == nil { + desiredDeployment.Spec.Template.Annotations = map[string]string{} + } + + desiredDeployment.Spec.Template.Annotations[consts.EdgeConnectAnnotationSecretHash] = secretHash _log = _log.WithValues("deploymentName", desiredDeployment.Name) if err := controllerutil.SetControllerReference(ec, desiredDeployment, scheme.Scheme); err != nil { diff --git a/pkg/controllers/edgeconnect/deployment/deployment.go b/pkg/controllers/edgeconnect/deployment/deployment.go index a40d3eb649..29c55123e1 100644 --- a/pkg/controllers/edgeconnect/deployment/deployment.go +++ b/pkg/controllers/edgeconnect/deployment/deployment.go @@ -3,22 +3,19 @@ package deployment import ( "github.com/Dynatrace/dynatrace-operator/pkg/api/v1alpha2/edgeconnect" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/edgeconnect/consts" - "github.com/Dynatrace/dynatrace-operator/pkg/util/address" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/labels" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/resources" maputils "github.com/Dynatrace/dynatrace-operator/pkg/util/map" - "github.com/Dynatrace/dynatrace-operator/pkg/util/prioritymap" "github.com/Dynatrace/dynatrace-operator/pkg/webhook" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" ) const ( - customEnvPriority = prioritymap.HighPriority - defaultEnvPriority = prioritymap.DefaultPriority - unprivilegedUser = int64(1000) - unprivilegedGroup = int64(1000) + unprivilegedUser = int64(1000) + unprivilegedGroup = int64(1000) ) func New(ec *edgeconnect.EdgeConnect) *appsv1.Deployment { @@ -27,9 +24,11 @@ func New(ec *edgeconnect.EdgeConnect) *appsv1.Deployment { func create(ec *edgeconnect.EdgeConnect) *appsv1.Deployment { appLabels := buildAppLabels(ec) - labels := maputils.MergeMap( - ec.Labels, - appLabels.BuildLabels(), + labels := appLabels.BuildLabels() + + customPodLabels := maputils.MergeMap( + ec.Spec.Labels, + labels, // higher priority ) log.Debug("EdgeConnect deployment app labels", "labels", labels) @@ -39,7 +38,7 @@ func create(ec *edgeconnect.EdgeConnect) *appsv1.Deployment { Name: ec.Name, Namespace: ec.Namespace, Labels: labels, - Annotations: buildAnnotations(ec), + Annotations: buildAnnotations(), }, Spec: appsv1.DeploymentSpec{ Replicas: ec.Spec.Replicas, @@ -48,14 +47,15 @@ func create(ec *edgeconnect.EdgeConnect) *appsv1.Deployment { }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ - Labels: labels, + Annotations: ec.Spec.Annotations, + Labels: customPodLabels, }, Spec: corev1.PodSpec{ Containers: []corev1.Container{edgeConnectContainer(ec)}, ImagePullSecrets: prepareImagePullSecrets(ec), ServiceAccountName: ec.GetServiceAccountName(), DeprecatedServiceAccount: ec.GetServiceAccountName(), - TerminationGracePeriodSeconds: address.Of(int64(30)), + TerminationGracePeriodSeconds: ptr.To(int64(30)), Volumes: prepareVolumes(ec), NodeSelector: ec.Spec.NodeSelector, Tolerations: ec.Spec.Tolerations, @@ -92,14 +92,11 @@ func buildAppLabels(ec *edgeconnect.EdgeConnect) *labels.AppLabels { ec.Status.Version.Version) } -func buildAnnotations(ec *edgeconnect.EdgeConnect) map[string]string { - annotations := map[string]string{ +func buildAnnotations() map[string]string { + return map[string]string{ consts.AnnotationEdgeConnectContainerAppArmor: "runtime/default", webhook.AnnotationDynatraceInject: "false", } - annotations = maputils.MergeMap(ec.Annotations, annotations) - - return annotations } func edgeConnectContainer(ec *edgeconnect.EdgeConnect) corev1.Container { @@ -110,12 +107,12 @@ func edgeConnectContainer(ec *edgeconnect.EdgeConnect) corev1.Container { Env: ec.Spec.Env, Resources: prepareResourceRequirements(ec), SecurityContext: &corev1.SecurityContext{ - AllowPrivilegeEscalation: address.Of(false), - Privileged: address.Of(false), - ReadOnlyRootFilesystem: address.Of(true), - RunAsGroup: address.Of(unprivilegedGroup), - RunAsUser: address.Of(unprivilegedUser), - RunAsNonRoot: address.Of(true), + AllowPrivilegeEscalation: ptr.To(false), + Privileged: ptr.To(false), + ReadOnlyRootFilesystem: ptr.To(true), + RunAsGroup: ptr.To(unprivilegedGroup), + RunAsUser: ptr.To(unprivilegedUser), + RunAsNonRoot: ptr.To(true), }, VolumeMounts: prepareVolumeMounts(ec), } diff --git a/pkg/controllers/edgeconnect/deployment/deployment_test.go b/pkg/controllers/edgeconnect/deployment/deployment_test.go index d9d21c92d4..97c431b729 100644 --- a/pkg/controllers/edgeconnect/deployment/deployment_test.go +++ b/pkg/controllers/edgeconnect/deployment/deployment_test.go @@ -6,8 +6,10 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/api/status" "github.com/Dynatrace/dynatrace-operator/pkg/api/v1alpha2/edgeconnect" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/labels" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/resources" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -62,7 +64,133 @@ func Test_buildAppLabels(t *testing.T) { t.Run("Check version label set correctly", func(t *testing.T) { labels := buildAppLabels(ec) - assert.Equal(t, "", labels.Version) + assert.Empty(t, labels.Version) + }) +} + +func TestLabels(t *testing.T) { + testObjectMetaLabelKey := "test-om-label-key" + testObjectMetaValue := "test-om-label-value" + + testLabelKey := "test-label-key" + testLabelValue := "test-label-value" + + t.Run("Check empty custom labels", func(t *testing.T) { + ec := &edgeconnect.EdgeConnect{ + ObjectMeta: metav1.ObjectMeta{ + Name: testName, + Namespace: testNamespace, + Labels: map[string]string{ + testObjectMetaLabelKey: testObjectMetaValue, + }, + }, + Spec: edgeconnect.EdgeConnectSpec{}, + } + + deployment := New(ec) + + require.Len(t, deployment.Spec.Template.Labels, 5) + assert.Contains(t, deployment.Spec.Template.Labels, labels.AppNameLabel) + assert.Contains(t, deployment.Spec.Template.Labels, labels.AppCreatedByLabel) + assert.Contains(t, deployment.Spec.Template.Labels, labels.AppManagedByLabel) + assert.Contains(t, deployment.Spec.Template.Labels, labels.AppVersionLabel) + assert.Contains(t, deployment.Spec.Template.Labels, labels.AppComponentLabel) + + require.Len(t, deployment.ObjectMeta.Labels, 5) + assert.Contains(t, deployment.ObjectMeta.Labels, labels.AppNameLabel) + assert.Contains(t, deployment.ObjectMeta.Labels, labels.AppCreatedByLabel) + assert.Contains(t, deployment.ObjectMeta.Labels, labels.AppManagedByLabel) + assert.Contains(t, deployment.ObjectMeta.Labels, labels.AppVersionLabel) + assert.Contains(t, deployment.ObjectMeta.Labels, labels.AppComponentLabel) + }) + + t.Run("Check custom label set correctly", func(t *testing.T) { + ec := &edgeconnect.EdgeConnect{ + ObjectMeta: metav1.ObjectMeta{ + Name: testName, + Namespace: testNamespace, + Labels: map[string]string{ + testObjectMetaLabelKey: testObjectMetaValue, + }, + }, + Spec: edgeconnect.EdgeConnectSpec{ + Labels: map[string]string{ + testLabelKey: testLabelValue, + }, + }, + } + + deployment := New(ec) + + assert.Len(t, deployment.Spec.Template.Labels, 6) + assert.Contains(t, deployment.Spec.Template.Labels, labels.AppNameLabel) + assert.Contains(t, deployment.Spec.Template.Labels, labels.AppCreatedByLabel) + assert.Contains(t, deployment.Spec.Template.Labels, labels.AppManagedByLabel) + assert.Contains(t, deployment.Spec.Template.Labels, labels.AppVersionLabel) + assert.Contains(t, deployment.Spec.Template.Labels, labels.AppComponentLabel) + + assert.Contains(t, deployment.Spec.Template.ObjectMeta.Labels, testLabelKey) + assert.Equal(t, testLabelValue, deployment.Spec.Template.ObjectMeta.Labels[testLabelKey]) + + require.Len(t, deployment.ObjectMeta.Labels, 5) + assert.Contains(t, deployment.ObjectMeta.Labels, labels.AppNameLabel) + assert.Contains(t, deployment.ObjectMeta.Labels, labels.AppCreatedByLabel) + assert.Contains(t, deployment.ObjectMeta.Labels, labels.AppManagedByLabel) + assert.Contains(t, deployment.ObjectMeta.Labels, labels.AppVersionLabel) + assert.Contains(t, deployment.ObjectMeta.Labels, labels.AppComponentLabel) + }) +} + +func TestAnnotations(t *testing.T) { + testObjectMetaAnnotationKey := "test-om-annotation-key" + testObjectMetaAnnotationValue := "test-om-annotation-value" + + testAnnotationKey := "test-annotation-key" + testAnnotationValue := "test-annotation-value" + + t.Run("Check empty annotations", func(t *testing.T) { + ec := &edgeconnect.EdgeConnect{ + ObjectMeta: metav1.ObjectMeta{ + Name: testName, + Namespace: testNamespace, + Annotations: map[string]string{ + testObjectMetaAnnotationKey: testObjectMetaAnnotationValue, + }, + }, + Spec: edgeconnect.EdgeConnectSpec{}, + } + + deployment := New(ec) + + assert.Nil(t, deployment.Spec.Template.ObjectMeta.Annotations) + + assert.NotContains(t, deployment.ObjectMeta.Annotations, testObjectMetaAnnotationKey) + }) + + t.Run("Check custom annotations set correctly", func(t *testing.T) { + ec := &edgeconnect.EdgeConnect{ + ObjectMeta: metav1.ObjectMeta{ + Name: testName, + Namespace: testNamespace, + Annotations: map[string]string{ + testObjectMetaAnnotationKey: testObjectMetaAnnotationValue, + }, + }, + Spec: edgeconnect.EdgeConnectSpec{ + Annotations: map[string]string{ + testAnnotationKey: testAnnotationValue, + }, + }, + } + + deployment := New(ec) + + assert.Len(t, deployment.Spec.Template.Annotations, 1) + assert.Contains(t, deployment.Spec.Template.ObjectMeta.Annotations, testAnnotationKey) + assert.Equal(t, testAnnotationValue, deployment.Spec.Template.ObjectMeta.Annotations[testAnnotationKey]) + + assert.NotContains(t, deployment.ObjectMeta.Annotations, testAnnotationKey) + assert.NotContains(t, deployment.ObjectMeta.Annotations, testObjectMetaAnnotationKey) }) } diff --git a/pkg/controllers/edgeconnect/version/updater_test.go b/pkg/controllers/edgeconnect/version/updater_test.go index a43a682723..991ca81d19 100644 --- a/pkg/controllers/edgeconnect/version/updater_test.go +++ b/pkg/controllers/edgeconnect/version/updater_test.go @@ -10,13 +10,13 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/image" "github.com/Dynatrace/dynatrace-operator/pkg/api/v1alpha2/edgeconnect" "github.com/Dynatrace/dynatrace-operator/pkg/oci/registry" - "github.com/Dynatrace/dynatrace-operator/pkg/util/address" "github.com/Dynatrace/dynatrace-operator/pkg/util/timeprovider" registrymock "github.com/Dynatrace/dynatrace-operator/test/mocks/pkg/oci/registry" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" ) const fakeDigest = "sha256:7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc" @@ -121,7 +121,7 @@ func TestReconcileRequired(t *testing.T) { edgeConnectTime := metav1.Now() edgeConnect.Status.Version.LastProbeTimestamp = &edgeConnectTime - edgeConnect.Spec.AutoUpdate = address.Of(true) + edgeConnect.Spec.AutoUpdate = ptr.To(true) edgeConnect.Status.Version.ImageID = edgeConnect.Image() assert.False(t, updater.RequiresReconcile()) @@ -133,7 +133,7 @@ func TestReconcileRequired(t *testing.T) { edgeConnectTime := metav1.NewTime(currentTime.Now().Add(-time.Hour)) edgeConnect.Status.Version.LastProbeTimestamp = &edgeConnectTime - edgeConnect.Spec.AutoUpdate = address.Of(true) + edgeConnect.Spec.AutoUpdate = ptr.To(true) edgeConnect.Status.Version.ImageID = edgeConnect.Image() assert.True(t, updater.RequiresReconcile()) diff --git a/pkg/controllers/nodes/nodes_controller.go b/pkg/controllers/nodes/nodes_controller.go index 2e0bf8f554..97130c523a 100644 --- a/pkg/controllers/nodes/nodes_controller.go +++ b/pkg/controllers/nodes/nodes_controller.go @@ -6,7 +6,7 @@ import ( "time" "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/dynatraceclient" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/token" diff --git a/pkg/controllers/nodes/nodes_controller_test.go b/pkg/controllers/nodes/nodes_controller_test.go index a3edbd3643..0ecdfb2bb1 100644 --- a/pkg/controllers/nodes/nodes_controller_test.go +++ b/pkg/controllers/nodes/nodes_controller_test.go @@ -6,7 +6,8 @@ import ( "time" "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/dynatraceclient" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/token" @@ -40,8 +41,8 @@ func TestReconcile(t *testing.T) { &dynakube.DynaKube{ ObjectMeta: metav1.ObjectMeta{Name: "oneagent1", Namespace: testNamespace}, Status: dynakube.DynaKubeStatus{ - OneAgent: dynakube.OneAgentStatus{ - Instances: map[string]dynakube.OneAgentInstance{node.Name: {IPAddress: "1.2.3.4"}}, + OneAgent: oneagent.Status{ + Instances: map[string]oneagent.Instance{node.Name: {IPAddress: "1.2.3.4"}}, }, }, }, @@ -289,16 +290,16 @@ func createDefaultFakeClient() client.Client { &dynakube.DynaKube{ ObjectMeta: metav1.ObjectMeta{Name: "oneagent1", Namespace: testNamespace}, Status: dynakube.DynaKubeStatus{ - OneAgent: dynakube.OneAgentStatus{ - Instances: map[string]dynakube.OneAgentInstance{"node1": {IPAddress: "1.2.3.4"}}, + OneAgent: oneagent.Status{ + Instances: map[string]oneagent.Instance{"node1": {IPAddress: "1.2.3.4"}}, }, }, }, &dynakube.DynaKube{ ObjectMeta: metav1.ObjectMeta{Name: "oneagent2", Namespace: testNamespace}, Status: dynakube.DynaKubeStatus{ - OneAgent: dynakube.OneAgentStatus{ - Instances: map[string]dynakube.OneAgentInstance{"node2": {IPAddress: "5.6.7.8"}}, + OneAgent: oneagent.Status{ + Instances: map[string]oneagent.Instance{"node2": {IPAddress: "5.6.7.8"}}, }, }, }, diff --git a/pkg/controllers/nodes/oneagent_dao.go b/pkg/controllers/nodes/oneagent_dao.go index 067817bd68..45dbe52c1b 100644 --- a/pkg/controllers/nodes/oneagent_dao.go +++ b/pkg/controllers/nodes/oneagent_dao.go @@ -3,7 +3,7 @@ package nodes import ( "context" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "sigs.k8s.io/controller-runtime/pkg/client" ) diff --git a/pkg/injection/codemodule/installer/image/installer.go b/pkg/injection/codemodule/installer/image/installer.go index 70cbcdcea6..de2fb92ff3 100644 --- a/pkg/injection/codemodule/installer/image/installer.go +++ b/pkg/injection/codemodule/installer/image/installer.go @@ -6,7 +6,7 @@ import ( "os" "path/filepath" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi/metadata" "github.com/Dynatrace/dynatrace-operator/pkg/injection/codemodule/installer" "github.com/Dynatrace/dynatrace-operator/pkg/injection/codemodule/installer/common" @@ -25,7 +25,6 @@ type Properties struct { ApiReader client.Reader Dynakube *dynakube.DynaKube PathResolver metadata.PathResolver - Metadata metadata.Access ImageDigest string } @@ -65,7 +64,7 @@ func (installer *Installer) InstallAgent(_ context.Context, targetDir string) (b if installer.isAlreadyPresent(targetDir) { log.Info("agent already installed", "image", installer.props.ImageUri, "target dir", targetDir) - return false, nil + return true, nil } err := installer.fs.MkdirAll(installer.props.PathResolver.AgentSharedBinaryDirBase(), common.MkDirFileMode) @@ -85,7 +84,7 @@ func (installer *Installer) InstallAgent(_ context.Context, targetDir string) (b return false, errors.WithStack(err) } - if err := symlink.CreateSymlinkForCurrentVersionIfNotExists(installer.fs, targetDir); err != nil { + if err := symlink.CreateForCurrentVersionIfNotExists(installer.fs, targetDir); err != nil { _ = installer.fs.RemoveAll(targetDir) log.Info("failed to create symlink for agent installation", "err", err) diff --git a/pkg/injection/codemodule/installer/image/installer_test.go b/pkg/injection/codemodule/installer/image/installer_test.go index 2d60753994..ea8bef6624 100644 --- a/pkg/injection/codemodule/installer/image/installer_test.go +++ b/pkg/injection/codemodule/installer/image/installer_test.go @@ -9,7 +9,7 @@ import ( "testing" "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/consts" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi/metadata" "github.com/Dynatrace/dynatrace-operator/pkg/injection/codemodule/installer/zip" diff --git a/pkg/injection/codemodule/installer/image/unpack.go b/pkg/injection/codemodule/installer/image/unpack.go index 337689c654..d9c2a39898 100644 --- a/pkg/injection/codemodule/installer/image/unpack.go +++ b/pkg/injection/codemodule/installer/image/unpack.go @@ -3,7 +3,6 @@ package image import ( "context" "encoding/base64" - "fmt" "path" "path/filepath" @@ -105,7 +104,7 @@ func (installer *Installer) unpackOciImage(layers []containerv1.Layer, imageCach for _, layer := range layers { mediaType, _ := layer.MediaType() switch mediaType { - case types.DockerLayer: + case types.DockerLayer, types.OCILayer: digest, _ := layer.Digest() sourcePath := filepath.Join(imageCacheDir, "blobs", digest.Algorithm, digest.Hex) log.Info("unpackOciImage", "sourcePath", sourcePath) @@ -113,12 +112,10 @@ func (installer *Installer) unpackOciImage(layers []containerv1.Layer, imageCach if err := installer.extractor.ExtractGzip(sourcePath, targetDir); err != nil { return err } - case types.OCILayer: - return errors.New("OCILayer is not implemented") case types.OCILayerZStd: return errors.New("OCILayerZStd is not implemented") default: - return fmt.Errorf("media type %s is not implemented", mediaType) + return errors.Errorf("media type %s is not implemented", mediaType) } } diff --git a/pkg/injection/codemodule/installer/job/bootstrapper.go b/pkg/injection/codemodule/installer/job/bootstrapper.go new file mode 100644 index 0000000000..ff1ac46fdf --- /dev/null +++ b/pkg/injection/codemodule/installer/job/bootstrapper.go @@ -0,0 +1,113 @@ +package job + +import ( + "github.com/Dynatrace/dynatrace-operator/pkg/util/hasher" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/env" + jobutil "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/job" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/labels" + "github.com/Dynatrace/dynatrace-operator/pkg/webhook" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/utils/ptr" +) + +const ( + namePrefix = "codemodule-download-" + + volumeName = "dynatrace-codemodules" + codeModuleSource = "/opt/dynatrace/oneagent" + + provisionerServiceAccount = "dynatrace-oneagent-csi-driver" // TODO: Get it from env + + activeDeadlineSeconds int64 = 600 // 10 min, after which the Job will be put into a Failed state + ttlSecondsAfterFinished int32 = 10 // 10 sec after the Job is put into a Succeeded or Failed state the Job and related Pods will be terminated +) + +func (inst *Installer) buildJobName() string { + hashPostfix, _ := hasher.GenerateHash(inst.props.ImageUri + inst.nodeName) + + return namePrefix + hashPostfix +} + +func (inst *Installer) buildJob(name, targetDir string) (*batchv1.Job, error) { + tolerations, err := env.GetTolerations() + if err != nil { + log.Info("failed to get tolerations from env") + + return nil, err + } + + appLabels := labels.NewAppLabels(labels.CodeModuleComponentLabel, inst.props.Owner.GetName(), "", "") + + container := corev1.Container{ + Name: "codemodule-download", + Image: inst.props.ImageUri, + ImagePullPolicy: corev1.PullIfNotPresent, + VolumeMounts: []corev1.VolumeMount{ + { + Name: volumeName, + MountPath: inst.props.PathResolver.RootDir, + }, + }, + SecurityContext: &corev1.SecurityContext{ // TODO: Use the SecurityContext from the `provisioner` container + RunAsUser: ptr.To(int64(0)), + RunAsNonRoot: ptr.To(false), + Privileged: ptr.To(true), + AllowPrivilegeEscalation: ptr.To(true), + ReadOnlyRootFilesystem: ptr.To(true), + SELinuxOptions: &corev1.SELinuxOptions{ + Level: "s0", + }, + SeccompProfile: &corev1.SeccompProfile{ + Type: corev1.SeccompProfileTypeRuntimeDefault, + }, + }, + } + + hostVolume := corev1.Volume{ + Name: volumeName, + VolumeSource: corev1.VolumeSource{ + HostPath: &corev1.HostPathVolumeSource{ + Path: env.GetCSIDataDir(), + }, + }, + } + + container.Args = inst.buildArgs() + container.Env = []corev1.EnvVar{ + { + Name: "TARGET_PATH", + Value: targetDir, + }, + { + Name: "WORK_PATH", + Value: inst.props.PathResolver.AgentJobWorkDirForJob(name), + }, + } + + annotations := map[string]string{ + webhook.AnnotationDynatraceInject: "false", + } + + return jobutil.Build(inst.props.Owner, name, container, + jobutil.SetPodAnnotations(annotations), + jobutil.SetNodeName(inst.nodeName), + jobutil.SetPullSecret(inst.props.PullSecrets...), + jobutil.SetTolerations(tolerations), + jobutil.SetAllLabels(appLabels.BuildLabels(), map[string]string{}, appLabels.BuildLabels(), map[string]string{}), + jobutil.SetVolumes([]corev1.Volume{hostVolume}), + jobutil.SetOnFailureRestartPolicy(), + jobutil.SetAutomountServiceAccountToken(false), + jobutil.SetActiveDeadlineSeconds(activeDeadlineSeconds), + jobutil.SetTTLSecondsAfterFinished(ttlSecondsAfterFinished), + jobutil.SetServiceAccount(provisionerServiceAccount), + ) +} + +func (inst *Installer) buildArgs() []string { + return []string{ + "--source=" + codeModuleSource, + "--target=$(TARGET_PATH)", + "--work=$(WORK_PATH)", + } +} diff --git a/pkg/injection/codemodule/installer/job/bootstrapper_test.go b/pkg/injection/codemodule/installer/job/bootstrapper_test.go new file mode 100644 index 0000000000..6680a0842e --- /dev/null +++ b/pkg/injection/codemodule/installer/job/bootstrapper_test.go @@ -0,0 +1,195 @@ +package job + +import ( + "encoding/json" + "slices" + "testing" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi/metadata" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/env" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestBuildJobName(t *testing.T) { + t.Run("job names are unique", func(t *testing.T) { + uris := []string{"test1.com", "test2.com", "test3.com", "test4.com", "test5.com"} + nodeNames := []string{"node1", "node2", "node3", "node4", "node5"} + + jobNames := []string{} + + for i := range nodeNames { + for j := range uris { + props := &Properties{ + ImageUri: uris[j], + } + inst := &Installer{ + nodeName: nodeNames[i], + props: props, + } + jobNames = append(jobNames, inst.buildJobName()) + } + } + + slices.Sort(jobNames) + jobNames = slices.Compact(jobNames) + + assert.Len(t, jobNames, len(nodeNames)*len(uris)) + }) +} + +func TestBuildArgs(t *testing.T) { + t.Run("args are built correctly", func(t *testing.T) { + props := &Properties{ + PathResolver: metadata.PathResolver{RootDir: "root"}, + } + inst := &Installer{ + props: props, + } + + args := inst.buildArgs() + + require.Len(t, args, 3) + assert.Equal(t, "--source=/opt/dynatrace/oneagent", args[0]) + assert.Equal(t, "--target=$(TARGET_PATH)", args[1]) + assert.Equal(t, "--work=$(WORK_PATH)", args[2]) + }) +} + +func TestBuildJob(t *testing.T) { + owner := dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-dk", + }, + } + name := "job-1" + imageURI := "test:5000/repo" + nodeName := "node1" + targetDir := "1.2.3" + pullSecrets := []string{"secret-1", "secret-2"} + + t.Run("job created correctly", func(t *testing.T) { + tolerations := setupTolerations(t) + dataDir := setupDataDir(t) + + props := &Properties{ + Owner: &owner, + ImageUri: imageURI, + PullSecrets: pullSecrets, + PathResolver: metadata.PathResolver{RootDir: "root"}, + } + inst := &Installer{ + nodeName: nodeName, + props: props, + } + + job, err := inst.buildJob(name, targetDir) + require.NoError(t, err) + require.NotNil(t, job) + + // Global/pod level checks + assert.Equal(t, name, job.Name) + assert.NotNil(t, job.Labels) + assert.NotNil(t, job.Spec.Template.Labels) + assert.Empty(t, job.Spec.Selector) // the Job objects handles this by default, our generated MatchLabels wound't even work + + assert.Equal(t, tolerations, job.Spec.Template.Spec.Tolerations) + assert.Equal(t, nodeName, job.Spec.Template.Spec.NodeName) + assert.False(t, *job.Spec.Template.Spec.AutomountServiceAccountToken) + assert.Equal(t, corev1.RestartPolicyOnFailure, job.Spec.Template.Spec.RestartPolicy) + + require.Len(t, job.Spec.Template.Spec.Volumes, 1) + require.NotNil(t, job.Spec.Template.Spec.Volumes[0].HostPath) + assert.Equal(t, dataDir, job.Spec.Template.Spec.Volumes[0].HostPath.Path) + + require.Equal(t, provisionerServiceAccount, job.Spec.Template.Spec.ServiceAccountName) + require.Equal(t, provisionerServiceAccount, job.Spec.Template.Spec.DeprecatedServiceAccount) + + require.Len(t, job.Spec.Template.Spec.ImagePullSecrets, len(pullSecrets)) + + for i, ps := range pullSecrets { + assert.Equal(t, ps, job.Spec.Template.Spec.ImagePullSecrets[i].Name) + } + + // Container level checks + require.Len(t, job.Spec.Template.Spec.Containers, 1) + container := job.Spec.Template.Spec.Containers[0] + + assert.Equal(t, imageURI, container.Image) + assert.Empty(t, container.Command) + assert.NotEmpty(t, container.Args) + assert.NotEmpty(t, container.SecurityContext) + + require.Len(t, container.Env, 2) + + targetEnv := corev1.EnvVar{ + Name: "TARGET_PATH", + Value: targetDir, + } + + workEnv := corev1.EnvVar{ + Name: "WORK_PATH", + Value: props.PathResolver.AgentJobWorkDirForJob(name), + } + + assert.Contains(t, container.Env, targetEnv) + assert.Contains(t, container.Env, workEnv) + + require.Len(t, container.VolumeMounts, 1) + assert.Equal(t, job.Spec.Template.Spec.Volumes[0].Name, container.VolumeMounts[0].Name) + assert.Equal(t, props.PathResolver.RootDir, container.VolumeMounts[0].MountPath) + }) + + t.Run("job create fail, can't parse tolerations", func(t *testing.T) { + setupMalformedTolerations(t) + + inst := &Installer{} + + _, err := inst.buildJob(name, targetDir) + require.Error(t, err) + }) +} + +func setupTolerations(t *testing.T) []corev1.Toleration { + t.Helper() + + expected := []corev1.Toleration{ + { + Key: "key1", + Operator: corev1.TolerationOpEqual, + Value: "value1", + Effect: corev1.TaintEffectNoSchedule, + }, + { + Key: "key2", + Operator: corev1.TolerationOpEqual, + Value: "value1", + Effect: corev1.TaintEffectNoSchedule, + }, + } + + raw, err := json.Marshal(expected) + require.NoError(t, err) + + t.Setenv(env.Tolerations, string(raw)) + + return expected +} + +func setupMalformedTolerations(t *testing.T) { + t.Helper() + t.Setenv(env.Tolerations, "{$^$&^%}") +} + +func setupDataDir(t *testing.T) string { + t.Helper() + + dataDir := "test/data" + + t.Setenv(env.CSIDataDir, dataDir) + + return dataDir +} diff --git a/cmd/csi/init/config.go b/pkg/injection/codemodule/installer/job/config.go similarity index 50% rename from cmd/csi/init/config.go rename to pkg/injection/codemodule/installer/job/config.go index 3ef0e4f665..e84e85bbc4 100644 --- a/cmd/csi/init/config.go +++ b/pkg/injection/codemodule/installer/job/config.go @@ -1,7 +1,9 @@ -package init +package job import ( "github.com/Dynatrace/dynatrace-operator/pkg/logd" ) -var log = logd.Get().WithName("csi-init") +var ( + log = logd.Get().WithName("oneagent-job") +) diff --git a/pkg/injection/codemodule/installer/job/installer.go b/pkg/injection/codemodule/installer/job/installer.go new file mode 100644 index 0000000000..364bbaa2a5 --- /dev/null +++ b/pkg/injection/codemodule/installer/job/installer.go @@ -0,0 +1,119 @@ +package job + +import ( + "context" + "os" + + "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi/metadata" + "github.com/Dynatrace/dynatrace-operator/pkg/injection/codemodule/installer" + "github.com/Dynatrace/dynatrace-operator/pkg/injection/codemodule/installer/common" + "github.com/Dynatrace/dynatrace-operator/pkg/injection/codemodule/installer/symlink" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/env" + jobutil "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/job" + "github.com/pkg/errors" + "github.com/spf13/afero" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type Properties struct { + Owner client.Object + ApiReader client.Reader + Client client.Client + ImageUri string + PathResolver metadata.PathResolver + PullSecrets []string +} + +func NewInstaller(ctx context.Context, fs afero.Fs, props *Properties) installer.Installer { + return &Installer{ + fs: fs, + props: props, + nodeName: env.GetNodeName(), + } +} + +type Installer struct { + fs afero.Fs + props *Properties + nodeName string +} + +func (inst *Installer) InstallAgent(ctx context.Context, targetDir string) (bool, error) { + log.Info("installing agent via Job", "image", inst.props.ImageUri, "target dir", targetDir) + + err := inst.fs.MkdirAll(inst.props.PathResolver.AgentSharedBinaryDirBase(), common.MkDirFileMode) + if err != nil { + log.Info("failed to create the base shared agent directory", "err", err) + + return false, errors.WithStack(err) + } + + jobName := inst.buildJobName() + + ready, err := inst.isReady(ctx, targetDir, jobName) + if err != nil { + return false, err + } + + if !ready { + return false, nil + } + + if err := symlink.CreateForCurrentVersionIfNotExists(inst.fs, targetDir); err != nil { + _ = inst.fs.RemoveAll(targetDir) + + log.Info("failed to create symlink for agent installation", "err", err) + + return false, errors.WithStack(err) + } + + return true, nil +} + +func (inst *Installer) isReady(ctx context.Context, targetDir, jobName string) (bool, error) { + if inst.isAlreadyPresent(targetDir) { + log.Info("agent already installed", "image", inst.props.ImageUri, "target dir", targetDir) + + _ = inst.fs.RemoveAll(inst.props.PathResolver.AgentJobWorkDirForJob(jobName)) + + return true, inst.query().DeleteForNamespace(ctx, jobName, inst.props.Owner.GetNamespace(), &client.DeleteOptions{PropagationPolicy: ptr.To(metav1.DeletePropagationBackground)}) + } + + job, err := inst.query().Get(ctx, types.NamespacedName{Name: jobName, Namespace: inst.props.Owner.GetNamespace()}) + if err != nil && !k8serrors.IsNotFound(err) { + log.Info("failed to determine the status of the download job", "err", err) + + return false, err + } else if err == nil { + log.Info("job is not finished", "job", jobName) + + if job.Status.Failed > 0 { + return false, errors.Errorf("the job is failing; job: %s", jobName) + } + + return false, nil + } + + log.Info("creating new download job", "job", jobName) + + job, err = inst.buildJob(jobName, targetDir) + if err != nil { + return false, err + } + + return false, inst.query().WithOwner(inst.props.Owner).Create(ctx, job) +} + +func (installer *Installer) isAlreadyPresent(targetDir string) bool { + _, err := installer.fs.Stat(targetDir) + + return !os.IsNotExist(err) +} + +func (inst *Installer) query() jobutil.QueryObject { + return jobutil.Query(inst.props.Client, inst.props.ApiReader, log) +} diff --git a/pkg/injection/codemodule/installer/job/installer_test.go b/pkg/injection/codemodule/installer/job/installer_test.go new file mode 100644 index 0000000000..28bb58b561 --- /dev/null +++ b/pkg/injection/codemodule/installer/job/installer_test.go @@ -0,0 +1,175 @@ +package job + +import ( + "context" + "os" + "testing" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi/metadata" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + batchv1 "k8s.io/api/batch/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func TestIsAlreadyPresent(t *testing.T) { + testImageURL := "test:5000/repo@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + testVersion := "1.2.3" + pathResolver := metadata.PathResolver{} + + t.Run("returns early if path doesn't exist", func(t *testing.T) { + installer := Installer{ + fs: afero.NewMemMapFs(), + props: &Properties{ + PathResolver: pathResolver, + ImageUri: testImageURL, + }, + } + isDownloaded := installer.isAlreadyPresent(pathResolver.AgentSharedBinaryDirForAgent(testVersion)) + assert.False(t, isDownloaded) + }) + t.Run("returns true if path present", func(t *testing.T) { + installer := Installer{ + fs: testFileSystemWithSharedDirPresent(pathResolver, testVersion), + props: &Properties{ + PathResolver: pathResolver, + }, + } + isDownloaded := installer.isAlreadyPresent(pathResolver.AgentSharedBinaryDirForAgent(testVersion)) + assert.True(t, isDownloaded) + }) +} + +func testFileSystemWithSharedDirPresent(pathResolver metadata.PathResolver, imageDigest string) afero.Fs { + fs := afero.NewMemMapFs() + _ = fs.MkdirAll(pathResolver.AgentSharedBinaryDirForAgent(imageDigest), 0777) + + return fs +} + +func TestIsReady(t *testing.T) { + ctx := context.Background() + owner := dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-dk", + Namespace: "test", + }, + } + name := "job-1" + targetDir := "/download/here/1.2.3" + nodeName := "node-1" + + t.Run("nothing present -> create job", func(t *testing.T) { + cl := fake.NewClient() + props := &Properties{ + ApiReader: cl, + Client: cl, + Owner: &owner, + } + inst := &Installer{ + fs: afero.NewMemMapFs(), + nodeName: nodeName, + props: props, + } + + ready, err := inst.isReady(ctx, targetDir, name) + require.NoError(t, err) + require.False(t, ready) + + var newJob batchv1.Job + + require.NoError(t, cl.Get(ctx, types.NamespacedName{Name: name, Namespace: owner.GetNamespace()}, &newJob)) + require.NotEmpty(t, newJob) + }) + + t.Run("job present, target not present -> return not ready, no error", func(t *testing.T) { + cl := fake.NewClient() + props := &Properties{ + ApiReader: setupInCompleteJob(t, name, owner.Namespace), + Client: cl, + Owner: &owner, + } + inst := &Installer{ + fs: afero.NewMemMapFs(), + nodeName: nodeName, + props: props, + } + + ready, err := inst.isReady(ctx, targetDir, name) + require.NoError(t, err) + require.False(t, ready) + + var newJob batchv1.Job // should not create a Job as it already exists, bit of a hack, that I use different clients for ApiReader and Client + err = cl.Get(ctx, types.NamespacedName{Name: name, Namespace: owner.GetNamespace()}, &newJob) + require.Error(t, err) + require.NoError(t, client.IgnoreNotFound(err)) + }) + + t.Run("job present, target present -> return ready, cleanup job", func(t *testing.T) { + cl := setupCompleteJob(t, name, owner.Namespace) + props := &Properties{ + ApiReader: cl, + Client: cl, + Owner: &owner, + } + inst := &Installer{ + fs: afero.NewMemMapFs(), + nodeName: nodeName, + props: props, + } + + setupTargetDir(t, inst.fs, targetDir) + + ready, err := inst.isReady(ctx, targetDir, name) + require.NoError(t, err) + require.True(t, ready) + + var newJob batchv1.Job + err = cl.Get(ctx, types.NamespacedName{Name: name, Namespace: owner.GetNamespace()}, &newJob) + require.Error(t, err) + require.NoError(t, client.IgnoreNotFound(err)) + }) +} + +func setupCompleteJob(t *testing.T, name, namespace string) client.Client { + t.Helper() + + fakeJob := batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Status: batchv1.JobStatus{ + Succeeded: 1, + }, + } + + return fake.NewClient(&fakeJob) +} + +func setupInCompleteJob(t *testing.T, name, namespace string) client.Client { + t.Helper() + + fakeJob := batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Status: batchv1.JobStatus{ + Succeeded: 0, + }, + } + + return fake.NewClient(&fakeJob) +} + +func setupTargetDir(t *testing.T, fs afero.Fs, targetDir string) { + t.Helper() + + require.NoError(t, fs.MkdirAll(targetDir, os.ModePerm)) +} diff --git a/pkg/injection/codemodule/installer/symlink/symlink.go b/pkg/injection/codemodule/installer/symlink/symlink.go index 53c4ab72b9..b9ad2b9edd 100644 --- a/pkg/injection/codemodule/installer/symlink/symlink.go +++ b/pkg/injection/codemodule/installer/symlink/symlink.go @@ -9,39 +9,48 @@ import ( "github.com/spf13/afero" ) -func CreateSymlinkForCurrentVersionIfNotExists(fs afero.Fs, targetDir string) error { - var relativeSymlinkPath string - +func CreateForCurrentVersionIfNotExists(fs afero.Fs, targetDir string) error { var err error - targetBindDir := filepath.Join(targetDir, binDir) - - // MemMapFs (used for testing) doesn't comply with the Linker interface - linker, ok := fs.(afero.Linker) + _, ok := fs.(afero.Linker) if !ok { log.Info("symlinking not possible", "targetDir", targetDir, "fs", fs) return nil } - relativeSymlinkPath, err = findVersionFromFileSystem(fs, targetBindDir) + targetBinDir := filepath.Join(targetDir, binDir) + + relativeSymlinkPath, err := findVersionFromFileSystem(fs, targetBinDir) if err != nil { log.Info("failed to get the version from the filesystem", "targetDir", targetDir) return err } - symlinkTargetPath := filepath.Join(targetBindDir, "current") - if fileInfo, _ := fs.Stat(symlinkTargetPath); fileInfo != nil { - log.Info("symlink already exists", "location", symlinkTargetPath) + return Create(fs, relativeSymlinkPath, filepath.Join(targetBinDir, "current")) +} + +func Create(fs afero.Fs, targetDir, symlinkDir string) error { + // MemMapFs (used for testing) doesn't comply with the Linker interface + linker, ok := fs.(afero.Linker) + if !ok { + log.Info("symlinking not possible", "targetDir", targetDir, "fs", fs) + + return nil + } + + // Check if the symlink already exists + if fileInfo, _ := fs.Stat(symlinkDir); fileInfo != nil { + log.Info("symlink already exists", "location", symlinkDir) return nil } - log.Info("creating symlink", "points-to(relative)", relativeSymlinkPath, "location", symlinkTargetPath) + log.Info("creating symlink", "points-to(relative)", targetDir, "location", symlinkDir) - if err := linker.SymlinkIfPossible(relativeSymlinkPath, symlinkTargetPath); err != nil { - log.Info("symlinking failed", "version", relativeSymlinkPath) + if err := linker.SymlinkIfPossible(targetDir, symlinkDir); err != nil { + log.Info("symlinking failed", "source", targetDir) return errors.WithStack(err) } @@ -49,6 +58,18 @@ func CreateSymlinkForCurrentVersionIfNotExists(fs afero.Fs, targetDir string) er return nil } +func Remove(fs afero.Fs, symlinkPath string) error { + if info, _ := fs.Stat(symlinkPath); info != nil { + log.Info("symlink to directory exists, removing it to ensure proper reinstallation or reconfiguration", "directory", symlinkPath) + + if err := fs.Remove(symlinkPath); err != nil { + return err + } + } + + return nil +} + func findVersionFromFileSystem(fs afero.Fs, targetDir string) (string, error) { var version string diff --git a/pkg/injection/codemodule/installer/url/download.go b/pkg/injection/codemodule/installer/url/download.go index 090261bb80..061b7cd777 100644 --- a/pkg/injection/codemodule/installer/url/download.go +++ b/pkg/injection/codemodule/installer/url/download.go @@ -58,7 +58,6 @@ func (installer Installer) downloadOneAgentWithVersion(ctx context.Context, tmpF installer.props.Os, installer.props.Type, installer.props.Flavor, - installer.props.Arch, ) if getVersionsError != nil { log.Info("failed to get available versions", "err", getVersionsError) diff --git a/pkg/injection/codemodule/installer/url/installer.go b/pkg/injection/codemodule/installer/url/installer.go index 2b7df03cd5..7b8b2c99e0 100644 --- a/pkg/injection/codemodule/installer/url/installer.go +++ b/pkg/injection/codemodule/installer/url/installer.go @@ -57,7 +57,7 @@ func (installer Installer) InstallAgent(ctx context.Context, targetDir string) ( if installer.isAlreadyDownloaded(targetDir) { log.Info("agent already installed", "target dir", targetDir) - return false, nil + return true, nil } err := installer.fs.MkdirAll(installer.props.PathResolver.AgentSharedBinaryDirBase(), common.MkDirFileMode) @@ -77,7 +77,7 @@ func (installer Installer) InstallAgent(ctx context.Context, targetDir string) ( return false, err } - if err := symlink.CreateSymlinkForCurrentVersionIfNotExists(installer.fs, targetDir); err != nil { + if err := symlink.CreateForCurrentVersionIfNotExists(installer.fs, targetDir); err != nil { _ = installer.fs.RemoveAll(targetDir) log.Info("failed to create symlink for agent installation", "targetDir", targetDir) diff --git a/pkg/injection/codemodule/installer/zip/gzip.go b/pkg/injection/codemodule/installer/zip/gzip.go index 92635574d9..8e8214af70 100644 --- a/pkg/injection/codemodule/installer/zip/gzip.go +++ b/pkg/injection/codemodule/installer/zip/gzip.go @@ -2,7 +2,6 @@ package zip import ( "archive/tar" - "fmt" "io" "os" "path/filepath" @@ -56,7 +55,7 @@ func extractFilesFromGzip(fs afero.Fs, targetDir string, reader *tar.Reader) err // Check for ZipSlip: https://snyk.io/research/zip-slip-vulnerability if !strings.HasPrefix(target, targetDir) { - return fmt.Errorf("illegal file path: %s", target) + return errors.Errorf("illegal file path: %s", target) } err = extract(fs, targetDir, reader, header, target) diff --git a/pkg/injection/codemodule/installer/zip/zip.go b/pkg/injection/codemodule/installer/zip/zip.go index a8e5803fe8..e9219b3530 100644 --- a/pkg/injection/codemodule/installer/zip/zip.go +++ b/pkg/injection/codemodule/installer/zip/zip.go @@ -1,7 +1,6 @@ package zip import ( - "fmt" "io" "os" "path/filepath" @@ -70,7 +69,7 @@ func extractFilesFromZip(fs afero.Fs, targetDir string, reader *zip.Reader) erro // Check for ZipSlip: https://snyk.io/research/zip-slip-vulnerability if !strings.HasPrefix(path, filepath.Clean(targetDir)+string(os.PathSeparator)) { - return fmt.Errorf("illegal file path: %s", path) + return errors.Errorf("illegal file path: %s", path) } mode := file.Mode() diff --git a/pkg/injection/codemodule/processmoduleconfig/merge_test.go b/pkg/injection/codemodule/processmoduleconfig/merge_test.go index 472ea54bc8..302a52fb5e 100644 --- a/pkg/injection/codemodule/processmoduleconfig/merge_test.go +++ b/pkg/injection/codemodule/processmoduleconfig/merge_test.go @@ -84,11 +84,11 @@ func TestConfSectionHeader(t *testing.T) { header := confSectionHeader("[general]") assert.Equal(t, "general", header) header = confSectionHeader("general") - assert.Equal(t, "", header) + assert.Empty(t, header) header = confSectionHeader("key val") - assert.Equal(t, "", header) + assert.Empty(t, header) header = confSectionHeader("") - assert.Equal(t, "", header) + assert.Empty(t, header) } func TestStoreConfFile(t *testing.T) { diff --git a/pkg/injection/codemodule/processmoduleconfig/update.go b/pkg/injection/codemodule/processmoduleconfig/update.go index b01d1d15be..c5e9a06408 100644 --- a/pkg/injection/codemodule/processmoduleconfig/update.go +++ b/pkg/injection/codemodule/processmoduleconfig/update.go @@ -15,7 +15,7 @@ var ( sourceRuxitAgentProcPath = filepath.Join("agent", "conf", "_ruxitagentproc.conf") ) -func UpdateProcessModuleConfigInPlace(fs afero.Fs, targetDir string, processModuleConfig *dtclient.ProcessModuleConfig) error { +func UpdateInPlace(fs afero.Fs, targetDir string, processModuleConfig *dtclient.ProcessModuleConfig) error { if processModuleConfig != nil { log.Info("updating ruxitagentproc.conf", "targetDir", targetDir) usedProcessModuleConfigPath := filepath.Join(targetDir, RuxitAgentProcPath) @@ -33,7 +33,7 @@ func UpdateProcessModuleConfigInPlace(fs afero.Fs, targetDir string, processModu return nil } -func CreateAgentConfigDir(fs afero.Fs, targetDir, sourceDir string, processModuleConfig *dtclient.ProcessModuleConfig) error { +func UpdateFromDir(fs afero.Fs, targetDir, sourceDir string, processModuleConfig *dtclient.ProcessModuleConfig) error { if processModuleConfig != nil { log.Info("updating ruxitagentproc.conf", "targetDir", targetDir, "sourceDir", sourceDir) sourceProcessModuleConfigPath := filepath.Join(sourceDir, RuxitAgentProcPath) diff --git a/pkg/injection/codemodule/processmoduleconfig/update_test.go b/pkg/injection/codemodule/processmoduleconfig/update_test.go index 3457da6733..ab445f5c58 100644 --- a/pkg/injection/codemodule/processmoduleconfig/update_test.go +++ b/pkg/injection/codemodule/processmoduleconfig/update_test.go @@ -34,7 +34,7 @@ func TestUpdateProcessModuleConfigInPlace(t *testing.T) { t.Run("no processModuleConfig", func(t *testing.T) { memFs := afero.NewMemMapFs() - err := UpdateProcessModuleConfigInPlace(memFs, "", nil) + err := UpdateInPlace(memFs, "", nil) require.NoError(t, err) }) t.Run("update file", func(t *testing.T) { @@ -49,7 +49,7 @@ key value test test3 ` - err := UpdateProcessModuleConfigInPlace(memFs, "", &testProcessModuleConfig) + err := UpdateInPlace(memFs, "", &testProcessModuleConfig) require.NoError(t, err) assertTestConf(t, memFs, RuxitAgentProcPath, expectedUsed) assertTestConf(t, memFs, sourceRuxitAgentProcPath, testRuxitConf) @@ -60,7 +60,7 @@ func TestCreateAgentConfigDir(t *testing.T) { t.Run("no processModuleConfig", func(t *testing.T) { memFs := afero.NewMemMapFs() - err := CreateAgentConfigDir(memFs, "", "", nil) + err := UpdateFromDir(memFs, "", "", nil) require.NoError(t, err) }) @@ -78,7 +78,7 @@ key value test test3 ` - err := CreateAgentConfigDir(memFs, targetDir, sourceDir, &testProcessModuleConfig) + err := UpdateFromDir(memFs, targetDir, sourceDir, &testProcessModuleConfig) require.NoError(t, err) assertTestConf(t, memFs, filepath.Join(targetDir, RuxitAgentProcPath), expectedUsed) assertTestConf(t, memFs, filepath.Join(sourceDir, RuxitAgentProcPath), testRuxitConf) diff --git a/pkg/injection/namespace/bootstrapperconfig/bootstrapperconfig.go b/pkg/injection/namespace/bootstrapperconfig/bootstrapperconfig.go new file mode 100644 index 0000000000..07451134fa --- /dev/null +++ b/pkg/injection/namespace/bootstrapperconfig/bootstrapperconfig.go @@ -0,0 +1,141 @@ +package bootstrapperconfig + +import ( + "context" + "strconv" + + "github.com/Dynatrace/dynatrace-bootstrapper/pkg/configure/enrichment/endpoint" + "github.com/Dynatrace/dynatrace-bootstrapper/pkg/configure/oneagent/ca" + "github.com/Dynatrace/dynatrace-bootstrapper/pkg/configure/oneagent/curl" + "github.com/Dynatrace/dynatrace-bootstrapper/pkg/configure/oneagent/pmc" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" + "github.com/Dynatrace/dynatrace-operator/pkg/consts" + "github.com/Dynatrace/dynatrace-operator/pkg/injection/namespace/mapper" + "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" + k8slabels "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/labels" + k8ssecret "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/secret" + "github.com/Dynatrace/dynatrace-operator/pkg/util/timeprovider" + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// SecretGenerator manages the bootstrapper init secret generation for the user namespaces. +type SecretGenerator struct { + client client.Client + dtClient dtclient.Client + apiReader client.Reader + timeProvider *timeprovider.Provider +} + +func NewSecretGenerator(client client.Client, apiReader client.Reader, dtClient dtclient.Client) *SecretGenerator { + return &SecretGenerator{ + client: client, + dtClient: dtClient, + apiReader: apiReader, + timeProvider: timeprovider.New(), + } +} + +// GenerateForDynakube creates/updates the init secret for EVERY namespace for the given dynakube. +// Used by the dynakube controller during reconcile. +func (s *SecretGenerator) GenerateForDynakube(ctx context.Context, dk *dynakube.DynaKube) error { + log.Info("reconciling namespace bootstrapper init secret for", "dynakube", dk.Name) + + data, err := s.generate(ctx, dk) + if err != nil { + return errors.WithStack(err) + } + + err = s.createSourceForWebhook(ctx, dk, data) + if err != nil { + return err + } + + nsList, err := mapper.GetNamespacesForDynakube(ctx, s.apiReader, dk.Name) + if err != nil { + conditions.SetKubeApiError(dk.Conditions(), ConditionType, err) + + return errors.WithStack(err) + } + + coreLabels := k8slabels.NewCoreLabels(dk.Name, k8slabels.WebhookComponentLabel) + + secret, err := k8ssecret.BuildForNamespace(consts.BootstrapperInitSecretName, "", data, k8ssecret.SetLabels(coreLabels.BuildLabels())) + if err != nil { + conditions.SetSecretGenFailed(dk.Conditions(), ConditionType, err) + + return err + } + + err = k8ssecret.Query(s.client, s.apiReader, log).CreateOrUpdateForNamespaces(ctx, secret, nsList) + if err != nil { + conditions.SetKubeApiError(dk.Conditions(), ConditionType, err) + + return err + } + + log.Info("done updating init secrets") + conditions.SetSecretCreatedOrUpdated(dk.Conditions(), ConditionType, GetSourceSecretName(dk.Name)) + + return nil +} + +func Cleanup(ctx context.Context, client client.Client, apiReader client.Reader, namespaces []corev1.Namespace, dk *dynakube.DynaKube) error { + defer meta.RemoveStatusCondition(dk.Conditions(), ConditionType) + + nsList := make([]string, 0, len(namespaces)) + for _, ns := range namespaces { + nsList = append(nsList, ns.Name) + } + + err := k8ssecret.Query(client, apiReader, log).DeleteForNamespace(ctx, GetSourceSecretName(dk.Name), dk.Namespace) + if err != nil { + log.Error(err, "failed to delete the source bootstrapper-config secret", "name", GetSourceSecretName(dk.Name)) + } + + return k8ssecret.Query(client, apiReader, log).DeleteForNamespaces(ctx, consts.BootstrapperInitSecretName, nsList) +} + +// generate gets the necessary info the create the init secret data +func (s *SecretGenerator) generate(ctx context.Context, dk *dynakube.DynaKube) (map[string][]byte, error) { + agCerts, err := dk.ActiveGateTLSCert(ctx, s.apiReader) + if err != nil { + conditions.SetKubeApiError(dk.Conditions(), ConditionType, err) + + return nil, errors.WithStack(err) + } + + trustedCAs, err := dk.TrustedCAs(ctx, s.apiReader) + if err != nil { + conditions.SetKubeApiError(dk.Conditions(), ConditionType, err) + + return nil, errors.WithStack(err) + } + + pmcSecret, err := s.preparePMC(ctx, dk) + if err != nil { + return nil, errors.WithStack(err) + } + + endpointProperties, err := s.prepareEndpoints(ctx, dk) + if err != nil { + return nil, errors.WithStack(err) + } + + data := map[string][]byte{ + pmc.InputFileName: pmcSecret, + ca.TrustedCertsInputFile: trustedCAs, + ca.AgCertsInputFile: agCerts, + endpoint.InputFileName: []byte(endpointProperties), + } + + if dk.FeatureAgentInitialConnectRetry() > -1 { + initialConnectRetryMs := strconv.Itoa(dk.FeatureAgentInitialConnectRetry()) + data[curl.InputFileName] = []byte(initialConnectRetryMs) + } + + return data, err +} diff --git a/pkg/injection/namespace/bootstrapperconfig/bootstrapperconfig_test.go b/pkg/injection/namespace/bootstrapperconfig/bootstrapperconfig_test.go new file mode 100644 index 0000000000..f9117d7c0f --- /dev/null +++ b/pkg/injection/namespace/bootstrapperconfig/bootstrapperconfig_test.go @@ -0,0 +1,424 @@ +package bootstrapperconfig + +import ( + "context" + "testing" + + "github.com/Dynatrace/dynatrace-bootstrapper/pkg/configure/enrichment/endpoint" + "github.com/Dynatrace/dynatrace-bootstrapper/pkg/configure/oneagent/ca" + "github.com/Dynatrace/dynatrace-bootstrapper/pkg/configure/oneagent/curl" + "github.com/Dynatrace/dynatrace-bootstrapper/pkg/configure/oneagent/pmc" + "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" + dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" + "github.com/Dynatrace/dynatrace-operator/pkg/consts" + dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" + dtclientmock "github.com/Dynatrace/dynatrace-operator/test/mocks/pkg/clients/dynatrace" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + testPaasToken = "test-paas-token" + testAPIToken = "test-api-token" + testDataIngestToken = "test-ingest-token" + + testUUID = "test-uuid" + testTenantToken = "abcd" + testCommunicationEndpoint = "https://tenant.dev.dynatracelabs.com:443" + + testHost = "test-host" + + testDynakube = "test-dynakube" + testNamespace = "test-namespace" + testNamespace2 = "test-namespace2" + + testNamespaceDynatrace = "dynatrace" + + testApiUrl = "https://" + testHost + "/e/" + testUUID + "/api" + + oldCertValue = "old-cert-value" + oldTrustedCa = "old-trusted-ca" +) + +func TestNewSecretGenerator(t *testing.T) { + client := fake.NewClient() + mockDTClient := dtclientmock.NewClient(t) + + secretGenerator := NewSecretGenerator(client, client, mockDTClient) + assert.NotNil(t, secretGenerator) + + assert.Equal(t, client, secretGenerator.client) + assert.Equal(t, client, secretGenerator.apiReader) + assert.Equal(t, mockDTClient, secretGenerator.dtClient) +} + +func TestGenerateForDynakube(t *testing.T) { + t.Run("succcessfully generate secret for dynakube", func(t *testing.T) { + dk := &dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Name: testDynakube, + Namespace: testNamespaceDynatrace, + }, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + }, + } + + clt := fake.NewClientWithIndex( + dk, + clientInjectedNamespace(testNamespace, testDynakube), + clientSecret(testDynakube, testNamespaceDynatrace, map[string][]byte{ + dtclient.ApiToken: []byte(testAPIToken), + dtclient.PaasToken: []byte(testPaasToken), + }), + clientSecret(dk.OneAgent().GetTenantSecret(), testNamespaceDynatrace, map[string][]byte{ + "tenant-token": []byte(testTenantToken), + }), + ) + + mockDTClient := dtclientmock.NewClient(t) + + mockDTClient.On("GetProcessModuleConfig", mock.AnythingOfType("context.backgroundCtx"), mock.AnythingOfType("uint")).Return(&dtclient.ProcessModuleConfig{}, nil) + + secretGenerator := NewSecretGenerator(clt, clt, mockDTClient) + err := secretGenerator.GenerateForDynakube(context.Background(), dk) + require.NoError(t, err) + + var secret corev1.Secret + err = clt.Get(context.Background(), client.ObjectKey{Name: consts.BootstrapperInitSecretName, Namespace: testNamespace}, &secret) + require.NoError(t, err) + require.Equal(t, consts.BootstrapperInitSecretName, secret.Name) + assert.NotEmpty(t, secret.Data) + + var sourceSecret corev1.Secret + err = clt.Get(context.Background(), client.ObjectKey{Name: GetSourceSecretName(dk.Name), Namespace: dk.Namespace}, &sourceSecret) + require.NoError(t, err) + + require.Equal(t, GetSourceSecretName(dk.Name), sourceSecret.Name) + assert.Equal(t, secret.Data, sourceSecret.Data) + + c := meta.FindStatusCondition(*dk.Conditions(), ConditionType) + require.NotNil(t, c) + assert.Equal(t, metav1.ConditionTrue, c.Status) + }) + t.Run("succcessfully generate secret with fields for dynakube", func(t *testing.T) { + dk := &dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Name: testDynakube, + Namespace: testNamespaceDynatrace, + Annotations: map[string]string{ + dynakube.AnnotationFeatureOneAgentInitialConnectRetry: "6500", + }, + }, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + TrustedCAs: "test-trusted-ca", + MetadataEnrichment: dynakube.MetadataEnrichment{ + Enabled: ptr.To(true), + }, + ActiveGate: activegate.Spec{ + Capabilities: []activegate.CapabilityDisplayName{ + activegate.KubeMonCapability.DisplayName, + }, + TlsSecretName: "test-tls-secret-name", + }, + }, + } + + clt := fake.NewClientWithIndex( + dk, + clientInjectedNamespace(testNamespace, testDynakube), + clientSecret(testDynakube, testNamespaceDynatrace, map[string][]byte{ + dtclient.ApiToken: []byte(testAPIToken), + dtclient.PaasToken: []byte(testPaasToken), + }), + clientSecret(dk.ActiveGate().TlsSecretName, testNamespaceDynatrace, map[string][]byte{ + dynakube.TLSCertKey: []byte("test-cert-value"), + }), + clientSecret(dk.OneAgent().GetTenantSecret(), testNamespaceDynatrace, map[string][]byte{ + "tenant-token": []byte(testTenantToken), + }), + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-trusted-ca", + Namespace: testNamespaceDynatrace, + }, + Data: map[string]string{ + dynakube.TrustedCAKey: "test-trusted-ca-value", + }, + }, + ) + + mockDTClient := dtclientmock.NewClient(t) + + mockDTClient.On("GetProcessModuleConfig", mock.AnythingOfType("context.backgroundCtx"), mock.AnythingOfType("uint")).Return(&dtclient.ProcessModuleConfig{}, nil) + + secretGenerator := NewSecretGenerator(clt, clt, mockDTClient) + err := secretGenerator.GenerateForDynakube(context.Background(), dk) + require.NoError(t, err) + + var secret corev1.Secret + err = clt.Get(context.Background(), client.ObjectKey{Name: consts.BootstrapperInitSecretName, Namespace: testNamespace}, &secret) + require.NoError(t, err) + + require.NotEmpty(t, secret) + + assert.Equal(t, consts.BootstrapperInitSecretName, secret.Name) + _, ok := secret.Data[pmc.InputFileName] + require.True(t, ok) + + _, ok = secret.Data[ca.TrustedCertsInputFile] + require.True(t, ok) + + _, ok = secret.Data[ca.AgCertsInputFile] + require.True(t, ok) + + _, ok = secret.Data[curl.InputFileName] + require.True(t, ok) + + _, ok = secret.Data[endpoint.InputFileName] + require.True(t, ok) + + var sourceSecret corev1.Secret + err = clt.Get(context.Background(), client.ObjectKey{Name: GetSourceSecretName(dk.Name), Namespace: dk.Namespace}, &sourceSecret) + require.NoError(t, err) + + require.Equal(t, GetSourceSecretName(dk.Name), sourceSecret.Name) + assert.Equal(t, secret.Data, sourceSecret.Data) + + c := meta.FindStatusCondition(*dk.Conditions(), ConditionType) + require.NotNil(t, c) + assert.Equal(t, metav1.ConditionTrue, c.Status) + }) + t.Run("update secret with preexisting secret + fields", func(t *testing.T) { + dk := &dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Name: testDynakube, + Namespace: testNamespaceDynatrace, + Annotations: map[string]string{ + dynakube.AnnotationFeatureOneAgentInitialConnectRetry: "6500", + dynakube.AnnotationFeatureActiveGateAutomaticTLSCertificate: "false", + }, + }, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + MetadataEnrichment: dynakube.MetadataEnrichment{ + Enabled: ptr.To(true), + }, + ActiveGate: activegate.Spec{ + Capabilities: []activegate.CapabilityDisplayName{ + activegate.KubeMonCapability.DisplayName, + }, + }, + }, + } + + clt := fake.NewClientWithIndex( + dk, + clientInjectedNamespace(testNamespace, testDynakube), + clientSecret(testDynakube, testNamespaceDynatrace, map[string][]byte{ + dtclient.ApiToken: []byte(testAPIToken), + dtclient.PaasToken: []byte(testPaasToken), + }), + clientSecret(dk.ActiveGate().TlsSecretName, testNamespaceDynatrace, map[string][]byte{ + dynakube.TLSCertKey: []byte("test-cert-value"), + }), + clientSecret(dk.OneAgent().GetTenantSecret(), testNamespaceDynatrace, map[string][]byte{ + "tenant-token": []byte(testTenantToken), + }), + clientSecret(consts.BootstrapperInitSecretName, testNamespace, map[string][]byte{ + pmc.InputFileName: nil, + ca.TrustedCertsInputFile: []byte(oldTrustedCa), + ca.AgCertsInputFile: []byte(oldCertValue), + }), + clientSecret(GetSourceSecretName(dk.Name), dk.Namespace, map[string][]byte{ + pmc.InputFileName: nil, + ca.TrustedCertsInputFile: []byte(oldTrustedCa), + ca.AgCertsInputFile: []byte(oldCertValue), + }), + ) + + mockDTClient := dtclientmock.NewClient(t) + + mockDTClient.On("GetProcessModuleConfig", mock.AnythingOfType("context.backgroundCtx"), mock.AnythingOfType("uint")).Return(&dtclient.ProcessModuleConfig{}, nil) + + secretGenerator := NewSecretGenerator(clt, clt, mockDTClient) + err := secretGenerator.GenerateForDynakube(context.Background(), dk) + require.NoError(t, err) + + var secret corev1.Secret + err = clt.Get(context.Background(), client.ObjectKey{Name: consts.BootstrapperInitSecretName, Namespace: testNamespace}, &secret) + require.NoError(t, err) + + require.NotEmpty(t, secret) + + assert.Equal(t, consts.BootstrapperInitSecretName, secret.Name) + _, ok := secret.Data[pmc.InputFileName] + require.True(t, ok) + + val, ok := secret.Data[ca.TrustedCertsInputFile] + require.True(t, ok) + assert.NotEqual(t, oldTrustedCa, val) + require.Empty(t, val) + + _, ok = secret.Data[ca.AgCertsInputFile] + require.True(t, ok) + assert.NotEqual(t, oldCertValue, val) + require.Empty(t, val) + + _, ok = secret.Data[curl.InputFileName] + require.True(t, ok) + + _, ok = secret.Data[endpoint.InputFileName] + require.True(t, ok) + + var sourceSecret corev1.Secret + err = clt.Get(context.Background(), client.ObjectKey{Name: GetSourceSecretName(dk.Name), Namespace: dk.Namespace}, &sourceSecret) + require.NoError(t, err) + + require.Equal(t, GetSourceSecretName(dk.Name), sourceSecret.Name) + assert.Equal(t, secret.Data, sourceSecret.Data) + + c := meta.FindStatusCondition(*dk.Conditions(), ConditionType) + require.NotNil(t, c) + assert.Equal(t, metav1.ConditionTrue, c.Status) + }) + t.Run("fail while generating secret for dynakube", func(t *testing.T) { + dk := &dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Name: testDynakube, + Namespace: testNamespaceDynatrace, + }, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + }, + } + + clt := fake.NewClientWithIndex( + dk, + clientInjectedNamespace(testNamespace, testDynakube), + clientSecret(testDynakube, testNamespaceDynatrace, map[string][]byte{ + dtclient.ApiToken: []byte(testAPIToken), + dtclient.PaasToken: []byte(testPaasToken), + }), + ) + + mockDTClient := dtclientmock.NewClient(t) + + mockDTClient.On("GetProcessModuleConfig", mock.AnythingOfType("context.backgroundCtx"), mock.AnythingOfType("uint")).Return(&dtclient.ProcessModuleConfig{}, nil) + + secretGenerator := NewSecretGenerator(clt, clt, mockDTClient) + err := secretGenerator.GenerateForDynakube(context.Background(), dk) + require.Error(t, err) + + c := meta.FindStatusCondition(*dk.Conditions(), ConditionType) + require.NotNil(t, c) + assert.Equal(t, metav1.ConditionFalse, c.Status) + }) +} + +func TestCleanup(t *testing.T) { + dk := &dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Name: testDynakube, + Namespace: testNamespaceDynatrace, + }, + Spec: dynakube.DynaKubeSpec{ + APIURL: testApiUrl, + }, + Status: dynakube.DynaKubeStatus{ + Conditions: []metav1.Condition{ + {Type: ConditionType}, + {Type: "other"}, + }, + }, + } + + clt := fake.NewClientWithIndex( + dk, + clientInjectedNamespace(testNamespace, testDynakube), + clientSecret(testDynakube, testNamespaceDynatrace, map[string][]byte{ + dtclient.ApiToken: []byte(testAPIToken), + dtclient.PaasToken: []byte(testPaasToken), + }), + clientSecret(dk.OneAgent().GetTenantSecret(), testNamespaceDynatrace, map[string][]byte{ + "tenant-token": []byte(testTenantToken), + }), + clientSecret(consts.BootstrapperInitSecretName, testNamespace, nil), + clientSecret(consts.BootstrapperInitSecretName, testNamespace2, nil), + clientSecret(GetSourceSecretName(dk.Name), dk.Namespace, nil), + ) + namespaces := []corev1.Namespace{ + {ObjectMeta: metav1.ObjectMeta{Name: testNamespace}}, + {ObjectMeta: metav1.ObjectMeta{Name: testNamespace2}}, + } + + var secretNS1 corev1.Secret + err := clt.Get(context.Background(), client.ObjectKey{Name: consts.BootstrapperInitSecretName, Namespace: testNamespace}, &secretNS1) + require.NoError(t, err) + + require.NotEmpty(t, secretNS1) + assert.Equal(t, consts.BootstrapperInitSecretName, secretNS1.Name) + + var secretNS2 corev1.Secret + err = clt.Get(context.Background(), client.ObjectKey{Name: consts.BootstrapperInitSecretName, Namespace: testNamespace}, &secretNS2) + require.NoError(t, err) + + require.NotEmpty(t, secretNS2) + assert.Equal(t, consts.BootstrapperInitSecretName, secretNS2.Name) + + err = Cleanup(context.Background(), clt, clt, namespaces, dk) + require.NoError(t, err) + + var deleted corev1.Secret + err = clt.Get(context.Background(), client.ObjectKey{Name: consts.BootstrapperInitSecretName, Namespace: testNamespace}, &deleted) + require.Error(t, err) + assert.True(t, errors.IsNotFound(err)) + + err = clt.Get(context.Background(), client.ObjectKey{Name: consts.BootstrapperInitSecretName, Namespace: testNamespace2}, &deleted) + require.Error(t, err) + assert.True(t, errors.IsNotFound(err)) + + err = clt.Get(context.Background(), client.ObjectKey{Name: GetSourceSecretName(dk.Name), Namespace: dk.Namespace}, &deleted) + require.Error(t, err) + assert.True(t, errors.IsNotFound(err)) + require.Nil(t, meta.FindStatusCondition(*dk.Conditions(), ConditionType)) +} + +func clientSecret(secretName string, namespaceName string, data map[string][]byte) *corev1.Secret { + return &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "core/v1", + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: namespaceName, + }, + Data: data, + } +} + +func clientInjectedNamespace(namespaceName string, dynakubeName string) *corev1.Namespace { + return &corev1.Namespace{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "corev1", + Kind: "Namespace", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: namespaceName, + Labels: map[string]string{ + dtwebhook.InjectionInstanceLabel: dynakubeName, + }, + }, + } +} diff --git a/pkg/injection/namespace/bootstrapperconfig/conditions.go b/pkg/injection/namespace/bootstrapperconfig/conditions.go new file mode 100644 index 0000000000..9d4d62f3cd --- /dev/null +++ b/pkg/injection/namespace/bootstrapperconfig/conditions.go @@ -0,0 +1,5 @@ +package bootstrapperconfig + +const ( + ConditionType = "BootstrapperConfig" +) diff --git a/pkg/injection/namespace/bootstrapperconfig/config.go b/pkg/injection/namespace/bootstrapperconfig/config.go new file mode 100644 index 0000000000..71d11fed35 --- /dev/null +++ b/pkg/injection/namespace/bootstrapperconfig/config.go @@ -0,0 +1,7 @@ +package bootstrapperconfig + +import "github.com/Dynatrace/dynatrace-operator/pkg/logd" + +var ( + log = logd.Get().WithName("bootstrapper-config") +) diff --git a/pkg/injection/namespace/bootstrapperconfig/endpoints.go b/pkg/injection/namespace/bootstrapperconfig/endpoints.go new file mode 100644 index 0000000000..266d7b623b --- /dev/null +++ b/pkg/injection/namespace/bootstrapperconfig/endpoints.go @@ -0,0 +1,67 @@ +package bootstrapperconfig + +import ( + "context" + "fmt" + "strings" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" + dtingestendpoint "github.com/Dynatrace/dynatrace-operator/pkg/injection/namespace/ingestendpoint" + "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" + k8ssecret "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/secret" + "github.com/pkg/errors" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func (s *SecretGenerator) prepareEndpoints(ctx context.Context, dk *dynakube.DynaKube) (string, error) { + fields, err := s.prepareFieldsForEndpoints(ctx, dk) + if err != nil { + return "", errors.WithStack(err) + } + + endpointPropertiesBuilder := strings.Builder{} + + if dk.MetadataEnrichmentEnabled() { + if _, err := endpointPropertiesBuilder.WriteString(fmt.Sprintf("%s=%s\n", dtingestendpoint.MetricsUrlSecretField, fields[dtingestendpoint.MetricsUrlSecretField])); err != nil { + conditions.SetSecretGenFailed(dk.Conditions(), ConditionType, err) + + return "", errors.WithStack(err) + } + + if _, err := endpointPropertiesBuilder.WriteString(fmt.Sprintf("%s=%s\n", dtingestendpoint.MetricsTokenSecretField, fields[dtingestendpoint.MetricsTokenSecretField])); err != nil { + conditions.SetSecretGenFailed(dk.Conditions(), ConditionType, err) + + return "", errors.WithStack(err) + } + } + + return endpointPropertiesBuilder.String(), nil +} + +func (s *SecretGenerator) prepareFieldsForEndpoints(ctx context.Context, dk *dynakube.DynaKube) (map[string]string, error) { + fields := make(map[string]string) + + tokens, err := k8ssecret.Query(s.client, s.apiReader, log).Get(ctx, client.ObjectKey{Name: dk.Tokens(), Namespace: dk.Namespace}) + if err != nil { + conditions.SetKubeApiError(dk.Conditions(), ConditionType, err) + + return nil, errors.WithMessage(err, "failed to query tokens") + } + + if dk.MetadataEnrichmentEnabled() { + if token, ok := tokens.Data[dtclient.DataIngestToken]; ok { + fields[dtingestendpoint.MetricsTokenSecretField] = string(token) + } else { + log.Info("data ingest token not found in secret", "dk", dk.Name) + } + + if ingestUrl, err := dtingestendpoint.IngestUrlFor(dk); err != nil { + return nil, err + } else { + fields[dtingestendpoint.MetricsUrlSecretField] = ingestUrl + } + } + + return fields, nil +} diff --git a/pkg/injection/namespace/bootstrapperconfig/pmc.go b/pkg/injection/namespace/bootstrapperconfig/pmc.go new file mode 100644 index 0000000000..a9fadf28ca --- /dev/null +++ b/pkg/injection/namespace/bootstrapperconfig/pmc.go @@ -0,0 +1,92 @@ +package bootstrapperconfig + +import ( + "context" + "encoding/json" + + "github.com/Dynatrace/dynatrace-bootstrapper/pkg/configure/oneagent/pmc" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/capability" + "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/connectioninfo" + "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" + k8ssecret "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/secret" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" +) + +func (s *SecretGenerator) preparePMC(ctx context.Context, dk *dynakube.DynaKube) ([]byte, error) { + if !conditions.IsOutdated(s.timeProvider, dk, ConditionType) { + log.Info("skipping Dynatrace API call, trying to get ruxitagentproc content from source secret") + + source, err := getSecretFromSource(ctx, *dk, k8ssecret.Query(s.client, s.apiReader, log), dk.Namespace) + if err != nil && !k8serrors.IsNotFound(err) { + conditions.SetKubeApiError(dk.Conditions(), ConditionType, err) + + return nil, err + } else if err == nil && source.Data[pmc.InputFileName] != nil { + return source.Data[pmc.InputFileName], nil + } + } + + log.Debug("calling the Dynatrace API for ruxitagentproc content") + + conditions.SetSecretOutdated(dk.Conditions(), ConditionType, "secret is outdated, update in progress") + + pmc, err := s.dtClient.GetProcessModuleConfig(ctx, 0) + if err != nil { + conditions.SetDynatraceApiError(dk.Conditions(), ConditionType, err) + + return nil, err + } + + tenantToken, err := k8ssecret.GetDataFromSecretName(ctx, s.apiReader, types.NamespacedName{ + Name: dk.OneAgent().GetTenantSecret(), + Namespace: dk.Namespace, + }, connectioninfo.TenantTokenKey, log) + if err != nil { + conditions.SetKubeApiError(dk.Conditions(), ConditionType, err) + + return nil, err + } + + pmc = pmc. + AddHostGroup(dk.OneAgent().GetHostGroup()). + AddConnectionInfo(dk.Status.OneAgent.ConnectionInfoStatus, tenantToken). + // set proxy explicitly empty, so old proxy settings get deleted where necessary + AddProxy("") + + if dk.NeedsOneAgentProxy() { + log.Debug("proxy is needed") + + proxy, err := dk.Proxy(ctx, s.apiReader) + if err != nil { + conditions.SetKubeApiError(dk.Conditions(), ConditionType, err) + + return nil, err + } + + pmc.AddProxy(proxy) + + multiCap := capability.NewMultiCapability(dk) + dnsEntry := capability.BuildDNSEntryPointWithoutEnvVars(dk.Name, dk.Namespace, multiCap) + + if dk.FeatureNoProxy() != "" { + dnsEntry += "," + dk.FeatureNoProxy() + } + + pmc.AddNoProxy(dnsEntry) + } + + pmc.SortPropertiesByKey() + + marshaled, err := json.Marshal(pmc) + if err != nil { + conditions.SetSecretGenFailed(dk.Conditions(), ConditionType, err) + + log.Info("could not marshal process module config") + + return nil, err + } + + return marshaled, nil +} diff --git a/pkg/injection/namespace/bootstrapperconfig/replicate.go b/pkg/injection/namespace/bootstrapperconfig/replicate.go new file mode 100644 index 0000000000..c226c855b9 --- /dev/null +++ b/pkg/injection/namespace/bootstrapperconfig/replicate.go @@ -0,0 +1,62 @@ +package bootstrapperconfig + +import ( + "context" + "fmt" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/consts" + "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" + k8slabels "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/labels" + k8ssecret "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/secret" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + sourceSecretTemplate = "%s-bootstrapper-config" +) + +func GetSourceSecretName(dkName string) string { + return fmt.Sprintf(sourceSecretTemplate, dkName) +} + +// Replicate will only create the secret once, doesn't meant for keeping the secret up to date +func Replicate(ctx context.Context, dk dynakube.DynaKube, query k8ssecret.QueryObject, targetNs string) error { + secret, err := getSecretFromSource(ctx, dk, query, targetNs) + if err != nil { + return err + } + + return client.IgnoreAlreadyExists(query.Create(ctx, secret)) +} + +func getSecretFromSource(ctx context.Context, dk dynakube.DynaKube, query k8ssecret.QueryObject, targetNs string) (*corev1.Secret, error) { + source, err := query.Get(ctx, types.NamespacedName{Name: GetSourceSecretName(dk.Name), Namespace: dk.Namespace}) + if err != nil { + return nil, err + } + + return k8ssecret.BuildForNamespace(consts.BootstrapperInitSecretName, targetNs, source.Data, k8ssecret.SetLabels(source.Labels)) +} + +func (s *SecretGenerator) createSourceForWebhook(ctx context.Context, dk *dynakube.DynaKube, data map[string][]byte) error { + coreLabels := k8slabels.NewCoreLabels(dk.Name, k8slabels.WebhookComponentLabel) + + secret, err := k8ssecret.BuildForNamespace(GetSourceSecretName(dk.Name), dk.Namespace, data, k8ssecret.SetLabels(coreLabels.BuildLabels())) + if err != nil { + conditions.SetSecretGenFailed(dk.Conditions(), ConditionType, err) + + return err + } + + _, err = k8ssecret.Query(s.client, s.apiReader, log).WithOwner(dk).CreateOrUpdate(ctx, secret) + if err != nil { + conditions.SetKubeApiError(dk.Conditions(), ConditionType, err) + + return err + } + + return nil +} diff --git a/pkg/injection/namespace/bootstrapperconfig/replicate_test.go b/pkg/injection/namespace/bootstrapperconfig/replicate_test.go new file mode 100644 index 0000000000..63cf45e29e --- /dev/null +++ b/pkg/injection/namespace/bootstrapperconfig/replicate_test.go @@ -0,0 +1,76 @@ +package bootstrapperconfig + +import ( + "context" + "testing" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/consts" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/secret" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func TestReplicate(t *testing.T) { + ctx := context.Background() + dk := &dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Name: "dk", + Namespace: "dk-ns", + }, + } + + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "user-ns", + }, + } + + data := map[string][]byte{ + "data": []byte("beep"), + } + + t.Run("create", func(t *testing.T) { + source := clientSecret(GetSourceSecretName(dk.Name), dk.Namespace, data) + source.Labels = map[string]string{ + "key": "value", + } + clt := fake.NewClientWithIndex( + dk, + ns, + source, + ) + + err := Replicate(ctx, *dk, secret.Query(clt, clt, log), ns.Name) + require.NoError(t, err) + + var replicated corev1.Secret + err = clt.Get(context.Background(), client.ObjectKey{Name: consts.BootstrapperInitSecretName, Namespace: ns.Name}, &replicated) + require.NoError(t, err) + assert.Equal(t, source.Data, replicated.Data) + assert.Equal(t, source.Labels, replicated.Labels) + }) + + t.Run("already exists => no update + no error", func(t *testing.T) { + source := clientSecret(GetSourceSecretName(dk.Name), dk.Namespace, data) + alreadyPresent := clientSecret(consts.BootstrapperInitSecretName, ns.Name, nil) + clt := fake.NewClientWithIndex( + dk, + ns, + source, + alreadyPresent, + ) + + err := Replicate(ctx, *dk, secret.Query(clt, clt, log), ns.Name) + require.NoError(t, err) + + var replicated corev1.Secret + err = clt.Get(context.Background(), client.ObjectKey{Name: consts.BootstrapperInitSecretName, Namespace: ns.Name}, &replicated) + require.NoError(t, err) + assert.NotEqual(t, source.Data, replicated.Data) + }) +} diff --git a/pkg/injection/namespace/ingestendpoint/secret.go b/pkg/injection/namespace/ingestendpoint/secret.go index a7e9005b81..557d785ddb 100644 --- a/pkg/injection/namespace/ingestendpoint/secret.go +++ b/pkg/injection/namespace/ingestendpoint/secret.go @@ -6,7 +6,7 @@ import ( "fmt" "strings" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/consts" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/capability" @@ -163,7 +163,7 @@ func (g *SecretGenerator) PrepareFields(ctx context.Context, dk *dynakube.DynaKu log.Info("data ingest token not found in secret", "dk", dk.Name) } - if ingestUrl, err := ingestUrlFor(dk); err != nil { + if ingestUrl, err := IngestUrlFor(dk); err != nil { return nil, err } else { fields[MetricsUrlSecretField] = ingestUrl @@ -173,23 +173,23 @@ func (g *SecretGenerator) PrepareFields(ctx context.Context, dk *dynakube.DynaKu return fields, nil } -func ingestUrlFor(dk *dynakube.DynaKube) (string, error) { +func IngestUrlFor(dk *dynakube.DynaKube) (string, error) { switch { case dk.ActiveGate().IsMetricsIngestEnabled(): - return metricsIngestUrlForClusterActiveGate(dk) + return MetricsIngestUrlForClusterActiveGate(dk) case len(dk.Spec.APIURL) > 0: - return metricsIngestUrlForDynatraceActiveGate(dk) + return MetricsIngestUrlForDynatraceActiveGate(dk) default: return "", errors.New("failed to create metadata-enrichment endpoint, DynaKube.spec.apiUrl is empty") } } -func metricsIngestUrlForDynatraceActiveGate(dk *dynakube.DynaKube) (string, error) { +func MetricsIngestUrlForDynatraceActiveGate(dk *dynakube.DynaKube) (string, error) { return dk.Spec.APIURL + "/v2/metrics/ingest", nil } -func metricsIngestUrlForClusterActiveGate(dk *dynakube.DynaKube) (string, error) { - tenant, err := dk.TenantUUIDFromConnectionInfoStatus() +func MetricsIngestUrlForClusterActiveGate(dk *dynakube.DynaKube) (string, error) { + tenant, err := dk.TenantUUID() if err != nil { return "", err } diff --git a/pkg/injection/namespace/ingestendpoint/secret_test.go b/pkg/injection/namespace/ingestendpoint/secret_test.go index 48433a24d7..55d7bd1946 100644 --- a/pkg/injection/namespace/ingestendpoint/secret_test.go +++ b/pkg/injection/namespace/ingestendpoint/secret_test.go @@ -5,17 +5,17 @@ import ( "testing" "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" "github.com/Dynatrace/dynatrace-operator/pkg/consts" "github.com/Dynatrace/dynatrace-operator/pkg/injection/namespace/mapper" - "github.com/Dynatrace/dynatrace-operator/pkg/util/address" dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -222,7 +222,7 @@ func TestGenerateMetadataEnrichmentSecret_ForDynakube(t *testing.T) { { dk := buildTestDynakube() - dk.Spec.MetadataEnrichment.Enabled = address.Of(false) + dk.Spec.MetadataEnrichment.Enabled = ptr.To(false) testGenerateEndpointsSecret(t, dk, fakeClient) @@ -304,7 +304,7 @@ func updatedTestDynakube() *dynakube.DynaKube { }, Spec: dynakube.DynaKubeSpec{ APIURL: testUpdatedApiUrl, - MetadataEnrichment: dynakube.MetadataEnrichment{Enabled: address.Of(true)}, + MetadataEnrichment: dynakube.MetadataEnrichment{Enabled: ptr.To(true)}, }, } } @@ -320,7 +320,7 @@ func updatedTestDynakubeWithMetricsIngestCapability(capabilities []activegate.Ca Capabilities: capabilities, }, APIURL: testUpdatedApiUrl, - MetadataEnrichment: dynakube.MetadataEnrichment{Enabled: address.Of(true)}, + MetadataEnrichment: dynakube.MetadataEnrichment{Enabled: ptr.To(true)}, }, } } @@ -344,7 +344,7 @@ func buildTestDynakube() *dynakube.DynaKube { }, Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, - MetadataEnrichment: dynakube.MetadataEnrichment{Enabled: address.Of(true)}, + MetadataEnrichment: dynakube.MetadataEnrichment{Enabled: ptr.To(true)}, }, } } @@ -361,7 +361,7 @@ func buildTestDynakubeWithMetricsIngestCapability(capabilities []activegate.Capa }, APIURL: testApiUrl, MetadataEnrichment: dynakube.MetadataEnrichment{ - Enabled: address.Of(true), + Enabled: ptr.To(true), }, }, } diff --git a/pkg/injection/namespace/initgeneration/initgeneration.go b/pkg/injection/namespace/initgeneration/initgeneration.go index fd97f6d630..f60da86e01 100644 --- a/pkg/injection/namespace/initgeneration/initgeneration.go +++ b/pkg/injection/namespace/initgeneration/initgeneration.go @@ -5,7 +5,7 @@ import ( "encoding/json" "strings" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/consts" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/capability" @@ -175,13 +175,13 @@ func (g *InitGenerator) createSecretConfigForDynaKube(ctx context.Context, dk *d OneAgentNoProxy: strings.Join(oneAgentNoProxyValues, ","), NetworkZone: dk.Spec.NetworkZone, SkipCertCheck: dk.Spec.SkipCertCheck, - HasHost: dk.CloudNativeFullstackMode(), + HasHost: dk.OneAgent().IsCloudNativeFullstackMode(), MonitoringNodes: hostMonitoringNodes, - HostGroup: dk.HostGroup(), + HostGroup: dk.OneAgent().GetHostGroup(), InitialConnectRetry: dk.FeatureAgentInitialConnectRetry(), EnforcementMode: dk.FeatureEnforcementMode(), ReadOnlyCSIDriver: dk.FeatureReadOnlyCsiVolume(), - CSIMode: dk.IsCSIAvailable(), + CSIMode: dk.OneAgent().IsCSIAvailable(), }, nil } @@ -208,7 +208,7 @@ func (g *InitGenerator) getHostMonitoringNodes(dk *dynakube.DynaKube) (map[strin tenantUUID := dk.Status.OneAgent.ConnectionInfoStatus.TenantUUID imNodes := map[string]string{} - if !dk.CloudNativeFullstackMode() { + if !dk.OneAgent().IsCloudNativeFullstackMode() { return imNodes, nil } @@ -232,7 +232,7 @@ func (g *InitGenerator) calculateImNodes(dk *dynakube.DynaKube, tenantUUID strin return nil, err } - nodeSelector := labels.SelectorFromSet(dk.OneAgentNodeSelector()) + nodeSelector := labels.SelectorFromSet(dk.OneAgent().GetNodeSelector(nil)) updateNodeInfImNodes(dk, nodeInf, nodeSelector, tenantUUID) return nodeInf.imNodes, nil diff --git a/pkg/injection/namespace/initgeneration/initgeneration_test.go b/pkg/injection/namespace/initgeneration/initgeneration_test.go index 71d63e9985..a15998555b 100644 --- a/pkg/injection/namespace/initgeneration/initgeneration_test.go +++ b/pkg/injection/namespace/initgeneration/initgeneration_test.go @@ -9,8 +9,9 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/communication" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/value" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" "github.com/Dynatrace/dynatrace-operator/pkg/consts" "github.com/Dynatrace/dynatrace-operator/pkg/injection/startup" dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" @@ -467,14 +468,14 @@ func createDynakube() *dynakube.DynaKube { Spec: dynakube.DynaKubeSpec{ APIURL: "https://test-url/e/tenant/api", Tokens: "dynakube-test", - OneAgent: dynakube.OneAgentSpec{ - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{ - HostInjectSpec: dynakube.HostInjectSpec{}, + OneAgent: oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{ + HostInjectSpec: oneagent.HostInjectSpec{}, }}, }, Status: dynakube.DynaKubeStatus{ - OneAgent: dynakube.OneAgentStatus{ - ConnectionInfoStatus: dynakube.OneAgentConnectionInfoStatus{ + OneAgent: oneagent.Status{ + ConnectionInfoStatus: oneagent.ConnectionInfoStatus{ ConnectionInfo: communication.ConnectionInfo{ TenantUUID: "test-tenant", Endpoints: "beep.com;bop.com", @@ -522,9 +523,9 @@ func setTlsSecret(dk *dynakube.DynaKube, value string) { } func setNodesToInstances(dk *dynakube.DynaKube, nodeNames ...string) { - instances := map[string]dynakube.OneAgentInstance{} + instances := map[string]oneagent.Instance{} for _, name := range nodeNames { - instances[name] = dynakube.OneAgentInstance{} + instances[name] = oneagent.Instance{} } dk.Status.OneAgent.Instances = instances diff --git a/pkg/injection/namespace/mapper/dynakubes.go b/pkg/injection/namespace/mapper/dynakubes.go index 7e2b1cdb1c..01f38f7e8e 100644 --- a/pkg/injection/namespace/mapper/dynakubes.go +++ b/pkg/injection/namespace/mapper/dynakubes.go @@ -3,7 +3,7 @@ package mapper import ( "context" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/consts" k8ssecret "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/secret" dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" @@ -60,7 +60,12 @@ func (dm DynakubeMapper) UnmapFromDynaKube(namespaces []corev1.Namespace) error return errors.WithMessagef(err, "failed to remove label %s from namespace %s", dtwebhook.InjectionInstanceLabel, ns.Name) } - err := k8ssecret.Query(dm.client, dm.apiReader, log).DeleteForNamespace(dm.ctx, consts.AgentInitSecretName, ns.Name) + err := k8ssecret.Query(dm.client, dm.apiReader, log).DeleteForNamespace(dm.ctx, consts.BootstrapperInitSecretName, ns.Name) + if err != nil { + return err + } + + err = k8ssecret.Query(dm.client, dm.apiReader, log).DeleteForNamespace(dm.ctx, consts.AgentInitSecretName, ns.Name) if err != nil { return err } diff --git a/pkg/injection/namespace/mapper/dynakubes_test.go b/pkg/injection/namespace/mapper/dynakubes_test.go index 2538d892e2..d2c90fc785 100644 --- a/pkg/injection/namespace/mapper/dynakubes_test.go +++ b/pkg/injection/namespace/mapper/dynakubes_test.go @@ -5,8 +5,9 @@ import ( "testing" "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/consts" + "github.com/Dynatrace/dynatrace-operator/pkg/util/installconfig" dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -199,4 +200,60 @@ func TestUnmapFromDynaKube(t *testing.T) { err = clt.Get(ctx, types.NamespacedName{Name: consts.EnrichmentEndpointSecretName, Namespace: namespace.Name}, &secret) assert.True(t, k8serrors.IsNotFound(err)) }) + t.Run("Remove "+consts.BootstrapperInitSecretName, func(t *testing.T) { + installconfig.SetModulesOverride(t, installconfig.Modules{CSIDriver: false}) + + dkNodeImagePull := createDynakubeWithNodeImagePullAndNoCSI("dk-test", convertToLabelSelector(labels)) + + labels := map[string]string{ + dtwebhook.InjectionInstanceLabel: dkNodeImagePull.Name, + } + + ns := createNamespace("ns-bootstrapper", labels) + ns2 := createNamespace("ns-bootstrapper2", labels) + + clt := fake.NewClient(ns, ns2) + ctx := context.Background() + + namespaces, err := GetNamespacesForDynakube(ctx, clt, dkNodeImagePull.Name) + require.NoError(t, err) + + var secretNS1 corev1.Secret + + clt.Create(ctx, &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: consts.BootstrapperInitSecretName, Namespace: ns.Name}}) + + err = clt.Get(ctx, types.NamespacedName{Name: consts.BootstrapperInitSecretName, Namespace: ns.Name}, &secretNS1) + require.NoError(t, err) + + require.NotEmpty(t, secretNS1) + assert.Equal(t, consts.BootstrapperInitSecretName, secretNS1.Name) + + var secretNS2 corev1.Secret + + clt.Create(ctx, &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: consts.BootstrapperInitSecretName, Namespace: ns2.Name}}) + + err = clt.Get(ctx, types.NamespacedName{Name: consts.BootstrapperInitSecretName, Namespace: ns2.Name}, &secretNS2) + require.NoError(t, err) + + require.NotEmpty(t, secretNS2) + assert.Equal(t, consts.BootstrapperInitSecretName, secretNS2.Name) + + dm := NewDynakubeMapper(ctx, clt, clt, "dynatrace", dkNodeImagePull) + err = dm.UnmapFromDynaKube(namespaces) + require.NoError(t, err) + + var deletedSecretNS1 corev1.Secret + err = clt.Get(ctx, types.NamespacedName{Name: consts.BootstrapperInitSecretName, Namespace: ns.Name}, &deletedSecretNS1) + + require.Empty(t, deletedSecretNS1) + assert.NotEqual(t, consts.BootstrapperInitSecretName, deletedSecretNS1.Name) + assert.True(t, k8serrors.IsNotFound(err)) + + var deletedSecretNS2 corev1.Secret + err = clt.Get(ctx, types.NamespacedName{Name: consts.BootstrapperInitSecretName, Namespace: ns2.Name}, &deletedSecretNS2) + + require.Empty(t, deletedSecretNS2) + assert.NotEqual(t, consts.BootstrapperInitSecretName, deletedSecretNS2.Name) + assert.True(t, k8serrors.IsNotFound(err)) + }) } diff --git a/pkg/injection/namespace/mapper/mapper.go b/pkg/injection/namespace/mapper/mapper.go index 44ff29e554..0c30d64db2 100644 --- a/pkg/injection/namespace/mapper/mapper.go +++ b/pkg/injection/namespace/mapper/mapper.go @@ -4,7 +4,7 @@ import ( "context" "regexp" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" @@ -18,7 +18,7 @@ type ConflictChecker struct { } func (c *ConflictChecker) check(dk *dynakube.DynaKube) error { - if !dk.NeedAppInjection() && !dk.MetadataEnrichmentEnabled() { + if !dk.OneAgent().IsAppInjectionNeeded() && !dk.MetadataEnrichmentEnabled() { return nil } @@ -82,13 +82,13 @@ func match(dk *dynakube.DynaKube, namespace *corev1.Namespace) (bool, error) { // matchOneAgent uses the namespace selector in the dynakube to check if it matches a given namespace // if the namespace selector is not set on the dynakube its an automatic match func matchOneAgent(dk *dynakube.DynaKube, namespace *corev1.Namespace) (bool, error) { - if !dk.NeedAppInjection() { + if !dk.OneAgent().IsAppInjectionNeeded() { return false, nil - } else if dk.OneAgentNamespaceSelector() == nil { + } else if dk.OneAgent().GetNamespaceSelector() == nil { return true, nil } - selector, err := metav1.LabelSelectorAsSelector(dk.OneAgentNamespaceSelector()) + selector, err := metav1.LabelSelectorAsSelector(dk.OneAgent().GetNamespaceSelector()) if err != nil { return false, errors.WithStack(err) } diff --git a/pkg/injection/namespace/mapper/mapper_test.go b/pkg/injection/namespace/mapper/mapper_test.go index 875a027db5..ab6accf2d7 100644 --- a/pkg/injection/namespace/mapper/mapper_test.go +++ b/pkg/injection/namespace/mapper/mapper_test.go @@ -3,7 +3,8 @@ package mapper import ( "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -22,8 +23,8 @@ func createBaseDynakube(name string, appInjection bool, metadataEnrichment bool) } if appInjection { - dk.Spec.OneAgent = dynakube.OneAgentSpec{ - ApplicationMonitoring: &dynakube.ApplicationMonitoringSpec{}} + dk.Spec.OneAgent = oneagent.Spec{ + ApplicationMonitoring: &oneagent.ApplicationMonitoringSpec{}} } return dk @@ -52,6 +53,18 @@ func createDynakubeWithMetadataAndAppInjection(name string, selector metav1.Labe return dk } +func createDynakubeWithNodeImagePullAndNoCSI(name string, selector metav1.LabelSelector) *dynakube.DynaKube { + dk := createBaseDynakube(name, true, false) + + dk.Annotations = make(map[string]string) + + dk.Annotations[dynakube.AnnotationFeatureNodeImagePull] = "true" + + dk.Spec.OneAgent.ApplicationMonitoring.NamespaceSelector = selector + + return dk +} + func createNamespace(name string, labels map[string]string) *corev1.Namespace { return &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ diff --git a/pkg/injection/namespace/mapper/namespaces.go b/pkg/injection/namespace/mapper/namespaces.go index 841c04fa69..f23bbd37f4 100644 --- a/pkg/injection/namespace/mapper/namespaces.go +++ b/pkg/injection/namespace/mapper/namespaces.go @@ -3,7 +3,7 @@ package mapper import ( "context" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" diff --git a/pkg/injection/namespace/mapper/namespaces_test.go b/pkg/injection/namespace/mapper/namespaces_test.go index e2ee633cae..b60dbeac9b 100644 --- a/pkg/injection/namespace/mapper/namespaces_test.go +++ b/pkg/injection/namespace/mapper/namespaces_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/pkg/injection/startup/run.go b/pkg/injection/startup/run.go index e6e95081be..415d31de35 100644 --- a/pkg/injection/startup/run.go +++ b/pkg/injection/startup/run.go @@ -173,7 +173,7 @@ func (runner *Runner) installOneAgent(ctx context.Context) error { return err } - err = processmoduleconfig.UpdateProcessModuleConfigInPlace(runner.fs, consts.AgentBinDirMount, processModuleConfig) + err = processmoduleconfig.UpdateInPlace(runner.fs, consts.AgentBinDirMount, processModuleConfig) if err != nil { return err } diff --git a/pkg/injection/startup/run_test.go b/pkg/injection/startup/run_test.go index b0369d3e5d..7a0c619b99 100644 --- a/pkg/injection/startup/run_test.go +++ b/pkg/injection/startup/run_test.go @@ -3,6 +3,7 @@ package startup import ( "context" "encoding/json" + "errors" "fmt" "os" "path/filepath" @@ -157,7 +158,7 @@ func TestInstallOneAgent(t *testing.T) { runner := createMockedRunner(t) runner.installer.(*installermock.Installer). On("InstallAgent", mock.AnythingOfType("context.backgroundCtx"), consts.AgentBinDirMount). - Return(false, fmt.Errorf("BOOM")) + Return(false, errors.New("BOOM")) err := runner.installOneAgent(ctx) @@ -180,7 +181,7 @@ func TestInstallOneAgent(t *testing.T) { runner := createMockedRunner(t) runner.dtclient.(*dtclientmock.Client). On("GetProcessModuleConfig", mock.AnythingOfType("context.backgroundCtx"), uint(0)). - Return(&dtclient.ProcessModuleConfig{}, fmt.Errorf("BOOM")) + Return(&dtclient.ProcessModuleConfig{}, errors.New("BOOM")) runner.installer.(*installermock.Installer). On("InstallAgent", mock.AnythingOfType("context.backgroundCtx"), consts.AgentBinDirMount). Return(true, nil) @@ -296,7 +297,7 @@ func TestGetProcessModuleConfig(t *testing.T) { runner := createMockedRunner(t) runner.dtclient.(*dtclientmock.Client). On("GetProcessModuleConfig", mock.AnythingOfType("context.backgroundCtx"), uint(0)). - Return(&dtclient.ProcessModuleConfig{}, fmt.Errorf("BOOM")) + Return(&dtclient.ProcessModuleConfig{}, errors.New("BOOM")) config, err := runner.getProcessModuleConfig(ctx) require.Error(t, err) diff --git a/pkg/oci/registry/client.go b/pkg/oci/registry/client.go index a646dbf86b..d08cc1e5ba 100644 --- a/pkg/oci/registry/client.go +++ b/pkg/oci/registry/client.go @@ -8,7 +8,7 @@ import ( "net/http" "net/url" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/arch" "github.com/Dynatrace/dynatrace-operator/pkg/oci/dockerkeychain" "github.com/google/go-containerregistry/pkg/authn" diff --git a/pkg/oci/registry/client_test.go b/pkg/oci/registry/client_test.go index f4824aacc1..6ca1dedea9 100644 --- a/pkg/oci/registry/client_test.go +++ b/pkg/oci/registry/client_test.go @@ -7,7 +7,8 @@ import ( "testing" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/value" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -28,8 +29,8 @@ func TestProxy(t *testing.T) { Spec: dynakube.DynaKubeSpec{ Proxy: &value.Source{Value: proxyRawURL}, APIURL: "https://testApiUrl.dev.dynatracelabs.com/api", - OneAgent: dynakube.OneAgentSpec{ - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{}, + OneAgent: oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{}, }, }, } @@ -57,8 +58,8 @@ func TestSkipCertCheck(t *testing.T) { }, Spec: dynakube.DynaKubeSpec{ APIURL: "https://testApiUrl.dev.dynatracelabs.com/api", - OneAgent: dynakube.OneAgentSpec{ - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{}, + OneAgent: oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{}, }, }, } diff --git a/pkg/otelcgen/consts.go b/pkg/otelcgen/consts.go new file mode 100644 index 0000000000..df42fc77d6 --- /dev/null +++ b/pkg/otelcgen/consts.go @@ -0,0 +1,17 @@ +package otelcgen + +const ( + OtlpGrpcPort = 4317 + OtlpHTTPPort = 4318 + + JaegerGrpcPort = 14250 + JaegerThriftBinaryPort = 6832 + JaegerThriftCompactPort = 6831 + JaegerThriftHTTPPort = 14268 + + ZipkinPort = 9411 + + StatsdPort = 8125 + + ExtensionsHealthCheckPort = 13133 +) diff --git a/pkg/otelcgen/exporters.go b/pkg/otelcgen/exporters.go new file mode 100644 index 0000000000..0cee344c00 --- /dev/null +++ b/pkg/otelcgen/exporters.go @@ -0,0 +1,33 @@ +package otelcgen + +import ( + "go.opentelemetry.io/collector/component" +) + +var ( + otlphttp = component.MustNewID("otlphttp") +) + +func (c *Config) buildExporters() map[component.ID]component.Config { + serverConfig := &ServerConfig{ + Endpoint: c.buildExportersEndpoint(), + } + + if c.caFile != "" { + serverConfig.TLSSetting = &TLSSetting{ + CAFile: c.caFile, + } + if c.includeSystemCACertsPool { + serverConfig.TLSSetting.IncludeSystemCACertsPool = &c.includeSystemCACertsPool + } + } + + if c.apiToken != "" { + serverConfig.Headers = make(map[string]string) + serverConfig.Headers["Authorization"] = "Api-Token " + c.apiToken + } + + return map[component.ID]component.Config{ + otlphttp: serverConfig, + } +} diff --git a/pkg/otelcgen/exporters_test.go b/pkg/otelcgen/exporters_test.go new file mode 100644 index 0000000000..594115ff42 --- /dev/null +++ b/pkg/otelcgen/exporters_test.go @@ -0,0 +1,29 @@ +package otelcgen + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewConfigWithExporters(t *testing.T) { + cfg, err := NewConfig( + "", + RegisteredProtocols, + WithExportersEndpoint("test"), + WithCA("/run/opensignals/cacerts/certs"), + WithApiToken("test-token"), + WithExporters(), + ) + require.NoError(t, err) + c, err := cfg.Marshal() + require.NoError(t, err) + + expectedOutput, err := os.ReadFile(filepath.Join("testdata", "exporters_only.yaml")) + require.NoError(t, err) + + assert.YAMLEq(t, string(expectedOutput), string(c)) +} diff --git a/pkg/otelcgen/extensions.go b/pkg/otelcgen/extensions.go new file mode 100644 index 0000000000..defed7acb4 --- /dev/null +++ b/pkg/otelcgen/extensions.go @@ -0,0 +1,15 @@ +package otelcgen + +import "go.opentelemetry.io/collector/component" + +var ( + healthCheck = component.MustNewID("health_check") +) + +func (c *Config) buildExtensions() map[component.ID]component.Config { + return map[component.ID]component.Config{ + healthCheck: &ServerConfig{ + Endpoint: c.buildEndpoint(ExtensionsHealthCheckPort), + }, + } +} diff --git a/pkg/otelcgen/extensions_test.go b/pkg/otelcgen/extensions_test.go new file mode 100644 index 0000000000..a439035b53 --- /dev/null +++ b/pkg/otelcgen/extensions_test.go @@ -0,0 +1,26 @@ +package otelcgen + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewConfigWithExtensions(t *testing.T) { + cfg, err := NewConfig( + "test", + RegisteredProtocols, + WithExtensions(), + ) + require.NoError(t, err) + c, err := cfg.Marshal() + require.NoError(t, err) + + expectedOutput, err := os.ReadFile(filepath.Join("testdata", "extensions_only.yaml")) + require.NoError(t, err) + + assert.YAMLEq(t, string(expectedOutput), string(c)) +} diff --git a/pkg/otelcgen/otelcgen.go b/pkg/otelcgen/otelcgen.go new file mode 100644 index 0000000000..70905926c2 --- /dev/null +++ b/pkg/otelcgen/otelcgen.go @@ -0,0 +1,268 @@ +package otelcgen + +import ( + "fmt" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/confmap" + "gopkg.in/yaml.v3" +) + +// HistogramConfig is based on: +// "go.opentelemetry.io/opentelemetry-collector-contrib/receiver/statsdreceiver/internal/protocol/statsd_parser.go.HistogramConfig" +// with reduced number of attributes to reduce the number of dependencies. +type HistogramConfig struct { + MaxSize int32 `mapstructure:"max_size"` +} + +// TimerHistogramMapping is based on: +// "go.opentelemetry.io/opentelemetry-collector-contrib/receiver/statsdreceiver/internal/protocol/statsd_parser.go.TLSSetting" +// with reduced number of attributes to reduce the number of dependencies. +type TimerHistogramMapping struct { + StatsDType string `mapstructure:"statsd_type"` + ObserverType string `mapstructure:"observer_type"` + Histogram HistogramConfig `mapstructure:"histogram"` +} + +// TLSSetting is based on: +// "go.opentelemetry.io/collector/config/configtls.TLSSetting" +// with reduced number of attributes to reduce the number of dependencies. +type TLSSetting struct { + IncludeSystemCACertsPool *bool `mapstructure:"include_system_ca_certs_pool,omitempty"` + CAFile string `mapstructure:"ca_file,omitempty"` + KeyFile string `mapstructure:"key_file,omitempty"` + CertFile string `mapstructure:"cert_file,omitempty"` +} + +// ServerConfig is based on "go.opentelemetry.io/collector/config/confighttp.ServerConfig" and +// "go.opentelemetry.io/collector/config/confighttp.ServerConfig" with reduced number of attributes +// to reduce the number of dependencies. +type ServerConfig struct { + // TLSSetting struct exposes TLS client configuration. + TLSSetting *TLSSetting `mapstructure:"tls,omitempty"` + + // Additional headers attached to each HTTP request sent by the client. + // Existing header values are overwritten if collision happens. + // Header values are opaque since they may be sensitive. + Headers map[string]string `mapstructure:"headers,omitempty"` + + // The target URL to send data to (e.g.: http://some.url:9411/v1/traces). + Endpoint string `mapstructure:"endpoint"` +} + +type Protocol string + +type Protocols []Protocol + +const ( + JaegerProtocol Protocol = "jaeger" + ZipkinProtocol Protocol = "zipkin" + OtlpProtocol Protocol = "otlp" + StatsdProtocol Protocol = "statsd" +) + +var ( + JaegerID = component.MustNewID(string(JaegerProtocol)) + OtlpID = component.MustNewID(string(OtlpProtocol)) + StatsdID = component.MustNewID(string(StatsdProtocol)) + ZipkinID = component.MustNewID(string(ZipkinProtocol)) + + RegisteredProtocols = Protocols{OtlpProtocol, JaegerProtocol, StatsdProtocol, ZipkinProtocol} +) + +type Config struct { + // Receivers is a map of ComponentID to Receivers. + Receivers map[component.ID]component.Config `mapstructure:"receivers"` + + // Exporters is a map of ComponentID to Exporters. + Exporters map[component.ID]component.Config `mapstructure:"exporters"` + + // Processors is a map of ComponentID to Processors. + Processors map[component.ID]component.Config `mapstructure:"processors"` + + // Connectors is a map of ComponentID to connectors. + Connectors map[component.ID]component.Config `mapstructure:"connectors"` + + // Extensions is a map of ComponentID to extensions. + Extensions map[component.ID]component.Config `mapstructure:"extensions"` + + tlsKey string + tlsCert string + caFile string + podIP string + endpoint string + apiToken string + + Service ServiceConfig `mapstructure:"service"` + protocols Protocols + + includeSystemCACertsPool bool +} + +type Option func(c *Config) error + +func NewConfig(podIP string, protocols Protocols, options ...Option) (*Config, error) { + c := Config{ + podIP: podIP, + protocols: protocols, + } + + for _, opt := range options { + if err := opt(&c); err != nil { + return nil, err + } + } + + return &c, nil +} + +func (c *Config) Marshal() ([]byte, error) { + conf := confmap.New() + + if err := conf.Marshal(c); err != nil { + return nil, err + } + + sm := conf.ToStringMap() + + return yaml.Marshal(sm) +} + +func (c *Config) buildTLSSetting() *TLSSetting { + if c.tlsCert == "" && c.tlsKey == "" { + return nil + } + + tls := &TLSSetting{} + + if c.tlsCert != "" { + tls.CertFile = c.tlsCert + } + + if c.tlsKey != "" { + tls.KeyFile = c.tlsKey + } + + return tls +} + +func (c *Config) buildEndpoint(port uint) string { + return fmt.Sprintf("%s:%d", c.podIP, port) +} + +func (c *Config) buildExportersEndpoint() string { + return c.endpoint +} + +func (c *Config) protocolsToIDs() []component.ID { + ids := []component.ID{} + + for _, p := range c.protocols { + switch p { + case JaegerProtocol: + ids = append(ids, JaegerID) + case ZipkinProtocol: + ids = append(ids, ZipkinID) + case StatsdProtocol: + ids = append(ids, StatsdID) + case OtlpProtocol: + ids = append(ids, OtlpID) + } + } + + return ids +} + +func WithReceivers() Option { + return func(c *Config) error { + receivers, err := c.buildReceivers() + if err != nil { + return err + } + + c.Receivers = receivers + + return nil + } +} + +func WithProcessors() Option { + return func(c *Config) error { + processors := c.buildProcessors() + + c.Processors = processors + + return nil + } +} + +func WithExporters() Option { + return func(c *Config) error { + exporters := c.buildExporters() + + c.Exporters = exporters + + return nil + } +} + +func WithExtensions() Option { + return func(c *Config) error { + extensions := c.buildExtensions() + + c.Extensions = extensions + + return nil + } +} + +func WithServices() Option { + return func(c *Config) error { + services := c.buildServices() + + c.Service = services + + return nil + } +} + +func WithTLS(tlsCert, tlsKey string) Option { + return func(c *Config) error { + c.tlsCert = tlsCert + c.tlsKey = tlsKey + + return nil + } +} + +func WithCA(caFile string) Option { + return func(c *Config) error { + c.caFile = caFile + + return nil + } +} + +func WithSystemCAs(useSystemCAs bool) Option { + return func(c *Config) error { + c.includeSystemCACertsPool = useSystemCAs + + return nil + } +} + +func WithApiToken(apiToken string) Option { + return func(c *Config) error { + c.apiToken = apiToken + + return nil + } +} + +func WithExportersEndpoint(endpoint string) Option { + return func(c *Config) error { + c.endpoint = endpoint + + return nil + } +} diff --git a/pkg/otelcgen/otelcgen_test.go b/pkg/otelcgen/otelcgen_test.go new file mode 100644 index 0000000000..0f690a3206 --- /dev/null +++ b/pkg/otelcgen/otelcgen_test.go @@ -0,0 +1,35 @@ +package otelcgen + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewConfigFull(t *testing.T) { + cfg, err := NewConfig( + "test", + Protocols{OtlpProtocol, JaegerProtocol, StatsdProtocol, ZipkinProtocol}, + WithCA("/run/opensignals/cacerts/certs"), + WithSystemCAs(true), + WithApiToken("test-token"), + WithTLS("/run/opensignals/tls/tls.crt", "/run/opensignals/tls/tls.key"), + WithReceivers(), + WithExportersEndpoint("exporters-test-endpoint"), + WithExtensions(), + WithExporters(), + WithServices(), + WithProcessors(), + ) + require.NoError(t, err) + c, err := cfg.Marshal() + require.NoError(t, err) + + expectedOutput, err := os.ReadFile(filepath.Join("testdata", "full_config.yaml")) + require.NoError(t, err) + + assert.YAMLEq(t, string(expectedOutput), string(c)) +} diff --git a/pkg/otelcgen/processors.go b/pkg/otelcgen/processors.go new file mode 100644 index 0000000000..de8151c26d --- /dev/null +++ b/pkg/otelcgen/processors.go @@ -0,0 +1,155 @@ +package otelcgen + +import ( + "go.opentelemetry.io/collector/component" +) + +// BatchConfig represents common attributes to config batch processor: +// inspired by +// https://github.com/open-telemetry/opentelemetry-collector/blob/main/processor/batchprocessor/config.go#L16 +type BatchConfig struct { + Timeout string `mapstructure:"timeout"` + SendBatchSize uint32 `mapstructure:"send_batch_size"` + SendBatchMaxSize uint32 `mapstructure:"send_batch_max_size"` +} + +// MemoryLimiter represents common attributes for memory limiter +// inspired by +// https://github.com/open-telemetry/opentelemetry-collector/blob/internal/memorylimiter/v0.117.0/internal/memorylimiter/config.go#L23 +type MemoryLimiter struct { + CheckInterval string `mapstructure:"check_interval"` + MemoryLimitPercentage uint32 `mapstructure:"limit_percentage"` + MemorySpikePercentage uint32 `mapstructure:"spike_limit_percentage"` +} + +// More details, about how to configure `processors,` can be found +// https://github.com/open-telemetry/opentelemetry-collector/blob/main/processor/batchprocessor/README.md +var ( + k8sattributes = component.MustNewID("k8sattributes") + transform = component.MustNewID("transform") + batch = component.MustNewType("batch") + batchTraces = component.NewIDWithName(batch, "traces") + batchMetrics = component.NewIDWithName(batch, "metrics") + batchLogs = component.NewIDWithName(batch, "logs") + memoryLimiter = component.MustNewID("memory_limiter") + cumulativeToDelta = component.MustNewID("cumulativetodelta") + + defaultK8Sattributes = []string{ + "k8s.cluster.uid", + "k8s.node.name", + "k8s.namespace.name", + "k8s.pod.name", + "k8s.pod.uid", + "k8s.pod.ip", + "k8s.deployment.name", + "k8s.replicaset.name", + "k8s.statefulset.name", + "k8s.daemonset.name", + "k8s.cronjob.name", + "k8s.job.name", + } +) + +func (c *Config) buildProcessors() map[component.ID]component.Config { + return map[component.ID]component.Config{ + cumulativeToDelta: map[string]any{}, + k8sattributes: map[string]any{ + "extract": map[string]any{ + "metadata": defaultK8Sattributes, + "annotations": []map[string]any{ + { + "from": "pod", + "key_regex": "metadata.dynatrace.com/(.*)", + "tag_name": "$$1", + }, + }, + }, + "pod_association": []map[string]any{ + { + "sources": []map[string]any{ + {"from": "resource_attribute", "name": "k8s.pod.name"}, + {"from": "resource_attribute", "name": "k8s.namespace.name"}, + }, + }, + { + "sources": []map[string]any{ + {"from": "resource_attribute", "name": "k8s.pod.ip"}, + }, + }, + { + "sources": []map[string]any{ + {"from": "resource_attribute", "name": "k8s.pod.uid"}, + }, + }, + { + "sources": []map[string]any{ + {"from": "connection"}, + }, + }, + }, + }, + transform: c.buildTransform(), + batchTraces: &BatchConfig{ + SendBatchSize: 5000, + SendBatchMaxSize: 5000, + Timeout: "60s", + }, + batchMetrics: &BatchConfig{ + SendBatchSize: 3000, + SendBatchMaxSize: 3000, + Timeout: "60s", + }, + batchLogs: &BatchConfig{ + SendBatchSize: 1800, + SendBatchMaxSize: 2000, + Timeout: "60s", + }, + memoryLimiter: &MemoryLimiter{ + CheckInterval: "1s", + MemoryLimitPercentage: 70, + MemorySpikePercentage: 30, + }, + } +} + +func (c *Config) buildTransform() map[string]any { + return map[string]any{ + "error_mode": "ignore", + "log_statements": c.dynatraceTransformations(), + "metric_statements": c.dynatraceTransformations(), + "trace_statements": c.dynatraceTransformations(), + } +} + +func (c *Config) dynatraceTransformations() []map[string]any { + return []map[string]any{ + { + "context": "resource", + "statements": []string{ + "set(attributes[\"k8s.workload.name\"], attributes[\"k8s.statefulset.name\"]) where IsString(attributes[\"k8s.statefulset.name\"])", + "set(attributes[\"k8s.workload.name\"], attributes[\"k8s.replicaset.name\"]) where IsString(attributes[\"k8s.replicaset.name\"])", + "set(attributes[\"k8s.workload.name\"], attributes[\"k8s.job.name\"]) where IsString(attributes[\"k8s.job.name\"])", + "set(attributes[\"k8s.workload.name\"], attributes[\"k8s.deployment.name\"]) where IsString(attributes[\"k8s.deployment.name\"])", + "set(attributes[\"k8s.workload.name\"], attributes[\"k8s.daemonset.name\"]) where IsString(attributes[\"k8s.daemonset.name\"])", + "set(attributes[\"k8s.workload.name\"], attributes[\"k8s.cronjob.name\"]) where IsString(attributes[\"k8s.cronjob.name\"])", + "set(attributes[\"k8s.workload.kind\"], \"statefulset\") where IsString(attributes[\"k8s.statefulset.name\"])", + "set(attributes[\"k8s.workload.kind\"], \"replicaset\") where IsString(attributes[\"k8s.replicaset.name\"])", + "set(attributes[\"k8s.workload.kind\"], \"job\") where IsString(attributes[\"k8s.job.name\"])", + "set(attributes[\"k8s.workload.kind\"], \"deployment\") where IsString(attributes[\"k8s.deployment.name\"])", + "set(attributes[\"k8s.workload.kind\"], \"daemonset\") where IsString(attributes[\"k8s.daemonset.name\"])", + "set(attributes[\"k8s.workload.kind\"], \"cronjob\") where IsString(attributes[\"k8s.cronjob.name\"])", + "set(attributes[\"k8s.cluster.uid\"], \"${env:K8S_CLUSTER_UID}\") where attributes[\"k8s.cluster.uid\"] == nil", + "set(attributes[\"k8s.cluster.name\"], \"${env:K8S_CLUSTER_NAME}\")", + "set(attributes[\"dt.kubernetes.workload.name\"], attributes[\"k8s.workload.name\"])", + "set(attributes[\"dt.kubernetes.workload.kind\"], attributes[\"k8s.workload.kind\"])", + "set(attributes[\"dt.entity.kubernetes_cluster\"], \"${env:DT_ENTITY_KUBERNETES_CLUSTER}\")", + "delete_key(attributes, \"k8s.statefulset.name\")", + "delete_key(attributes, \"k8s.replicaset.name\")", + "delete_key(attributes, \"k8s.job.name\")", + "delete_key(attributes, \"k8s.deployment.name\")", + "delete_key(attributes, \"k8s.daemonset.name\")", + "delete_key(attributes, \"k8s.cronjob.name\")", + }, + }, + } +} diff --git a/pkg/otelcgen/processors_test.go b/pkg/otelcgen/processors_test.go new file mode 100644 index 0000000000..84c2318bf4 --- /dev/null +++ b/pkg/otelcgen/processors_test.go @@ -0,0 +1,26 @@ +package otelcgen + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewConfigWithProcessors(t *testing.T) { + cfg, err := NewConfig( + "", + RegisteredProtocols, + WithProcessors(), + ) + require.NoError(t, err) + c, err := cfg.Marshal() + require.NoError(t, err) + + expectedOutput, err := os.ReadFile(filepath.Join("testdata", "processors_only.yaml")) + require.NoError(t, err) + + assert.YAMLEq(t, string(expectedOutput), string(c)) +} diff --git a/pkg/otelcgen/receivers.go b/pkg/otelcgen/receivers.go new file mode 100644 index 0000000000..913ca1e657 --- /dev/null +++ b/pkg/otelcgen/receivers.go @@ -0,0 +1,66 @@ +package otelcgen + +import ( + "github.com/pkg/errors" + "go.opentelemetry.io/collector/component" +) + +func (c *Config) buildReceiverComponent(componentID component.ID) component.Config { + switch componentID { + case OtlpID: + return map[string]any{"protocols": map[string]any{ + "grpc": &ServerConfig{TLSSetting: c.buildTLSSetting(), Endpoint: c.buildEndpoint(OtlpGrpcPort)}, + "http": &ServerConfig{TLSSetting: c.buildTLSSetting(), Endpoint: c.buildEndpoint(OtlpHTTPPort)}, + }} + case JaegerID: + return map[string]any{"protocols": map[string]any{ + "grpc": &ServerConfig{Endpoint: c.buildEndpoint(JaegerGrpcPort), TLSSetting: c.buildTLSSetting()}, + "thrift_binary": &ServerConfig{Endpoint: c.buildEndpoint(JaegerThriftBinaryPort)}, + "thrift_compact": &ServerConfig{Endpoint: c.buildEndpoint(JaegerThriftCompactPort)}, + "thrift_http": &ServerConfig{Endpoint: c.buildEndpoint(JaegerThriftHTTPPort), TLSSetting: c.buildTLSSetting()}, + }} + case ZipkinID: + return &ServerConfig{ + Endpoint: c.buildEndpoint(ZipkinPort), + TLSSetting: c.buildTLSSetting(), + } + case StatsdID: + return map[string]any{ + "endpoint": c.buildEndpoint(StatsdPort), + "timer_histogram_mapping": []TimerHistogramMapping{ + { + StatsDType: "histogram", ObserverType: "histogram", Histogram: HistogramConfig{MaxSize: 10}, + }, + { + StatsDType: "timing", ObserverType: "histogram", Histogram: HistogramConfig{MaxSize: 100}, + }, + { + StatsDType: "distribution", ObserverType: "histogram", Histogram: HistogramConfig{MaxSize: 100}, + }, + }, + } + } + + return nil +} + +func (c *Config) buildReceivers() (map[component.ID]component.Config, error) { + receivers := make(map[component.ID]component.Config) + + for _, p := range c.protocols { + switch p { + case StatsdProtocol: + receivers[StatsdID] = c.buildReceiverComponent(StatsdID) + case ZipkinProtocol: + receivers[ZipkinID] = c.buildReceiverComponent(ZipkinID) + case JaegerProtocol: + receivers[JaegerID] = c.buildReceiverComponent(JaegerID) + case OtlpProtocol: + receivers[OtlpID] = c.buildReceiverComponent(OtlpID) + default: + return nil, errors.Errorf("unknown protocol: %s", p) + } + } + + return receivers, nil +} diff --git a/pkg/otelcgen/receivers_test.go b/pkg/otelcgen/receivers_test.go new file mode 100644 index 0000000000..c9b611d612 --- /dev/null +++ b/pkg/otelcgen/receivers_test.go @@ -0,0 +1,88 @@ +package otelcgen + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewConfig(t *testing.T) { + t.Run("with statsd protocol only", func(t *testing.T) { + cfg, err := NewConfig( + "test", + Protocols{StatsdProtocol}, + WithReceivers(), + ) + require.NoError(t, err) + c, err := cfg.Marshal() + require.NoError(t, err) + + expectedOutput, err := os.ReadFile(filepath.Join("testdata", "receivers_statsd.yaml")) + require.NoError(t, err) + + assert.YAMLEq(t, string(expectedOutput), string(c)) + }) + + t.Run("with zipkin protocol only with tls key and tls cert", func(t *testing.T) { + cfg, err := NewConfig( + "test", + Protocols{ZipkinProtocol}, + WithTLS("/run/opensignals/tls/tls.crt", "/run/opensignals/tls/tls.key"), + WithReceivers(), + ) + require.NoError(t, err) + c, err := cfg.Marshal() + require.NoError(t, err) + + expectedOutput, err := os.ReadFile(filepath.Join("testdata", "receivers_zipkin_only.yaml")) + require.NoError(t, err) + assert.YAMLEq(t, string(expectedOutput), string(c)) + }) + + t.Run("with jaeger protocol only with tls key and tls cert", func(t *testing.T) { + cfg, err := NewConfig( + "test", + Protocols{JaegerProtocol}, + WithTLS("/run/opensignals/tls/tls.crt", "/run/opensignals/tls/tls.key"), + WithReceivers(), + ) + require.NoError(t, err) + c, err := cfg.Marshal() + require.NoError(t, err) + + expectedOutput, err := os.ReadFile(filepath.Join("testdata", "receivers_jaeger_only.yaml")) + require.NoError(t, err) + + assert.YAMLEq(t, string(expectedOutput), string(c)) + }) + + t.Run("with otlp protocol only with tls key and tls cert", func(t *testing.T) { + cfg, err := NewConfig( + "test", + Protocols{OtlpProtocol}, + WithTLS("/run/opensignals/tls/tls.crt", "/run/opensignals/tls/tls.key"), + WithReceivers(), + ) + require.NoError(t, err) + c, err := cfg.Marshal() + require.NoError(t, err) + + expectedOutput, err := os.ReadFile(filepath.Join("testdata", "receivers_otlp_only.yaml")) + require.NoError(t, err) + + assert.YAMLEq(t, string(expectedOutput), string(c)) + }) + + t.Run("with unknown protocol", func(t *testing.T) { + _, err := NewConfig( + "", + Protocols{"unknown"}, + WithReceivers(), + ) + require.Error(t, err) + assert.Contains(t, err.Error(), "unknown protocol") + }) +} diff --git a/pkg/otelcgen/services.go b/pkg/otelcgen/services.go new file mode 100644 index 0000000000..f30b9f2059 --- /dev/null +++ b/pkg/otelcgen/services.go @@ -0,0 +1,110 @@ +package otelcgen + +import ( + "slices" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/pipeline" + "go.opentelemetry.io/collector/service/extensions" + "go.opentelemetry.io/collector/service/pipelines" + "go.opentelemetry.io/collector/service/telemetry" +) + +var ( + traces = pipeline.MustNewID("traces") + metrics = pipeline.MustNewID("metrics") + logs = pipeline.MustNewID("logs") + + allowedPipelinesLogsReceiversIDs = []component.ID{OtlpID} + + // based on + // stasd https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/d4372922ec79cb052c7f7e2fcc0fba9f492bd948/receiver/statsdreceiver/factory.go#L33 + allowedPipelinesMetricsReceiversIDs = []component.ID{OtlpID, StatsdID} + + // based on + // zipkin https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/d4372922ec79cb052c7f7e2fcc0fba9f492bd948/receiver/zipkinreceiver/factory.go#L24 + // jaeger https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/d4372922ec79cb052c7f7e2fcc0fba9f492bd948/receiver/jaegerreceiver/factory.go + allowedPipelinesTracesReceiversIDs = []component.ID{OtlpID, JaegerID, ZipkinID} +) + +// ServiceConfig defines the configurable components of the Service. +// based on "go.opentelemetry.io/collector/service.Config +type ServiceConfig struct { + // Telemetry is the configuration for collector's own telemetry. + Telemetry telemetry.Config `mapstructure:"telemetry,omitempty"` + + // Pipelines are the set of data pipelines configured for the service. + Pipelines pipelines.Config `mapstructure:"pipelines"` + + // Extensions are the ordered list of extensions configured for the service. + Extensions extensions.Config `mapstructure:"extensions"` +} + +func (c *Config) buildServices() ServiceConfig { + pipelinesCfg := pipelines.Config{} + + // traces + tracesReceivers := c.buildPipelinesReceivers(allowedPipelinesTracesReceiversIDs) + if len(tracesReceivers) != 0 { + pipelinesCfg[traces] = &pipelines.PipelineConfig{ + Receivers: tracesReceivers, + Processors: append(buildProcessors(), batchTraces), + Exporters: buildExporters(), + } + } + + // metrics + metricsReceivers := c.buildPipelinesReceivers(allowedPipelinesMetricsReceiversIDs) + if len(metricsReceivers) != 0 { + pipelinesCfg[metrics] = &pipelines.PipelineConfig{ + Receivers: metricsReceivers, + Processors: append(buildProcessors(), cumulativeToDelta, batchMetrics), + Exporters: buildExporters(), + } + } + + // logs + logsReceivers := c.buildPipelinesReceivers(allowedPipelinesLogsReceiversIDs) + if len(logsReceivers) != 0 { + pipelinesCfg[logs] = &pipelines.PipelineConfig{ + Receivers: logsReceivers, + Processors: append(buildProcessors(), batchLogs), + Exporters: buildExporters(), + } + } + + return ServiceConfig{ + Extensions: extensions.Config{healthCheck}, + Pipelines: pipelinesCfg, + } +} + +func (c *Config) buildPipelinesReceivers(allowed []component.ID) []component.ID { + return filter(c.protocolsToIDs(), func(id component.ID) bool { + return slices.Contains(allowed, id) + }) +} + +func buildExporters() []component.ID { + return []component.ID{ + otlphttp, + } +} + +func buildProcessors() []component.ID { + return []component.ID{ + memoryLimiter, k8sattributes, transform, + } +} + +func filter(componentIDs []component.ID, f func(component.ID) bool) []component.ID { + filtered := make([]component.ID, 0) + + for _, componentID := range componentIDs { + if f(componentID) { + filtered = append(filtered, componentID) + } + } + + return filtered +} diff --git a/pkg/otelcgen/services_test.go b/pkg/otelcgen/services_test.go new file mode 100644 index 0000000000..69255f38db --- /dev/null +++ b/pkg/otelcgen/services_test.go @@ -0,0 +1,55 @@ +package otelcgen + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewConfigWithServices(t *testing.T) { + cfg, err := NewConfig( + "", + RegisteredProtocols, + WithServices(), + ) + require.NoError(t, err) + c, err := cfg.Marshal() + require.NoError(t, err) + + expectedOutput, err := os.ReadFile(filepath.Join("testdata", "services_only.yaml")) + require.NoError(t, err) + assert.YAMLEq(t, string(expectedOutput), string(c)) +} + +func TestNewConfigWithServicesZipkinOnly(t *testing.T) { + cfg, err := NewConfig( + "", + Protocols{ZipkinProtocol}, + WithServices(), + ) + require.NoError(t, err) + c, err := cfg.Marshal() + require.NoError(t, err) + + expectedOutput, err := os.ReadFile(filepath.Join("testdata", "services_zipkin_only.yaml")) + require.NoError(t, err) + assert.YAMLEq(t, string(expectedOutput), string(c)) +} + +func TestNewConfigWithServicesStatsdOnly(t *testing.T) { + cfg, err := NewConfig( + "", + Protocols{StatsdProtocol}, + WithServices(), + ) + require.NoError(t, err) + c, err := cfg.Marshal() + require.NoError(t, err) + + expectedOutput, err := os.ReadFile(filepath.Join("testdata", "services_statsd_only.yaml")) + require.NoError(t, err) + assert.YAMLEq(t, string(expectedOutput), string(c)) +} diff --git a/pkg/otelcgen/testdata/exporters_only.yaml b/pkg/otelcgen/testdata/exporters_only.yaml new file mode 100644 index 0000000000..0fd8925728 --- /dev/null +++ b/pkg/otelcgen/testdata/exporters_only.yaml @@ -0,0 +1,15 @@ +connectors: {} +exporters: + otlphttp: + endpoint: "test" + tls: + ca_file: "/run/opensignals/cacerts/certs" + + headers: + Authorization: "Api-Token test-token" +extensions: {} +processors: {} +receivers: {} +service: + extensions: [] + pipelines: {} diff --git a/pkg/otelcgen/testdata/extensions_only.yaml b/pkg/otelcgen/testdata/extensions_only.yaml new file mode 100644 index 0000000000..d4c586ce3b --- /dev/null +++ b/pkg/otelcgen/testdata/extensions_only.yaml @@ -0,0 +1,10 @@ +connectors: {} +exporters: {} +extensions: + health_check: + endpoint: test:13133 +processors: {} +receivers: {} +service: + extensions: [] + pipelines: {} diff --git a/pkg/otelcgen/testdata/full_config.yaml b/pkg/otelcgen/testdata/full_config.yaml new file mode 100644 index 0000000000..c25af689b8 --- /dev/null +++ b/pkg/otelcgen/testdata/full_config.yaml @@ -0,0 +1,230 @@ +connectors: {} +exporters: + otlphttp: + endpoint: "exporters-test-endpoint" + tls: + ca_file: "/run/opensignals/cacerts/certs" + include_system_ca_certs_pool: true + headers: + Authorization: "Api-Token test-token" +extensions: + health_check: + endpoint: test:13133 +processors: + cumulativetodelta: {} + k8sattributes: + extract: + annotations: + - from: pod + key_regex: metadata.dynatrace.com/(.*) + tag_name: $$1 + metadata: + - k8s.cluster.uid + - k8s.node.name + - k8s.namespace.name + - k8s.pod.name + - k8s.pod.uid + - k8s.pod.ip + - k8s.deployment.name + - k8s.replicaset.name + - k8s.statefulset.name + - k8s.daemonset.name + - k8s.cronjob.name + - k8s.job.name + pod_association: + - sources: + - from: resource_attribute + name: k8s.pod.name + - from: resource_attribute + name: k8s.namespace.name + - sources: + - from: resource_attribute + name: k8s.pod.ip + - sources: + - from: resource_attribute + name: k8s.pod.uid + - sources: + - from: connection + transform: + error_mode: ignore + log_statements: + - context: resource + statements: + - set(attributes["k8s.workload.name"], attributes["k8s.statefulset.name"]) where IsString(attributes["k8s.statefulset.name"]) + - set(attributes["k8s.workload.name"], attributes["k8s.replicaset.name"]) where IsString(attributes["k8s.replicaset.name"]) + - set(attributes["k8s.workload.name"], attributes["k8s.job.name"]) where IsString(attributes["k8s.job.name"]) + - set(attributes["k8s.workload.name"], attributes["k8s.deployment.name"]) where IsString(attributes["k8s.deployment.name"]) + - set(attributes["k8s.workload.name"], attributes["k8s.daemonset.name"]) where IsString(attributes["k8s.daemonset.name"]) + - set(attributes["k8s.workload.name"], attributes["k8s.cronjob.name"]) where IsString(attributes["k8s.cronjob.name"]) + - set(attributes["k8s.workload.kind"], "statefulset") where IsString(attributes["k8s.statefulset.name"]) + - set(attributes["k8s.workload.kind"], "replicaset") where IsString(attributes["k8s.replicaset.name"]) + - set(attributes["k8s.workload.kind"], "job") where IsString(attributes["k8s.job.name"]) + - set(attributes["k8s.workload.kind"], "deployment") where IsString(attributes["k8s.deployment.name"]) + - set(attributes["k8s.workload.kind"], "daemonset") where IsString(attributes["k8s.daemonset.name"]) + - set(attributes["k8s.workload.kind"], "cronjob") where IsString(attributes["k8s.cronjob.name"]) + - set(attributes["k8s.cluster.uid"], "${env:K8S_CLUSTER_UID}") where attributes["k8s.cluster.uid"] == nil + - set(attributes["k8s.cluster.name"], "${env:K8S_CLUSTER_NAME}") + - set(attributes["dt.kubernetes.workload.name"], attributes["k8s.workload.name"]) + - set(attributes["dt.kubernetes.workload.kind"], attributes["k8s.workload.kind"]) + - set(attributes["dt.entity.kubernetes_cluster"], "${env:DT_ENTITY_KUBERNETES_CLUSTER}") + - delete_key(attributes, "k8s.statefulset.name") + - delete_key(attributes, "k8s.replicaset.name") + - delete_key(attributes, "k8s.job.name") + - delete_key(attributes, "k8s.deployment.name") + - delete_key(attributes, "k8s.daemonset.name") + - delete_key(attributes, "k8s.cronjob.name") + metric_statements: + - context: resource + statements: + - set(attributes["k8s.workload.name"], attributes["k8s.statefulset.name"]) where IsString(attributes["k8s.statefulset.name"]) + - set(attributes["k8s.workload.name"], attributes["k8s.replicaset.name"]) where IsString(attributes["k8s.replicaset.name"]) + - set(attributes["k8s.workload.name"], attributes["k8s.job.name"]) where IsString(attributes["k8s.job.name"]) + - set(attributes["k8s.workload.name"], attributes["k8s.deployment.name"]) where IsString(attributes["k8s.deployment.name"]) + - set(attributes["k8s.workload.name"], attributes["k8s.daemonset.name"]) where IsString(attributes["k8s.daemonset.name"]) + - set(attributes["k8s.workload.name"], attributes["k8s.cronjob.name"]) where IsString(attributes["k8s.cronjob.name"]) + - set(attributes["k8s.workload.kind"], "statefulset") where IsString(attributes["k8s.statefulset.name"]) + - set(attributes["k8s.workload.kind"], "replicaset") where IsString(attributes["k8s.replicaset.name"]) + - set(attributes["k8s.workload.kind"], "job") where IsString(attributes["k8s.job.name"]) + - set(attributes["k8s.workload.kind"], "deployment") where IsString(attributes["k8s.deployment.name"]) + - set(attributes["k8s.workload.kind"], "daemonset") where IsString(attributes["k8s.daemonset.name"]) + - set(attributes["k8s.workload.kind"], "cronjob") where IsString(attributes["k8s.cronjob.name"]) + - set(attributes["k8s.cluster.uid"], "${env:K8S_CLUSTER_UID}") where attributes["k8s.cluster.uid"] == nil + - set(attributes["k8s.cluster.name"], "${env:K8S_CLUSTER_NAME}") + - set(attributes["dt.kubernetes.workload.name"], attributes["k8s.workload.name"]) + - set(attributes["dt.kubernetes.workload.kind"], attributes["k8s.workload.kind"]) + - set(attributes["dt.entity.kubernetes_cluster"], "${env:DT_ENTITY_KUBERNETES_CLUSTER}") + - delete_key(attributes, "k8s.statefulset.name") + - delete_key(attributes, "k8s.replicaset.name") + - delete_key(attributes, "k8s.job.name") + - delete_key(attributes, "k8s.deployment.name") + - delete_key(attributes, "k8s.daemonset.name") + - delete_key(attributes, "k8s.cronjob.name") + trace_statements: + - context: resource + statements: + - set(attributes["k8s.workload.name"], attributes["k8s.statefulset.name"]) where IsString(attributes["k8s.statefulset.name"]) + - set(attributes["k8s.workload.name"], attributes["k8s.replicaset.name"]) where IsString(attributes["k8s.replicaset.name"]) + - set(attributes["k8s.workload.name"], attributes["k8s.job.name"]) where IsString(attributes["k8s.job.name"]) + - set(attributes["k8s.workload.name"], attributes["k8s.deployment.name"]) where IsString(attributes["k8s.deployment.name"]) + - set(attributes["k8s.workload.name"], attributes["k8s.daemonset.name"]) where IsString(attributes["k8s.daemonset.name"]) + - set(attributes["k8s.workload.name"], attributes["k8s.cronjob.name"]) where IsString(attributes["k8s.cronjob.name"]) + - set(attributes["k8s.workload.kind"], "statefulset") where IsString(attributes["k8s.statefulset.name"]) + - set(attributes["k8s.workload.kind"], "replicaset") where IsString(attributes["k8s.replicaset.name"]) + - set(attributes["k8s.workload.kind"], "job") where IsString(attributes["k8s.job.name"]) + - set(attributes["k8s.workload.kind"], "deployment") where IsString(attributes["k8s.deployment.name"]) + - set(attributes["k8s.workload.kind"], "daemonset") where IsString(attributes["k8s.daemonset.name"]) + - set(attributes["k8s.workload.kind"], "cronjob") where IsString(attributes["k8s.cronjob.name"]) + - set(attributes["k8s.cluster.uid"], "${env:K8S_CLUSTER_UID}") where attributes["k8s.cluster.uid"] == nil + - set(attributes["k8s.cluster.name"], "${env:K8S_CLUSTER_NAME}") + - set(attributes["dt.kubernetes.workload.name"], attributes["k8s.workload.name"]) + - set(attributes["dt.kubernetes.workload.kind"], attributes["k8s.workload.kind"]) + - set(attributes["dt.entity.kubernetes_cluster"], "${env:DT_ENTITY_KUBERNETES_CLUSTER}") + - delete_key(attributes, "k8s.statefulset.name") + - delete_key(attributes, "k8s.replicaset.name") + - delete_key(attributes, "k8s.job.name") + - delete_key(attributes, "k8s.deployment.name") + - delete_key(attributes, "k8s.daemonset.name") + - delete_key(attributes, "k8s.cronjob.name") + batch/traces: + send_batch_size: 5000 + send_batch_max_size: 5000 + timeout: 60s + batch/metrics: + send_batch_size: 3000 + send_batch_max_size: 3000 + timeout: 60s + batch/logs: + send_batch_size: 1800 + send_batch_max_size: 2000 + timeout: 60s + memory_limiter: + check_interval: 1s + limit_percentage: 70 + spike_limit_percentage: 30 +receivers: + otlp: + protocols: + grpc: + endpoint: test:4317 + tls: + cert_file: /run/opensignals/tls/tls.crt + key_file: /run/opensignals/tls/tls.key + http: + endpoint: test:4318 + tls: + cert_file: /run/opensignals/tls/tls.crt + key_file: /run/opensignals/tls/tls.key + jaeger: + protocols: + grpc: + endpoint: test:14250 + tls: + cert_file: /run/opensignals/tls/tls.crt + key_file: /run/opensignals/tls/tls.key + thrift_binary: + endpoint: test:6832 + thrift_compact: + endpoint: test:6831 + thrift_http: + endpoint: test:14268 + tls: + cert_file: /run/opensignals/tls/tls.crt + key_file: /run/opensignals/tls/tls.key + zipkin: + endpoint: test:9411 + tls: + cert_file: /run/opensignals/tls/tls.crt + key_file: /run/opensignals/tls/tls.key + statsd: + endpoint: test:8125 + timer_histogram_mapping: + - histogram: + max_size: 10 + observer_type: histogram + statsd_type: histogram + - histogram: + max_size: 100 + observer_type: histogram + statsd_type: timing + - histogram: + max_size: 100 + observer_type: histogram + statsd_type: distribution +service: + extensions: + - health_check + pipelines: + logs: + exporters: + - otlphttp + receivers: + - otlp + processors: + - memory_limiter + - k8sattributes + - transform + - batch/logs + metrics: + exporters: + - otlphttp + receivers: + - otlp + - statsd + processors: + - memory_limiter + - k8sattributes + - transform + - cumulativetodelta + - batch/metrics + traces: + exporters: + - otlphttp + receivers: + - otlp + - jaeger + - zipkin + processors: + - memory_limiter + - k8sattributes + - transform + - batch/traces diff --git a/pkg/otelcgen/testdata/open-signals.yaml b/pkg/otelcgen/testdata/open-signals.yaml new file mode 100644 index 0000000000..495fee1976 --- /dev/null +++ b/pkg/otelcgen/testdata/open-signals.yaml @@ -0,0 +1,191 @@ +### receiveres ### +receivers: + otlp: + protocols: + grpc: + endpoint: ${env:MY_POD_IP}:4317 + tls: # if .spec.openSignals.tlsRef + cert_file: /run/opensignals/tls/tls.crt + key_file: /run/opensignals/tls/tls.key + http: + endpoint: ${env:MY_POD_IP}:4318 + tls: # if .spec.openSignals.tlsRef + cert_file: /run/opensignals/tls/tls.crt + key_file: /run/opensignals/tls/tls.key + jaeger: + protocols: + grpc: + endpoint: ${env:MY_POD_IP}:14250 + tls: # if .spec.openSignals.tlsRef + cert_file: /run/opensignals/tls/tls.crt + key_file: /run/opensignals/tls/tls.key + thrift_binary: + endpoint: ${env:MY_POD_IP}:6832 + thrift_compact: + endpoint: ${env:MY_POD_IP}:6831 + thrift_http: + endpoint: ${env:MY_POD_IP}:14268 + tls: # if .spec.openSignals.tlsRef + cert_file: /run/opensignals/tls/tls.crt + key_file: /run/opensignals/tls/tls.key + zipkin: + endpoint: ${env:MY_POD_IP}:9411 + tls: # if .spec.openSignals.tlsRef + cert_file: /run/opensignals/tls/tls.crt + key_file: /run/opensignals/tls/tls.key + statsd: + endpoint: ${env:MY_POD_IP}:8125 + timer_histogram_mapping: + - statsd_type: "histogram" + observer_type: "histogram" + histogram: + max_size: 100 + - statsd_type: "timing" + observer_type: "histogram" + histogram: + max_size: 100 + - statsd_type: "distribution" + observer_type: "histogram" + histogram: + max_size: 100 + +### processors ### +processors: + cumulativetodelta: + k8sattributes: + extract: + metadata: + - k8s.cluster.uid + - k8s.node.name + - k8s.namespace.name + - k8s.pod.name + - k8s.pod.uid + - k8s.pod.ip + - k8s.deployment.name + - k8s.replicaset.name + - k8s.statefulset.name + - k8s.daemonset.name + - k8s.cronjob.name + - k8s.job.name + annotations: + - from: pod + key_regex: metadata.dynatrace.com/(.*) + tag_name: $$1 + pod_association: + - sources: + - from: resource_attribute + name: k8s.pod.name + - from: resource_attribute + name: k8s.namespace.name + - sources: + - from: resource_attribute + name: k8s.pod.ip + - sources: + - from: resource_attribute + name: k8s.pod.uid + - sources: + - from: connection + transform: + error_mode: ignore + trace_statements: &dynatrace_transformations + - context: resource + statements: + - set(attributes["k8s.workload.kind"], "job") where IsString(attributes["k8s.job.name"]) + - set(attributes["k8s.workload.name"], attributes["k8s.job.name"]) where IsString(attributes["k8s.job.name"]) + - set(attributes["k8s.workload.kind"], "cronjob") where IsString(attributes["k8s.cronjob.name"]) + - set(attributes["k8s.workload.name"], attributes["k8s.cronjob.name"]) where IsString(attributes["k8s.cronjob.name"]) + - set(attributes["k8s.workload.kind"], "daemonset") where IsString(attributes["k8s.daemonset.name"]) + - set(attributes["k8s.workload.name"], attributes["k8s.daemonset.name"]) where IsString(attributes["k8s.daemonset.name"]) + - set(attributes["k8s.workload.kind"], "statefulset") where IsString(attributes["k8s.statefulset.name"]) + - set(attributes["k8s.workload.name"], attributes["k8s.statefulset.name"]) where IsString(attributes["k8s.statefulset.name"]) + - set(attributes["k8s.workload.kind"], "replicaset") where IsString(attributes["k8s.replicaset.name"]) + - set(attributes["k8s.workload.name"], attributes["k8s.replicaset.name"]) where IsString(attributes["k8s.replicaset.name"]) + - set(attributes["k8s.workload.kind"], "deployment") where IsString(attributes["k8s.deployment.name"]) + - set(attributes["k8s.workload.name"], attributes["k8s.deployment.name"]) where IsString(attributes["k8s.deployment.name"]) + - set(attributes["k8s.cluster.name"], "${env:K8S_CLUSTER_NAME}") + - set(attributes["k8s.cluster.uid"], "${env:K8S_CLUSTER_UID}") where attributes["k8s.cluster.uid"] == nil + - set(attributes["dt.entity.kubernetes_cluster"], "${env:DT_ENTITY_KUBERNETES_CLUSTER}") + - delete_key(attributes, "k8s.deployment.name") + - delete_key(attributes, "k8s.replicaset.name") + - delete_key(attributes, "k8s.statefulset.name") + - delete_key(attributes, "k8s.daemonset.name") + - delete_key(attributes, "k8s.cronjob.name") + - delete_key(attributes, "k8s.job.name") + - set(attributes["dt.kubernetes.workload.name"], attributes["k8s.workload.name"]) + - set(attributes["dt.kubernetes.workload.kind"], attributes["k8s.workload.kind"]) + metric_statements: *dynatrace_transformations + log_statements: *dynatrace_transformations + batch/traces: + send_batch_size: 5000 + send_batch_max_size: 5000 + timeout: 60s + batch/metrics: + send_batch_size: 3000 + send_batch_max_size: 3000 + timeout: 60s + batch/logs: + send_batch_size: 1800 + send_batch_max_size: 2000 + timeout: 60s + memory_limiter: + check_interval: 1s + limit_percentage: 70 + spike_limit_percentage: 30 + +### exporters ### +exporters: + debug: + otlphttp: + endpoint: "${env:DT_ENDPOINT}" + tls: # if in-cluster AG + ca_file: /run/opensignals/cacerts/certs + headers: + Authorization: "Api-Token ${env:DT_API_TOKEN}" + +### extensions ### +extensions: + health_check: + endpoint: ${env:MY_POD_IP}:13133 + +### service ### +service: + extensions: + - health_check + pipelines: + traces: + receivers: + - otlp + - jaeger + - zipkin + processors: + - memory_limiter + - k8sattributes + - transform + - batch/traces + exporters: + - otlphttp + - debug + metrics: + receivers: + - otlp + - statsd + processors: + - memory_limiter + - cumulativetodelta + - k8sattributes + - transform + - batch/metrics + exporters: + - otlphttp + - debug + logs: + receivers: + - otlp + processors: + - memory_limiter + - k8sattributes + - transform + - batch/logs + exporters: + - otlphttp + - debug diff --git a/pkg/otelcgen/testdata/processors_only.yaml b/pkg/otelcgen/testdata/processors_only.yaml new file mode 100644 index 0000000000..64a69c6cdd --- /dev/null +++ b/pkg/otelcgen/testdata/processors_only.yaml @@ -0,0 +1,138 @@ +connectors: {} +exporters: {} +extensions: {} +processors: + cumulativetodelta: {} + k8sattributes: + extract: + annotations: + - from: pod + key_regex: metadata.dynatrace.com/(.*) + tag_name: $$1 + metadata: + - k8s.cluster.uid + - k8s.node.name + - k8s.namespace.name + - k8s.pod.name + - k8s.pod.uid + - k8s.pod.ip + - k8s.deployment.name + - k8s.replicaset.name + - k8s.statefulset.name + - k8s.daemonset.name + - k8s.cronjob.name + - k8s.job.name + pod_association: + - sources: + - from: resource_attribute + name: k8s.pod.name + - from: resource_attribute + name: k8s.namespace.name + - sources: + - from: resource_attribute + name: k8s.pod.ip + - sources: + - from: resource_attribute + name: k8s.pod.uid + - sources: + - from: connection + transform: + error_mode: ignore + log_statements: + - context: resource + statements: + - set(attributes["k8s.workload.name"], attributes["k8s.statefulset.name"]) where IsString(attributes["k8s.statefulset.name"]) + - set(attributes["k8s.workload.name"], attributes["k8s.replicaset.name"]) where IsString(attributes["k8s.replicaset.name"]) + - set(attributes["k8s.workload.name"], attributes["k8s.job.name"]) where IsString(attributes["k8s.job.name"]) + - set(attributes["k8s.workload.name"], attributes["k8s.deployment.name"]) where IsString(attributes["k8s.deployment.name"]) + - set(attributes["k8s.workload.name"], attributes["k8s.daemonset.name"]) where IsString(attributes["k8s.daemonset.name"]) + - set(attributes["k8s.workload.name"], attributes["k8s.cronjob.name"]) where IsString(attributes["k8s.cronjob.name"]) + - set(attributes["k8s.workload.kind"], "statefulset") where IsString(attributes["k8s.statefulset.name"]) + - set(attributes["k8s.workload.kind"], "replicaset") where IsString(attributes["k8s.replicaset.name"]) + - set(attributes["k8s.workload.kind"], "job") where IsString(attributes["k8s.job.name"]) + - set(attributes["k8s.workload.kind"], "deployment") where IsString(attributes["k8s.deployment.name"]) + - set(attributes["k8s.workload.kind"], "daemonset") where IsString(attributes["k8s.daemonset.name"]) + - set(attributes["k8s.workload.kind"], "cronjob") where IsString(attributes["k8s.cronjob.name"]) + - set(attributes["k8s.cluster.uid"], "${env:K8S_CLUSTER_UID}") where attributes["k8s.cluster.uid"] == nil + - set(attributes["k8s.cluster.name"], "${env:K8S_CLUSTER_NAME}") + - set(attributes["dt.kubernetes.workload.name"], attributes["k8s.workload.name"]) + - set(attributes["dt.kubernetes.workload.kind"], attributes["k8s.workload.kind"]) + - set(attributes["dt.entity.kubernetes_cluster"], "${env:DT_ENTITY_KUBERNETES_CLUSTER}") + - delete_key(attributes, "k8s.statefulset.name") + - delete_key(attributes, "k8s.replicaset.name") + - delete_key(attributes, "k8s.job.name") + - delete_key(attributes, "k8s.deployment.name") + - delete_key(attributes, "k8s.daemonset.name") + - delete_key(attributes, "k8s.cronjob.name") + metric_statements: + - context: resource + statements: + - set(attributes["k8s.workload.name"], attributes["k8s.statefulset.name"]) where IsString(attributes["k8s.statefulset.name"]) + - set(attributes["k8s.workload.name"], attributes["k8s.replicaset.name"]) where IsString(attributes["k8s.replicaset.name"]) + - set(attributes["k8s.workload.name"], attributes["k8s.job.name"]) where IsString(attributes["k8s.job.name"]) + - set(attributes["k8s.workload.name"], attributes["k8s.deployment.name"]) where IsString(attributes["k8s.deployment.name"]) + - set(attributes["k8s.workload.name"], attributes["k8s.daemonset.name"]) where IsString(attributes["k8s.daemonset.name"]) + - set(attributes["k8s.workload.name"], attributes["k8s.cronjob.name"]) where IsString(attributes["k8s.cronjob.name"]) + - set(attributes["k8s.workload.kind"], "statefulset") where IsString(attributes["k8s.statefulset.name"]) + - set(attributes["k8s.workload.kind"], "replicaset") where IsString(attributes["k8s.replicaset.name"]) + - set(attributes["k8s.workload.kind"], "job") where IsString(attributes["k8s.job.name"]) + - set(attributes["k8s.workload.kind"], "deployment") where IsString(attributes["k8s.deployment.name"]) + - set(attributes["k8s.workload.kind"], "daemonset") where IsString(attributes["k8s.daemonset.name"]) + - set(attributes["k8s.workload.kind"], "cronjob") where IsString(attributes["k8s.cronjob.name"]) + - set(attributes["k8s.cluster.uid"], "${env:K8S_CLUSTER_UID}") where attributes["k8s.cluster.uid"] == nil + - set(attributes["k8s.cluster.name"], "${env:K8S_CLUSTER_NAME}") + - set(attributes["dt.kubernetes.workload.name"], attributes["k8s.workload.name"]) + - set(attributes["dt.kubernetes.workload.kind"], attributes["k8s.workload.kind"]) + - set(attributes["dt.entity.kubernetes_cluster"], "${env:DT_ENTITY_KUBERNETES_CLUSTER}") + - delete_key(attributes, "k8s.statefulset.name") + - delete_key(attributes, "k8s.replicaset.name") + - delete_key(attributes, "k8s.job.name") + - delete_key(attributes, "k8s.deployment.name") + - delete_key(attributes, "k8s.daemonset.name") + - delete_key(attributes, "k8s.cronjob.name") + trace_statements: + - context: resource + statements: + - set(attributes["k8s.workload.name"], attributes["k8s.statefulset.name"]) where IsString(attributes["k8s.statefulset.name"]) + - set(attributes["k8s.workload.name"], attributes["k8s.replicaset.name"]) where IsString(attributes["k8s.replicaset.name"]) + - set(attributes["k8s.workload.name"], attributes["k8s.job.name"]) where IsString(attributes["k8s.job.name"]) + - set(attributes["k8s.workload.name"], attributes["k8s.deployment.name"]) where IsString(attributes["k8s.deployment.name"]) + - set(attributes["k8s.workload.name"], attributes["k8s.daemonset.name"]) where IsString(attributes["k8s.daemonset.name"]) + - set(attributes["k8s.workload.name"], attributes["k8s.cronjob.name"]) where IsString(attributes["k8s.cronjob.name"]) + - set(attributes["k8s.workload.kind"], "statefulset") where IsString(attributes["k8s.statefulset.name"]) + - set(attributes["k8s.workload.kind"], "replicaset") where IsString(attributes["k8s.replicaset.name"]) + - set(attributes["k8s.workload.kind"], "job") where IsString(attributes["k8s.job.name"]) + - set(attributes["k8s.workload.kind"], "deployment") where IsString(attributes["k8s.deployment.name"]) + - set(attributes["k8s.workload.kind"], "daemonset") where IsString(attributes["k8s.daemonset.name"]) + - set(attributes["k8s.workload.kind"], "cronjob") where IsString(attributes["k8s.cronjob.name"]) + - set(attributes["k8s.cluster.uid"], "${env:K8S_CLUSTER_UID}") where attributes["k8s.cluster.uid"] == nil + - set(attributes["k8s.cluster.name"], "${env:K8S_CLUSTER_NAME}") + - set(attributes["dt.kubernetes.workload.name"], attributes["k8s.workload.name"]) + - set(attributes["dt.kubernetes.workload.kind"], attributes["k8s.workload.kind"]) + - set(attributes["dt.entity.kubernetes_cluster"], "${env:DT_ENTITY_KUBERNETES_CLUSTER}") + - delete_key(attributes, "k8s.statefulset.name") + - delete_key(attributes, "k8s.replicaset.name") + - delete_key(attributes, "k8s.job.name") + - delete_key(attributes, "k8s.deployment.name") + - delete_key(attributes, "k8s.daemonset.name") + - delete_key(attributes, "k8s.cronjob.name") + batch/traces: + send_batch_size: 5000 + send_batch_max_size: 5000 + timeout: 60s + batch/metrics: + send_batch_size: 3000 + send_batch_max_size: 3000 + timeout: 60s + batch/logs: + send_batch_size: 1800 + send_batch_max_size: 2000 + timeout: 60s + memory_limiter: + check_interval: 1s + limit_percentage: 70 + spike_limit_percentage: 30 +receivers: {} +service: + extensions: [] + pipelines: {} diff --git a/pkg/otelcgen/testdata/receivers_jaeger_only.yaml b/pkg/otelcgen/testdata/receivers_jaeger_only.yaml new file mode 100644 index 0000000000..e58d4d2ab4 --- /dev/null +++ b/pkg/otelcgen/testdata/receivers_jaeger_only.yaml @@ -0,0 +1,24 @@ +connectors: {} +exporters: {} +extensions: {} +processors: {} +receivers: + jaeger: + protocols: + grpc: + endpoint: test:14250 + tls: + cert_file: /run/opensignals/tls/tls.crt + key_file: /run/opensignals/tls/tls.key + thrift_binary: + endpoint: test:6832 + thrift_compact: + endpoint: test:6831 + thrift_http: + endpoint: test:14268 + tls: + cert_file: /run/opensignals/tls/tls.crt + key_file: /run/opensignals/tls/tls.key +service: + extensions: [] + pipelines: {} diff --git a/pkg/otelcgen/testdata/receivers_otlp_only.yaml b/pkg/otelcgen/testdata/receivers_otlp_only.yaml new file mode 100644 index 0000000000..4a1e56f702 --- /dev/null +++ b/pkg/otelcgen/testdata/receivers_otlp_only.yaml @@ -0,0 +1,20 @@ +connectors: {} +exporters: {} +extensions: {} +processors: {} +receivers: + otlp: + protocols: + grpc: + endpoint: test:4317 + tls: + cert_file: /run/opensignals/tls/tls.crt + key_file: /run/opensignals/tls/tls.key + http: + endpoint: test:4318 + tls: + cert_file: /run/opensignals/tls/tls.crt + key_file: /run/opensignals/tls/tls.key +service: + extensions: [] + pipelines: {} diff --git a/pkg/otelcgen/testdata/receivers_statsd.yaml b/pkg/otelcgen/testdata/receivers_statsd.yaml new file mode 100644 index 0000000000..b3602e65a3 --- /dev/null +++ b/pkg/otelcgen/testdata/receivers_statsd.yaml @@ -0,0 +1,23 @@ +connectors: {} +exporters: {} +extensions: {} +processors: {} +receivers: + statsd: + endpoint: test:8125 + timer_histogram_mapping: + - histogram: + max_size: 10 + observer_type: histogram + statsd_type: histogram + - histogram: + max_size: 100 + observer_type: histogram + statsd_type: timing + - histogram: + max_size: 100 + observer_type: histogram + statsd_type: distribution +service: + extensions: [] + pipelines: {} diff --git a/pkg/otelcgen/testdata/receivers_zipkin_only.yaml b/pkg/otelcgen/testdata/receivers_zipkin_only.yaml new file mode 100644 index 0000000000..e21c174bfa --- /dev/null +++ b/pkg/otelcgen/testdata/receivers_zipkin_only.yaml @@ -0,0 +1,13 @@ +connectors: {} +exporters: {} +extensions: {} +processors: {} +receivers: + zipkin: + endpoint: test:9411 + tls: + cert_file: /run/opensignals/tls/tls.crt + key_file: /run/opensignals/tls/tls.key +service: + extensions: [] + pipelines: {} diff --git a/pkg/otelcgen/testdata/services_only.yaml b/pkg/otelcgen/testdata/services_only.yaml new file mode 100644 index 0000000000..e6681c54a2 --- /dev/null +++ b/pkg/otelcgen/testdata/services_only.yaml @@ -0,0 +1,43 @@ +connectors: {} +exporters: {} +extensions: {} +processors: {} +receivers: {} +service: + extensions: + - health_check + pipelines: + logs: + exporters: + - otlphttp + receivers: + - otlp + processors: + - memory_limiter + - k8sattributes + - transform + - batch/logs + metrics: + exporters: + - otlphttp + receivers: + - otlp + - statsd + processors: + - memory_limiter + - k8sattributes + - transform + - cumulativetodelta + - batch/metrics + traces: + exporters: + - otlphttp + receivers: + - otlp + - jaeger + - zipkin + processors: + - memory_limiter + - k8sattributes + - transform + - batch/traces diff --git a/pkg/otelcgen/testdata/services_statsd_only.yaml b/pkg/otelcgen/testdata/services_statsd_only.yaml new file mode 100644 index 0000000000..f1f8265ef4 --- /dev/null +++ b/pkg/otelcgen/testdata/services_statsd_only.yaml @@ -0,0 +1,20 @@ +connectors: {} +exporters: {} +extensions: {} +processors: {} +receivers: {} +service: + extensions: + - health_check + pipelines: + metrics: + exporters: + - otlphttp + receivers: + - statsd + processors: + - memory_limiter + - k8sattributes + - transform + - cumulativetodelta + - batch/metrics diff --git a/pkg/otelcgen/testdata/services_zipkin_only.yaml b/pkg/otelcgen/testdata/services_zipkin_only.yaml new file mode 100644 index 0000000000..3984c6e43a --- /dev/null +++ b/pkg/otelcgen/testdata/services_zipkin_only.yaml @@ -0,0 +1,19 @@ +connectors: {} +exporters: {} +extensions: {} +processors: {} +receivers: {} +service: + extensions: + - health_check + pipelines: + traces: + exporters: + - otlphttp + receivers: + - zipkin + processors: + - memory_limiter + - k8sattributes + - transform + - batch/traces diff --git a/pkg/util/address/address.go b/pkg/util/address/address.go deleted file mode 100644 index eded0f6c75..0000000000 --- a/pkg/util/address/address.go +++ /dev/null @@ -1,5 +0,0 @@ -package address - -func Of[T any](i T) *T { - return &i -} diff --git a/pkg/util/address/address_test.go b/pkg/util/address/address_test.go deleted file mode 100644 index 9b755ec036..0000000000 --- a/pkg/util/address/address_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package address - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestOf(t *testing.T) { - t.Run("bool", func(t *testing.T) { - assert.True(t, *Of(true)) - assert.False(t, *Of(false)) - - const constantBool = true - - assert.True(t, *Of(constantBool)) - - mutableBool := true - assert.True(t, *Of(mutableBool)) - - mutableBool = false - assert.False(t, *Of(mutableBool)) - }) - - t.Run("int64", func(t *testing.T) { - assert.Equal(t, 23, *Of(23)) - - const constantInt = 27 - - assert.Equal(t, 27, *Of(constantInt)) - - mutableInt := int64(4) - assert.Equal(t, int64(4), *Of(mutableInt)) - }) -} diff --git a/pkg/util/certificates/certificates.go b/pkg/util/certificates/certificates.go index 4a5d570e41..7045badbeb 100644 --- a/pkg/util/certificates/certificates.go +++ b/pkg/util/certificates/certificates.go @@ -116,3 +116,15 @@ func ValidateCertificateExpiration(certData []byte, renewalThreshold time.Durati return true, nil } + +func CommonName(dkName string, dkNamespace string, componentName string) string { + return dkName + "-" + componentName + "." + dkNamespace +} + +func AltNames(dkName string, dkNamespace string, componentName string) []string { + return []string{ + dkName + "-" + componentName + "." + dkNamespace, + dkName + "-" + componentName + "." + dkNamespace + ".svc", + dkName + "-" + componentName + "." + dkNamespace + ".svc.cluster.local", + } +} diff --git a/pkg/util/conditions/configmap.go b/pkg/util/conditions/configmap.go new file mode 100644 index 0000000000..7218cd668e --- /dev/null +++ b/pkg/util/conditions/configmap.go @@ -0,0 +1,44 @@ +package conditions + +import ( + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + ConfigMapCreatedReason = "ConfigMapCreated" + ConfigMapUpdatedReason = "ConfigMapUpdated" + ConfigMapCreatedOrUpdatedReason = "ConfigMapCreatedOrUpdated" + ConfigMapOutdatedReason = "ConfigMapOutdated" + ConfigMapGenerationFailed = "ConfigMapGenerationFailed" +) + +func SetConfigMapCreatedOrUpdated(conditions *[]metav1.Condition, conditionType, name string) { + condition := metav1.Condition{ + Type: conditionType, + Status: metav1.ConditionTrue, + Reason: ConfigMapCreatedOrUpdatedReason, + Message: appendCreatedOrUpdatedSuffix(name), + } + _ = meta.SetStatusCondition(conditions, condition) +} + +func SetConfigMapGenFailed(conditions *[]metav1.Condition, conditionType string, err error) { + condition := metav1.Condition{ + Type: conditionType, + Status: metav1.ConditionFalse, + Reason: ConfigMapGenerationFailed, + Message: "Failed to generate configmap: " + err.Error(), + } + _ = meta.SetStatusCondition(conditions, condition) +} + +func SetConfigMapOutdated(conditions *[]metav1.Condition, conditionType, message string) { + condition := metav1.Condition{ + Type: conditionType, + Status: metav1.ConditionFalse, + Reason: ConfigMapOutdatedReason, + Message: message, + } + _ = meta.SetStatusCondition(conditions, condition) +} diff --git a/pkg/util/conditions/time.go b/pkg/util/conditions/time.go index 56af881ef6..e436ae3088 100644 --- a/pkg/util/conditions/time.go +++ b/pkg/util/conditions/time.go @@ -1,7 +1,7 @@ package conditions import ( - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/util/timeprovider" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/pkg/util/conditions/time_test.go b/pkg/util/conditions/time_test.go index a0d90d4bb9..34668be349 100644 --- a/pkg/util/conditions/time_test.go +++ b/pkg/util/conditions/time_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/util/timeprovider" "github.com/pkg/errors" "github.com/stretchr/testify/assert" diff --git a/pkg/util/conditions/token.go b/pkg/util/conditions/token.go new file mode 100644 index 0000000000..aaceafc14e --- /dev/null +++ b/pkg/util/conditions/token.go @@ -0,0 +1,20 @@ +package conditions + +import ( + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + DataIngestTokenMissing string = "DataIngestTokenMissing" +) + +func SetDataIngestTokenMissing(conditions *[]metav1.Condition, conditionType string, msg string) { + condition := metav1.Condition{ + Type: conditionType, + Status: metav1.ConditionFalse, + Reason: DataIngestTokenMissing, + Message: msg, + } + _ = meta.SetStatusCondition(conditions, condition) +} diff --git a/pkg/util/installconfig/modules.go b/pkg/util/installconfig/modules.go index 312d415cdc..1e200c9eab 100644 --- a/pkg/util/installconfig/modules.go +++ b/pkg/util/installconfig/modules.go @@ -54,6 +54,16 @@ func GetModules() Modules { return *override } + ReadModules() + + return modules +} + +func ReadModules() { + ReadModulesToLogger(log) +} + +func ReadModulesToLogger(log logd.Logger) { once.Do(func() { modulesJson := os.Getenv(ModulesJsonEnv) if modulesJson == "" { @@ -71,8 +81,6 @@ func GetModules() Modules { log.Info("envvar content read and set", "envvar", ModulesJsonEnv, "value", modulesJson) }) - - return modules } // SetModulesOverride is a testing function, so you can easily unittest function using the GetModules() func diff --git a/pkg/util/kubeobjects/activegate/capability.go b/pkg/util/kubeobjects/activegate/capability.go index 554a755ce5..caa529be4f 100644 --- a/pkg/util/kubeobjects/activegate/capability.go +++ b/pkg/util/kubeobjects/activegate/capability.go @@ -1,8 +1,8 @@ package activegate import ( - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" ) func SwitchCapability(dk *dynakube.DynaKube, capability activegate.Capability, wantEnabled bool) { diff --git a/pkg/util/kubeobjects/daemonset/builder.go b/pkg/util/kubeobjects/daemonset/builder.go index ee1bbb6a4d..de9c697d1e 100644 --- a/pkg/util/kubeobjects/daemonset/builder.go +++ b/pkg/util/kubeobjects/daemonset/builder.go @@ -46,6 +46,12 @@ func SetTolerations(tolerations []corev1.Toleration) builder.Option[*appsv1.Daem } } +func SetNodeSelector(nodeSelector map[string]string) builder.Option[*appsv1.DaemonSet] { + return func(s *appsv1.DaemonSet) { + s.Spec.Template.Spec.NodeSelector = nodeSelector + } +} + func SetDNSPolicy(policy corev1.DNSPolicy) builder.Option[*appsv1.DaemonSet] { return func(s *appsv1.DaemonSet) { s.Spec.Template.Spec.DNSPolicy = policy diff --git a/pkg/util/kubeobjects/env/env.go b/pkg/util/kubeobjects/env/env.go index 1acca5603b..118a7740f7 100644 --- a/pkg/util/kubeobjects/env/env.go +++ b/pkg/util/kubeobjects/env/env.go @@ -1,12 +1,16 @@ package env import ( + "encoding/json" "os" corev1 "k8s.io/api/core/v1" ) const ( + Tolerations = "TOLERATIONS" + NodeName = "KUBE_NODE_NAME" + CSIDataDir = "CSI_DATA_DIR" PodNamespace = "POD_NAMESPACE" PodName = "POD_NAME" ) @@ -56,3 +60,24 @@ func DefaultNamespace() string { return namespace } + +func GetNodeName() string { + return os.Getenv(NodeName) +} + +func GetCSIDataDir() string { + return os.Getenv(CSIDataDir) +} + +func GetTolerations() ([]corev1.Toleration, error) { + var tolerations []corev1.Toleration + + raw := os.Getenv(Tolerations) + if raw == "" { + return tolerations, nil + } + + err := json.Unmarshal([]byte(os.Getenv(Tolerations)), &tolerations) + + return tolerations, err +} diff --git a/pkg/util/kubeobjects/env/env_test.go b/pkg/util/kubeobjects/env/env_test.go index f1314224ac..3597ba3a31 100644 --- a/pkg/util/kubeobjects/env/env_test.go +++ b/pkg/util/kubeobjects/env/env_test.go @@ -1,9 +1,11 @@ package env import ( + "encoding/json" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" ) @@ -82,3 +84,44 @@ func TestDefaultNamespace(t *testing.T) { assert.Equal(t, "dynatrace", got) }) } + +func TestGetToleration(t *testing.T) { + t.Run("Get tolerations from env var", func(t *testing.T) { + expected := []corev1.Toleration{ + { + Key: "key1", + Operator: corev1.TolerationOpEqual, + Value: "value1", + Effect: corev1.TaintEffectNoSchedule, + }, + { + Key: "key2", + Operator: corev1.TolerationOpEqual, + Value: "value1", + Effect: corev1.TaintEffectNoSchedule, + }, + } + + raw, err := json.Marshal(expected) + require.NoError(t, err) + + t.Setenv(Tolerations, string(raw)) + + actual, err := GetTolerations() + require.NoError(t, err) + assert.Equal(t, expected, actual) + }) + t.Run("Error incase of malformed", func(t *testing.T) { + t.Setenv(Tolerations, "{!@@@#}") + + _, err := GetTolerations() + require.Error(t, err) + }) + + t.Run("no error incase of empty", func(t *testing.T) { + t.Setenv(Tolerations, "") + + _, err := GetTolerations() + require.NoError(t, err) + }) +} diff --git a/pkg/util/kubeobjects/internal/query/query.go b/pkg/util/kubeobjects/internal/query/query.go index 36c198f530..c4db09c1b2 100644 --- a/pkg/util/kubeobjects/internal/query/query.go +++ b/pkg/util/kubeobjects/internal/query/query.go @@ -38,7 +38,7 @@ func (c Generic[T, L]) WithOwner(owner client.Object) Generic[T, L] { func (c Generic[T, L]) Get(ctx context.Context, objectKey client.ObjectKey) (T, error) { err := c.KubeReader.Get(ctx, objectKey, c.Target) - return c.Target, err + return c.Target, errors.WithStack(err) } func (c Generic[T, L]) Create(ctx context.Context, object T) error { @@ -77,10 +77,10 @@ func (c Generic[T, L]) Update(ctx context.Context, object T) error { return errors.WithStack(c.KubeClient.Update(ctx, object)) } -func (c Generic[T, L]) Delete(ctx context.Context, object T) error { +func (c Generic[T, L]) Delete(ctx context.Context, object T, options ...client.DeleteOption) error { c.log(object).Info("deleting") - err := c.KubeClient.Delete(ctx, object) + err := c.KubeClient.Delete(ctx, object, options...) return errors.WithStack(client.IgnoreNotFound(err)) } @@ -218,13 +218,13 @@ func (c Generic[T, L]) createOrUpdateForNamespaces(ctx context.Context, object T return goerrors.Join(errs...) } -func (c Generic[T, L]) DeleteForNamespace(ctx context.Context, objectName string, namespace string) error { +func (c Generic[T, L]) DeleteForNamespace(ctx context.Context, objectName string, namespace string, options ...client.DeleteOption) error { c.Log.Info("deleting object from namespace", "name", objectName, "namespace", namespace) c.Target.SetName(objectName) c.Target.SetNamespace(namespace) - return c.Delete(ctx, c.Target) + return c.Delete(ctx, c.Target, options...) } func (c Generic[T, L]) DeleteForNamespaces(ctx context.Context, objectName string, namespaces []string) error { diff --git a/pkg/util/kubeobjects/job/builder.go b/pkg/util/kubeobjects/job/builder.go new file mode 100644 index 0000000000..f69602f309 --- /dev/null +++ b/pkg/util/kubeobjects/job/builder.go @@ -0,0 +1,128 @@ +package job + +import ( + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/internal/builder" + maputils "github.com/Dynatrace/dynatrace-operator/pkg/util/map" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" +) + +var ( + // Mandatory fields, provided in constructor as named params + setName = builder.SetName[*batchv1.Job] + setNamespace = builder.SetNamespace[*batchv1.Job] + + // Optional fields, provided in constructor as list of options + SetLabels = builder.SetLabels[*batchv1.Job] +) + +func Build(owner metav1.Object, name string, container corev1.Container, options ...builder.Option[*batchv1.Job]) (*batchv1.Job, error) { + neededOpts := []builder.Option[*batchv1.Job]{ + setName(name), + SetContainer(container), + setNamespace(owner.GetNamespace()), + } + neededOpts = append(neededOpts, options...) + + return builder.Build(owner, &batchv1.Job{}, neededOpts...) +} + +func SetVolumes(volumes []corev1.Volume) builder.Option[*batchv1.Job] { + return func(s *batchv1.Job) { + s.Spec.Template.Spec.Volumes = volumes + } +} + +func SetTolerations(tolerations []corev1.Toleration) builder.Option[*batchv1.Job] { + return func(s *batchv1.Job) { + s.Spec.Template.Spec.Tolerations = tolerations + } +} + +func SetNodeName(nodeName string) builder.Option[*batchv1.Job] { + return func(s *batchv1.Job) { + s.Spec.Template.Spec.NodeName = nodeName + } +} + +func SetOnFailureRestartPolicy() builder.Option[*batchv1.Job] { + return func(s *batchv1.Job) { + s.Spec.Template.Spec.RestartPolicy = corev1.RestartPolicyOnFailure + } +} + +func SetPullSecret(pullSecrets ...string) builder.Option[*batchv1.Job] { + return func(s *batchv1.Job) { + imagePullSecrets := make([]corev1.LocalObjectReference, 0) + for _, pullSecretName := range pullSecrets { + imagePullSecrets = append(imagePullSecrets, corev1.LocalObjectReference{ + Name: pullSecretName, + }) + } + + s.Spec.Template.Spec.ImagePullSecrets = append(s.Spec.Template.Spec.ImagePullSecrets, imagePullSecrets...) + } +} + +func SetAutomountServiceAccountToken(isEnabled bool) builder.Option[*batchv1.Job] { + return func(s *batchv1.Job) { + s.Spec.Template.Spec.AutomountServiceAccountToken = &isEnabled + } +} + +func SetPodAnnotations(annotations map[string]string) builder.Option[*batchv1.Job] { + return func(s *batchv1.Job) { + s.Spec.Template.ObjectMeta.Annotations = annotations + } +} + +func SetServiceAccount(serviceAccountName string) builder.Option[*batchv1.Job] { + return func(s *batchv1.Job) { + s.Spec.Template.Spec.ServiceAccountName = serviceAccountName + s.Spec.Template.Spec.DeprecatedServiceAccount = serviceAccountName + } +} + +func SetTTLSecondsAfterFinished(ttl int32) builder.Option[*batchv1.Job] { + return func(s *batchv1.Job) { + s.Spec.TTLSecondsAfterFinished = ptr.To(ttl) + } +} + +func SetActiveDeadlineSeconds(deadline int64) builder.Option[*batchv1.Job] { + return func(s *batchv1.Job) { + s.Spec.ActiveDeadlineSeconds = ptr.To(deadline) + } +} + +func SetAllLabels(labels, matchLabels, templateLabels, customLabels map[string]string) builder.Option[*batchv1.Job] { + return func(s *batchv1.Job) { + s.ObjectMeta.Labels = labels + if matchLabels == nil { + s.Spec.Selector = &metav1.LabelSelector{MatchLabels: matchLabels} + } + + s.Spec.Template.ObjectMeta.Labels = maputils.MergeMap(customLabels, templateLabels) + } +} + +func SetContainer(container corev1.Container) builder.Option[*batchv1.Job] { + return func(s *batchv1.Job) { + targetIndex := 0 + for index := range s.Spec.Template.Spec.Containers { + if s.Spec.Template.Spec.Containers[targetIndex].Name == container.Name { + targetIndex = index + + break + } + } + + if targetIndex == 0 { + s.Spec.Template.Spec.Containers = make([]corev1.Container, 1) + } + + s.Spec.Template.Spec.Containers[targetIndex] = container + } +} diff --git a/pkg/util/kubeobjects/job/query.go b/pkg/util/kubeobjects/job/query.go new file mode 100644 index 0000000000..bb2a954de8 --- /dev/null +++ b/pkg/util/kubeobjects/job/query.go @@ -0,0 +1,45 @@ +package job + +import ( + "github.com/Dynatrace/dynatrace-operator/pkg/logd" + "github.com/Dynatrace/dynatrace-operator/pkg/util/hasher" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/internal/query" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/labels" + batchv1 "k8s.io/api/batch/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type QueryObject struct { + query.Generic[*batchv1.Job, *batchv1.JobList] +} + +func Query(kubeClient client.Client, kubeReader client.Reader, log logd.Logger) QueryObject { + return QueryObject{ + query.Generic[*batchv1.Job, *batchv1.JobList]{ + Target: &batchv1.Job{}, + ListTarget: &batchv1.JobList{}, + ToList: func(sl *batchv1.JobList) []*batchv1.Job { + out := []*batchv1.Job{} + for _, s := range sl.Items { + out = append(out, &s) + } + + return out + }, + IsEqual: isEqual, + MustRecreate: mustRecreate, + + KubeClient: kubeClient, + KubeReader: kubeReader, + Log: log, + }, + } +} + +func isEqual(current, desired *batchv1.Job) bool { + return !hasher.IsAnnotationDifferent(current, desired) +} + +func mustRecreate(current, desired *batchv1.Job) bool { + return labels.NotEqual(current.Spec.Selector.MatchLabels, desired.Spec.Selector.MatchLabels) +} diff --git a/pkg/util/kubeobjects/labels/labels.go b/pkg/util/kubeobjects/labels/labels.go index 0ac2bee445..7a99fad401 100644 --- a/pkg/util/kubeobjects/labels/labels.go +++ b/pkg/util/kubeobjects/labels/labels.go @@ -15,13 +15,14 @@ const ( AppVersionLabel = "app.kubernetes.io/version" OneAgentComponentLabel = "oneagent" + CodeModuleComponentLabel = "codemodule" LogMonitoringComponentLabel = "logmonitoring" KSPMComponentLabel = "kspm" ActiveGateComponentLabel = "activegate" WebhookComponentLabel = "webhook" EdgeConnectComponentLabel = "edgeconnect" ExtensionComponentLabel = "dynatrace-extensions-controller" - CollectorComponentLabel = "dynatrace-extensions-collector" + OtelCComponentLabel = "dynatrace-opentelemetry-collector" ) type AppMatchLabels struct { diff --git a/pkg/util/kubeobjects/mounts/volumes.go b/pkg/util/kubeobjects/mounts/volumes.go new file mode 100644 index 0000000000..27a9633368 --- /dev/null +++ b/pkg/util/kubeobjects/mounts/volumes.go @@ -0,0 +1,38 @@ +package mounts + +import ( + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" +) + +func GetByName(mounts []corev1.VolumeMount, volumeName string) (*corev1.VolumeMount, error) { + for _, mount := range mounts { + if mount.Name == volumeName { + return &mount, nil + } + } + + return nil, errors.Errorf(`Cannot find volume mount "%s" in the provided slice (len %d)`, + volumeName, len(mounts), + ) +} + +func IsIn(mounts []corev1.VolumeMount, volumeName string) bool { + for _, vm := range mounts { + if vm.Name == volumeName { + return true + } + } + + return false +} + +func IsPathIn(mounts []corev1.VolumeMount, path string) bool { + for _, vm := range mounts { + if vm.MountPath == path { + return true + } + } + + return false +} diff --git a/pkg/util/kubeobjects/node/affinity.go b/pkg/util/kubeobjects/node/affinity.go index e74078785d..db39995353 100644 --- a/pkg/util/kubeobjects/node/affinity.go +++ b/pkg/util/kubeobjects/node/affinity.go @@ -11,12 +11,22 @@ const ( ) func Affinity() corev1.Affinity { + return AffinityForArches(arch.AMDImage, arch.ARMImage, arch.PPCLEImage, arch.S390Image) +} + +// AMDOnlyAffinity provides an affinity that will only allow deployment on AMD64 nodes. +// This is manly needed for the Dynatrace tenant-registry as it only has AMD64 images. +func AMDOnlyAffinity() corev1.Affinity { + return AffinityForArches(arch.AMDImage) +} + +func AffinityForArches(arches ...string) corev1.Affinity { return corev1.Affinity{ NodeAffinity: &corev1.NodeAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{ NodeSelectorTerms: []corev1.NodeSelectorTerm{ { - MatchExpressions: AffinityNodeRequirementForSupportedArches(), + MatchExpressions: affinityNodeRequirementsForArches(arches...), }, }, }, @@ -24,10 +34,6 @@ func Affinity() corev1.Affinity { } } -func AffinityNodeRequirementForSupportedArches() []corev1.NodeSelectorRequirement { - return affinityNodeRequirementsForArches(arch.AMDImage, arch.ARMImage, arch.PPCLEImage, arch.S390Image) -} - func affinityNodeRequirementsForArches(arches ...string) []corev1.NodeSelectorRequirement { return []corev1.NodeSelectorRequirement{ { diff --git a/pkg/util/kubeobjects/node/affinity_test.go b/pkg/util/kubeobjects/node/affinity_test.go index b5fdd187c5..041a8b6c40 100644 --- a/pkg/util/kubeobjects/node/affinity_test.go +++ b/pkg/util/kubeobjects/node/affinity_test.go @@ -5,12 +5,33 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/arch" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" ) -func TestAffinityNodeRequirement(t *testing.T) { - assert.Equal(t, AffinityNodeRequirementForSupportedArches(), affinityNodeRequirementsForArches(arch.AMDImage, arch.ARMImage, arch.PPCLEImage, arch.S390Image)) - assert.Contains(t, AffinityNodeRequirementForSupportedArches(), linuxRequirement()) +func TestAffinity(t *testing.T) { + affinity := Affinity() + + require.NotNil(t, affinity) + require.NotNil(t, affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution) + require.Len(t, affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms, 1) + + matchExpression := affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms[0].MatchExpressions + assert.Equal(t, matchExpression, affinityNodeRequirementsForArches(arch.AMDImage, arch.ARMImage, arch.PPCLEImage, arch.S390Image)) + assert.Contains(t, matchExpression, linuxRequirement()) +} + +func TestAffinityForArches(t *testing.T) { + expectedArches := []string{"arch1", "arch2", "arch3"} + affinity := AffinityForArches(expectedArches...) + + require.NotNil(t, affinity) + require.NotNil(t, affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution) + require.Len(t, affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms, 1) + + matchExpression := affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms[0].MatchExpressions + assert.Equal(t, matchExpression, affinityNodeRequirementsForArches(expectedArches...)) + assert.Contains(t, matchExpression, linuxRequirement()) } func linuxRequirement() corev1.NodeSelectorRequirement { diff --git a/pkg/util/kubeobjects/service/builder.go b/pkg/util/kubeobjects/service/builder.go index 947d63beae..45785ab63c 100644 --- a/pkg/util/kubeobjects/service/builder.go +++ b/pkg/util/kubeobjects/service/builder.go @@ -15,7 +15,7 @@ var ( SetLabels = builder.SetLabels[*corev1.Service] ) -func Build(owner metav1.Object, name string, selectorLabels map[string]string, svcPort corev1.ServicePort, options ...builder.Option[*corev1.Service]) (*corev1.Service, error) { +func Build(owner metav1.Object, name string, selectorLabels map[string]string, svcPort []corev1.ServicePort, options ...builder.Option[*corev1.Service]) (*corev1.Service, error) { neededOpts := []builder.Option[*corev1.Service]{ setName(name), setPorts(svcPort), @@ -27,25 +27,9 @@ func Build(owner metav1.Object, name string, selectorLabels map[string]string, s return builder.Build(owner, &corev1.Service{}, neededOpts...) } -func setPorts(svcPort corev1.ServicePort) builder.Option[*corev1.Service] { +func setPorts(svcPorts []corev1.ServicePort) builder.Option[*corev1.Service] { return func(s *corev1.Service) { - targetIndex := 0 - for index := range s.Spec.Ports { - if s.Spec.Ports[targetIndex].Name == svcPort.Name { - targetIndex = index - - break - } - } - - if targetIndex == 0 { - s.Spec.Ports = make([]corev1.ServicePort, 1) - } - - s.Spec.Ports[targetIndex].Name = svcPort.Name - s.Spec.Ports[targetIndex].Port = svcPort.Port - s.Spec.Ports[targetIndex].Protocol = svcPort.Protocol - s.Spec.Ports[targetIndex].TargetPort = svcPort.TargetPort + s.Spec.Ports = svcPorts } } diff --git a/pkg/util/kubeobjects/service/builder_test.go b/pkg/util/kubeobjects/service/builder_test.go index 6d804d4fae..37e192b425 100644 --- a/pkg/util/kubeobjects/service/builder_test.go +++ b/pkg/util/kubeobjects/service/builder_test.go @@ -6,7 +6,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -35,7 +34,7 @@ func TestServiceBuilder(t *testing.T) { service, err := Build(createDeployment(), testServiceName, labels, - corev1.ServicePort{}, + nil, setNamespace(testNamespace)) require.NoError(t, err) require.Len(t, service.OwnerReferences, 1) @@ -47,7 +46,7 @@ func TestServiceBuilder(t *testing.T) { secret, err := Build(createDeployment(), testServiceName, labels, - corev1.ServicePort{}, + nil, SetLabels(labels), setNamespace(testNamespace), ) diff --git a/pkg/util/kubeobjects/statefulset/builder.go b/pkg/util/kubeobjects/statefulset/builder.go index 27d505caac..d8a285b207 100644 --- a/pkg/util/kubeobjects/statefulset/builder.go +++ b/pkg/util/kubeobjects/statefulset/builder.go @@ -1,12 +1,13 @@ package statefulset import ( - "github.com/Dynatrace/dynatrace-operator/pkg/util/address" + "github.com/Dynatrace/dynatrace-operator/pkg/util/hasher" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/internal/builder" maputils "github.com/Dynatrace/dynatrace-operator/pkg/util/map" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" ) var ( @@ -26,12 +27,14 @@ func Build(owner metav1.Object, name string, container corev1.Container, options } neededOpts = append(neededOpts, options...) + neededOpts = append(neededOpts, SetPVCAnnotation()) + return builder.Build(owner, &appsv1.StatefulSet{}, neededOpts...) } func SetReplicas(replicas int32) builder.Option[*appsv1.StatefulSet] { return func(s *appsv1.StatefulSet) { - s.Spec.Replicas = address.Of(replicas) + s.Spec.Replicas = ptr.To(replicas) } } @@ -106,8 +109,25 @@ func SetSecurityContext(securityContext *corev1.PodSecurityContext) builder.Opti } } -func SetUpdateStrategy(updateStartegy appsv1.StatefulSetUpdateStrategy) builder.Option[*appsv1.StatefulSet] { +func SetRollingUpdateStrategyType() builder.Option[*appsv1.StatefulSet] { return func(s *appsv1.StatefulSet) { - s.Spec.UpdateStrategy = updateStartegy + s.Spec.UpdateStrategy = appsv1.StatefulSetUpdateStrategy{ + RollingUpdate: &appsv1.RollingUpdateStatefulSetStrategy{ + Partition: ptr.To(int32(0)), + }, + Type: appsv1.RollingUpdateStatefulSetStrategyType, + } + } +} + +func SetPVCAnnotation() builder.Option[*appsv1.StatefulSet] { + return func(s *appsv1.StatefulSet) { + if s.Spec.VolumeClaimTemplates != nil { + if s.ObjectMeta.Annotations == nil { + s.ObjectMeta.Annotations = map[string]string{} + } + + s.ObjectMeta.Annotations[AnnotationPVCHash], _ = hasher.GenerateHash(s.Spec.VolumeClaimTemplates) + } } } diff --git a/pkg/util/kubeobjects/statefulset/consts.go b/pkg/util/kubeobjects/statefulset/consts.go new file mode 100644 index 0000000000..885d16756e --- /dev/null +++ b/pkg/util/kubeobjects/statefulset/consts.go @@ -0,0 +1,7 @@ +package statefulset + +import "github.com/Dynatrace/dynatrace-operator/pkg/api" + +const ( + AnnotationPVCHash = api.InternalFlagPrefix + "pvc-hash" +) diff --git a/pkg/util/kubeobjects/statefulset/query.go b/pkg/util/kubeobjects/statefulset/query.go index 65568feff7..c5c76401c8 100644 --- a/pkg/util/kubeobjects/statefulset/query.go +++ b/pkg/util/kubeobjects/statefulset/query.go @@ -1,8 +1,6 @@ package statefulset import ( - "reflect" - "github.com/Dynatrace/dynatrace-operator/pkg/logd" "github.com/Dynatrace/dynatrace-operator/pkg/util/hasher" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/internal/query" @@ -37,5 +35,8 @@ func isEqual(current, desired *appsv1.StatefulSet) bool { } func mustRecreate(current, desired *appsv1.StatefulSet) bool { - return labels.NotEqual(current.Spec.Selector.MatchLabels, desired.Spec.Selector.MatchLabels) || !reflect.DeepEqual(current.Spec.VolumeClaimTemplates, desired.Spec.VolumeClaimTemplates) + currentHash := current.Annotations[AnnotationPVCHash] + desiredHash := desired.Annotations[AnnotationPVCHash] + + return labels.NotEqual(current.Spec.Selector.MatchLabels, desired.Spec.Selector.MatchLabels) || currentHash != desiredHash } diff --git a/pkg/util/kubeobjects/topology/constraint.go b/pkg/util/kubeobjects/topology/constraint.go new file mode 100644 index 0000000000..0bff759d33 --- /dev/null +++ b/pkg/util/kubeobjects/topology/constraint.go @@ -0,0 +1,24 @@ +package topology + +import ( + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/labels" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func MaxOnePerNode(appLabels *labels.AppLabels) []corev1.TopologySpreadConstraint { + return []corev1.TopologySpreadConstraint{ + { + MaxSkew: 1, + TopologyKey: "topology.kubernetes.io/zone", + WhenUnsatisfiable: "ScheduleAnyway", + LabelSelector: &metav1.LabelSelector{MatchLabels: appLabels.BuildMatchLabels()}, + }, + { + MaxSkew: 1, + TopologyKey: "kubernetes.io/hostname", + WhenUnsatisfiable: "DoNotSchedule", + LabelSelector: &metav1.LabelSelector{MatchLabels: appLabels.BuildMatchLabels()}, + }, + } +} diff --git a/pkg/util/kubeobjects/volumes/volumes.go b/pkg/util/kubeobjects/volumes/volumes.go index 9ba6c8685c..54f3274715 100644 --- a/pkg/util/kubeobjects/volumes/volumes.go +++ b/pkg/util/kubeobjects/volumes/volumes.go @@ -1,25 +1,12 @@ package volumes import ( - "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" ) -func GetVolumeMountByName(mounts []corev1.VolumeMount, volumeName string) (*corev1.VolumeMount, error) { - for _, mount := range mounts { - if mount.Name == volumeName { - return &mount, nil - } - } - - return nil, errors.Errorf(`Cannot find volume mount "%s" in the provided slice (len %d)`, - volumeName, len(mounts), - ) -} - -func IsIn(mounts []corev1.VolumeMount, volumeName string) bool { - for _, vm := range mounts { - if vm.Name == volumeName { +func IsIn(vol []corev1.Volume, volumeName string) bool { + for _, v := range vol { + if v.Name == volumeName { return true } } diff --git a/pkg/util/prioritymap/map.go b/pkg/util/prioritymap/map.go index 484ccbb281..49fe92956f 100644 --- a/pkg/util/prioritymap/map.go +++ b/pkg/util/prioritymap/map.go @@ -1,6 +1,7 @@ package prioritymap import ( + "slices" "strings" corev1 "k8s.io/api/core/v1" @@ -18,6 +19,7 @@ type Map struct { type entry struct { value any + key string delimiter string priority int allowDuplicates bool @@ -37,13 +39,34 @@ func WithSeparator(separator string) Option { } } -// WithAllowDuplicatesForKey allows to add multiple values for the same key (covers all keys) func WithAllowDuplicates() Option { return func(a *entry) { a.allowDuplicates = true } } +func WithAvoidDuplicates() Option { + return func(a *entry) { + a.allowDuplicates = false + } +} + +func WithAvoidDuplicatesFor(key string) Option { + return func(a *entry) { + if a.key == key { + a.allowDuplicates = false + } + } +} + +func WithAllowDuplicatesFor(key string) Option { + return func(a *entry) { + if a.key == key { + a.allowDuplicates = true + } + } +} + func New(defaultOptions ...Option) *Map { m := &Map{ entries: make(map[string][]entry), @@ -59,6 +82,7 @@ func (m Map) Append(key string, value any, opts ...Option) { } newArg := entry{ + key: key, value: value, priority: DefaultPriority, allowDuplicates: false, @@ -79,7 +103,11 @@ func (m Map) Append(key string, value any, opts ...Option) { m.entries[key] = make([]entry, 0) } - m.entries[key] = append(m.entries[key], newArg) + if !slices.ContainsFunc(m.entries[key], func(e entry) bool { + return e.value == value + }) { + m.entries[key] = append(m.entries[key], newArg) + } } } diff --git a/pkg/util/prioritymap/map_test.go b/pkg/util/prioritymap/map_test.go index e26c94cea8..68e8780ce6 100644 --- a/pkg/util/prioritymap/map_test.go +++ b/pkg/util/prioritymap/map_test.go @@ -158,3 +158,132 @@ func TestArgumentSlice(t *testing.T) { assert.Equal(t, expectedArgs, argMap.AsKeyValueStrings()) } + +func TestDuplicateArguments(t *testing.T) { + defaultArgs := []string{ + "--set-proxy=127.0.0.1", + "--set-host-id-source=auto", + "--set-server=localhost", + "--set-host-property=prop1", + "--set-host-property=prop2", + "--set-host-property=prop3", + "--set-host-property=prop3", + "--set-host-property=prop3", + } + + customArgs := []string{ + "--set-host-id-source=fqdn", + "--set-server=foobar.com", + "--set-host-property=custom-prop1", + "--set-host-property=custom-prop2", + "--set-host-property=custom-prop3", + "--set-host-property=custom-prop3", + "--set-host-property=custom-prop3", + } + + var tests = []struct { + title string + expectedArgs []string + defaultArgPrio int + customArgsPrio int + mapOptions []Option + }{ + { + title: "Enter avoided duplicates with higher prio", + expectedArgs: []string{ + "--set-host-id-source=fqdn", + "--set-host-property=prop1", + "--set-host-property=prop2", + "--set-host-property=prop3", + "--set-host-property=custom-prop1", + "--set-host-property=custom-prop2", + "--set-host-property=custom-prop3", + "--set-proxy=127.0.0.1", + "--set-server=foobar.com", + }, + defaultArgPrio: DefaultPriority, + customArgsPrio: HighPriority, + mapOptions: []Option{ + WithSeparator("="), + WithAllowDuplicates(), + WithAvoidDuplicatesFor("--set-host-id-source"), + WithAvoidDuplicatesFor("--set-server"), + }, + }, + { + title: "Enter avoided duplicates with lower prio", + expectedArgs: []string{ + "--set-host-id-source=auto", + "--set-host-property=custom-prop1", + "--set-host-property=custom-prop2", + "--set-host-property=custom-prop3", + "--set-host-property=prop1", + "--set-host-property=prop2", + "--set-host-property=prop3", + "--set-proxy=127.0.0.1", + "--set-server=localhost", + }, + defaultArgPrio: HighPriority, + customArgsPrio: DefaultPriority, + mapOptions: []Option{ + WithSeparator("="), + WithAllowDuplicates(), + WithAvoidDuplicatesFor("--set-host-id-source"), + WithAvoidDuplicatesFor("--set-server"), + }, + }, + { + title: "Enter avoided duplicates with higher prio", + expectedArgs: []string{ + "--set-host-id-source=fqdn", + "--set-host-property=prop1", + "--set-host-property=prop2", + "--set-host-property=prop3", + "--set-host-property=custom-prop1", + "--set-host-property=custom-prop2", + "--set-host-property=custom-prop3", + "--set-proxy=127.0.0.1", + "--set-server=foobar.com", + }, + defaultArgPrio: DefaultPriority, + customArgsPrio: HighPriority, + mapOptions: []Option{ + WithSeparator("="), + WithAvoidDuplicates(), + WithAllowDuplicatesFor("--set-host-property"), + }, + }, + { + title: "Enter avoided duplicates with lower prio", + expectedArgs: []string{ + "--set-host-id-source=auto", + "--set-host-property=custom-prop1", + "--set-host-property=custom-prop2", + "--set-host-property=custom-prop3", + "--set-host-property=prop1", + "--set-host-property=prop2", + "--set-host-property=prop3", + "--set-proxy=127.0.0.1", + "--set-server=localhost", + }, + defaultArgPrio: HighPriority, + customArgsPrio: DefaultPriority, + mapOptions: []Option{ + WithSeparator("="), + WithAvoidDuplicates(), + WithAllowDuplicatesFor("--set-host-property"), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.title, func(t *testing.T) { + argMap := New(tt.mapOptions...) + + Append(argMap, defaultArgs, WithPriority(tt.defaultArgPrio)) + Append(argMap, customArgs, WithPriority(tt.customArgsPrio)) + + assert.Equal(t, tt.expectedArgs, argMap.AsKeyValueStrings()) + }) + } +} diff --git a/pkg/util/testing/events.go b/pkg/util/testing/events.go deleted file mode 100644 index 20cfa76196..0000000000 --- a/pkg/util/testing/events.go +++ /dev/null @@ -1,38 +0,0 @@ -package testing - -import ( - "strings" - "testing" - - "github.com/stretchr/testify/assert" -) - -type Event struct { - EventType string - Reason string - Message string -} -type Events []Event - -// FakeEventRecorders(aka.: FakeRecorders) push the "sent" events into a string channel, using this format: "eventType eventReason eventMessage" -// So this function just parses this format and compares the produced fields with the fields of the provided Event structs IN ORDER. -// In case of the Event.Message field it only checks if it "CONTAINED" in sent event. -func AssertEvents(t *testing.T, eventsCh chan string, expectedEvents Events) { - assert.Equal(t, len(eventsCh), len(expectedEvents)) - - for _, event := range expectedEvents { - eventString := <-eventsCh - assert.NotNil(t, eventString) - tmp := strings.Split(eventString, " ") - eventType := tmp[0] - reason := tmp[1] - message := strings.Join(tmp[2:], " ") - - assert.Equal(t, eventType, event.EventType) - assert.Equal(t, reason, event.Reason) - - if len(event.Message) > 0 { - assert.Contains(t, message, event.Message) - } - } -} diff --git a/pkg/util/testing/mocks/sigs.k8s.io/controller-runtime/pkg/client/reader.go b/pkg/util/testing/mocks/sigs.k8s.io/controller-runtime/pkg/client/reader.go deleted file mode 100644 index 8391aad654..0000000000 --- a/pkg/util/testing/mocks/sigs.k8s.io/controller-runtime/pkg/client/reader.go +++ /dev/null @@ -1,75 +0,0 @@ -// Code generated by mockery v2.15.0. DO NOT EDIT. - -package mocks - -import ( - context "context" - - mock "github.com/stretchr/testify/mock" - types "k8s.io/apimachinery/pkg/types" - client "sigs.k8s.io/controller-runtime/pkg/client" -) - -// Reader is an autogenerated mock type for the Reader type -type Reader struct { - mock.Mock -} - -// Get provides a mock function with given fields: ctx, key, obj, opts -func (_m *Reader) Get(ctx context.Context, key types.NamespacedName, obj client.Object, opts ...client.GetOption) error { - _va := make([]interface{}, len(opts)) - for _i := range opts { - _va[_i] = opts[_i] - } - - var _ca []interface{} - _ca = append(_ca, ctx, key, obj) - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, types.NamespacedName, client.Object, ...client.GetOption) error); ok { - r0 = rf(ctx, key, obj, opts...) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// List provides a mock function with given fields: ctx, list, opts -func (_m *Reader) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { - _va := make([]interface{}, len(opts)) - for _i := range opts { - _va[_i] = opts[_i] - } - - var _ca []interface{} - _ca = append(_ca, ctx, list) - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, client.ObjectList, ...client.ListOption) error); ok { - r0 = rf(ctx, list, opts...) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -type mockConstructorTestingTNewReader interface { - mock.TestingT - Cleanup(func()) -} - -// NewReader creates a new instance of Reader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewReader(t mockConstructorTestingTNewReader) *Reader { - mock := &Reader{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/version/semantic.go b/pkg/version/semantic.go index f669e429f3..f4b9401c7e 100644 --- a/pkg/version/semantic.go +++ b/pkg/version/semantic.go @@ -47,7 +47,7 @@ func ExtractSemanticVersion(versionString string) (SemanticVersion, error) { version := versionRegex.FindStringSubmatch(versionString) if len(version) < maxStringSubMatch { - return SemanticVersion{}, fmt.Errorf("version malformed: %s", versionString) + return SemanticVersion{}, errors.Errorf("version malformed: %s", versionString) } major, err := strconv.Atoi(version[1]) diff --git a/pkg/webhook/config.go b/pkg/webhook/config.go index f590a16610..30bb46785e 100644 --- a/pkg/webhook/config.go +++ b/pkg/webhook/config.go @@ -4,48 +4,21 @@ const ( // InjectionInstanceLabel can be set in a Namespace and indicates the corresponding DynaKube object assigned to it. InjectionInstanceLabel = "dynakube.internal.dynatrace.com/instance" + // AnnotationFailurePolicy can be set on a Pod to control what the init container does on failures. When set to + // "fail", the init container will exit with error code 1. Defaults to "silent". + AnnotationFailurePolicy = "oneagent.dynatrace.com/failure-policy" + // AnnotationDynatraceInjected is set to "true" by the webhook to Pods to indicate that it has been injected. AnnotationDynatraceInjected = "dynakube.dynatrace.com/injected" + // AnnotationDynatraceReason is add to provide extra info why an injection didn't happen. + AnnotationDynatraceReason = "dynakube.dynatrace.com/reason" + // AnnotationDynatraceInject is set to "false" on the Pod to indicate that does not want any injection. AnnotationDynatraceInject = "dynatrace.com/inject" - OneAgentPrefix = "oneagent" - // AnnotationOneAgentInject can be set at pod level to enable/disable OneAgent injection. - AnnotationOneAgentInject = OneAgentPrefix + ".dynatrace.com/inject" - AnnotationOneAgentInjected = OneAgentPrefix + ".dynatrace.com/injected" - AnnotationOneAgentReason = OneAgentPrefix + ".dynatrace.com/reason" - - MetadataEnrichmentPrefix = "metadata-enrichment" - // AnnotationMetadataEnrichmentInject can be set at pod level to enable/disable metadata-enrichment injection. - AnnotationMetadataEnrichmentInject = MetadataEnrichmentPrefix + ".dynatrace.com/inject" - AnnotationMetadataEnrichmentInjected = MetadataEnrichmentPrefix + ".dynatrace.com/injected" - - // AnnotationFlavor can be set on a Pod to configure which code modules flavor to download. It's set to "default" - // if not set. - AnnotationFlavor = "oneagent.dynatrace.com/flavor" - - // AnnotationTechnologies can be set on a Pod to configure which code module technologies to download. It's set to - // "all" if not set. - AnnotationTechnologies = "oneagent.dynatrace.com/technologies" - - // AnnotationInstallPath can be set on a Pod to configure on which directory the OneAgent will be available from, - // defaults to DefaultInstallPath if not set. - AnnotationInstallPath = "oneagent.dynatrace.com/install-path" - - // AnnotationInstallerUrl can be set on a Pod to configure the installer url for downloading the agent - // defaults to the PaaS installer download url of your tenant - AnnotationInstallerUrl = "oneagent.dynatrace.com/installer-url" - - // AnnotationFailurePolicy can be set on a Pod to control what the init container does on failures. When set to - // "fail", the init container will exit with error code 1. Defaults to "silent". - AnnotationFailurePolicy = "oneagent.dynatrace.com/failure-policy" - AnnotationContainerInjection = "container.inject.dynatrace.com" - // DefaultInstallPath is the default directory to install the app-only OneAgent package. - DefaultInstallPath = "/opt/dynatrace/oneagent-paas" - // SecretCertsName is the name of the secret where the webhook certificates are stored. SecretCertsName = "dynatrace-webhook-certs" @@ -56,9 +29,4 @@ const ( // InstallContainerName is the name used for the install container InstallContainerName = "dynatrace-operator" - - // AnnotationWorkloadKind is added to any injected pods when the metadata-enrichment feature is enabled - AnnotationWorkloadKind = "metadata.dynatrace.com/k8s.workload.kind" - // AnnotationWorkloadName is added to any injected pods when the metadata-enrichment feature is enabled - AnnotationWorkloadName = "metadata.dynatrace.com/k8s.workload.name" ) diff --git a/pkg/webhook/interface.go b/pkg/webhook/interface.go new file mode 100644 index 0000000000..afc3af81f7 --- /dev/null +++ b/pkg/webhook/interface.go @@ -0,0 +1,24 @@ +package webhook + +import "context" + +type PodInjector interface { + Handle(context.Context, *MutationRequest) error +} + +type PodMutator interface { + // Enabled returns true if the mutator needs to be executed for the given request. + // This is used to filter out mutators that are not needed for the given request. + Enabled(request *BaseRequest) bool + + // Injected returns true if the mutator has already injected into the pod of the given request. + // This is used during reinvocation to prevent multiple injections. + Injected(request *BaseRequest) bool + + // Mutate mutates the elements of the given MutationRequest, specifically the pod and installContainer. + Mutate(ctx context.Context, request *MutationRequest) error + + // Reinvocation mutates the pod of the given ReinvocationRequest. + // It only mutates the parts of the pod that haven't been mutated yet. (example: another webhook mutated the pod after our webhook was executed) + Reinvoke(request *ReinvocationRequest) bool +} diff --git a/pkg/webhook/mutation/namespace/webhook_test.go b/pkg/webhook/mutation/namespace/webhook_test.go index fb1b146823..7038919ef1 100644 --- a/pkg/webhook/mutation/namespace/webhook_test.go +++ b/pkg/webhook/mutation/namespace/webhook_test.go @@ -5,7 +5,8 @@ import ( "testing" "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" jsonpatch "github.com/evanphx/json-patch" "github.com/stretchr/testify/assert" @@ -22,9 +23,9 @@ func TestInjection(t *testing.T) { dk := &dynakube.DynaKube{ ObjectMeta: metav1.ObjectMeta{Name: "codeModules-1", Namespace: "dynatrace"}, Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - ApplicationMonitoring: &dynakube.ApplicationMonitoringSpec{ - AppInjectionSpec: dynakube.AppInjectionSpec{ + OneAgent: oneagent.Spec{ + ApplicationMonitoring: &oneagent.ApplicationMonitoringSpec{ + AppInjectionSpec: oneagent.AppInjectionSpec{ NamespaceSelector: metav1.LabelSelector{ MatchLabels: map[string]string{ "inject": "true", diff --git a/pkg/webhook/mutation/pod/events.go b/pkg/webhook/mutation/pod/common/events/events.go similarity index 56% rename from pkg/webhook/mutation/pod/events.go rename to pkg/webhook/mutation/pod/common/events/events.go index 72823d4b5f..f2d98e2c5d 100644 --- a/pkg/webhook/mutation/pod/events.go +++ b/pkg/webhook/mutation/pod/common/events/events.go @@ -1,37 +1,50 @@ -package pod +package events import ( - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/tools/record" ) -type eventRecorder struct { +const ( + injectEvent = "Inject" + updatePodEvent = "UpdatePod" + IncompatibleCRDEvent = "IncompatibleCRDPresent" + missingDynakubeEvent = "MissingDynakube" +) + +type EventRecorder struct { dk *dynakube.DynaKube pod *corev1.Pod recorder record.EventRecorder } -func newPodMutatorEventRecorder(recorder record.EventRecorder) eventRecorder { - return eventRecorder{recorder: recorder} +func NewRecorder(recorder record.EventRecorder) EventRecorder { + return EventRecorder{recorder: recorder} +} + +func (er *EventRecorder) Setup(mutationRequest *dtwebhook.MutationRequest) { + er.dk = &mutationRequest.DynaKube + er.pod = mutationRequest.Pod } -func (er *eventRecorder) sendPodInjectEvent() { +func (er *EventRecorder) SendPodInjectEvent() { er.recorder.Eventf(er.dk, corev1.EventTypeNormal, injectEvent, "Injecting the necessary info into pod %s in namespace %s", er.pod.GenerateName, er.pod.Namespace) } -func (er *eventRecorder) sendPodUpdateEvent() { +func (er *EventRecorder) SendPodUpdateEvent() { er.recorder.Eventf(er.dk, corev1.EventTypeNormal, updatePodEvent, "Updating pod %s in namespace %s with missing containers", er.pod.GenerateName, er.pod.Namespace) } -func (er *eventRecorder) sendMissingDynaKubeEvent(namespaceName, dynakubeName string) { +func (er *EventRecorder) SendMissingDynaKubeEvent(namespaceName, dynakubeName string) { template := "Namespace '%s' is assigned to DynaKube instance '%s' but this instance doesn't exist" er.recorder.Eventf( &dynakube.DynaKube{ObjectMeta: metav1.ObjectMeta{Name: dynakubeName, Namespace: namespaceName}}, @@ -40,7 +53,7 @@ func (er *eventRecorder) sendMissingDynaKubeEvent(namespaceName, dynakubeName st template, namespaceName, dynakubeName) } -func (er *eventRecorder) sendOneAgentAPMWarningEvent(webhookPod *corev1.Pod) { +func (er *EventRecorder) SendOneAgentAPMWarningEvent(webhookPod *corev1.Pod) { er.recorder.Event(webhookPod, corev1.EventTypeWarning, IncompatibleCRDEvent, diff --git a/pkg/webhook/mutation/pod/common/metadata/annotations.go b/pkg/webhook/mutation/pod/common/metadata/annotations.go new file mode 100644 index 0000000000..54325113bf --- /dev/null +++ b/pkg/webhook/mutation/pod/common/metadata/annotations.go @@ -0,0 +1,56 @@ +package metadata + +import ( + "strings" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + corev1 "k8s.io/api/core/v1" +) + +func CopyMetadataFromNamespace(pod *corev1.Pod, namespace corev1.Namespace, dk dynakube.DynaKube) { + copyAccordingToCustomRules(pod, namespace, dk) + copyAccordingToPrefix(pod, namespace) +} + +func copyAccordingToPrefix(pod *corev1.Pod, namespace corev1.Namespace) { + for key, value := range namespace.Annotations { + if strings.HasPrefix(key, dynakube.MetadataPrefix) { + setPodAnnotationIfNotExists(pod, key, value) + } + } +} + +func copyAccordingToCustomRules(pod *corev1.Pod, namespace corev1.Namespace, dk dynakube.DynaKube) { + for _, rule := range dk.Status.MetadataEnrichment.Rules { + if rule.Target == "" { + log.Info("rule without target set found, ignoring", "source", rule.Source, "type", rule.Type) + + continue + } + + var valueFromNamespace string + + var exists bool + + switch rule.Type { + case dynakube.EnrichmentLabelRule: + valueFromNamespace, exists = namespace.Labels[rule.Source] + case dynakube.EnrichmentAnnotationRule: + valueFromNamespace, exists = namespace.Annotations[rule.Source] + } + + if exists { + setPodAnnotationIfNotExists(pod, rule.ToAnnotationKey(), valueFromNamespace) + } + } +} + +func setPodAnnotationIfNotExists(pod *corev1.Pod, key, value string) { + if pod.Annotations == nil { + pod.Annotations = make(map[string]string) + } + + if _, ok := pod.Annotations[key]; !ok { + pod.Annotations[key] = value + } +} diff --git a/pkg/webhook/mutation/pod/metadata/annotations_test.go b/pkg/webhook/mutation/pod/common/metadata/annotations_test.go similarity index 63% rename from pkg/webhook/mutation/pod/metadata/annotations_test.go rename to pkg/webhook/mutation/pod/common/metadata/annotations_test.go index 9ee377afc7..4b16a92555 100644 --- a/pkg/webhook/mutation/pod/metadata/annotations_test.go +++ b/pkg/webhook/mutation/pod/common/metadata/annotations_test.go @@ -1,20 +1,19 @@ package metadata import ( - "encoding/json" + "context" "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/consts" - "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/env" - "github.com/stretchr/testify/assert" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestCopyMetadataFromNamespace(t *testing.T) { t.Run("should copy annotations not labels with prefix from namespace to pod", func(t *testing.T) { - mutator := createTestPodMutator(nil) - request := createTestMutationRequest(nil, nil, false) + request := createTestMutationRequest(nil, nil) request.Namespace.Labels = map[string]string{ dynakube.MetadataPrefix + "nocopyoflabels": "nocopyoflabels", "test-label": "test-value", @@ -24,16 +23,14 @@ func TestCopyMetadataFromNamespace(t *testing.T) { "test-annotation": "test-value", } - require.False(t, mutator.Injected(request.BaseRequest)) - copyMetadataFromNamespace(request.Pod, request.Namespace, request.DynaKube) + CopyMetadataFromNamespace(request.Pod, request.Namespace, request.DynaKube) require.Len(t, request.Pod.Annotations, 1) require.Empty(t, request.Pod.Labels) require.Equal(t, "copyofannotations", request.Pod.Annotations[dynakube.MetadataPrefix+"copyofannotations"]) }) t.Run("should copy all labels and annotations defined without override", func(t *testing.T) { - mutator := createTestPodMutator(nil) - request := createTestMutationRequest(nil, nil, false) + request := createTestMutationRequest(nil, nil) request.Namespace.Labels = map[string]string{ dynakube.MetadataPrefix + "nocopyoflabels": "nocopyoflabels", dynakube.MetadataPrefix + "copyifruleexists": "copyifruleexists", @@ -70,8 +67,7 @@ func TestCopyMetadataFromNamespace(t *testing.T) { dynakube.MetadataPrefix + "copyofannotations": "do-not-overwrite", } - require.False(t, mutator.Injected(request.BaseRequest)) - copyMetadataFromNamespace(request.Pod, request.Namespace, request.DynaKube) + CopyMetadataFromNamespace(request.Pod, request.Namespace, request.DynaKube) require.Len(t, request.Pod.Annotations, 4) require.Empty(t, request.Pod.Labels) @@ -83,8 +79,7 @@ func TestCopyMetadataFromNamespace(t *testing.T) { }) t.Run("are custom rule types handled correctly", func(t *testing.T) { - mutator := createTestPodMutator(nil) - request := createTestMutationRequest(nil, nil, false) + request := createTestMutationRequest(nil, nil) request.Namespace.Labels = map[string]string{ "test": "test-label-value", "test2": "test-label-value2", @@ -114,8 +109,7 @@ func TestCopyMetadataFromNamespace(t *testing.T) { }, } - require.False(t, mutator.Injected(request.BaseRequest)) - copyMetadataFromNamespace(request.Pod, request.Namespace, request.DynaKube) + CopyMetadataFromNamespace(request.Pod, request.Namespace, request.DynaKube) require.Len(t, request.Pod.Annotations, 2) require.Empty(t, request.Pod.Labels) require.Equal(t, "test-label-value", request.Pod.Annotations[dynakube.MetadataPrefix+"dt.test-label"]) @@ -123,40 +117,77 @@ func TestCopyMetadataFromNamespace(t *testing.T) { }) } -func TestAddMetadataToInitEnv(t *testing.T) { - t.Run("should copy annotations not labels with prefix from pod to env", func(t *testing.T) { - expectedKeys := []string{ - "beep", - "boop", - "hello", - } - notExpectedKey := "no-prop" - request := createTestMutationRequest(nil, nil, false) - request.Pod.Labels = map[string]string{ - dynakube.MetadataPrefix + notExpectedKey: "beep-boop", - "test-label": "boom", - } - request.Pod.Annotations = map[string]string{ - "test-annotation": "boom", - } - - for _, key := range expectedKeys { - request.Pod.Annotations[dynakube.MetadataPrefix+key] = key + "-value" - } - - addMetadataToInitEnv(request.Pod, request.InstallContainer) - - annotationsEnv := env.FindEnvVar(request.InstallContainer.Env, consts.EnrichmentWorkloadAnnotationsEnv) - require.NotNil(t, annotationsEnv) +func createTestMutationRequest(dk *dynakube.DynaKube, annotations map[string]string) *dtwebhook.MutationRequest { + if dk == nil { + dk = &dynakube.DynaKube{} + } + + return dtwebhook.NewMutationRequest( + context.Background(), + *getTestNamespace(dk), + &corev1.Container{ + Name: dtwebhook.InstallContainerName, + }, + getTestPod(annotations), + *dk, + ) +} - propagatedAnnotations := map[string]string{} - err := json.Unmarshal([]byte(annotationsEnv.Value), &propagatedAnnotations) - require.NoError(t, err) +func getTestNamespace(dk *dynakube.DynaKube) *corev1.Namespace { + return &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ns", + Labels: map[string]string{ + dtwebhook.InjectionInstanceLabel: dk.Name, + }, + }, + } +} - for _, key := range expectedKeys { - require.Contains(t, propagatedAnnotations, key) - assert.Equal(t, key+"-value", propagatedAnnotations[key]) - assert.NotContains(t, propagatedAnnotations, notExpectedKey) - } - }) +func getTestPod(annotations map[string]string) *corev1.Pod { + return &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: "test-ns", + Annotations: annotations, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "container-1", + Image: "alpine-1", + VolumeMounts: []corev1.VolumeMount{ + { + Name: "volume", + MountPath: "/volume", + }, + }, + }, + { + Name: "container-2", + Image: "alpine-2", + VolumeMounts: []corev1.VolumeMount{ + { + Name: "volume", + MountPath: "/volume", + }, + }, + }, + }, + InitContainers: []corev1.Container{ + { + Name: "init-container", + Image: "alpine", + }, + }, + Volumes: []corev1.Volume{ + { + Name: "volume", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + }, + }, + } } diff --git a/pkg/webhook/mutation/pod/common/metadata/config.go b/pkg/webhook/mutation/pod/common/metadata/config.go new file mode 100644 index 0000000000..ebcdcb9cb0 --- /dev/null +++ b/pkg/webhook/mutation/pod/common/metadata/config.go @@ -0,0 +1,19 @@ +package metadata + +import "github.com/Dynatrace/dynatrace-operator/pkg/logd" + +const ( + AnnotationPrefix = "metadata-enrichment" + // AnnotationMetadataEnrichmentInject can be set at pod level to enable/disable metadata-enrichment injection. + AnnotationInject = AnnotationPrefix + ".dynatrace.com/inject" + AnnotationInjected = AnnotationPrefix + ".dynatrace.com/injected" + + // AnnotationWorkloadKind is added to any injected pods when the metadata-enrichment feature is enabled + AnnotationWorkloadKind = "metadata.dynatrace.com/k8s.workload.kind" + // AnnotationWorkloadName is added to any injected pods when the metadata-enrichment feature is enabled + AnnotationWorkloadName = "metadata.dynatrace.com/k8s.workload.name" +) + +var ( + log = logd.Get().WithName("metadata-enrichment-pod-common") +) diff --git a/pkg/webhook/mutation/pod/common/metadata/mutator.go b/pkg/webhook/mutation/pod/common/metadata/mutator.go new file mode 100644 index 0000000000..5574156757 --- /dev/null +++ b/pkg/webhook/mutation/pod/common/metadata/mutator.go @@ -0,0 +1,46 @@ +package metadata + +import ( + maputils "github.com/Dynatrace/dynatrace-operator/pkg/util/map" + dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" +) + +func IsEnabled(request *dtwebhook.BaseRequest) bool { + enabledOnPod := maputils.GetFieldBool(request.Pod.Annotations, AnnotationInject, + request.DynaKube.FeatureAutomaticInjection()) + enabledOnDynakube := request.DynaKube.MetadataEnrichmentEnabled() + + matchesNamespace := true // if no namespace selector is configured, we just pass set this to true + + if request.DynaKube.MetadataEnrichmentNamespaceSelector().Size() > 0 { + selector, _ := metav1.LabelSelectorAsSelector(request.DynaKube.MetadataEnrichmentNamespaceSelector()) + + matchesNamespace = selector.Matches(labels.Set(request.Namespace.Labels)) + } + + return matchesNamespace && enabledOnPod && enabledOnDynakube +} + +func IsInjected(request *dtwebhook.BaseRequest) bool { + return maputils.GetFieldBool(request.Pod.Annotations, AnnotationInjected, false) +} + +func SetInjectedAnnotation(pod *corev1.Pod) { + if pod.Annotations == nil { + pod.Annotations = make(map[string]string) + } + + pod.Annotations[AnnotationInjected] = "true" +} + +func SetWorkloadAnnotations(pod *corev1.Pod, workload *WorkloadInfo) { + if pod.Annotations == nil { + pod.Annotations = make(map[string]string) + } + + pod.Annotations[AnnotationWorkloadKind] = workload.Kind + pod.Annotations[AnnotationWorkloadName] = workload.Name +} diff --git a/pkg/webhook/mutation/pod/common/metadata/mutator_test.go b/pkg/webhook/mutation/pod/common/metadata/mutator_test.go new file mode 100644 index 0000000000..2a67281c5a --- /dev/null +++ b/pkg/webhook/mutation/pod/common/metadata/mutator_test.go @@ -0,0 +1,47 @@ +package metadata + +import ( + "testing" + + maputils "github.com/Dynatrace/dynatrace-operator/pkg/util/map" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestSetInjectedAnnotation(t *testing.T) { + t.Run("should add annotation to nil map", func(t *testing.T) { + request := createTestMutationRequest(nil, nil) + + require.False(t, IsInjected(request.BaseRequest)) + SetInjectedAnnotation(request.Pod) + require.Len(t, request.Pod.Annotations, 1) + require.True(t, IsInjected(request.BaseRequest)) + }) +} + +func TestWorkloadAnnotations(t *testing.T) { + workloadInfoName := "workload-name" + workloadInfoKind := "workload-kind" + + t.Run("should add annotation to nil map", func(t *testing.T) { + request := createTestMutationRequest(nil, nil) + + require.Equal(t, "not-found", maputils.GetField(request.Pod.Annotations, AnnotationWorkloadName, "not-found")) + SetWorkloadAnnotations(request.Pod, &WorkloadInfo{Name: workloadInfoName, Kind: workloadInfoKind}) + require.Len(t, request.Pod.Annotations, 2) + assert.Equal(t, workloadInfoName, maputils.GetField(request.Pod.Annotations, AnnotationWorkloadName, "not-found")) + assert.Equal(t, workloadInfoKind, maputils.GetField(request.Pod.Annotations, AnnotationWorkloadKind, "not-found")) + }) + t.Run("should lower case kind annotation", func(t *testing.T) { + request := createTestMutationRequest(nil, nil) + objectMeta := &metav1.PartialObjectMetadata{ + ObjectMeta: metav1.ObjectMeta{Name: workloadInfoName}, + TypeMeta: metav1.TypeMeta{Kind: "SuperWorkload"}, + } + + SetWorkloadAnnotations(request.Pod, newWorkloadInfo(objectMeta)) + assert.Contains(t, request.Pod.Annotations, AnnotationWorkloadKind) + assert.Equal(t, "superworkload", request.Pod.Annotations[AnnotationWorkloadKind]) + }) +} diff --git a/pkg/webhook/mutation/pod/metadata/workload.go b/pkg/webhook/mutation/pod/common/metadata/workload.go similarity index 81% rename from pkg/webhook/mutation/pod/metadata/workload.go rename to pkg/webhook/mutation/pod/common/metadata/workload.go index 7042bda7ac..4517d32255 100644 --- a/pkg/webhook/mutation/pod/metadata/workload.go +++ b/pkg/webhook/mutation/pod/common/metadata/workload.go @@ -2,6 +2,7 @@ package metadata import ( "context" + "strings" kubeobjects "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/pod" dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" @@ -11,20 +12,23 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) -type workloadInfo struct { - name string - kind string +type WorkloadInfo struct { + Name string + Kind string } -func newWorkloadInfo(partialObjectMetadata *metav1.PartialObjectMetadata) *workloadInfo { - return &workloadInfo{ - name: partialObjectMetadata.ObjectMeta.Name, - kind: partialObjectMetadata.Kind, +func newWorkloadInfo(partialObjectMetadata *metav1.PartialObjectMetadata) *WorkloadInfo { + return &WorkloadInfo{ + Name: partialObjectMetadata.ObjectMeta.Name, + + // workload kind in lower case according to dt semantic-dictionary + // https://docs.dynatrace.com/docs/discover-dynatrace/references/semantic-dictionary/fields#kubernetes + Kind: strings.ToLower(partialObjectMetadata.Kind), } } -func (mut *Mutator) retrieveWorkload(request *dtwebhook.MutationRequest) (*workloadInfo, error) { - workload, err := findRootOwnerOfPod(request.Context, mut.metaClient, request.Pod, request.Namespace.Name) +func RetrieveWorkload(metaClient client.Client, request *dtwebhook.MutationRequest) (*WorkloadInfo, error) { + workload, err := findRootOwnerOfPod(request.Context, metaClient, request.Pod, request.Namespace.Name) if err != nil { return nil, err } @@ -32,7 +36,7 @@ func (mut *Mutator) retrieveWorkload(request *dtwebhook.MutationRequest) (*workl return workload, nil } -func findRootOwnerOfPod(ctx context.Context, clt client.Client, pod *corev1.Pod, namespace string) (*workloadInfo, error) { +func findRootOwnerOfPod(ctx context.Context, clt client.Client, pod *corev1.Pod, namespace string) (*WorkloadInfo, error) { podPartialMetadata := &metav1.PartialObjectMetadata{ TypeMeta: metav1.TypeMeta{ APIVersion: pod.APIVersion, diff --git a/pkg/webhook/mutation/pod/metadata/workload_test.go b/pkg/webhook/mutation/pod/common/metadata/workload_test.go similarity index 83% rename from pkg/webhook/mutation/pod/metadata/workload_test.go rename to pkg/webhook/mutation/pod/common/metadata/workload_test.go index 2095a75362..420008dfc7 100644 --- a/pkg/webhook/mutation/pod/metadata/workload_test.go +++ b/pkg/webhook/mutation/pod/common/metadata/workload_test.go @@ -5,13 +5,13 @@ import ( "testing" "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" - "github.com/Dynatrace/dynatrace-operator/pkg/util/address" "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/interceptor" ) @@ -29,7 +29,7 @@ func TestFindRootOwnerOfPod(t *testing.T) { APIVersion: "apps/v1", Kind: "Deployment", Name: "test", - Controller: address.Of(true), + Controller: ptr.To(true), }, }, Name: resourceName, @@ -44,7 +44,7 @@ func TestFindRootOwnerOfPod(t *testing.T) { APIVersion: "apps/v1", Kind: "DaemonSet", Name: "test", - Controller: address.Of(true), + Controller: ptr.To(true), }, }, Name: resourceName, @@ -73,8 +73,8 @@ func TestFindRootOwnerOfPod(t *testing.T) { workloadInfo, err := findRootOwnerOfPod(ctx, client, &pod, namespaceName) require.NoError(t, err) - assert.Equal(t, resourceName, workloadInfo.name) - assert.Equal(t, "DaemonSet", workloadInfo.kind) + assert.Equal(t, resourceName, workloadInfo.Name) + assert.Equal(t, "daemonset", workloadInfo.Kind) }) t.Run("should return Pod if owner references are empty", func(t *testing.T) { @@ -90,8 +90,8 @@ func TestFindRootOwnerOfPod(t *testing.T) { client := fake.NewClient(&pod) workloadInfo, err := findRootOwnerOfPod(ctx, client, &pod, namespaceName) require.NoError(t, err) - assert.Equal(t, resourceName, workloadInfo.name) - assert.Equal(t, "Pod", workloadInfo.kind) + assert.Equal(t, resourceName, workloadInfo.Name) + assert.Equal(t, "pod", workloadInfo.Kind) }) t.Run("should be pod if owner is not well known", func(t *testing.T) { @@ -105,7 +105,7 @@ func TestFindRootOwnerOfPod(t *testing.T) { APIVersion: "v1", Kind: "Secret", Name: "test", - Controller: address.Of(true), + Controller: ptr.To(true), }, }, Name: resourceName, @@ -120,8 +120,8 @@ func TestFindRootOwnerOfPod(t *testing.T) { client := fake.NewClient(&pod, &secret) workloadInfo, err := findRootOwnerOfPod(ctx, client, &pod, namespaceName) require.NoError(t, err) - assert.Equal(t, resourceName, workloadInfo.name) - assert.Equal(t, "Pod", workloadInfo.kind) + assert.Equal(t, resourceName, workloadInfo.Name) + assert.Equal(t, "pod", workloadInfo.Kind) }) t.Run("should be pod if no controller is the owner", func(t *testing.T) { @@ -135,8 +135,8 @@ func TestFindRootOwnerOfPod(t *testing.T) { APIVersion: "some.unknown.kind.com/v1alpha1", Kind: "SomeUnknownKind", Name: "some-owner", - Controller: address.Of(false), - BlockOwnerDeletion: address.Of(false), + Controller: ptr.To(false), + BlockOwnerDeletion: ptr.To(false), }, }, Name: resourceName, @@ -145,8 +145,8 @@ func TestFindRootOwnerOfPod(t *testing.T) { client := fake.NewClient(&pod) workloadInfo, err := findRootOwnerOfPod(ctx, client, &pod, namespaceName) require.NoError(t, err) - assert.Equal(t, namespaceName, workloadInfo.name) - assert.Equal(t, "Pod", workloadInfo.kind) + assert.Equal(t, namespaceName, workloadInfo.Name) + assert.Equal(t, "pod", workloadInfo.Kind) }) t.Run("should find the root owner of the pod if the root owner is unknown", func(t *testing.T) { pod := corev1.Pod{ @@ -156,7 +156,7 @@ func TestFindRootOwnerOfPod(t *testing.T) { APIVersion: "apps/v1", Kind: "Deployment", Name: "test", - Controller: address.Of(true), + Controller: ptr.To(true), }, }, Name: resourceName, @@ -175,7 +175,7 @@ func TestFindRootOwnerOfPod(t *testing.T) { APIVersion: "v1", Kind: "Secret", Name: "test", - Controller: address.Of(true), + Controller: ptr.To(true), }, }, Name: resourceName, @@ -203,8 +203,8 @@ func TestFindRootOwnerOfPod(t *testing.T) { workloadInfo, err := findRootOwnerOfPod(ctx, client, &pod, namespaceName) require.NoError(t, err) - assert.Equal(t, resourceName, workloadInfo.name) - assert.Equal(t, "Deployment", workloadInfo.kind) + assert.Equal(t, resourceName, workloadInfo.Name) + assert.Equal(t, "deployment", workloadInfo.Kind) }) t.Run("should not make an api-call if workload is not well known", func(t *testing.T) { pod := corev1.Pod{ @@ -214,7 +214,7 @@ func TestFindRootOwnerOfPod(t *testing.T) { APIVersion: "some.unknown.kind.com/v1alpha1", Kind: "SomeUnknownKind", Name: "some-owner", - Controller: address.Of(true), + Controller: ptr.To(true), }, }, Name: resourceName, @@ -226,19 +226,10 @@ func TestFindRootOwnerOfPod(t *testing.T) { workloadInfo, err := findRootOwnerOfPod(ctx, client, &pod, namespaceName) require.NoError(t, err) - assert.Equal(t, resourceName, workloadInfo.name) + assert.Equal(t, resourceName, workloadInfo.Name) }) } -func createTestWorkloadInfo(t *testing.T) *workloadInfo { - t.Helper() - - return &workloadInfo{ - kind: "test", - name: "test", - } -} - func createFailK8sClient(t *testing.T) client.Client { t.Helper() diff --git a/pkg/webhook/mutation/pod/common/oneagent/config.go b/pkg/webhook/mutation/pod/common/oneagent/config.go new file mode 100644 index 0000000000..037715fd69 --- /dev/null +++ b/pkg/webhook/mutation/pod/common/oneagent/config.go @@ -0,0 +1,41 @@ +package oneagent + +import "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + +const ( + AnnotationPrefix = "oneagent" + // AnnotationOneAgentInject can be set at pod level to enable/disable OneAgent injection. + AnnotationInject = AnnotationPrefix + ".dynatrace.com/inject" + AnnotationInjected = AnnotationPrefix + ".dynatrace.com/injected" + AnnotationReason = AnnotationPrefix + ".dynatrace.com/reason" + + // AnnotationTechnologies can be set on a Pod to configure which code module technologies to download. It's set to + // "all" if not set. + AnnotationTechnologies = dynakube.AnnotationTechnologies + + // AnnotationInstallPath can be set on a Pod to configure on which directory the OneAgent will be available from, + // defaults to DefaultInstallPath if not set. + AnnotationInstallPath = AnnotationPrefix + ".dynatrace.com/install-path" + + AnnotationVolumeType = AnnotationPrefix + ".dynatrace.com/volume-type" + + // DefaultInstallPath is the default directory to install the app-only OneAgent package. + DefaultInstallPath = "/opt/dynatrace/oneagent-paas" + + PreloadEnv = "LD_PRELOAD" + NetworkZoneEnv = "DT_NETWORK_ZONE" + DynatraceMetadataEnv = "DT_DEPLOYMENT_METADATA" + + ReleaseVersionEnv = "DT_RELEASE_VERSION" + ReleaseProductEnv = "DT_RELEASE_PRODUCT" + ReleaseStageEnv = "DT_RELEASE_STAGE" + ReleaseBuildVersionEnv = "DT_RELEASE_BUILD_VERSION" + + EmptyConnectionInfoReason = "EmptyConnectionInfo" + UnknownCodeModuleReason = "UnknownCodeModule" + EmptyTenantUUIDReason = "EmptyTenantUUID" + + DefaultUser int64 = 1001 + DefaultGroup int64 = 1001 + RootUserGroup int64 = 0 +) diff --git a/pkg/webhook/mutation/pod/oneagent/env.go b/pkg/webhook/mutation/pod/common/oneagent/env.go similarity index 54% rename from pkg/webhook/mutation/pod/oneagent/env.go rename to pkg/webhook/mutation/pod/common/oneagent/env.go index bfc55f6c81..9a98d7184d 100644 --- a/pkg/webhook/mutation/pod/oneagent/env.go +++ b/pkg/webhook/mutation/pod/common/oneagent/env.go @@ -4,50 +4,37 @@ import ( "path/filepath" "strings" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/consts" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/deploymentmetadata" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/env" corev1 "k8s.io/api/core/v1" ) -func addPreloadEnv(container *corev1.Container, installPath string) { - preloadPath := filepath.Join(installPath, consts.LibAgentProcPath) - - ldPreloadEnv := env.FindEnvVar(container.Env, preloadEnv) - if ldPreloadEnv != nil { - if strings.Contains(ldPreloadEnv.Value, installPath) { - return - } - - ldPreloadEnv.Value = concatPreloadPaths(ldPreloadEnv.Value, preloadPath) - } else { - container.Env = append(container.Env, - corev1.EnvVar{ - Name: preloadEnv, - Value: preloadPath, - }) +func AddDeploymentMetadataEnv(container *corev1.Container, dk dynakube.DynaKube) { + if env.IsIn(container.Env, DynatraceMetadataEnv) { + return } -} -func concatPreloadPaths(originalPaths, additionalPath string) string { - if strings.Contains(originalPaths, " ") { - return originalPaths + " " + additionalPath - } else { - return originalPaths + ":" + additionalPath - } + deploymentMetadata := deploymentmetadata.NewDeploymentMetadata(dk.Status.KubeSystemUUID, deploymentmetadata.GetOneAgentDeploymentType(dk)) + container.Env = append(container.Env, + corev1.EnvVar{ + Name: DynatraceMetadataEnv, + Value: deploymentMetadata.AsString(), + }) } -func addNetworkZoneEnv(container *corev1.Container, networkZone string) { +func AddNetworkZoneEnv(container *corev1.Container, networkZone string) { container.Env = append(container.Env, corev1.EnvVar{ - Name: networkZoneEnv, + Name: NetworkZoneEnv, Value: networkZone, }, ) } -func addVersionDetectionEnvs(container *corev1.Container, labelMapping VersionLabelMapping) { +func AddVersionDetectionEnvs(container *corev1.Container, namespace corev1.Namespace) { + labelMapping := NewVersionLabelMapping(namespace) for envName, fieldPath := range labelMapping { if env.IsIn(container.Env, envName) { continue @@ -66,26 +53,29 @@ func addVersionDetectionEnvs(container *corev1.Container, labelMapping VersionLa } } -func addInstallerInitEnvs(initContainer *corev1.Container, installer installerInfo) { - initContainer.Env = append(initContainer.Env, - corev1.EnvVar{Name: consts.AgentInstallerFlavorEnv, Value: installer.flavor}, - corev1.EnvVar{Name: consts.AgentInstallerTechEnv, Value: installer.technologies}, - corev1.EnvVar{Name: consts.AgentInstallPathEnv, Value: installer.installPath}, - corev1.EnvVar{Name: consts.AgentInstallerUrlEnv, Value: installer.installerURL}, - corev1.EnvVar{Name: consts.AgentInstallerVersionEnv, Value: installer.version}, - corev1.EnvVar{Name: consts.AgentInjectedEnv, Value: "true"}, - ) -} +func AddPreloadEnv(container *corev1.Container, installPath string) { + preloadPath := filepath.Join(installPath, consts.LibAgentProcPath) -func addDeploymentMetadataEnv(container *corev1.Container, dk dynakube.DynaKube, clusterID string) { - if env.IsIn(container.Env, dynatraceMetadataEnv) { - return + ldPreloadEnv := env.FindEnvVar(container.Env, PreloadEnv) + if ldPreloadEnv != nil { + if strings.Contains(ldPreloadEnv.Value, installPath) { + return + } + + ldPreloadEnv.Value = concatPreloadPaths(ldPreloadEnv.Value, preloadPath) + } else { + container.Env = append(container.Env, + corev1.EnvVar{ + Name: PreloadEnv, + Value: preloadPath, + }) } +} - deploymentMetadata := deploymentmetadata.NewDeploymentMetadata(clusterID, deploymentmetadata.GetOneAgentDeploymentType(dk)) - container.Env = append(container.Env, - corev1.EnvVar{ - Name: dynatraceMetadataEnv, - Value: deploymentMetadata.AsString(), - }) +func concatPreloadPaths(originalPaths, additionalPath string) string { + if strings.Contains(originalPaths, " ") { + return originalPaths + " " + additionalPath + } else { + return originalPaths + ":" + additionalPath + } } diff --git a/pkg/webhook/mutation/pod/oneagent/env_test.go b/pkg/webhook/mutation/pod/common/oneagent/env_test.go similarity index 65% rename from pkg/webhook/mutation/pod/oneagent/env_test.go rename to pkg/webhook/mutation/pod/common/oneagent/env_test.go index 9afe5edf8d..8fd5cb1808 100644 --- a/pkg/webhook/mutation/pod/oneagent/env_test.go +++ b/pkg/webhook/mutation/pod/common/oneagent/env_test.go @@ -3,7 +3,8 @@ package oneagent import ( "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/deploymentmetadata" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/env" "github.com/stretchr/testify/assert" @@ -17,7 +18,7 @@ func TestAddPreloadEnv(t *testing.T) { t.Run("Add preload env", func(t *testing.T) { container := createContainerWithPreloadEnv("") - addPreloadEnv(container, installPath) + AddPreloadEnv(container, installPath) verifyContainerWithPreloadEnv(t, container, installPath) }) @@ -25,7 +26,7 @@ func TestAddPreloadEnv(t *testing.T) { existingPath := "path/user" container := createContainerWithPreloadEnv(existingPath) - addPreloadEnv(container, installPath) + AddPreloadEnv(container, installPath) verifyContainerWithPreloadEnv(t, container, existingPath+":"+installPath) }) @@ -33,7 +34,7 @@ func TestAddPreloadEnv(t *testing.T) { existingPath := "path1/user path2/user" container := createContainerWithPreloadEnv(existingPath) - addPreloadEnv(container, installPath) + AddPreloadEnv(container, installPath) verifyContainerWithPreloadEnv(t, container, existingPath+" "+installPath) }) @@ -42,7 +43,7 @@ func TestAddPreloadEnv(t *testing.T) { existingPath += " " + installPath container := createContainerWithPreloadEnv(existingPath) - addPreloadEnv(container, installPath) + AddPreloadEnv(container, installPath) verifyContainerWithPreloadEnv(t, container, existingPath) }) @@ -59,7 +60,7 @@ func createContainerWithPreloadEnv(existingPath string) *corev1.Container { } if existingPath != "" { container.Env = append(container.Env, corev1.EnvVar{ - Name: preloadEnv, + Name: PreloadEnv, Value: existingPath, }) } @@ -69,7 +70,7 @@ func createContainerWithPreloadEnv(existingPath string) *corev1.Container { func verifyContainerWithPreloadEnv(t *testing.T, container *corev1.Container, expectedPath string) { require.NotEmpty(t, container.Env) - containerEnv := env.FindEnvVar(container.Env, preloadEnv) + containerEnv := env.FindEnvVar(container.Env, PreloadEnv) require.NotNil(t, containerEnv) assert.Contains(t, containerEnv.Value, expectedPath) } @@ -79,42 +80,32 @@ func TestAddNetworkZoneEnv(t *testing.T) { container := &corev1.Container{} networkZone := "testZone" - addNetworkZoneEnv(container, networkZone) + AddNetworkZoneEnv(container, networkZone) require.Len(t, container.Env, 1) - assert.Equal(t, networkZoneEnv, container.Env[0].Name) + assert.Equal(t, NetworkZoneEnv, container.Env[0].Name) assert.Equal(t, networkZone, container.Env[0].Value) }) } -func TestAddInstallerInitEnvs(t *testing.T) { - t.Run("Add installer init env", func(t *testing.T) { - container := &corev1.Container{} - installerInfo := getTestInstallerInfo() - addInstallerInitEnvs(container, installerInfo) - require.Len(t, container.Env, expectedBaseInitContainerEnvCount) - assert.Equal(t, installerInfo.flavor, container.Env[0].Value) - assert.Equal(t, installerInfo.technologies, container.Env[1].Value) - assert.Equal(t, installerInfo.installPath, container.Env[2].Value) - assert.Equal(t, installerInfo.installerURL, container.Env[3].Value) - assert.Equal(t, installerInfo.version, container.Env[4].Value) - assert.Equal(t, "true", container.Env[5].Value) - }) -} - func TestAddDeploymentMetadataEnv(t *testing.T) { + clusterID := "cluster-id" + t.Run("Add cloudNative deployment metadata env", func(t *testing.T) { container := &corev1.Container{} dk := dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{}, + OneAgent: oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{}, }, }, + Status: dynakube.DynaKubeStatus{ + KubeSystemUUID: clusterID, + }, } - addDeploymentMetadataEnv(container, dk, testClusterID) + AddDeploymentMetadataEnv(container, dk) require.Len(t, container.Env, 1) - assert.Contains(t, container.Env[0].Value, testClusterID) + assert.Contains(t, container.Env[0].Value, clusterID) assert.Contains(t, container.Env[0].Value, deploymentmetadata.CloudNativeDeploymentType) }) @@ -122,23 +113,26 @@ func TestAddDeploymentMetadataEnv(t *testing.T) { container := &corev1.Container{} dk := dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - ApplicationMonitoring: &dynakube.ApplicationMonitoringSpec{}, + OneAgent: oneagent.Spec{ + ApplicationMonitoring: &oneagent.ApplicationMonitoringSpec{}, }, }, + Status: dynakube.DynaKubeStatus{ + KubeSystemUUID: clusterID, + }, } - addDeploymentMetadataEnv(container, dk, testClusterID) + AddDeploymentMetadataEnv(container, dk) require.Len(t, container.Env, 1) - assert.Contains(t, container.Env[0].Value, testClusterID) + assert.Contains(t, container.Env[0].Value, clusterID) assert.Contains(t, container.Env[0].Value, deploymentmetadata.ApplicationMonitoringDeploymentType) }) } -func TestAddVersionDetectionEnvs(t *testing.T) { +func TestAddVersionDetection(t *testing.T) { t.Run("adds defaults", func(t *testing.T) { container := &corev1.Container{} - addVersionDetectionEnvs(container, defaultVersionLabelMapping) + AddVersionDetectionEnvs(container, getTestNamespace(defaultVersionLabelMapping)) require.Len(t, container.Env, len(defaultVersionLabelMapping)) @@ -152,12 +146,12 @@ func TestAddVersionDetectionEnvs(t *testing.T) { testProduct := "testy" container := &corev1.Container{ Env: []corev1.EnvVar{ - {Name: releaseVersionEnv, Value: testVersion}, - {Name: releaseProductEnv, Value: testProduct}, + {Name: ReleaseVersionEnv, Value: testVersion}, + {Name: ReleaseProductEnv, Value: testProduct}, }, } - addVersionDetectionEnvs(container, defaultVersionLabelMapping) + AddVersionDetectionEnvs(container, getTestNamespace(defaultVersionLabelMapping)) require.Len(t, container.Env, 2) assert.Equal(t, testVersion, container.Env[0].Value) @@ -168,14 +162,14 @@ func TestAddVersionDetectionEnvs(t *testing.T) { testVersion := "1.2.3" container := &corev1.Container{ Env: []corev1.EnvVar{ - {Name: releaseVersionEnv, Value: testVersion}, + {Name: ReleaseVersionEnv, Value: testVersion}, }, } - addVersionDetectionEnvs(container, defaultVersionLabelMapping) + AddVersionDetectionEnvs(container, getTestNamespace(defaultVersionLabelMapping)) require.Len(t, container.Env, 2) assert.Equal(t, testVersion, container.Env[0].Value) - assert.Equal(t, defaultVersionLabelMapping[releaseProductEnv], container.Env[1].ValueFrom.FieldRef.FieldPath) + assert.Equal(t, defaultVersionLabelMapping[ReleaseProductEnv], container.Env[1].ValueFrom.FieldRef.FieldPath) }) } diff --git a/pkg/webhook/mutation/pod/common/oneagent/init.go b/pkg/webhook/mutation/pod/common/oneagent/init.go new file mode 100644 index 0000000000..b4d18024b1 --- /dev/null +++ b/pkg/webhook/mutation/pod/common/oneagent/init.go @@ -0,0 +1,17 @@ +package oneagent + +import corev1 "k8s.io/api/core/v1" + +func HasPodUserSet(ctx *corev1.PodSecurityContext) bool { + return ctx != nil && ctx.RunAsUser != nil +} + +func HasPodGroupSet(ctx *corev1.PodSecurityContext) bool { + return ctx != nil && ctx.RunAsGroup != nil +} + +func IsNonRoot(ctx *corev1.SecurityContext) bool { + return ctx != nil && + (ctx.RunAsUser != nil && *ctx.RunAsUser != RootUserGroup) && + (ctx.RunAsGroup != nil && *ctx.RunAsGroup != RootUserGroup) +} diff --git a/pkg/webhook/mutation/pod/common/oneagent/mutator.go b/pkg/webhook/mutation/pod/common/oneagent/mutator.go new file mode 100644 index 0000000000..0665cbdd17 --- /dev/null +++ b/pkg/webhook/mutation/pod/common/oneagent/mutator.go @@ -0,0 +1,50 @@ +package oneagent + +import ( + maputils "github.com/Dynatrace/dynatrace-operator/pkg/util/map" + dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" +) + +const ( + CSIVolumeType = "csi" + EphemeralVolumeType = "ephemeral" +) + +func IsEnabled(request *dtwebhook.BaseRequest) bool { + enabledOnPod := maputils.GetFieldBool(request.Pod.Annotations, AnnotationInject, request.DynaKube.FeatureAutomaticInjection()) + enabledOnDynakube := request.DynaKube.OneAgent().GetNamespaceSelector() != nil + + matchesNamespaceSelector := true // if no namespace selector is configured, we just pass set this to true + + if request.DynaKube.OneAgent().GetNamespaceSelector().Size() > 0 { + selector, _ := metav1.LabelSelectorAsSelector(request.DynaKube.OneAgent().GetNamespaceSelector()) + + matchesNamespaceSelector = selector.Matches(labels.Set(request.Namespace.Labels)) + } + + return matchesNamespaceSelector && enabledOnPod && enabledOnDynakube +} + +func IsInjected(request *dtwebhook.BaseRequest) bool { + return maputils.GetFieldBool(request.Pod.Annotations, AnnotationInjected, false) +} + +func SetInjectedAnnotation(pod *corev1.Pod) { + if pod.Annotations == nil { + pod.Annotations = make(map[string]string) + } + + pod.Annotations[AnnotationInjected] = "true" +} + +func SetNotInjectedAnnotations(pod *corev1.Pod, reason string) { + if pod.Annotations == nil { + pod.Annotations = make(map[string]string) + } + + pod.Annotations[AnnotationInjected] = "false" + pod.Annotations[AnnotationReason] = reason +} diff --git a/pkg/webhook/mutation/pod/oneagent/versionlabel.go b/pkg/webhook/mutation/pod/common/oneagent/versionlabel.go similarity index 74% rename from pkg/webhook/mutation/pod/oneagent/versionlabel.go rename to pkg/webhook/mutation/pod/common/oneagent/versionlabel.go index 094f31fb32..01ce69e5a8 100644 --- a/pkg/webhook/mutation/pod/oneagent/versionlabel.go +++ b/pkg/webhook/mutation/pod/common/oneagent/versionlabel.go @@ -14,23 +14,23 @@ const ( var ( defaultVersionLabelMapping = VersionLabelMapping{ - releaseVersionEnv: "metadata.labels['app.kubernetes.io/version']", - releaseProductEnv: "metadata.labels['app.kubernetes.io/part-of']", + ReleaseVersionEnv: "metadata.labels['app.kubernetes.io/version']", + ReleaseProductEnv: "metadata.labels['app.kubernetes.io/part-of']", } ) type VersionLabelMapping map[string]string -func newVersionLabelMapping(namespace corev1.Namespace) VersionLabelMapping { +func NewVersionLabelMapping(namespace corev1.Namespace) VersionLabelMapping { return mergeMappingWithDefault(getMappingFromNamespace(namespace)) } func getMappingFromNamespace(namespace corev1.Namespace) VersionLabelMapping { annotationLabelMap := map[string]string{ - versionMappingAnnotationName: releaseVersionEnv, - productMappingAnnotationName: releaseProductEnv, - stageMappingAnnotationName: releaseStageEnv, - buildVersionAnnotationName: releaseBuildVersionEnv, + versionMappingAnnotationName: ReleaseVersionEnv, + productMappingAnnotationName: ReleaseProductEnv, + stageMappingAnnotationName: ReleaseStageEnv, + buildVersionAnnotationName: ReleaseBuildVersionEnv, } versionLabelMapping := VersionLabelMapping{} diff --git a/pkg/webhook/mutation/pod/common/oneagent/versionlabel_test.go b/pkg/webhook/mutation/pod/common/oneagent/versionlabel_test.go new file mode 100644 index 0000000000..6d01594f3f --- /dev/null +++ b/pkg/webhook/mutation/pod/common/oneagent/versionlabel_test.go @@ -0,0 +1,121 @@ +package oneagent + +import ( + "testing" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestAddVersionDetectionEnvs(t *testing.T) { + const ( + customVersionValue = "my awesome custom version" + customProductValue = "my awesome custom product" + customReleaseStageValue = "my awesome custom stage" + customBuildVersionValue = "my awesome custom build version" + customVersionAnnotationName = "custom-version" + customProductAnnotationName = "custom-product" + customStageAnnotationName = "custom-stage" + customBuildVersionAnnotationName = "custom-build-version" + customVersionFieldPath = "metadata.podAnnotations['" + customVersionAnnotationName + "']" + customProductFieldPath = "metadata.podAnnotations['" + customProductAnnotationName + "']" + customStageFieldPath = "metadata.podAnnotations['" + customStageAnnotationName + "']" + customBuildVersionFieldPath = "metadata.podAnnotations['" + customBuildVersionAnnotationName + "']" + ) + + t.Run("version and product env vars are set using values referenced in namespace podAnnotations", func(t *testing.T) { + namespaceAnnotations := map[string]string{ + versionMappingAnnotationName: customVersionFieldPath, + productMappingAnnotationName: customProductFieldPath, + } + expectedMappings := map[string]string{ + ReleaseVersionEnv: customVersionFieldPath, + ReleaseProductEnv: customProductFieldPath, + } + unexpectedMappingsKeys := []string{ReleaseStageEnv, ReleaseBuildVersionEnv} + + doTestMappings(t, namespaceAnnotations, expectedMappings, unexpectedMappingsKeys) + }) + t.Run("only version env vars is set using value referenced in namespace podAnnotations, product is default", func(t *testing.T) { + namespaceAnnotations := map[string]string{ + versionMappingAnnotationName: customVersionFieldPath, + } + expectedMappings := map[string]string{ + ReleaseVersionEnv: customVersionFieldPath, + ReleaseProductEnv: defaultVersionLabelMapping[ReleaseProductEnv], + } + unexpectedMappingsKeys := []string{ReleaseStageEnv, ReleaseBuildVersionEnv} + + doTestMappings(t, namespaceAnnotations, expectedMappings, unexpectedMappingsKeys) + }) + t.Run("optional env vars (stage, build-version) are set using values referenced in namespace podAnnotations, default ones remain default", func(t *testing.T) { + namespaceAnnotations := map[string]string{ + stageMappingAnnotationName: customStageFieldPath, + buildVersionAnnotationName: customBuildVersionFieldPath, + } + expectedMappings := map[string]string{ + ReleaseVersionEnv: defaultVersionLabelMapping[ReleaseVersionEnv], + ReleaseProductEnv: defaultVersionLabelMapping[ReleaseProductEnv], + ReleaseStageEnv: customStageFieldPath, + ReleaseBuildVersionEnv: customBuildVersionFieldPath, + } + + doTestMappings(t, namespaceAnnotations, expectedMappings, nil) + }) + t.Run("all env vars are namespace-podAnnotations driven", func(t *testing.T) { + namespaceAnnotations := map[string]string{ + versionMappingAnnotationName: customVersionFieldPath, + productMappingAnnotationName: customProductFieldPath, + stageMappingAnnotationName: customStageFieldPath, + buildVersionAnnotationName: customBuildVersionFieldPath, + } + expectedMappings := map[string]string{ + ReleaseVersionEnv: customVersionFieldPath, + ReleaseProductEnv: customProductFieldPath, + ReleaseStageEnv: customStageFieldPath, + ReleaseBuildVersionEnv: customBuildVersionFieldPath, + } + + doTestMappings(t, namespaceAnnotations, expectedMappings, nil) + }) +} + +func doTestMappings(t *testing.T, namespaceAnnotations map[string]string, expectedMappings map[string]string, unexpectedMappingsKeys []string) { + container := corev1.Container{} + + AddVersionDetectionEnvs(&container, getTestNamespace(namespaceAnnotations)) + + assertContainsMappings(t, expectedMappings, container) + assertNotContainsMappings(t, unexpectedMappingsKeys, container) +} + +func assertContainsMappings(t *testing.T, expectedMappings map[string]string, container corev1.Container) { + for envName, fieldPath := range expectedMappings { + assert.Contains(t, container.Env, corev1.EnvVar{ + Name: envName, + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + APIVersion: "", + FieldPath: fieldPath, + }, + }, + }) + } +} + +func assertNotContainsMappings(t *testing.T, unexpectedMappingKeys []string, container corev1.Container) { + for _, env := range container.Env { + assert.NotContains(t, unexpectedMappingKeys, env.Name) + } +} + +func getTestNamespace(annotations map[string]string) corev1.Namespace { + return corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ns", + Labels: map[string]string{}, + Annotations: annotations, + }, + } +} diff --git a/pkg/webhook/mutation/pod/config.go b/pkg/webhook/mutation/pod/config.go index 5559b1a80c..03bb685337 100644 --- a/pkg/webhook/mutation/pod/config.go +++ b/pkg/webhook/mutation/pod/config.go @@ -4,17 +4,6 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/logd" ) -const ( - injectEvent = "Inject" - updatePodEvent = "UpdatePod" - IncompatibleCRDEvent = "IncompatibleCRDPresent" - missingDynakubeEvent = "MissingDynakube" - - defaultUser int64 = 1001 - defaultGroup int64 = 1001 - rootUserGroup int64 = 0 -) - var ( log = logd.Get().WithName("pod-mutation") ) diff --git a/pkg/webhook/mutation/pod/metadata/annotations.go b/pkg/webhook/mutation/pod/metadata/annotations.go deleted file mode 100644 index 6b34b4403a..0000000000 --- a/pkg/webhook/mutation/pod/metadata/annotations.go +++ /dev/null @@ -1,83 +0,0 @@ -package metadata - -import ( - "encoding/json" - "strings" - - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/consts" - dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" - corev1 "k8s.io/api/core/v1" -) - -func propagateMetadataAnnotations(request *dtwebhook.MutationRequest) { - copyMetadataFromNamespace(request.Pod, request.Namespace, request.DynaKube) - addMetadataToInitEnv(request.Pod, request.InstallContainer) -} - -func copyMetadataFromNamespace(pod *corev1.Pod, namespace corev1.Namespace, dk dynakube.DynaKube) { - copyMetadataAccordingToCustomRules(pod, namespace, dk) - copyMetadataAccordingToPrefix(pod, namespace) -} - -func copyMetadataAccordingToPrefix(pod *corev1.Pod, namespace corev1.Namespace) { - for key, value := range namespace.Annotations { - if strings.HasPrefix(key, dynakube.MetadataPrefix) { - setPodAnnotationIfNotExists(pod, key, value) - } - } -} - -func copyMetadataAccordingToCustomRules(pod *corev1.Pod, namespace corev1.Namespace, dk dynakube.DynaKube) { - for _, rule := range dk.Status.MetadataEnrichment.Rules { - if rule.Target == "" { - log.Info("rule without target set found, ignoring", "source", rule.Source, "type", rule.Type) - - continue - } - - var valueFromNamespace string - - var exists bool - - switch rule.Type { - case dynakube.EnrichmentLabelRule: - valueFromNamespace, exists = namespace.Labels[rule.Source] - case dynakube.EnrichmentAnnotationRule: - valueFromNamespace, exists = namespace.Annotations[rule.Source] - } - - if exists { - setPodAnnotationIfNotExists(pod, rule.ToAnnotationKey(), valueFromNamespace) - } - } -} - -func setPodAnnotationIfNotExists(pod *corev1.Pod, key, value string) { - if pod.Annotations == nil { - pod.Annotations = make(map[string]string) - } - - if _, ok := pod.Annotations[key]; !ok { - pod.Annotations[key] = value - } -} - -func addMetadataToInitEnv(pod *corev1.Pod, installContainer *corev1.Container) { - metadataAnnotations := map[string]string{} - - for key, value := range pod.Annotations { - if !strings.HasPrefix(key, dynakube.MetadataPrefix) { - continue - } - - split := strings.Split(key, dynakube.MetadataPrefix) - metadataAnnotations[split[1]] = value - } - - workloadAnnotationsJson, _ := json.Marshal(metadataAnnotations) - installContainer.Env = append(installContainer.Env, - corev1.EnvVar{ - Name: consts.EnrichmentWorkloadAnnotationsEnv, Value: string(workloadAnnotationsJson)}, - ) -} diff --git a/pkg/webhook/mutation/pod/oneagent/annotations.go b/pkg/webhook/mutation/pod/oneagent/annotations.go deleted file mode 100644 index 3aea944516..0000000000 --- a/pkg/webhook/mutation/pod/oneagent/annotations.go +++ /dev/null @@ -1,45 +0,0 @@ -package oneagent - -import ( - "net/url" - - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - maputils "github.com/Dynatrace/dynatrace-operator/pkg/util/map" - dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" - corev1 "k8s.io/api/core/v1" -) - -type installerInfo struct { - flavor string - technologies string - installPath string - installerURL string - version string -} - -func setInjectedAnnotation(pod *corev1.Pod) { - if pod.Annotations == nil { - pod.Annotations = make(map[string]string) - } - - pod.Annotations[dtwebhook.AnnotationOneAgentInjected] = "true" -} - -func setNotInjectedAnnotations(pod *corev1.Pod, reason string) { - if pod.Annotations == nil { - pod.Annotations = make(map[string]string) - } - - pod.Annotations[dtwebhook.AnnotationOneAgentInjected] = "false" - pod.Annotations[dtwebhook.AnnotationOneAgentReason] = reason -} - -func getInstallerInfo(pod *corev1.Pod, dk dynakube.DynaKube) installerInfo { - return installerInfo{ - flavor: maputils.GetField(pod.Annotations, dtwebhook.AnnotationFlavor, ""), - technologies: url.QueryEscape(maputils.GetField(pod.Annotations, dtwebhook.AnnotationTechnologies, "all")), - installPath: maputils.GetField(pod.Annotations, dtwebhook.AnnotationInstallPath, dtwebhook.DefaultInstallPath), - installerURL: maputils.GetField(pod.Annotations, dtwebhook.AnnotationInstallerUrl, ""), - version: dk.CodeModulesVersion(), - } -} diff --git a/pkg/webhook/mutation/pod/register.go b/pkg/webhook/mutation/pod/register.go index bb045349ad..152e8c3b9e 100644 --- a/pkg/webhook/mutation/pod/register.go +++ b/pkg/webhook/mutation/pod/register.go @@ -9,8 +9,9 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/util/kubesystem" "github.com/Dynatrace/dynatrace-operator/pkg/util/oneagentapm" dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" - "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/metadata" - oamutation "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/oneagent" + "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/common/events" + podv1 "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/v1" + podv2 "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/v2" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" @@ -20,7 +21,7 @@ import ( ) func registerInjectEndpoint(ctx context.Context, mgr manager.Manager, webhookNamespace string, webhookPodName string) error { - eventRecorder := newPodMutatorEventRecorder(mgr.GetEventRecorderFor("dynatrace-webhook")) + eventRecorder := events.NewRecorder(mgr.GetEventRecorderFor("dynatrace-webhook")) kubeConfig := mgr.GetConfig() kubeClient := mgr.GetClient() apiReader := mgr.GetAPIReader() @@ -36,7 +37,7 @@ func registerInjectEndpoint(ctx context.Context, mgr manager.Manager, webhookNam } if apmExists { - eventRecorder.sendOneAgentAPMWarningEvent(webhookPod) + eventRecorder.SendOneAgentAPMWarningEvent(webhookPod) return errors.New("OneAgentAPM object detected - the Dynatrace webhook will not inject until the deprecated OneAgent Operator has been fully uninstalled") } @@ -58,28 +59,12 @@ func registerInjectEndpoint(ctx context.Context, mgr manager.Manager, webhookNam } mgr.GetWebhookServer().Register("/inject", &webhooks.Admission{Handler: &webhook{ + v1: podv1.NewInjector(apiReader, kubeClient, metaClient, eventRecorder, clusterID, webhookPodImage, webhookNamespace), + v2: podv2.NewInjector(kubeClient, apiReader, metaClient, eventRecorder), apiReader: apiReader, webhookNamespace: webhookNamespace, - webhookImage: webhookPodImage, deployedViaOLM: kubesystem.IsDeployedViaOlm(*webhookPod), - clusterID: clusterID, - recorder: eventRecorder, - mutators: []dtwebhook.PodMutator{ - oamutation.NewMutator( - webhookPodImage, - clusterID, - webhookNamespace, - kubeClient, - apiReader, - ), - metadata.NewMutator( - webhookNamespace, - kubeClient, - apiReader, - metaClient, - ), - }, - decoder: admission.NewDecoder(mgr.GetScheme()), + decoder: admission.NewDecoder(mgr.GetScheme()), }}) log.Info("registered /inject endpoint") diff --git a/pkg/webhook/mutation/pod/request.go b/pkg/webhook/mutation/pod/request.go index f8dd2f541f..2b00b89809 100644 --- a/pkg/webhook/mutation/pod/request.go +++ b/pkg/webhook/mutation/pod/request.go @@ -3,7 +3,7 @@ package pod import ( "context" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" @@ -83,7 +83,7 @@ func (wh *webhook) getDynakube(ctx context.Context, dynakubeName string) (*dynak err := wh.apiReader.Get(ctx, client.ObjectKey{Name: dynakubeName, Namespace: wh.webhookNamespace}, &dk) if k8serrors.IsNotFound(err) { - wh.recorder.sendMissingDynaKubeEvent(wh.webhookNamespace, dynakubeName) + wh.recorder.SendMissingDynaKubeEvent(wh.webhookNamespace, dynakubeName) return nil, err } else if err != nil { diff --git a/pkg/webhook/mutation/pod/request_test.go b/pkg/webhook/mutation/pod/request_test.go index d6a14dd2ca..01fbcfdcfc 100644 --- a/pkg/webhook/mutation/pod/request_test.go +++ b/pkg/webhook/mutation/pod/request_test.go @@ -5,15 +5,16 @@ import ( "encoding/json" "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/util/address" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" + webhookmock "github.com/Dynatrace/dynatrace-operator/test/mocks/pkg/webhook" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" admissionv1 "k8s.io/api/admission/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" ) @@ -22,8 +23,8 @@ const testUser int64 = 420 func getTestSecurityContext() *corev1.SecurityContext { return &corev1.SecurityContext{ - RunAsUser: address.Of(testUser), - RunAsGroup: address.Of(testUser), + RunAsUser: ptr.To(testUser), + RunAsGroup: ptr.To(testUser), } } @@ -31,7 +32,8 @@ func TestCreateMutationRequestBase(t *testing.T) { t.Run("should create a mutation request", func(t *testing.T) { dk := getTestDynakube() podWebhook := createTestWebhook( - []dtwebhook.PodMutator{}, + webhookmock.NewPodInjector(t), + webhookmock.NewPodInjector(t), []client.Object{ getTestNamespace(), getTestPod(), @@ -53,7 +55,8 @@ func TestCreateMutationRequestBase(t *testing.T) { func TestGetPodFromRequest(t *testing.T) { t.Run("should return the pod struct", func(t *testing.T) { podWebhook := createTestWebhook( - []dtwebhook.PodMutator{}, + webhookmock.NewPodInjector(t), + webhookmock.NewPodInjector(t), []client.Object{}, ) expected := getTestPod() @@ -68,7 +71,8 @@ func TestGetNamespaceFromRequest(t *testing.T) { t.Run("should return the namespace struct", func(t *testing.T) { expected := getTestNamespace() podWebhook := createTestWebhook( - []dtwebhook.PodMutator{}, + webhookmock.NewPodInjector(t), + webhookmock.NewPodInjector(t), []client.Object{expected}, ) @@ -91,7 +95,8 @@ func TestGetDynakube(t *testing.T) { t.Run("should return the dynakube struct", func(t *testing.T) { expected := getTestDynakube() podWebhook := createTestWebhook( - []dtwebhook.PodMutator{}, + webhookmock.NewPodInjector(t), + webhookmock.NewPodInjector(t), []client.Object{expected}, ) @@ -106,10 +111,6 @@ func createTestMutationRequest(dk *dynakube.DynaKube) *dtwebhook.MutationRequest return dtwebhook.NewMutationRequest(context.Background(), *getTestNamespace(), nil, getTestPod(), *dk) } -func createTestMutationRequestWithInjectedPod(dk *dynakube.DynaKube) *dtwebhook.MutationRequest { - return dtwebhook.NewMutationRequest(context.Background(), *getTestNamespace(), nil, getInjectedPod(), *dk) -} - func createTestAdmissionRequest(pod *corev1.Pod) *admission.Request { basePodBytes, _ := json.Marshal(pod) @@ -155,42 +156,6 @@ func getTestPod() *corev1.Pod { } } -func getInjectedPod() *corev1.Pod { - pod := &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: testPodName, - Namespace: testNamespaceName, - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "container", - Image: "alpine", - SecurityContext: getTestSecurityContext(), - }, - }, - InitContainers: []corev1.Container{ - { - Name: "init-container", - Image: "alpine", - }, - }, - Volumes: []corev1.Volume{ - { - Name: "volume", - VolumeSource: corev1.VolumeSource{ - EmptyDir: &corev1.EmptyDirVolumeSource{}, - }, - }, - }, - }, - } - installContainer := createInstallInitContainerBase("test", "test", pod, *getTestDynakube()) - pod.Spec.InitContainers = append(pod.Spec.InitContainers, *installContainer) - - return pod -} - func getTestNamespace() *corev1.Namespace { return &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ diff --git a/pkg/webhook/mutation/pod/v1/config.go b/pkg/webhook/mutation/pod/v1/config.go new file mode 100644 index 0000000000..5122e0713a --- /dev/null +++ b/pkg/webhook/mutation/pod/v1/config.go @@ -0,0 +1,9 @@ +package v1 + +import ( + "github.com/Dynatrace/dynatrace-operator/pkg/logd" +) + +var ( + log = logd.Get().WithName("v1-pod-mutation") +) diff --git a/pkg/webhook/mutation/pod/init_container.go b/pkg/webhook/mutation/pod/v1/init.go similarity index 85% rename from pkg/webhook/mutation/pod/init_container.go rename to pkg/webhook/mutation/pod/v1/init.go index 65a5be9e8d..86dd859a45 100644 --- a/pkg/webhook/mutation/pod/init_container.go +++ b/pkg/webhook/mutation/pod/v1/init.go @@ -1,21 +1,22 @@ -package pod +package v1 import ( "encoding/json" "strings" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/consts" "github.com/Dynatrace/dynatrace-operator/pkg/injection/startup" - "github.com/Dynatrace/dynatrace-operator/pkg/util/address" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/env" k8spod "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/pod" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/resources" maputils "github.com/Dynatrace/dynatrace-operator/pkg/util/map" dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" - "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/metadata" - oamutation "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/oneagent" + oacommon "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/common/oneagent" + "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/v1/metadata" + oamutation "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/v1/oneagent" corev1 "k8s.io/api/core/v1" + "k8s.io/utils/ptr" ) func createInstallInitContainerBase(webhookImage, clusterID string, pod *corev1.Pod, dk dynakube.DynaKube) *corev1.Container { @@ -39,12 +40,16 @@ func createInstallInitContainerBase(webhookImage, clusterID string, pod *corev1. } func initContainerResources(dk dynakube.DynaKube) corev1.ResourceRequirements { - customInitResources := dk.InitResources() + customInitResources := dk.OneAgent().GetInitResources() if customInitResources != nil { return *customInitResources } - if !dk.IsCSIAvailable() { + if !dk.OneAgent().IsCSIAvailable() { + if dk.MetadataEnrichmentEnabled() && !dk.OneAgent().IsAppInjectionNeeded() { + return defaultInitContainerResources() + } + return corev1.ResourceRequirements{} } @@ -60,9 +65,9 @@ func defaultInitContainerResources() corev1.ResourceRequirements { func securityContextForInitContainer(pod *corev1.Pod, dk dynakube.DynaKube) *corev1.SecurityContext { initSecurityCtx := corev1.SecurityContext{ - ReadOnlyRootFilesystem: address.Of(true), - AllowPrivilegeEscalation: address.Of(false), - Privileged: address.Of(false), + ReadOnlyRootFilesystem: ptr.To(true), + AllowPrivilegeEscalation: ptr.To(false), + Privileged: ptr.To(false), Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{ "ALL", @@ -81,14 +86,14 @@ func combineSecurityContexts(baseSecurityCtx corev1.SecurityContext, pod corev1. containerSecurityCtx := pod.Spec.Containers[0].SecurityContext podSecurityCtx := pod.Spec.SecurityContext - baseSecurityCtx.RunAsUser = address.Of(defaultUser) - baseSecurityCtx.RunAsGroup = address.Of(defaultGroup) + baseSecurityCtx.RunAsUser = ptr.To(oacommon.DefaultUser) + baseSecurityCtx.RunAsGroup = ptr.To(oacommon.DefaultGroup) - if hasPodUserSet(podSecurityCtx) { + if oacommon.HasPodUserSet(podSecurityCtx) { baseSecurityCtx.RunAsUser = podSecurityCtx.RunAsUser } - if hasPodGroupSet(podSecurityCtx) { + if oacommon.HasPodGroupSet(podSecurityCtx) { baseSecurityCtx.RunAsGroup = podSecurityCtx.RunAsGroup } @@ -100,19 +105,11 @@ func combineSecurityContexts(baseSecurityCtx corev1.SecurityContext, pod corev1. baseSecurityCtx.RunAsGroup = containerSecurityCtx.RunAsGroup } - baseSecurityCtx.RunAsNonRoot = address.Of(isNonRoot(&baseSecurityCtx)) + baseSecurityCtx.RunAsNonRoot = ptr.To(oacommon.IsNonRoot(&baseSecurityCtx)) return &baseSecurityCtx } -func hasPodUserSet(ctx *corev1.PodSecurityContext) bool { - return ctx != nil && ctx.RunAsUser != nil -} - -func hasPodGroupSet(ctx *corev1.PodSecurityContext) bool { - return ctx != nil && ctx.RunAsGroup != nil -} - func hasContainerUserSet(ctx *corev1.SecurityContext) bool { return ctx != nil && ctx.RunAsUser != nil } @@ -121,12 +118,6 @@ func hasContainerGroupSet(ctx *corev1.SecurityContext) bool { return ctx != nil && ctx.RunAsGroup != nil } -func isNonRoot(ctx *corev1.SecurityContext) bool { - return ctx != nil && - (ctx.RunAsUser != nil && *ctx.RunAsUser != rootUserGroup) && - (ctx.RunAsGroup != nil && *ctx.RunAsGroup != rootUserGroup) -} - func getBasePodName(pod *corev1.Pod) string { basePodName := k8spod.GetName(*pod) diff --git a/pkg/webhook/mutation/pod/init_container_test.go b/pkg/webhook/mutation/pod/v1/init_test.go similarity index 80% rename from pkg/webhook/mutation/pod/init_container_test.go rename to pkg/webhook/mutation/pod/v1/init_test.go index 0b8a0bdaf0..2654bd93e6 100644 --- a/pkg/webhook/mutation/pod/init_container_test.go +++ b/pkg/webhook/mutation/pod/v1/init_test.go @@ -1,16 +1,18 @@ -package pod +package v1 import ( "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/util/address" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" "github.com/Dynatrace/dynatrace-operator/pkg/util/installconfig" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/env" dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" + oacommon "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/common/oneagent" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" + "k8s.io/utils/ptr" ) func TestCreateInstallInitContainerBase(t *testing.T) { @@ -41,17 +43,17 @@ func TestCreateInstallInitContainerBase(t *testing.T) { assert.True(t, *initContainer.SecurityContext.RunAsNonRoot) require.NotNil(t, initContainer.SecurityContext.RunAsUser) - assert.Equal(t, defaultUser, *initContainer.SecurityContext.RunAsUser) + assert.Equal(t, oacommon.DefaultUser, *initContainer.SecurityContext.RunAsUser) require.NotNil(t, initContainer.SecurityContext.RunAsGroup) - assert.Equal(t, defaultGroup, *initContainer.SecurityContext.RunAsGroup) + assert.Equal(t, oacommon.DefaultGroup, *initContainer.SecurityContext.RunAsGroup) assert.Nil(t, initContainer.SecurityContext.SeccompProfile) }) t.Run("should overwrite partially", func(t *testing.T) { dk := getTestDynakube() pod := getTestPod() - testUser := address.Of(int64(420)) + testUser := ptr.To(int64(420)) pod.Spec.Containers[0].SecurityContext.RunAsUser = nil pod.Spec.Containers[0].SecurityContext.RunAsGroup = testUser webhookImage := "test-image" @@ -63,7 +65,7 @@ func TestCreateInstallInitContainerBase(t *testing.T) { assert.True(t, *initContainer.SecurityContext.RunAsNonRoot) require.NotNil(t, *initContainer.SecurityContext.RunAsUser) - assert.Equal(t, defaultUser, *initContainer.SecurityContext.RunAsUser) + assert.Equal(t, oacommon.DefaultUser, *initContainer.SecurityContext.RunAsUser) require.NotNil(t, *initContainer.SecurityContext.RunAsGroup) assert.Equal(t, *initContainer.SecurityContext.RunAsGroup, *testUser) @@ -71,8 +73,8 @@ func TestCreateInstallInitContainerBase(t *testing.T) { t.Run("container SecurityContext overrules defaults", func(t *testing.T) { dk := getTestDynakube() pod := getTestPod() - overruledUser := address.Of(int64(420)) - testUser := address.Of(int64(420)) + overruledUser := ptr.To(int64(420)) + testUser := ptr.To(int64(420)) pod.Spec.SecurityContext = &corev1.PodSecurityContext{} pod.Spec.SecurityContext.RunAsUser = overruledUser pod.Spec.SecurityContext.RunAsGroup = overruledUser @@ -93,7 +95,7 @@ func TestCreateInstallInitContainerBase(t *testing.T) { }) t.Run("PodSecurityContext overrules defaults", func(t *testing.T) { dk := getTestDynakube() - testUser := address.Of(int64(420)) + testUser := ptr.To(int64(420)) pod := getTestPod() pod.Spec.Containers[0].SecurityContext = nil pod.Spec.SecurityContext = &corev1.PodSecurityContext{} @@ -116,8 +118,8 @@ func TestCreateInstallInitContainerBase(t *testing.T) { t.Run("should set RunAsNonRoot if root user is used", func(t *testing.T) { dk := getTestDynakube() pod := getTestPod() - pod.Spec.Containers[0].SecurityContext.RunAsUser = address.Of(rootUserGroup) - pod.Spec.Containers[0].SecurityContext.RunAsGroup = address.Of(rootUserGroup) + pod.Spec.Containers[0].SecurityContext.RunAsUser = ptr.To(oacommon.RootUserGroup) + pod.Spec.Containers[0].SecurityContext.RunAsGroup = ptr.To(oacommon.RootUserGroup) webhookImage := "test-image" clusterID := "id" @@ -127,10 +129,10 @@ func TestCreateInstallInitContainerBase(t *testing.T) { assert.False(t, *initContainer.SecurityContext.RunAsNonRoot) require.NotNil(t, *initContainer.SecurityContext.RunAsUser) - assert.Equal(t, rootUserGroup, *initContainer.SecurityContext.RunAsUser) + assert.Equal(t, oacommon.RootUserGroup, *initContainer.SecurityContext.RunAsUser) require.NotNil(t, *initContainer.SecurityContext.RunAsGroup) - assert.Equal(t, rootUserGroup, *initContainer.SecurityContext.RunAsGroup) + assert.Equal(t, oacommon.RootUserGroup, *initContainer.SecurityContext.RunAsGroup) }) t.Run("should handle failure policy feature flag correctly", func(t *testing.T) { dk := getTestDynakube() @@ -238,4 +240,35 @@ func TestInitContainerResources(t *testing.T) { require.Empty(t, initResources) }) + + t.Run("should have default if metadata enrichment is enabled", func(t *testing.T) { + dk := getTestDynakubeDefaultAppMon() + dk.Spec.MetadataEnrichment.Enabled = ptr.To(true) + + initResources := initContainerResources(*dk) + + assert.Equal(t, defaultInitContainerResources(), initResources) + }) + t.Run("should have default if only metadata enrichment is enabled and csi is disabled", func(t *testing.T) { + installconfig.SetModulesOverride(t, installconfig.Modules{CSIDriver: false}) + + dk := getTestDynakubeDefaultAppMon() + dk.Spec.MetadataEnrichment.Enabled = ptr.To(true) + dk.Spec.OneAgent.ApplicationMonitoring = nil + initResources := initContainerResources(*dk) + + assert.Equal(t, defaultInitContainerResources(), initResources) + }) + t.Run("should have no limit if OA is enabled and csi is disabled", func(t *testing.T) { + installconfig.SetModulesOverride(t, installconfig.Modules{CSIDriver: false}) + + dk := getTestDynakubeDefaultAppMon() + + dk.Spec.MetadataEnrichment.Enabled = ptr.To(true) + dk.Spec.OneAgent.ApplicationMonitoring = &oneagent.ApplicationMonitoringSpec{} + + initResources := initContainerResources(*dk) + + require.Empty(t, initResources) + }) } diff --git a/pkg/webhook/mutation/pod/v1/metadata/annotations.go b/pkg/webhook/mutation/pod/v1/metadata/annotations.go new file mode 100644 index 0000000000..7b26eed15e --- /dev/null +++ b/pkg/webhook/mutation/pod/v1/metadata/annotations.go @@ -0,0 +1,36 @@ +package metadata + +import ( + "encoding/json" + "strings" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/consts" + dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" + metacommon "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/common/metadata" + corev1 "k8s.io/api/core/v1" +) + +func propagateMetadataAnnotations(request *dtwebhook.MutationRequest) { + metacommon.CopyMetadataFromNamespace(request.Pod, request.Namespace, request.DynaKube) + addMetadataToInitEnv(request.Pod, request.InstallContainer) +} + +func addMetadataToInitEnv(pod *corev1.Pod, installContainer *corev1.Container) { + metadataAnnotations := map[string]string{} + + for key, value := range pod.Annotations { + if !strings.HasPrefix(key, dynakube.MetadataPrefix) { + continue + } + + split := strings.Split(key, dynakube.MetadataPrefix) + metadataAnnotations[split[1]] = value + } + + workloadAnnotationsJson, _ := json.Marshal(metadataAnnotations) + installContainer.Env = append(installContainer.Env, + corev1.EnvVar{ + Name: consts.EnrichmentWorkloadAnnotationsEnv, Value: string(workloadAnnotationsJson)}, + ) +} diff --git a/pkg/webhook/mutation/pod/v1/metadata/annotations_test.go b/pkg/webhook/mutation/pod/v1/metadata/annotations_test.go new file mode 100644 index 0000000000..330af60f89 --- /dev/null +++ b/pkg/webhook/mutation/pod/v1/metadata/annotations_test.go @@ -0,0 +1,50 @@ +package metadata + +import ( + "encoding/json" + "testing" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/consts" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/env" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAddMetadataToInitEnv(t *testing.T) { + t.Run("should copy annotations not labels with prefix from pod to env", func(t *testing.T) { + expectedKeys := []string{ + "beep", + "boop", + "hello", + } + notExpectedKey := "no-prop" + request := createTestMutationRequest(nil, nil, false) + request.Pod.Labels = map[string]string{ + dynakube.MetadataPrefix + notExpectedKey: "beep-boop", + "test-label": "boom", + } + request.Pod.Annotations = map[string]string{ + "test-annotation": "boom", + } + + for _, key := range expectedKeys { + request.Pod.Annotations[dynakube.MetadataPrefix+key] = key + "-value" + } + + addMetadataToInitEnv(request.Pod, request.InstallContainer) + + annotationsEnv := env.FindEnvVar(request.InstallContainer.Env, consts.EnrichmentWorkloadAnnotationsEnv) + require.NotNil(t, annotationsEnv) + + propagatedAnnotations := map[string]string{} + err := json.Unmarshal([]byte(annotationsEnv.Value), &propagatedAnnotations) + require.NoError(t, err) + + for _, key := range expectedKeys { + require.Contains(t, propagatedAnnotations, key) + assert.Equal(t, key+"-value", propagatedAnnotations[key]) + assert.NotContains(t, propagatedAnnotations, notExpectedKey) + } + }) +} diff --git a/pkg/webhook/mutation/pod/metadata/config.go b/pkg/webhook/mutation/pod/v1/metadata/config.go similarity index 81% rename from pkg/webhook/mutation/pod/metadata/config.go rename to pkg/webhook/mutation/pod/v1/metadata/config.go index f816935959..9de4fa7891 100644 --- a/pkg/webhook/mutation/pod/metadata/config.go +++ b/pkg/webhook/mutation/pod/v1/metadata/config.go @@ -11,5 +11,5 @@ const ( ) var ( - log = logd.Get().WithName("metadata-enrichment-pod-mutation") + log = logd.Get().WithName("v1-pod-mutation-metadata-enrichment") ) diff --git a/pkg/webhook/mutation/pod/metadata/containers.go b/pkg/webhook/mutation/pod/v1/metadata/containers.go similarity index 85% rename from pkg/webhook/mutation/pod/metadata/containers.go rename to pkg/webhook/mutation/pod/v1/metadata/containers.go index 07666e97d3..f76fd6b3fb 100644 --- a/pkg/webhook/mutation/pod/metadata/containers.go +++ b/pkg/webhook/mutation/pod/v1/metadata/containers.go @@ -2,6 +2,7 @@ package metadata import ( dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" + metacommon "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/common/metadata" corev1 "k8s.io/api/core/v1" ) @@ -32,7 +33,7 @@ func reinvokeUserContainers(request *dtwebhook.BaseRequest) bool { return updated } -func updateInstallContainer(installContainer *corev1.Container, workload *workloadInfo, entityID, clusterName string) { +func updateInstallContainer(installContainer *corev1.Container, workload *metacommon.WorkloadInfo, entityID, clusterName string) { addInjectedEnv(installContainer) addDTClusterEnvs(installContainer, entityID, clusterName) addWorkloadInfoEnvs(installContainer, workload) diff --git a/pkg/webhook/mutation/pod/metadata/containers_test.go b/pkg/webhook/mutation/pod/v1/metadata/containers_test.go similarity index 92% rename from pkg/webhook/mutation/pod/metadata/containers_test.go rename to pkg/webhook/mutation/pod/v1/metadata/containers_test.go index ff88b2e0db..7e9b273d51 100644 --- a/pkg/webhook/mutation/pod/metadata/containers_test.go +++ b/pkg/webhook/mutation/pod/v1/metadata/containers_test.go @@ -3,6 +3,7 @@ package metadata import ( "testing" + metacommon "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/common/metadata" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" ) @@ -93,3 +94,12 @@ func TestUpdateInstallContainer(t *testing.T) { require.Len(t, container.Env, 5) }) } + +func createTestWorkloadInfo(t *testing.T) *metacommon.WorkloadInfo { + t.Helper() + + return &metacommon.WorkloadInfo{ + Kind: "test", + Name: "test", + } +} diff --git a/pkg/webhook/mutation/pod/metadata/env.go b/pkg/webhook/mutation/pod/v1/metadata/env.go similarity index 83% rename from pkg/webhook/mutation/pod/metadata/env.go rename to pkg/webhook/mutation/pod/v1/metadata/env.go index 24794b1a7d..322d58e2f3 100644 --- a/pkg/webhook/mutation/pod/metadata/env.go +++ b/pkg/webhook/mutation/pod/v1/metadata/env.go @@ -2,6 +2,7 @@ package metadata import ( "github.com/Dynatrace/dynatrace-operator/pkg/consts" + metacommon "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/common/metadata" corev1 "k8s.io/api/core/v1" ) @@ -11,10 +12,10 @@ func addInjectedEnv(container *corev1.Container) { ) } -func addWorkloadInfoEnvs(container *corev1.Container, workload *workloadInfo) { +func addWorkloadInfoEnvs(container *corev1.Container, workload *metacommon.WorkloadInfo) { container.Env = append(container.Env, - corev1.EnvVar{Name: consts.EnrichmentWorkloadKindEnv, Value: workload.kind}, - corev1.EnvVar{Name: consts.EnrichmentWorkloadNameEnv, Value: workload.name}, + corev1.EnvVar{Name: consts.EnrichmentWorkloadKindEnv, Value: workload.Kind}, + corev1.EnvVar{Name: consts.EnrichmentWorkloadNameEnv, Value: workload.Name}, ) } diff --git a/pkg/webhook/mutation/pod/metadata/env_test.go b/pkg/webhook/mutation/pod/v1/metadata/env_test.go similarity index 100% rename from pkg/webhook/mutation/pod/metadata/env_test.go rename to pkg/webhook/mutation/pod/v1/metadata/env_test.go diff --git a/pkg/webhook/mutation/pod/metadata/mutator.go b/pkg/webhook/mutation/pod/v1/metadata/mutator.go similarity index 62% rename from pkg/webhook/mutation/pod/metadata/mutator.go rename to pkg/webhook/mutation/pod/v1/metadata/mutator.go index db7b385ee9..ad7b7bbf3e 100644 --- a/pkg/webhook/mutation/pod/metadata/mutator.go +++ b/pkg/webhook/mutation/pod/v1/metadata/mutator.go @@ -2,16 +2,13 @@ package metadata import ( "context" - "strings" "github.com/Dynatrace/dynatrace-operator/pkg/consts" dtingestendpoint "github.com/Dynatrace/dynatrace-operator/pkg/injection/namespace/ingestendpoint" - maputils "github.com/Dynatrace/dynatrace-operator/pkg/util/map" dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" + metacommon "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/common/metadata" corev1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -32,29 +29,17 @@ func NewMutator(webhookNamespace string, client client.Client, apiReader client. } func (mut *Mutator) Enabled(request *dtwebhook.BaseRequest) bool { - enabledOnPod := maputils.GetFieldBool(request.Pod.Annotations, dtwebhook.AnnotationMetadataEnrichmentInject, - request.DynaKube.FeatureAutomaticInjection()) - enabledOnDynakube := request.DynaKube.MetadataEnrichmentEnabled() - - matchesNamespace := true // if no namespace selector is configured, we just pass set this to true - - if request.DynaKube.MetadataEnrichmentNamespaceSelector().Size() > 0 { - selector, _ := metav1.LabelSelectorAsSelector(request.DynaKube.MetadataEnrichmentNamespaceSelector()) - - matchesNamespace = selector.Matches(labels.Set(request.Namespace.Labels)) - } - - return matchesNamespace && enabledOnPod && enabledOnDynakube + return metacommon.IsEnabled(request) } func (mut *Mutator) Injected(request *dtwebhook.BaseRequest) bool { - return maputils.GetFieldBool(request.Pod.Annotations, dtwebhook.AnnotationMetadataEnrichmentInjected, false) + return metacommon.IsInjected(request) } func (mut *Mutator) Mutate(ctx context.Context, request *dtwebhook.MutationRequest) error { log.Info("injecting metadata-enrichment into pod", "podName", request.PodName()) - workload, err := mut.retrieveWorkload(request) + workload, err := metacommon.RetrieveWorkload(mut.metaClient, request) if err != nil { return err } @@ -68,8 +53,8 @@ func (mut *Mutator) Mutate(ctx context.Context, request *dtwebhook.MutationReque mutateUserContainers(request.BaseRequest) updateInstallContainer(request.InstallContainer, workload, request.DynaKube.Status.KubernetesClusterName, request.DynaKube.Status.KubernetesClusterMEID) propagateMetadataAnnotations(request) - setInjectedAnnotation(request.Pod) - setWorkloadAnnotations(request.Pod, workload) + metacommon.SetInjectedAnnotation(request.Pod) + metacommon.SetWorkloadAnnotations(request.Pod, workload) return nil } @@ -114,25 +99,6 @@ func (mut *Mutator) ensureIngestEndpointSecret(request *dtwebhook.MutationReques return nil } -func setInjectedAnnotation(pod *corev1.Pod) { - if pod.Annotations == nil { - pod.Annotations = make(map[string]string) - } - - pod.Annotations[dtwebhook.AnnotationMetadataEnrichmentInjected] = "true" -} - -func setWorkloadAnnotations(pod *corev1.Pod, workload *workloadInfo) { - if pod.Annotations == nil { - pod.Annotations = make(map[string]string) - } - - // workload kind annotation in lower case according to dt semantic-dictionary - // https://bitbucket.lab.dynatrace.org/projects/DEUS/repos/semantic-dictionary/browse/source/fields/k8s.yaml - pod.Annotations[dtwebhook.AnnotationWorkloadKind] = strings.ToLower(workload.kind) - pod.Annotations[dtwebhook.AnnotationWorkloadName] = workload.name -} - func ContainerIsInjected(container corev1.Container) bool { for _, volumeMount := range container.VolumeMounts { if volumeMount.Name == workloadEnrichmentVolumeName || volumeMount.Name == ingestEndpointVolumeName { diff --git a/pkg/webhook/mutation/pod/metadata/mutator_test.go b/pkg/webhook/mutation/pod/v1/metadata/mutator_test.go similarity index 79% rename from pkg/webhook/mutation/pod/metadata/mutator_test.go rename to pkg/webhook/mutation/pod/v1/metadata/mutator_test.go index bf35f478cb..f88f96d388 100644 --- a/pkg/webhook/mutation/pod/metadata/mutator_test.go +++ b/pkg/webhook/mutation/pod/v1/metadata/mutator_test.go @@ -6,17 +6,18 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/communication" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/consts" - "github.com/Dynatrace/dynatrace-operator/pkg/util/address" - maputils "github.com/Dynatrace/dynatrace-operator/pkg/util/map" dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" + metacommon "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/common/metadata" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -35,7 +36,7 @@ const ( func TestEnabled(t *testing.T) { t.Run("turned off", func(t *testing.T) { mutator := createTestPodMutator(nil) - request := createTestMutationRequest(nil, map[string]string{dtwebhook.AnnotationMetadataEnrichmentInject: "false"}, false) + request := createTestMutationRequest(nil, map[string]string{metacommon.AnnotationInject: "false"}, false) enabled := mutator.Enabled(request.BaseRequest) @@ -44,7 +45,7 @@ func TestEnabled(t *testing.T) { t.Run("off by feature flag", func(t *testing.T) { mutator := createTestPodMutator(nil) request := createTestMutationRequest(nil, nil, false) - request.DynaKube.Spec.MetadataEnrichment.Enabled = address.Of(true) + request.DynaKube.Spec.MetadataEnrichment.Enabled = ptr.To(true) request.DynaKube.Annotations = map[string]string{dynakube.AnnotationFeatureAutomaticInjection: "false"} enabled := mutator.Enabled(request.BaseRequest) @@ -55,7 +56,7 @@ func TestEnabled(t *testing.T) { mutator := createTestPodMutator(nil) dk := dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ - MetadataEnrichment: dynakube.MetadataEnrichment{Enabled: address.Of(true)}, + MetadataEnrichment: dynakube.MetadataEnrichment{Enabled: ptr.To(true)}, }, } request := createTestMutationRequest(&dk, nil, false) @@ -70,7 +71,7 @@ func TestEnabled(t *testing.T) { dk := dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ MetadataEnrichment: dynakube.MetadataEnrichment{ - Enabled: address.Of(true), + Enabled: ptr.To(true), NamespaceSelector: metav1.LabelSelector{ MatchLabels: map[string]string{ testLabelKeyMatching: testLabelValue, @@ -91,7 +92,7 @@ func TestEnabled(t *testing.T) { dk := dynakube.DynaKube{ Spec: dynakube.DynaKubeSpec{ MetadataEnrichment: dynakube.MetadataEnrichment{ - Enabled: address.Of(true), + Enabled: ptr.To(true), NamespaceSelector: metav1.LabelSelector{ MatchLabels: map[string]string{ testLabelKeyNotMatching: testLabelValue, @@ -112,7 +113,7 @@ func TestEnabled(t *testing.T) { func TestInjected(t *testing.T) { t.Run("already marked", func(t *testing.T) { mutator := createTestPodMutator(nil) - request := createTestMutationRequest(nil, map[string]string{dtwebhook.AnnotationMetadataEnrichmentInjected: "true"}, false) + request := createTestMutationRequest(nil, map[string]string{metacommon.AnnotationInjected: "true"}, false) enabled := mutator.Injected(request.BaseRequest) @@ -151,7 +152,7 @@ func TestMutate(t *testing.T) { func TestReinvoke(t *testing.T) { t.Run("should only mutate the containers in the pod", func(t *testing.T) { mutator := createTestPodMutator([]client.Object{getTestInitSecret()}) - request := createTestReinvocationRequest(getTestDynakube(), map[string]string{dtwebhook.AnnotationMetadataEnrichmentInjected: "true"}) + request := createTestReinvocationRequest(getTestDynakube(), map[string]string{metacommon.AnnotationInjected: "true"}) initialContainerVolumeMountsLen := len(request.Pod.Spec.Containers[0].VolumeMounts) @@ -167,7 +168,7 @@ func TestReinvoke(t *testing.T) { DynaKube: *getTestDynakube(), Pod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{dtwebhook.AnnotationMetadataEnrichmentInjected: "true"}, + Annotations: map[string]string{metacommon.AnnotationInjected: "true"}, }, }, }, @@ -199,36 +200,6 @@ func TestIngestEndpointSecret(t *testing.T) { }) } -func TestSetInjectedAnnotation(t *testing.T) { - t.Run("should add annotation to nil map", func(t *testing.T) { - request := createTestMutationRequest(nil, nil, false) - mutator := createTestPodMutator(nil) - - require.False(t, mutator.Injected(request.BaseRequest)) - setInjectedAnnotation(request.Pod) - require.Len(t, request.Pod.Annotations, 1) - require.True(t, mutator.Injected(request.BaseRequest)) - }) -} - -func TestWorkloadAnnotations(t *testing.T) { - t.Run("should add annotation to nil map", func(t *testing.T) { - request := createTestMutationRequest(nil, nil, false) - - require.Equal(t, "not-found", maputils.GetField(request.Pod.Annotations, dtwebhook.AnnotationWorkloadName, "not-found")) - setWorkloadAnnotations(request.Pod, &workloadInfo{name: testWorkloadInfoName, kind: testWorkloadInfoKind}) - require.Len(t, request.Pod.Annotations, 2) - assert.Equal(t, testWorkloadInfoName, maputils.GetField(request.Pod.Annotations, dtwebhook.AnnotationWorkloadName, "not-found")) - assert.Equal(t, testWorkloadInfoKind, maputils.GetField(request.Pod.Annotations, dtwebhook.AnnotationWorkloadKind, "not-found")) - }) - t.Run("should lower case kind annotation", func(t *testing.T) { - request := createTestMutationRequest(nil, nil, false) - setWorkloadAnnotations(request.Pod, &workloadInfo{name: testWorkloadInfoName, kind: "SuperWorkload"}) - assert.Contains(t, request.Pod.Annotations, dtwebhook.AnnotationWorkloadKind) - assert.Equal(t, "superworkload", request.Pod.Annotations[dtwebhook.AnnotationWorkloadKind]) - }) -} - func TestContainerIsInjected(t *testing.T) { t.Run("is not injected", func(t *testing.T) { container := corev1.Container{} @@ -368,16 +339,16 @@ func getTestDynakube() *dynakube.DynaKube { }, Spec: dynakube.DynaKubeSpec{ APIURL: testApiUrl, - OneAgent: dynakube.OneAgentSpec{ - ApplicationMonitoring: &dynakube.ApplicationMonitoringSpec{}, + OneAgent: oneagent.Spec{ + ApplicationMonitoring: &oneagent.ApplicationMonitoringSpec{}, }, ActiveGate: activegate.Spec{ Capabilities: []activegate.CapabilityDisplayName{activegate.MetricsIngestCapability.DisplayName}, }, }, Status: dynakube.DynaKubeStatus{ - OneAgent: dynakube.OneAgentStatus{ - ConnectionInfoStatus: dynakube.OneAgentConnectionInfoStatus{ + OneAgent: oneagent.Status{ + ConnectionInfoStatus: oneagent.ConnectionInfoStatus{ ConnectionInfo: communication.ConnectionInfo{ TenantUUID: "test-tenant", }, diff --git a/pkg/webhook/mutation/pod/metadata/volumes.go b/pkg/webhook/mutation/pod/v1/metadata/volumes.go similarity index 100% rename from pkg/webhook/mutation/pod/metadata/volumes.go rename to pkg/webhook/mutation/pod/v1/metadata/volumes.go diff --git a/pkg/webhook/mutation/pod/metadata/volumes_test.go b/pkg/webhook/mutation/pod/v1/metadata/volumes_test.go similarity index 100% rename from pkg/webhook/mutation/pod/metadata/volumes_test.go rename to pkg/webhook/mutation/pod/v1/metadata/volumes_test.go diff --git a/pkg/webhook/mutation/pod/v1/oneagent/annotations.go b/pkg/webhook/mutation/pod/v1/oneagent/annotations.go new file mode 100644 index 0000000000..de09616500 --- /dev/null +++ b/pkg/webhook/mutation/pod/v1/oneagent/annotations.go @@ -0,0 +1,28 @@ +package oneagent + +import ( + "net/url" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + maputils "github.com/Dynatrace/dynatrace-operator/pkg/util/map" + oacommon "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/common/oneagent" + corev1 "k8s.io/api/core/v1" +) + +type installerInfo struct { + flavor string + technologies string + installPath string + installerURL string + version string +} + +func getInstallerInfo(pod *corev1.Pod, dk dynakube.DynaKube) installerInfo { + return installerInfo{ + flavor: maputils.GetField(pod.Annotations, AnnotationFlavor, ""), + technologies: url.QueryEscape(maputils.GetField(pod.Annotations, oacommon.AnnotationTechnologies, "all")), + installPath: maputils.GetField(pod.Annotations, oacommon.AnnotationInstallPath, oacommon.DefaultInstallPath), + installerURL: maputils.GetField(pod.Annotations, AnnotationInstallerUrl, ""), + version: dk.OneAgent().GetCodeModulesVersion(), + } +} diff --git a/pkg/webhook/mutation/pod/oneagent/annotations_test.go b/pkg/webhook/mutation/pod/v1/oneagent/annotations_test.go similarity index 100% rename from pkg/webhook/mutation/pod/oneagent/annotations_test.go rename to pkg/webhook/mutation/pod/v1/oneagent/annotations_test.go diff --git a/pkg/webhook/mutation/pod/oneagent/config.go b/pkg/webhook/mutation/pod/v1/oneagent/config.go similarity index 60% rename from pkg/webhook/mutation/pod/oneagent/config.go rename to pkg/webhook/mutation/pod/v1/oneagent/config.go index 2059f25577..8a54e4a2a6 100644 --- a/pkg/webhook/mutation/pod/oneagent/config.go +++ b/pkg/webhook/mutation/pod/v1/oneagent/config.go @@ -5,23 +5,10 @@ import ( ) var ( - log = logd.Get().WithName("oneagent-pod-mutation") + log = logd.Get().WithName("v1-pod-mutation-oneagent") ) const ( - preloadEnv = "LD_PRELOAD" - networkZoneEnv = "DT_NETWORK_ZONE" - dynatraceMetadataEnv = "DT_DEPLOYMENT_METADATA" - - releaseVersionEnv = "DT_RELEASE_VERSION" - releaseProductEnv = "DT_RELEASE_PRODUCT" - releaseStageEnv = "DT_RELEASE_STAGE" - releaseBuildVersionEnv = "DT_RELEASE_BUILD_VERSION" - - EmptyConnectionInfoReason = "EmptyConnectionInfo" - UnknownCodeModuleReason = "UnknownCodeModule" - EmptyTenantUUIDReason = "EmptyTenantUUID" - OneAgentBinVolumeName = "oneagent-bin" oneAgentShareVolumeName = "oneagent-share" injectionConfigVolumeName = "injection-config" @@ -40,4 +27,12 @@ const ( oneagentLogVolumeName = "oneagent-log" oneagentLogMountPath = "/opt/dynatrace/oneagent-paas/log" + + // AnnotationFlavor can be set on a Pod to configure which code modules flavor to download. It's set to "default" + // if not set. + AnnotationFlavor = "oneagent.dynatrace.com/flavor" + + // AnnotationInstallerUrl can be set on a Pod to configure the installer url for downloading the agent + // defaults to the PaaS installer download url of your tenant + AnnotationInstallerUrl = "oneagent.dynatrace.com/installer-url" ) diff --git a/pkg/webhook/mutation/pod/oneagent/containers.go b/pkg/webhook/mutation/pod/v1/oneagent/containers.go similarity index 78% rename from pkg/webhook/mutation/pod/oneagent/containers.go rename to pkg/webhook/mutation/pod/v1/oneagent/containers.go index ded821d38d..f9a218fc2e 100644 --- a/pkg/webhook/mutation/pod/oneagent/containers.go +++ b/pkg/webhook/mutation/pod/v1/oneagent/containers.go @@ -3,6 +3,7 @@ package oneagent import ( maputils "github.com/Dynatrace/dynatrace-operator/pkg/util/map" dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" + oacommon "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/common/oneagent" corev1 "k8s.io/api/core/v1" ) @@ -38,13 +39,13 @@ func (mut *Mutator) reinvokeUserContainers(request *dtwebhook.ReinvocationReques func (mut *Mutator) addOneAgentToContainer(request *dtwebhook.ReinvocationRequest, container *corev1.Container) { log.Info("adding OneAgent to container", "name", container.Name) - installPath := maputils.GetField(request.Pod.Annotations, dtwebhook.AnnotationInstallPath, dtwebhook.DefaultInstallPath) + installPath := maputils.GetField(request.Pod.Annotations, oacommon.AnnotationInstallPath, oacommon.DefaultInstallPath) dk := request.DynaKube addOneAgentVolumeMounts(container, installPath) - addDeploymentMetadataEnv(container, dk, mut.clusterID) - addPreloadEnv(container, installPath) + oacommon.AddDeploymentMetadataEnv(container, dk) + oacommon.AddPreloadEnv(container, installPath) addCertVolumeMounts(container, dk) @@ -53,11 +54,11 @@ func (mut *Mutator) addOneAgentToContainer(request *dtwebhook.ReinvocationReques } if dk.Spec.NetworkZone != "" { - addNetworkZoneEnv(container, dk.Spec.NetworkZone) + oacommon.AddNetworkZoneEnv(container, dk.Spec.NetworkZone) } if dk.FeatureLabelVersionDetection() { - addVersionDetectionEnvs(container, newVersionLabelMapping(request.Namespace)) + oacommon.AddVersionDetectionEnvs(container, request.Namespace) } if dk.FeatureReadOnlyCsiVolume() { diff --git a/pkg/webhook/mutation/pod/oneagent/containers_test.go b/pkg/webhook/mutation/pod/v1/oneagent/containers_test.go similarity index 63% rename from pkg/webhook/mutation/pod/oneagent/containers_test.go rename to pkg/webhook/mutation/pod/v1/oneagent/containers_test.go index 324a8240d0..3ffbdf5a48 100644 --- a/pkg/webhook/mutation/pod/oneagent/containers_test.go +++ b/pkg/webhook/mutation/pod/v1/oneagent/containers_test.go @@ -4,7 +4,7 @@ import ( "maps" "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -209,122 +209,3 @@ func TestContainerExclusion(t *testing.T) { }) } } - -func TestVersionDetectionMappingDrivenByNamespaceAnnotations(t *testing.T) { - const ( - customVersionValue = "my awesome custom version" - customProductValue = "my awesome custom product" - customReleaseStageValue = "my awesome custom stage" - customBuildVersionValue = "my awesome custom build version" - customVersionAnnotationName = "custom-version" - customProductAnnotationName = "custom-product" - customStageAnnotationName = "custom-stage" - customBuildVersionAnnotationName = "custom-build-version" - customVersionFieldPath = "metadata.podAnnotations['" + customVersionAnnotationName + "']" - customProductFieldPath = "metadata.podAnnotations['" + customProductAnnotationName + "']" - customStageFieldPath = "metadata.podAnnotations['" + customStageAnnotationName + "']" - customBuildVersionFieldPath = "metadata.podAnnotations['" + customBuildVersionAnnotationName + "']" - ) - - t.Run("version and product env vars are set using values referenced in namespace podAnnotations", func(t *testing.T) { - podAnnotations := map[string]string{ - customVersionAnnotationName: customVersionValue, - customProductAnnotationName: customProductValue, - } - namespaceAnnotations := map[string]string{ - versionMappingAnnotationName: customVersionFieldPath, - productMappingAnnotationName: customProductFieldPath, - } - expectedMappings := map[string]string{ - releaseVersionEnv: customVersionFieldPath, - releaseProductEnv: customProductFieldPath, - } - unexpectedMappingsKeys := []string{releaseStageEnv, releaseBuildVersionEnv} - - doTestMappings(t, podAnnotations, namespaceAnnotations, expectedMappings, unexpectedMappingsKeys) - }) - t.Run("only version env vars is set using value referenced in namespace podAnnotations, product is default", func(t *testing.T) { - podAnnotations := map[string]string{ - customVersionAnnotationName: customVersionValue, - } - namespaceAnnotations := map[string]string{ - versionMappingAnnotationName: customVersionFieldPath, - } - expectedMappings := map[string]string{ - releaseVersionEnv: customVersionFieldPath, - releaseProductEnv: defaultVersionLabelMapping[releaseProductEnv], - } - unexpectedMappingsKeys := []string{releaseStageEnv, releaseBuildVersionEnv} - - doTestMappings(t, podAnnotations, namespaceAnnotations, expectedMappings, unexpectedMappingsKeys) - }) - t.Run("optional env vars (stage, build-version) are set using values referenced in namespace podAnnotations, default ones remain default", func(t *testing.T) { - podAnnotations := map[string]string{ - customStageAnnotationName: customReleaseStageValue, - customBuildVersionAnnotationName: customBuildVersionValue, - } - namespaceAnnotations := map[string]string{ - stageMappingAnnotationName: customStageFieldPath, - buildVersionAnnotationName: customBuildVersionFieldPath, - } - expectedMappings := map[string]string{ - releaseVersionEnv: defaultVersionLabelMapping[releaseVersionEnv], - releaseProductEnv: defaultVersionLabelMapping[releaseProductEnv], - releaseStageEnv: customStageFieldPath, - releaseBuildVersionEnv: customBuildVersionFieldPath, - } - - doTestMappings(t, podAnnotations, namespaceAnnotations, expectedMappings, nil) - }) - t.Run("all env vars are namespace-podAnnotations driven", func(t *testing.T) { - podAnnotations := map[string]string{ - customVersionAnnotationName: customVersionValue, - customProductAnnotationName: customProductValue, - customStageAnnotationName: customReleaseStageValue, - customBuildVersionAnnotationName: customBuildVersionValue, - } - namespaceAnnotations := map[string]string{ - versionMappingAnnotationName: customVersionFieldPath, - productMappingAnnotationName: customProductFieldPath, - stageMappingAnnotationName: customStageFieldPath, - buildVersionAnnotationName: customBuildVersionFieldPath, - } - expectedMappings := map[string]string{ - releaseVersionEnv: customVersionFieldPath, - releaseProductEnv: customProductFieldPath, - releaseStageEnv: customStageFieldPath, - releaseBuildVersionEnv: customBuildVersionFieldPath, - } - - doTestMappings(t, podAnnotations, namespaceAnnotations, expectedMappings, nil) - }) -} - -func doTestMappings(t *testing.T, podAnnotations map[string]string, namespaceAnnotations map[string]string, expectedMappings map[string]string, unexpectedMappingsKeys []string) { - mutator := createTestPodMutator([]client.Object{getTestInitSecret()}) - request := createTestMutationRequest(getTestComplexDynakube(), podAnnotations, getTestNamespace(namespaceAnnotations)) - mutator.mutateUserContainers(request) - - assertContainsMappings(t, expectedMappings, request) - assertNotContainsMappings(t, unexpectedMappingsKeys, request) -} - -func assertContainsMappings(t *testing.T, expectedMappings map[string]string, request *dtwebhook.MutationRequest) { - for envName, fieldPath := range expectedMappings { - assert.Contains(t, request.Pod.Spec.Containers[0].Env, corev1.EnvVar{ - Name: envName, - ValueFrom: &corev1.EnvVarSource{ - FieldRef: &corev1.ObjectFieldSelector{ - APIVersion: "", - FieldPath: fieldPath, - }, - }, - }) - } -} - -func assertNotContainsMappings(t *testing.T, unexpectedMappingKeys []string, request *dtwebhook.MutationRequest) { - for _, env := range request.Pod.Spec.Containers[0].Env { - assert.NotContains(t, unexpectedMappingKeys, env.Name) - } -} diff --git a/pkg/webhook/mutation/pod/v1/oneagent/env.go b/pkg/webhook/mutation/pod/v1/oneagent/env.go new file mode 100644 index 0000000000..f742af3ca8 --- /dev/null +++ b/pkg/webhook/mutation/pod/v1/oneagent/env.go @@ -0,0 +1,17 @@ +package oneagent + +import ( + "github.com/Dynatrace/dynatrace-operator/pkg/consts" + corev1 "k8s.io/api/core/v1" +) + +func addInstallerInitEnvs(initContainer *corev1.Container, installer installerInfo) { + initContainer.Env = append(initContainer.Env, + corev1.EnvVar{Name: consts.AgentInstallerFlavorEnv, Value: installer.flavor}, + corev1.EnvVar{Name: consts.AgentInstallerTechEnv, Value: installer.technologies}, + corev1.EnvVar{Name: consts.AgentInstallPathEnv, Value: installer.installPath}, + corev1.EnvVar{Name: consts.AgentInstallerUrlEnv, Value: installer.installerURL}, + corev1.EnvVar{Name: consts.AgentInstallerVersionEnv, Value: installer.version}, + corev1.EnvVar{Name: consts.AgentInjectedEnv, Value: "true"}, + ) +} diff --git a/pkg/webhook/mutation/pod/v1/oneagent/env_test.go b/pkg/webhook/mutation/pod/v1/oneagent/env_test.go new file mode 100644 index 0000000000..3afa3ae553 --- /dev/null +++ b/pkg/webhook/mutation/pod/v1/oneagent/env_test.go @@ -0,0 +1,24 @@ +package oneagent + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" +) + +func TestAddInstallerInitEnvs(t *testing.T) { + t.Run("Add installer init env", func(t *testing.T) { + container := &corev1.Container{} + installerInfo := getTestInstallerInfo() + addInstallerInitEnvs(container, installerInfo) + require.Len(t, container.Env, expectedBaseInitContainerEnvCount) + assert.Equal(t, installerInfo.flavor, container.Env[0].Value) + assert.Equal(t, installerInfo.technologies, container.Env[1].Value) + assert.Equal(t, installerInfo.installPath, container.Env[2].Value) + assert.Equal(t, installerInfo.installerURL, container.Env[3].Value) + assert.Equal(t, installerInfo.version, container.Env[4].Value) + assert.Equal(t, "true", container.Env[5].Value) + }) +} diff --git a/pkg/webhook/mutation/pod/oneagent/mutator.go b/pkg/webhook/mutation/pod/v1/oneagent/mutator.go similarity index 65% rename from pkg/webhook/mutation/pod/oneagent/mutator.go rename to pkg/webhook/mutation/pod/v1/oneagent/mutator.go index ef8790066c..22e2a389ea 100644 --- a/pkg/webhook/mutation/pod/oneagent/mutator.go +++ b/pkg/webhook/mutation/pod/v1/oneagent/mutator.go @@ -7,30 +7,26 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/consts" "github.com/Dynatrace/dynatrace-operator/pkg/injection/namespace/initgeneration" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/env" - "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/volumes" - maputils "github.com/Dynatrace/dynatrace-operator/pkg/util/map" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/mounts" dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" + oacommon "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/common/oneagent" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" "sigs.k8s.io/controller-runtime/pkg/client" ) type Mutator struct { client client.Client apiReader client.Reader - image string clusterID string webhookNamespace string } var _ dtwebhook.PodMutator = &Mutator{} -func NewMutator(image, clusterID, webhookNamespace string, client client.Client, apiReader client.Reader) *Mutator { +func NewMutator(clusterID, webhookNamespace string, client client.Client, apiReader client.Reader) *Mutator { return &Mutator{ - image: image, clusterID: clusterID, webhookNamespace: webhookNamespace, client: client, @@ -39,27 +35,16 @@ func NewMutator(image, clusterID, webhookNamespace string, client client.Client, } func (mut *Mutator) Enabled(request *dtwebhook.BaseRequest) bool { - enabledOnPod := maputils.GetFieldBool(request.Pod.Annotations, dtwebhook.AnnotationOneAgentInject, request.DynaKube.FeatureAutomaticInjection()) - enabledOnDynakube := request.DynaKube.OneAgentNamespaceSelector() != nil - - matchesNamespaceSelector := true // if no namespace selector is configured, we just pass set this to true - - if request.DynaKube.OneAgentNamespaceSelector().Size() > 0 { - selector, _ := metav1.LabelSelectorAsSelector(request.DynaKube.OneAgentNamespaceSelector()) - - matchesNamespaceSelector = selector.Matches(labels.Set(request.Namespace.Labels)) - } - - return matchesNamespaceSelector && enabledOnPod && enabledOnDynakube + return oacommon.IsEnabled(request) } func (mut *Mutator) Injected(request *dtwebhook.BaseRequest) bool { - return maputils.GetFieldBool(request.Pod.Annotations, dtwebhook.AnnotationOneAgentInjected, false) + return oacommon.IsInjected(request) } func (mut *Mutator) Mutate(ctx context.Context, request *dtwebhook.MutationRequest) error { if ok, reason := mut.isInjectionPossible(request); !ok { - setNotInjectedAnnotations(request.Pod, reason) + oacommon.SetNotInjectedAnnotations(request.Pod, reason) return nil } @@ -75,7 +60,7 @@ func (mut *Mutator) Mutate(ctx context.Context, request *dtwebhook.MutationReque mut.configureInitContainer(request, installerInfo) mut.mutateUserContainers(request) addInjectionConfigVolumeMount(request.InstallContainer) - setInjectedAnnotation(request.Pod) + oacommon.SetInjectedAnnotation(request.Pod) return nil } @@ -87,6 +72,8 @@ func (mut *Mutator) Reinvoke(request *dtwebhook.ReinvocationRequest) bool { log.Info("reinvoking", "podName", request.PodName()) + oacommon.SetInjectedAnnotation(request.Pod) + return mut.reinvokeUserContainers(request) } @@ -119,23 +106,23 @@ func (mut *Mutator) isInjectionPossible(request *dtwebhook.MutationRequest) (boo dk := request.DynaKube - _, err := dk.TenantUUIDFromConnectionInfoStatus() + _, err := dk.TenantUUID() if err != nil { log.Info("tenant UUID is not available, OneAgent cannot be injected", "pod", request.PodName()) - reasons = append(reasons, EmptyTenantUUIDReason) + reasons = append(reasons, oacommon.EmptyTenantUUIDReason) } - if !dk.IsOneAgentCommunicationRouteClear() { + if !dk.OneAgent().IsCommunicationRouteClear() { log.Info("OneAgent communication route is not clear, OneAgent cannot be injected", "pod", request.PodName()) - reasons = append(reasons, EmptyConnectionInfoReason) + reasons = append(reasons, oacommon.EmptyConnectionInfoReason) } - if dk.CodeModulesVersion() == "" && dk.CodeModulesImage() == "" { + if dk.OneAgent().GetCodeModulesVersion() == "" && dk.OneAgent().GetCodeModulesImage() == "" { log.Info("information about the codemodules (version or image) is not available, OneAgent cannot be injected", "pod", request.PodName()) - reasons = append(reasons, UnknownCodeModuleReason) + reasons = append(reasons, oacommon.UnknownCodeModuleReason) } if len(reasons) > 0 { @@ -146,8 +133,8 @@ func (mut *Mutator) isInjectionPossible(request *dtwebhook.MutationRequest) (boo } func ContainerIsInjected(container corev1.Container) bool { - return env.IsIn(container.Env, dynatraceMetadataEnv) && - env.IsIn(container.Env, preloadEnv) && - volumes.IsIn(container.VolumeMounts, OneAgentBinVolumeName) && - volumes.IsIn(container.VolumeMounts, oneAgentShareVolumeName) + return env.IsIn(container.Env, oacommon.DynatraceMetadataEnv) && + env.IsIn(container.Env, oacommon.PreloadEnv) && + mounts.IsIn(container.VolumeMounts, OneAgentBinVolumeName) && + mounts.IsIn(container.VolumeMounts, oneAgentShareVolumeName) } diff --git a/pkg/webhook/mutation/pod/oneagent/mutator_test.go b/pkg/webhook/mutation/pod/v1/oneagent/mutator_test.go similarity index 89% rename from pkg/webhook/mutation/pod/oneagent/mutator_test.go rename to pkg/webhook/mutation/pod/v1/oneagent/mutator_test.go index 1b7aaf47fe..6aa91082f2 100644 --- a/pkg/webhook/mutation/pod/oneagent/mutator_test.go +++ b/pkg/webhook/mutation/pod/v1/oneagent/mutator_test.go @@ -8,10 +8,12 @@ import ( "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/communication" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/value" "github.com/Dynatrace/dynatrace-operator/pkg/api/status" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" "github.com/Dynatrace/dynatrace-operator/pkg/consts" dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" + oacommon "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/common/oneagent" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" @@ -33,7 +35,7 @@ const ( func TestEnabled(t *testing.T) { t.Run("turned off", func(t *testing.T) { mutator := createTestPodMutator(nil) - request := createTestMutationRequest(nil, map[string]string{dtwebhook.AnnotationOneAgentInject: "false"}, getTestNamespace(nil)) + request := createTestMutationRequest(nil, map[string]string{oacommon.AnnotationInject: "false"}, getTestNamespace(nil)) enabled := mutator.Enabled(request.BaseRequest) @@ -42,7 +44,7 @@ func TestEnabled(t *testing.T) { t.Run("on by default", func(t *testing.T) { mutator := createTestPodMutator(nil) request := createTestMutationRequest(nil, nil, getTestNamespace(nil)) - request.DynaKube.Spec.OneAgent.ApplicationMonitoring = &dynakube.ApplicationMonitoringSpec{} + request.DynaKube.Spec.OneAgent.ApplicationMonitoring = &oneagent.ApplicationMonitoringSpec{} enabled := mutator.Enabled(request.BaseRequest) @@ -60,7 +62,7 @@ func TestEnabled(t *testing.T) { t.Run("on with feature flag", func(t *testing.T) { mutator := createTestPodMutator(nil) request := createTestMutationRequest(nil, nil, getTestNamespace(nil)) - request.DynaKube.Spec.OneAgent.ApplicationMonitoring = &dynakube.ApplicationMonitoringSpec{} + request.DynaKube.Spec.OneAgent.ApplicationMonitoring = &oneagent.ApplicationMonitoringSpec{} request.DynaKube.Annotations = map[string]string{dynakube.AnnotationFeatureAutomaticInjection: "true"} enabled := mutator.Enabled(request.BaseRequest) @@ -92,7 +94,7 @@ func TestEnabled(t *testing.T) { func TestInjected(t *testing.T) { t.Run("already marked", func(t *testing.T) { mutator := createTestPodMutator(nil) - request := createTestMutationRequest(nil, map[string]string{dtwebhook.AnnotationOneAgentInjected: "true"}, getTestNamespace(nil)) + request := createTestMutationRequest(nil, map[string]string{oacommon.AnnotationInjected: "true"}, getTestNamespace(nil)) enabled := mutator.Injected(request.BaseRequest) @@ -206,13 +208,13 @@ func TestNoInjectionMutate(t *testing.T) { assert.Equal(t, initialInitContainers, request.Pod.Spec.InitContainers) assert.Len(t, request.Pod.Annotations, initialAnnotationsLen+2) // +2 == injected-annotation, reason-annotation - require.Contains(t, request.Pod.Annotations, dtwebhook.AnnotationOneAgentInjected) - require.Contains(t, request.Pod.Annotations, dtwebhook.AnnotationOneAgentReason) + require.Contains(t, request.Pod.Annotations, oacommon.AnnotationInjected) + require.Contains(t, request.Pod.Annotations, oacommon.AnnotationReason) - assert.Equal(t, "false", request.Pod.Annotations[dtwebhook.AnnotationOneAgentInjected]) - assert.Contains(t, request.Pod.Annotations[dtwebhook.AnnotationOneAgentReason], EmptyConnectionInfoReason) - assert.Contains(t, request.Pod.Annotations[dtwebhook.AnnotationOneAgentReason], EmptyTenantUUIDReason) - assert.Contains(t, request.Pod.Annotations[dtwebhook.AnnotationOneAgentReason], UnknownCodeModuleReason) + assert.Equal(t, "false", request.Pod.Annotations[oacommon.AnnotationInjected]) + assert.Contains(t, request.Pod.Annotations[oacommon.AnnotationReason], oacommon.EmptyConnectionInfoReason) + assert.Contains(t, request.Pod.Annotations[oacommon.AnnotationReason], oacommon.EmptyTenantUUIDReason) + assert.Contains(t, request.Pod.Annotations[oacommon.AnnotationReason], oacommon.UnknownCodeModuleReason) assert.Empty(t, request.InstallContainer.Env) assert.Empty(t, request.InstallContainer.VolumeMounts) @@ -250,7 +252,7 @@ func TestReinvoke(t *testing.T) { for index, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { mutator := createTestPodMutator([]client.Object{getTestInitSecret()}) - request := createTestReinvocationRequest(&testCases[index].dk, map[string]string{dtwebhook.AnnotationOneAgentInjected: "true"}) + request := createTestReinvocationRequest(&testCases[index].dk, map[string]string{oacommon.AnnotationInjected: "true"}) initialNumberOfContainerEnvsLen := len(request.Pod.Spec.Containers[0].Env) initialNumberOfVolumesLen := len(request.Pod.Spec.Volumes) @@ -275,7 +277,7 @@ func TestReinvoke(t *testing.T) { DynaKube: *getTestDynakube(), Pod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{dtwebhook.AnnotationOneAgentInjected: "true"}, + Annotations: map[string]string{oacommon.AnnotationInjected: "true"}, }, }, }, @@ -326,19 +328,19 @@ func injectionNotPossibleWithoutTenantUUID(t *testing.T) { ok, reason := mutator.isInjectionPossible(request) require.False(t, ok) - require.Contains(t, reason, EmptyTenantUUIDReason) + require.Contains(t, reason, oacommon.EmptyTenantUUIDReason) } func injectionNotPossibleWithoutCommunicationRoute(t *testing.T) { mutator := createTestPodMutator(nil) dk := getTestDynakube() - dk.Status.OneAgent.ConnectionInfoStatus.CommunicationHosts = []dynakube.CommunicationHostStatus{} + dk.Status.OneAgent.ConnectionInfoStatus.CommunicationHosts = []oneagent.CommunicationHostStatus{} request := createTestMutationRequest(dk, nil, getTestNamespace(nil)) ok, reason := mutator.isInjectionPossible(request) require.False(t, ok) - require.Contains(t, reason, EmptyConnectionInfoReason) + require.Contains(t, reason, oacommon.EmptyConnectionInfoReason) } func injectionNotPossibleWithoutCodeModulesVersion(t *testing.T) { @@ -350,30 +352,29 @@ func injectionNotPossibleWithoutCodeModulesVersion(t *testing.T) { ok, reason := mutator.isInjectionPossible(request) require.False(t, ok) - require.Contains(t, reason, UnknownCodeModuleReason) + require.Contains(t, reason, oacommon.UnknownCodeModuleReason) } func injectionNotPossibleWithMultipleIssues(t *testing.T) { mutator := createTestPodMutator(nil) dk := getTestDynakube() dk.Status.OneAgent.ConnectionInfoStatus.TenantUUID = "" - dk.Status.OneAgent.ConnectionInfoStatus.CommunicationHosts = []dynakube.CommunicationHostStatus{} + dk.Status.OneAgent.ConnectionInfoStatus.CommunicationHosts = []oneagent.CommunicationHostStatus{} dk.Status.CodeModules.VersionStatus.Version = "" request := createTestMutationRequest(dk, nil, getTestNamespace(nil)) ok, reason := mutator.isInjectionPossible(request) require.False(t, ok) - require.Contains(t, reason, EmptyTenantUUIDReason) - require.Contains(t, reason, EmptyConnectionInfoReason) - require.Contains(t, reason, UnknownCodeModuleReason) + require.Contains(t, reason, oacommon.EmptyTenantUUIDReason) + require.Contains(t, reason, oacommon.EmptyConnectionInfoReason) + require.Contains(t, reason, oacommon.UnknownCodeModuleReason) } func createTestPodMutator(objects []client.Object) *Mutator { return &Mutator{ client: fake.NewClient(objects...), apiReader: fake.NewClient(objects...), - image: testImage, clusterID: testClusterID, webhookNamespace: testNamespaceName, } @@ -389,7 +390,7 @@ func getTestInitSecret() *corev1.Secret { } func addNamespaceSelector(dk *dynakube.DynaKube) *dynakube.DynaKube { - dk.Spec.OneAgent.ApplicationMonitoring = &dynakube.ApplicationMonitoringSpec{} + dk.Spec.OneAgent.ApplicationMonitoring = &oneagent.ApplicationMonitoringSpec{} dk.Spec.OneAgent.ApplicationMonitoring.NamespaceSelector = metav1.LabelSelector{ MatchLabels: map[string]string{ @@ -427,8 +428,8 @@ func getTestCSIDynakube() *dynakube.DynaKube { return &dynakube.DynaKube{ ObjectMeta: getTestDynakubeMeta(), Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{}, + OneAgent: oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{}, }, }, Status: getTestDynakubeStatus(), @@ -444,7 +445,7 @@ func getTestReadOnlyCSIDynakube() *dynakube.DynaKube { func getTestNoInjectionDynakube() *dynakube.DynaKube { dk := getTestCSIDynakube() - dk.Status.OneAgent.ConnectionInfoStatus.CommunicationHosts = []dynakube.CommunicationHostStatus{} + dk.Status.OneAgent.ConnectionInfoStatus.CommunicationHosts = []oneagent.CommunicationHostStatus{} dk.Status.CodeModules.VersionStatus.Version = "" dk.Status.OneAgent.ConnectionInfoStatus.TenantUUID = "" @@ -455,8 +456,8 @@ func getTestDynakube() *dynakube.DynaKube { return &dynakube.DynaKube{ ObjectMeta: getTestDynakubeMeta(), Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - ApplicationMonitoring: &dynakube.ApplicationMonitoringSpec{}, + OneAgent: oneagent.Spec{ + ApplicationMonitoring: &oneagent.ApplicationMonitoringSpec{}, }, }, Status: getTestDynakubeStatus(), @@ -467,8 +468,8 @@ func getTestDynakubeWithContainerExclusion() *dynakube.DynaKube { dk := &dynakube.DynaKube{ ObjectMeta: getTestDynakubeMeta(), Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - ApplicationMonitoring: &dynakube.ApplicationMonitoringSpec{}, + OneAgent: oneagent.Spec{ + ApplicationMonitoring: &oneagent.ApplicationMonitoringSpec{}, }, }, Status: getTestDynakubeStatus(), @@ -480,12 +481,12 @@ func getTestDynakubeWithContainerExclusion() *dynakube.DynaKube { func getTestDynakubeStatus() dynakube.DynaKubeStatus { return dynakube.DynaKubeStatus{ - OneAgent: dynakube.OneAgentStatus{ - ConnectionInfoStatus: dynakube.OneAgentConnectionInfoStatus{ + OneAgent: oneagent.Status{ + ConnectionInfoStatus: oneagent.ConnectionInfoStatus{ ConnectionInfo: communication.ConnectionInfo{ TenantUUID: "test-tenant-uuid", }, - CommunicationHosts: []dynakube.CommunicationHostStatus{ + CommunicationHosts: []oneagent.CommunicationHostStatus{ { Protocol: "http", Host: "dummyhost", @@ -494,7 +495,7 @@ func getTestDynakubeStatus() dynakube.DynaKubeStatus { }, }, }, - CodeModules: dynakube.CodeModulesStatus{ + CodeModules: oneagent.CodeModulesStatus{ VersionStatus: status.VersionStatus{ Version: "test-version", }, diff --git a/pkg/webhook/mutation/pod/oneagent/volumes.go b/pkg/webhook/mutation/pod/v1/oneagent/volumes.go similarity index 96% rename from pkg/webhook/mutation/pod/oneagent/volumes.go rename to pkg/webhook/mutation/pod/v1/oneagent/volumes.go index dd21940128..aec44f69c1 100644 --- a/pkg/webhook/mutation/pod/oneagent/volumes.go +++ b/pkg/webhook/mutation/pod/v1/oneagent/volumes.go @@ -4,13 +4,13 @@ import ( "fmt" "path/filepath" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/consts" dtcsi "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi" csivolumes "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi/driver/volumes" appvolumes "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi/driver/volumes/app" - "github.com/Dynatrace/dynatrace-operator/pkg/util/address" corev1 "k8s.io/api/core/v1" + "k8s.io/utils/ptr" ) func (mut *Mutator) addVolumes(pod *corev1.Pod, dk dynakube.DynaKube) { @@ -161,10 +161,10 @@ func addVolumesForReadOnlyCSI(pod *corev1.Pod) { func getInstallerVolumeSource(dk dynakube.DynaKube) corev1.VolumeSource { volumeSource := corev1.VolumeSource{} - if dk.IsCSIAvailable() { + if dk.OneAgent().IsCSIAvailable() { volumeSource.CSI = &corev1.CSIVolumeSource{ Driver: dtcsi.DriverName, - ReadOnly: address.Of(dk.FeatureReadOnlyCsiVolume()), + ReadOnly: ptr.To(dk.FeatureReadOnlyCsiVolume()), VolumeAttributes: map[string]string{ csivolumes.CSIVolumeAttributeModeField: appvolumes.Mode, csivolumes.CSIVolumeAttributeDynakubeField: dk.Name, diff --git a/pkg/webhook/mutation/pod/oneagent/volumes_test.go b/pkg/webhook/mutation/pod/v1/oneagent/volumes_test.go similarity index 93% rename from pkg/webhook/mutation/pod/oneagent/volumes_test.go rename to pkg/webhook/mutation/pod/v1/oneagent/volumes_test.go index 0cd42a4912..7e5a553ca6 100644 --- a/pkg/webhook/mutation/pod/oneagent/volumes_test.go +++ b/pkg/webhook/mutation/pod/v1/oneagent/volumes_test.go @@ -4,11 +4,11 @@ import ( "path/filepath" "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" "github.com/Dynatrace/dynatrace-operator/pkg/consts" "github.com/Dynatrace/dynatrace-operator/pkg/util/installconfig" - "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/volumes" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/mounts" "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -39,7 +39,7 @@ func TestAddReadOnlyCSIVolumeMounts(t *testing.T) { require.Len(t, container.VolumeMounts, 3) for expectedVolumeName, expectedMountPath := range expectedMounts { - mount, err := volumes.GetVolumeMountByName(container.VolumeMounts, expectedVolumeName) + mount, err := mounts.GetByName(container.VolumeMounts, expectedVolumeName) require.NoError(t, err) require.NotNil(t, mount) assert.Equal(t, expectedMountPath, mount.MountPath) @@ -111,7 +111,7 @@ func TestAddInitVolumeMounts(t *testing.T) { addInitVolumeMounts(container, *getTestReadOnlyCSIDynakube()) require.Len(t, container.VolumeMounts, 3) - mount, err := volumes.GetVolumeMountByName(container.VolumeMounts, oneagentConfVolumeName) + mount, err := mounts.GetByName(container.VolumeMounts, oneagentConfVolumeName) require.NoError(t, err) assert.Equal(t, consts.AgentConfInitDirMount, mount.MountPath) }) diff --git a/pkg/webhook/mutation/pod/v1/webhook.go b/pkg/webhook/mutation/pod/v1/webhook.go new file mode 100644 index 0000000000..cd3b91512c --- /dev/null +++ b/pkg/webhook/mutation/pod/v1/webhook.go @@ -0,0 +1,148 @@ +package v1 + +import ( + "context" + + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/container" + dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" + "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/common/events" + "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/v1/metadata" + oamutation "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/v1/oneagent" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type Injector struct { + recorder events.EventRecorder + isContainerInjected func(corev1.Container) bool + webhookImage string + clusterID string + + mutators []dtwebhook.PodMutator +} + +var _ dtwebhook.PodInjector = &Injector{} + +func NewInjector(apiReader client.Reader, kubeClient, metaClient client.Client, recorder events.EventRecorder, clusterID, webhookPodImage, webhookNamespace string) *Injector { //nolint:revive + return &Injector{ + webhookImage: webhookPodImage, + recorder: recorder, + clusterID: clusterID, + mutators: []dtwebhook.PodMutator{ + oamutation.NewMutator( + clusterID, + webhookNamespace, + kubeClient, + apiReader, + ), + metadata.NewMutator( + webhookNamespace, + kubeClient, + apiReader, + metaClient, + ), + }, + isContainerInjected: containerIsInjected, + } +} + +func (wh *Injector) Handle(ctx context.Context, mutationRequest *dtwebhook.MutationRequest) error { + wh.recorder.Setup(mutationRequest) + + if wh.isInjected(mutationRequest) { + if wh.handlePodReinvocation(mutationRequest) { + log.Info("reinvocation policy applied", "podName", mutationRequest.PodName()) + wh.recorder.SendPodUpdateEvent() + } + + log.Info("no change, all containers already injected", "podName", mutationRequest.PodName()) + } else { + if err := wh.handlePodMutation(ctx, mutationRequest); err != nil { + return err + } + } + + setDynatraceInjectedAnnotation(mutationRequest) + + log.Info("injection finished for pod", "podName", mutationRequest.PodName(), "namespace", mutationRequest.Namespace.Name) + + return nil +} + +func (wh *Injector) isInjected(mutationRequest *dtwebhook.MutationRequest) bool { + for _, mutator := range wh.mutators { + if mutator.Injected(mutationRequest.BaseRequest) { + return true + } + } + + installContainer := container.FindInitContainerInPodSpec(&mutationRequest.Pod.Spec, dtwebhook.InstallContainerName) + if installContainer != nil { + log.Info("Dynatrace init-container already present, skipping mutation, doing reinvocation", "containerName", dtwebhook.InstallContainerName) + + return true + } + + return false +} + +func (wh *Injector) handlePodMutation(ctx context.Context, mutationRequest *dtwebhook.MutationRequest) error { + mutationRequest.InstallContainer = createInstallInitContainerBase(wh.webhookImage, wh.clusterID, mutationRequest.Pod, mutationRequest.DynaKube) + + _ = updateContainerInfo(mutationRequest.BaseRequest, mutationRequest.InstallContainer) + + var isMutated bool + + for _, mutator := range wh.mutators { + if !mutator.Enabled(mutationRequest.BaseRequest) { + continue + } + + if err := mutator.Mutate(ctx, mutationRequest); err != nil { + return err + } + + isMutated = true + } + + if !isMutated { + log.Info("no mutation is enabled") + + return nil + } + + addInitContainerToPod(mutationRequest.Pod, mutationRequest.InstallContainer) + wh.recorder.SendPodInjectEvent() + + return nil +} + +func (wh *Injector) handlePodReinvocation(mutationRequest *dtwebhook.MutationRequest) bool { + var needsUpdate bool + + reinvocationRequest := mutationRequest.ToReinvocationRequest() + + isMutated := updateContainerInfo(reinvocationRequest.BaseRequest, nil) + + if !isMutated { // == no new containers were detected, we only mutate new containers during reinvoke + return false + } + + for _, mutator := range wh.mutators { + if mutator.Enabled(mutationRequest.BaseRequest) { + if update := mutator.Reinvoke(reinvocationRequest); update { + needsUpdate = true + } + } + } + + return needsUpdate +} + +func setDynatraceInjectedAnnotation(mutationRequest *dtwebhook.MutationRequest) { + if mutationRequest.Pod.Annotations == nil { + mutationRequest.Pod.Annotations = make(map[string]string) + } + + mutationRequest.Pod.Annotations[dtwebhook.AnnotationDynatraceInjected] = "true" +} diff --git a/pkg/webhook/mutation/pod/v1/webhook_test.go b/pkg/webhook/mutation/pod/v1/webhook_test.go new file mode 100644 index 0000000000..88262876b2 --- /dev/null +++ b/pkg/webhook/mutation/pod/v1/webhook_test.go @@ -0,0 +1,494 @@ +package v1 + +import ( + "context" + "encoding/json" + "testing" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" + "github.com/Dynatrace/dynatrace-operator/pkg/consts" + "github.com/Dynatrace/dynatrace-operator/pkg/injection/startup" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/env" + dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" + "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/common/events" + "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/v1/metadata" + oamutation "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/v1/oneagent" + webhookmock "github.com/Dynatrace/dynatrace-operator/test/mocks/pkg/webhook" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/record" + "k8s.io/utils/ptr" +) + +const ( + testImage = "test-image" + testNamespaceName = "test-namespace" + testClusterID = "test-cluster-id" + testPodName = "test-pod" + testDynakubeName = "test-dynakube" +) + +var testResourceRequirements = corev1.ResourceRequirements{ + Limits: map[corev1.ResourceName]resource.Quantity{ + corev1.ResourceCPU: resource.MustParse("100m"), + corev1.ResourceMemory: resource.MustParse("100Mi"), + }, +} + +func TestInjector(t *testing.T) { + t.Run("not yet injected => mutate", func(t *testing.T) { + ctx := context.Background() + + injector := createTestInjector([]dtwebhook.PodMutator{createSimplePodMutatorMock(t), createSimplePodMutatorMock(t)}, false) + request := createTestMutationRequest(getTestDynakube()) + + err := injector.Handle(ctx, request) + require.NoError(t, err) + + for _, mutator := range injector.mutators { + assertMutateCalls(t, mutator, 1) + } + }) + + t.Run("fail => error", func(t *testing.T) { + ctx := context.Background() + + injector := createTestInjector([]dtwebhook.PodMutator{createFailPodMutatorMock(t)}, false) + request := createTestMutationRequest(getTestDynakube()) + + err := injector.Handle(ctx, request) + require.Error(t, err) + + for _, mutator := range injector.mutators { + assertMutateCalls(t, mutator, 1) + } + }) + + t.Run("already injected => reinvoke", func(t *testing.T) { + ctx := context.Background() + + injector := createTestInjector([]dtwebhook.PodMutator{createAlreadyInjectedPodMutatorMock(t), createAlreadyInjectedPodMutatorMock(t)}, true) + request := createTestMutationRequestWithInjectedPod(getTestDynakube()) + + err := injector.Handle(ctx, request) + require.NoError(t, err) + + for _, mutator := range injector.mutators { + assertReinvokeCalls(t, mutator, 1) + } + }) +} + +func TestHandlePodMutation(t *testing.T) { + t.Run("should call both mutators, initContainer and annotation added, no error", func(t *testing.T) { + mutator1 := createSimplePodMutatorMock(t) + mutator2 := createSimplePodMutatorMock(t) + dk := getTestDynakube() + podWebhook := createTestInjector([]dtwebhook.PodMutator{mutator1, mutator2}, false) + mutationRequest := createTestMutationRequest(dk) + podWebhook.recorder.Setup(mutationRequest) + + err := podWebhook.handlePodMutation(context.Background(), mutationRequest) + require.NoError(t, err) + assert.NotNil(t, mutationRequest.InstallContainer) + + require.Len(t, mutationRequest.Pod.Spec.InitContainers, 2) + + assertContainersInfo(t, mutationRequest.ToReinvocationRequest(), &mutationRequest.Pod.Spec.InitContainers[1]) + + initSecurityContext := mutationRequest.Pod.Spec.InitContainers[1].SecurityContext + require.NotNil(t, initSecurityContext) + + require.NotNil(t, initSecurityContext.Privileged) + assert.False(t, *initSecurityContext.Privileged) + + require.NotNil(t, initSecurityContext.AllowPrivilegeEscalation) + assert.False(t, *initSecurityContext.AllowPrivilegeEscalation) + + require.NotNil(t, initSecurityContext.ReadOnlyRootFilesystem) + assert.True(t, *initSecurityContext.ReadOnlyRootFilesystem) + + assert.NotNil(t, initSecurityContext.RunAsNonRoot) + assert.True(t, *initSecurityContext.RunAsNonRoot) + + assert.Equal(t, mutationRequest.Pod.Spec.InitContainers[1].Resources, testResourceRequirements) + mutator1.AssertCalled(t, "Enabled", mutationRequest.BaseRequest) + mutator1.AssertCalled(t, "Mutate", mock.Anything, mutationRequest) + mutator2.AssertCalled(t, "Enabled", mutationRequest.BaseRequest) + mutator2.AssertCalled(t, "Mutate", mock.Anything, mutationRequest) + }) + t.Run("should call 1 webhook, 1 error, no initContainer and annotation", func(t *testing.T) { + sadMutator := createFailPodMutatorMock(t) + emptyMutator := webhookmock.NewPodMutator(t) + dk := getTestDynakube() + podWebhook := createTestInjector([]dtwebhook.PodMutator{sadMutator, emptyMutator}, false) + mutationRequest := createTestMutationRequest(dk) + podWebhook.recorder.Setup(mutationRequest) + + err := podWebhook.handlePodMutation(context.Background(), mutationRequest) + require.Error(t, err) + assert.NotNil(t, mutationRequest.InstallContainer) + assert.Len(t, mutationRequest.Pod.Spec.InitContainers, 1) + sadMutator.AssertCalled(t, "Enabled", mutationRequest.BaseRequest) + sadMutator.AssertCalled(t, "Mutate", mock.Anything, mutationRequest) + }) +} + +func TestHandlePodReinvocation(t *testing.T) { + t.Run("should call both mutators, updated == true", func(t *testing.T) { + mutator1 := createAlreadyInjectedPodMutatorMock(t) + mutator2 := createAlreadyInjectedPodMutatorMock(t) + dk := getTestDynakube() + podWebhook := createTestInjector([]dtwebhook.PodMutator{mutator1, mutator2}, true) + mutationRequest := createTestMutationRequestWithInjectedPod(dk) + podWebhook.recorder.Setup(mutationRequest) + + updated := podWebhook.handlePodReinvocation(mutationRequest) + require.True(t, updated) + + require.Len(t, mutationRequest.Pod.Spec.InitContainers, 2) + assertContainersInfo(t, mutationRequest.ToReinvocationRequest(), &mutationRequest.Pod.Spec.InitContainers[1]) + + mutator1.AssertCalled(t, "Enabled", mutationRequest.BaseRequest) + mutator1.AssertCalled(t, "Reinvoke", mutationRequest.ToReinvocationRequest()) + mutator2.AssertCalled(t, "Enabled", mutationRequest.BaseRequest) + mutator2.AssertCalled(t, "Reinvoke", mutationRequest.ToReinvocationRequest()) + }) + t.Run("should call both webhook, only 1 update, updated == true", func(t *testing.T) { + noUpdateMutator := createNoUpdatePodMutatorMock(t) + workingMutator := createAlreadyInjectedPodMutatorMock(t) + dk := getTestDynakube() + podWebhook := createTestInjector([]dtwebhook.PodMutator{noUpdateMutator, workingMutator}, true) + mutationRequest := createTestMutationRequestWithInjectedPod(dk) + podWebhook.recorder.Setup(mutationRequest) + + updated := podWebhook.handlePodReinvocation(mutationRequest) + require.True(t, updated) + noUpdateMutator.AssertCalled(t, "Enabled", mutationRequest.BaseRequest) + noUpdateMutator.AssertCalled(t, "Reinvoke", mutationRequest.ToReinvocationRequest()) + workingMutator.AssertCalled(t, "Enabled", mutationRequest.BaseRequest) + workingMutator.AssertCalled(t, "Reinvoke", mutationRequest.ToReinvocationRequest()) + }) + t.Run("should call webhook, no update", func(t *testing.T) { + noUpdateMutator := createNoUpdatePodMutatorMock(t) + dk := getTestDynakube() + podWebhook := createTestInjector([]dtwebhook.PodMutator{noUpdateMutator}, true) + mutationRequest := createTestMutationRequestWithInjectedPod(dk) + podWebhook.recorder.Setup(mutationRequest) + + updated := podWebhook.handlePodReinvocation(mutationRequest) + require.False(t, updated) + noUpdateMutator.AssertCalled(t, "Enabled", mutationRequest.BaseRequest) + noUpdateMutator.AssertCalled(t, "Reinvoke", mutationRequest.ToReinvocationRequest()) + }) +} + +func assertContainersInfo(t *testing.T, request *dtwebhook.ReinvocationRequest, installContainer *corev1.Container) { + rawContainerInfo := env.FindEnvVar(installContainer.Env, consts.ContainerInfoEnv) + require.NotNil(t, rawContainerInfo) + + var containerInfo []startup.ContainerInfo + err := json.Unmarshal([]byte(rawContainerInfo.Value), &containerInfo) + require.NoError(t, err) + + for _, container := range request.Pod.Spec.Containers { + found := false + + for _, info := range containerInfo { + if container.Name == info.Name { + assert.Equal(t, container.Image, info.Image) + + found = true + + break + } + } + + require.True(t, found) + } +} + +func assertMutateCalls(t *testing.T, mutator dtwebhook.PodMutator, expectedCalls int) { + mock, ok := mutator.(*webhookmock.PodMutator) + if !ok { + t.Fatalf("assertPodMutatorCalls: webhook is not a mock") + } + + mock.AssertNumberOfCalls(t, "Mutate", expectedCalls) +} + +func assertReinvokeCalls(t *testing.T, mutator dtwebhook.PodMutator, expectedCalls int) { + mock, ok := mutator.(*webhookmock.PodMutator) + if !ok { + t.Fatalf("assertPodMutatorCalls: webhook is not a mock") + } + + mock.AssertNumberOfCalls(t, "Reinvoke", expectedCalls) +} + +func createTestInjector(mutators []dtwebhook.PodMutator, isContainerInjected bool) *Injector { + return &Injector{ + recorder: events.NewRecorder(record.NewFakeRecorder(10)), + webhookImage: testImage, + clusterID: testClusterID, + mutators: mutators, + isContainerInjected: func(c corev1.Container) bool { return isContainerInjected }, + } +} + +func createSimplePodMutatorMock(t *testing.T) *webhookmock.PodMutator { + t.Helper() + + mutator := webhookmock.NewPodMutator(t) + mutator.On("Injected", mock.Anything).Return(false).Maybe() // It is a Maybe, because it is only checked at the very beginning + mutator.On("Enabled", mock.Anything).Return(true) + mutator.On("Mutate", mock.Anything, mock.Anything).Return(nil) + + return mutator +} + +func createAlreadyInjectedPodMutatorMock(t *testing.T) *webhookmock.PodMutator { + t.Helper() + + mutator := webhookmock.NewPodMutator(t) + mutator.On("Injected", mock.Anything).Return(true).Maybe() // It is a Maybe, because if there are multiple mutators, the first "Injected" that returns true will break the loop -> Reinvoke + mutator.On("Enabled", mock.Anything).Return(true) + mutator.On("Reinvoke", mock.Anything).Return(true) + + return mutator +} + +func createNoUpdatePodMutatorMock(t *testing.T) *webhookmock.PodMutator { + t.Helper() + + mutator := webhookmock.NewPodMutator(t) + mutator.On("Injected", mock.Anything).Return(true).Maybe() // It is a Maybe, because if there are multiple mutators, the first "Injected" that returns true will break the loop -> Reinvoke + mutator.On("Enabled", mock.Anything).Return(true) + mutator.On("Reinvoke", mock.Anything).Return(false) + + return mutator +} + +func createFailPodMutatorMock(t *testing.T) *webhookmock.PodMutator { + t.Helper() + + mutator := webhookmock.NewPodMutator(t) + mutator.On("Injected", mock.Anything).Return(false).Maybe() // It is a Maybe, because it is only checked at the very beginning + mutator.On("Enabled", mock.Anything).Return(true) + mutator.On("Mutate", mock.Anything, mock.Anything).Return(errors.New("BOOM")) + + return mutator +} + +func getTestDynakube() *dynakube.DynaKube { + return &dynakube.DynaKube{ + ObjectMeta: getTestDynakubeMeta(), + Spec: dynakube.DynaKubeSpec{ + OneAgent: getCloudNativeSpec(&testResourceRequirements), + }, + } +} + +func getTestDynakubeNoInitLimits() *dynakube.DynaKube { + return &dynakube.DynaKube{ + ObjectMeta: getTestDynakubeMeta(), + Spec: dynakube.DynaKubeSpec{ + OneAgent: getCloudNativeSpec(nil), + }, + } +} + +func getTestDynakubeDefaultAppMon() *dynakube.DynaKube { + return &dynakube.DynaKube{ + ObjectMeta: getTestDynakubeMeta(), + Spec: dynakube.DynaKubeSpec{ + OneAgent: oneagent.Spec{ + ApplicationMonitoring: &oneagent.ApplicationMonitoringSpec{}, + }, + }, + } +} + +func getTestDynakubeMeta() metav1.ObjectMeta { + return metav1.ObjectMeta{ + Name: testDynakubeName, + Namespace: testNamespaceName, + } +} + +func getCloudNativeSpec(initResources *corev1.ResourceRequirements) oneagent.Spec { + return oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{ + AppInjectionSpec: oneagent.AppInjectionSpec{ + InitResources: initResources, + }, + }, + } +} + +func getTestPod() *corev1.Pod { + return &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: testPodName, + Namespace: testNamespaceName, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "container", + Image: "alpine", + SecurityContext: getTestSecurityContext(), + }, + }, + InitContainers: []corev1.Container{ + { + Name: "init-container", + Image: "alpine", + }, + }, + Volumes: []corev1.Volume{ + { + Name: "volume", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + }, + }, + } +} + +const testUser int64 = 420 + +func getTestSecurityContext() *corev1.SecurityContext { + return &corev1.SecurityContext{ + RunAsUser: ptr.To(testUser), + RunAsGroup: ptr.To(testUser), + } +} + +func createTestMutationRequest(dk *dynakube.DynaKube) *dtwebhook.MutationRequest { + return dtwebhook.NewMutationRequest(context.Background(), *getTestNamespace(), nil, getTestPod(), *dk) +} + +func createTestMutationRequestWithInjectedPod(dk *dynakube.DynaKube) *dtwebhook.MutationRequest { + return dtwebhook.NewMutationRequest(context.Background(), *getTestNamespace(), nil, getInjectedPod(), *dk) +} + +func getInjectedPod() *corev1.Pod { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: testPodName, + Namespace: testNamespaceName, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "container", + Image: "alpine", + SecurityContext: getTestSecurityContext(), + }, + }, + InitContainers: []corev1.Container{ + { + Name: "init-container", + Image: "alpine", + }, + }, + Volumes: []corev1.Volume{ + { + Name: "volume", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + }, + }, + } + installContainer := createInstallInitContainerBase("test", "test", pod, *getTestDynakube()) + pod.Spec.InitContainers = append(pod.Spec.InitContainers, *installContainer) + + return pod +} + +func getTestNamespace() *corev1.Namespace { + return &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: testNamespaceName, + Labels: map[string]string{ + dtwebhook.InjectionInstanceLabel: testDynakubeName, + }, + }, + } +} + +// TestDoubleInjection is special test case for making sure that we do not inject the init-container 2 times incase 1 of the mutators are skipped. +// The mutators are intentionally NOT mocked, as to mock them properly for this scenario you would need to basically reimplement them in the mock. +// This test is necessary as the current interface is not ready to handle the scenario properly. +// Scenario: OneAgent mutation is Enabled however needs to be skipped due to not meeting the requirements, so it needs to annotate but not fully inject +func TestDoubleInjection(t *testing.T) { + noCommunicationHostDK := getTestDynakube() + fakeClient := fake.NewClient(noCommunicationHostDK, getTestNamespace()) + podWebhook := &Injector{ + recorder: events.NewRecorder(record.NewFakeRecorder(10)), + webhookImage: testImage, + clusterID: testClusterID, + mutators: []dtwebhook.PodMutator{ + oamutation.NewMutator( + testImage, + testClusterID, + fakeClient, + fakeClient, + ), + metadata.NewMutator( + testNamespaceName, + fakeClient, + fakeClient, + fakeClient, + ), + }, + } + + pod := getTestPod() + + request := createTestMutationRequest(noCommunicationHostDK) + request.Pod = pod + + // simulate initial mutation, annotations + init-container <== skip in case on communication hosts + err := podWebhook.Handle(context.Background(), request) + require.NoError(t, err) + require.NotNil(t, findInstallContainer(pod.Spec.InitContainers)) + + // adding communicationHost to the dynakube to make the scenario more complicated + // it shouldn't try to mutate the pod because now it could be enabled, that is just asking for trouble. + communicationHostDK := getTestDynakube() + communicationHostDK.Status.OneAgent.ConnectionInfoStatus.CommunicationHosts = []oneagent.CommunicationHostStatus{{Host: "test"}} + fakeClient = fake.NewClient(communicationHostDK, getTestNamespace()) + podWebhook.mutators = []dtwebhook.PodMutator{ + oamutation.NewMutator( + testImage, + testClusterID, + fakeClient, + fakeClient, + ), + metadata.NewMutator( + testNamespaceName, + fakeClient, + fakeClient, + fakeClient, + ), + } + + // simulate a Reinvocation + request = createTestMutationRequest(noCommunicationHostDK) + request.Pod = pod + err = podWebhook.Handle(context.Background(), request) + require.NoError(t, err) +} diff --git a/pkg/webhook/mutation/pod/v2/attributes.go b/pkg/webhook/mutation/pod/v2/attributes.go new file mode 100644 index 0000000000..200d52fed1 --- /dev/null +++ b/pkg/webhook/mutation/pod/v2/attributes.go @@ -0,0 +1,116 @@ +package v2 + +import ( + "fmt" + "strings" + + containerattr "github.com/Dynatrace/dynatrace-bootstrapper/cmd/configure/attributes/container" + podattr "github.com/Dynatrace/dynatrace-bootstrapper/cmd/configure/attributes/pod" + "github.com/Dynatrace/dynatrace-operator/pkg/consts" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/env" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/mounts" + dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" + "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/v2/common/volumes" + "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/v2/metadata" + corev1 "k8s.io/api/core/v1" +) + +func (wh *Injector) addPodAttributes(request *dtwebhook.MutationRequest) error { + attrs := podattr.Attributes{ + PodInfo: podattr.PodInfo{ + PodName: createEnvVarRef(consts.K8sPodNameEnv), + PodUID: createEnvVarRef(consts.K8sPodUIDEnv), + NodeName: createEnvVarRef(consts.K8sNodeNameEnv), + NamespaceName: request.Pod.Namespace, + }, + ClusterInfo: podattr.ClusterInfo{ + ClusterUID: request.DynaKube.Status.KubeSystemUUID, + DTClusterEntity: request.DynaKube.Status.KubernetesClusterMEID, + ClusterName: request.DynaKube.Status.KubernetesClusterName, + }, + } + + envs := []corev1.EnvVar{ + {Name: consts.K8sPodNameEnv, ValueFrom: env.NewEnvVarSourceForField("metadata.name")}, + {Name: consts.K8sPodUIDEnv, ValueFrom: env.NewEnvVarSourceForField("metadata.uid")}, + {Name: consts.K8sNodeNameEnv, ValueFrom: env.NewEnvVarSourceForField("spec.nodeName")}, + } + + request.InstallContainer.Env = append(request.InstallContainer.Env, envs...) + + err := metadata.Mutate(wh.metaClient, request, &attrs) + if err != nil { + return err + } + + args, err := podattr.ToArgs(attrs) + if err != nil { + return err + } + + request.InstallContainer.Args = append(request.InstallContainer.Args, args...) + + return nil +} + +func createEnvVarRef(envName string) string { + return fmt.Sprintf("$(%s)", envName) +} + +func addContainerAttributes(request *dtwebhook.MutationRequest) error { + attributes := []containerattr.Attributes{} + for _, c := range request.NewContainers(isInjected) { + attributes = append(attributes, containerattr.Attributes{ + ImageInfo: createImageInfo(c.Image), + ContainerName: c.Name, + }) + } + + if len(attributes) > 0 { + args, err := containerattr.ToArgs(attributes) + if err != nil { + return err + } + + request.InstallContainer.Args = append(request.InstallContainer.Args, args...) + } + + return nil +} + +func isInjected(container corev1.Container) bool { + return mounts.IsIn(container.VolumeMounts, volumes.ConfigVolumeName) +} + +func createImageInfo(imageURI string) containerattr.ImageInfo { // TODO: move to bootstrapper repo + // can't use the name.ParseReference() as that will fill in some defaults if certain things are defined, but we want to preserve the original string value, without any modification. Tried it with a regexp, was worse. + imageInfo := containerattr.ImageInfo{} + + repoPart := "" + + registrySplit := strings.SplitN(imageURI, "/", 2) + if len(registrySplit) == 1 { + repoPart = registrySplit[0] + } else if len(registrySplit) == 2 { + imageInfo.Registry = registrySplit[0] + repoPart = registrySplit[1] + } + + digestSplit := strings.SplitN(repoPart, "@", 2) + if len(digestSplit) == 1 { + repoPart = digestSplit[0] + } else if len(digestSplit) == 2 { + imageInfo.ImageDigest = digestSplit[1] + repoPart = digestSplit[0] + } + + tagSplit := strings.SplitN(repoPart, ":", 2) + if len(tagSplit) == 1 { + imageInfo.Repository = tagSplit[0] + } else if len(tagSplit) == 2 { + imageInfo.Tag = tagSplit[1] + imageInfo.Repository = tagSplit[0] + } + + return imageInfo +} diff --git a/pkg/webhook/mutation/pod/v2/attributes_test.go b/pkg/webhook/mutation/pod/v2/attributes_test.go new file mode 100644 index 0000000000..b2e8367782 --- /dev/null +++ b/pkg/webhook/mutation/pod/v2/attributes_test.go @@ -0,0 +1,442 @@ +package v2 + +import ( + "encoding/json" + "strings" + "testing" + + containerattr "github.com/Dynatrace/dynatrace-bootstrapper/cmd/configure/attributes/container" + podattr "github.com/Dynatrace/dynatrace-bootstrapper/cmd/configure/attributes/pod" + "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/consts" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/env" + dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" + metacommon "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/common/metadata" + "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/v2/common/volumes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" +) + +func TestAddPodAttributes(t *testing.T) { + validateAttributes := func(t *testing.T, request dtwebhook.MutationRequest) podattr.Attributes { + t.Helper() + + require.NotEmpty(t, request.InstallContainer.Args) + + rawArgs := []string{} + + for _, arg := range request.InstallContainer.Args { + splitArg := strings.SplitN(arg, "=", 2) + require.Len(t, splitArg, 2) + rawArgs = append(rawArgs, splitArg[1]) + } + + attr, err := podattr.ParseAttributes(rawArgs) + require.NoError(t, err) + + assert.Equal(t, request.DynaKube.Status.KubernetesClusterMEID, attr.DTClusterEntity) + assert.Equal(t, request.DynaKube.Status.KubernetesClusterName, attr.ClusterName) + assert.Equal(t, request.DynaKube.Status.KubeSystemUUID, attr.ClusterUID) + assert.Contains(t, attr.PodName, consts.K8sPodNameEnv) + assert.Contains(t, attr.PodUID, consts.K8sPodUIDEnv) + assert.Contains(t, attr.NodeName, consts.K8sNodeNameEnv) + assert.Equal(t, request.Pod.Namespace, attr.NamespaceName) + + require.Len(t, request.InstallContainer.Env, 3) + assert.NotNil(t, env.FindEnvVar(request.InstallContainer.Env, consts.K8sPodNameEnv)) + assert.NotNil(t, env.FindEnvVar(request.InstallContainer.Env, consts.K8sPodUIDEnv)) + assert.NotNil(t, env.FindEnvVar(request.InstallContainer.Env, consts.K8sNodeNameEnv)) + + return attr + } + + validateAdditionAttributes := func(t *testing.T, request dtwebhook.MutationRequest) { + t.Helper() + + attr := validateAttributes(t, request) + + require.NotEmpty(t, request.Pod.OwnerReferences) + assert.Equal(t, strings.ToLower(request.Pod.OwnerReferences[0].Kind), attr.WorkloadKind) + assert.Equal(t, request.Pod.OwnerReferences[0].Name, attr.WorkloadName) + + metaAnnotationCount := 0 + + for key := range request.Namespace.Annotations { + if strings.Contains(key, metacommon.AnnotationPrefix) { + metaAnnotationCount++ + } + } + + assert.Len(t, attr.UserDefined, metaAnnotationCount) + require.Len(t, request.Pod.Annotations, 3+metaAnnotationCount) + assert.Equal(t, strings.ToLower(request.Pod.OwnerReferences[0].Kind), request.Pod.Annotations[metacommon.AnnotationWorkloadKind]) + assert.Equal(t, request.Pod.OwnerReferences[0].Name, request.Pod.Annotations[metacommon.AnnotationWorkloadName]) + assert.Equal(t, "true", request.Pod.Annotations[metacommon.AnnotationInjected]) + } + + t.Run("add attributes and related envs, do not change pod or app-container", func(t *testing.T) { + injector := createTestInjectorBase() + + initContainer := corev1.Container{ + Args: []string{}, + } + pod := corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + }, + } + + expectedPod := pod.DeepCopy() + + request := dtwebhook.MutationRequest{ + BaseRequest: &dtwebhook.BaseRequest{ + Pod: &pod, + DynaKube: dynakube.DynaKube{ + Status: dynakube.DynaKubeStatus{ + KubernetesClusterMEID: "meid", + KubeSystemUUID: "systemuuid", + KubernetesClusterName: "meidname", + }, + }, + }, + InstallContainer: &initContainer, + } + + err := injector.addPodAttributes(&request) + require.NoError(t, err) + + require.Equal(t, *expectedPod, *request.BaseRequest.Pod) + validateAttributes(t, request) + }) + + t.Run("metadata enrichment passes => additional args and annotations", func(t *testing.T) { + initContainer := corev1.Container{ + Args: []string{}, + } + pod := corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + OwnerReferences: []metav1.OwnerReference{ + { + Name: "owner", + APIVersion: "v1", + Kind: "ReplicationController", + Controller: ptr.To(true), + }, + }, + }, + } + owner := corev1.ReplicationController{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "ReplicationController", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "owner", + }, + } + injector := createTestInjectorBase() + injector.metaClient = fake.NewClient(&owner, &pod) + + expectedPod := pod.DeepCopy() + + request := dtwebhook.MutationRequest{ + BaseRequest: &dtwebhook.BaseRequest{ + Pod: &pod, + DynaKube: dynakube.DynaKube{ + Spec: dynakube.DynaKubeSpec{ + MetadataEnrichment: dynakube.MetadataEnrichment{ + Enabled: ptr.To(true), + }, + }, + Status: dynakube.DynaKubeStatus{ + KubernetesClusterMEID: "meid", + KubeSystemUUID: "systemuuid", + KubernetesClusterName: "meidname", + }, + }, + }, + InstallContainer: &initContainer, + } + + err := injector.addPodAttributes(&request) + require.NoError(t, err) + require.NotEqual(t, *expectedPod, *request.BaseRequest.Pod) + + validateAdditionAttributes(t, request) + }) + + t.Run("metadata enrichment fails => error", func(t *testing.T) { + injector := createTestInjectorBase() + injector.metaClient = fake.NewClient() + + initContainer := corev1.Container{ + Args: []string{}, + } + pod := corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + OwnerReferences: []metav1.OwnerReference{ + { + Name: "owner", + APIVersion: "v1", + Kind: "ReplicationController", + Controller: ptr.To(true), + }, + }, + }, + } + + expectedPod := pod.DeepCopy() + + request := dtwebhook.MutationRequest{ + BaseRequest: &dtwebhook.BaseRequest{ + Pod: &pod, + Namespace: corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + metacommon.AnnotationPrefix + "/test": "test", + }, + }, + }, + DynaKube: dynakube.DynaKube{ + Spec: dynakube.DynaKubeSpec{ + MetadataEnrichment: dynakube.MetadataEnrichment{ + Enabled: ptr.To(true), + }, + }, + Status: dynakube.DynaKubeStatus{ + KubernetesClusterMEID: "meid", + KubeSystemUUID: "systemuuid", + KubernetesClusterName: "meidname", + }, + }, + }, + InstallContainer: &initContainer, + } + + err := injector.addPodAttributes(&request) + require.Error(t, err) + require.Equal(t, *expectedPod, *request.BaseRequest.Pod) + }) +} + +func TestAddContainerAttributes(t *testing.T) { + validateContainerAttributes := func(t *testing.T, pod corev1.Pod, args []string) { + t.Helper() + + require.NotEmpty(t, args) + + for _, arg := range args { + splitArg := strings.Split(arg, "=") + require.Len(t, splitArg, 2) + + var attr containerattr.Attributes + + require.NoError(t, json.Unmarshal([]byte(splitArg[1]), &attr)) + assert.Contains(t, pod.Spec.Containers, corev1.Container{ + Name: attr.ContainerName, + Image: attr.ToURI(), + }) + } + } + + t.Run("add container-attributes, do not change pod or app-container", func(t *testing.T) { + app1Container := corev1.Container{ + Name: "app-1-name", + Image: "registry1.example.com/repository/image:tag", + } + app2Container := corev1.Container{ + Name: "app-2-name", + Image: "registry2.example.com/repository/image:tag", + } + initContainer := corev1.Container{ + Args: []string{}, + } + pod := corev1.Pod{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + app1Container, + app2Container, + }, + }, + } + + expectedPod := pod.DeepCopy() + + request := dtwebhook.MutationRequest{ + BaseRequest: &dtwebhook.BaseRequest{ + Pod: &pod, + }, + InstallContainer: &initContainer, + } + + addContainerAttributes(&request) + + require.Equal(t, *expectedPod, *request.BaseRequest.Pod) + + validateContainerAttributes(t, pod, initContainer.Args) + }) + + t.Run("no new container ==> no new arg", func(t *testing.T) { + app1Container := corev1.Container{ + Name: "app-1-name", + Image: "registry1.example.com/repository/image:tag", + } + volumes.AddConfigVolumeMount(&app1Container) + + app2Container := corev1.Container{ + Name: "app-2-name", + Image: "registry2.example.com/repository/image:tag", + } + volumes.AddConfigVolumeMount(&app2Container) + + initContainer := corev1.Container{ + Args: []string{}, + } + pod := corev1.Pod{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + app1Container, + app2Container, + }, + }, + } + + expectedPod := pod.DeepCopy() + + request := dtwebhook.MutationRequest{ + BaseRequest: &dtwebhook.BaseRequest{ + Pod: &pod, + }, + InstallContainer: &initContainer, + } + + addContainerAttributes(&request) + + require.Equal(t, *expectedPod, *request.BaseRequest.Pod) + require.Empty(t, initContainer.Args) + }) + + t.Run("partially new => only add new", func(t *testing.T) { + app1Container := corev1.Container{ + Name: "app-1-name", + Image: "registry1.example.com/repository/image:tag", + } + volumes.AddConfigVolumeMount(&app1Container) + + app2Container := corev1.Container{ + Name: "app-2-name", + Image: "registry2.example.com/repository/image:tag", + } + + initContainer := corev1.Container{ + Args: []string{}, + } + pod := corev1.Pod{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + app1Container, + app2Container, + }, + }, + } + + expectedPod := pod.DeepCopy() + + request := dtwebhook.MutationRequest{ + BaseRequest: &dtwebhook.BaseRequest{ + Pod: &pod, + }, + InstallContainer: &initContainer, + } + + addContainerAttributes(&request) + + require.Equal(t, *expectedPod, *request.BaseRequest.Pod) + require.Len(t, initContainer.Args, 1) + validateContainerAttributes(t, pod, initContainer.Args) + }) +} + +func TestCreateImageInfo(t *testing.T) { + type testCase struct { + title string + in string + out containerattr.ImageInfo + } + + testCases := []testCase{ + { + title: "empty URI", + in: "", + out: containerattr.ImageInfo{}, + }, + { + title: "URI with tag", + in: "registry.example.com/repository/image:tag", + out: containerattr.ImageInfo{ + Registry: "registry.example.com", + Repository: "repository/image", + Tag: "tag", + ImageDigest: "", + }, + }, + { + title: "URI with digest", + in: "registry.example.com/repository/image@sha256:7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc", + out: containerattr.ImageInfo{ + Registry: "registry.example.com", + Repository: "repository/image", + Tag: "", + ImageDigest: "sha256:7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc", + }, + }, + { + title: "URI with digest and tag", + in: "registry.example.com/repository/image:tag@sha256:7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc", + out: containerattr.ImageInfo{ + Registry: "registry.example.com", + Repository: "repository/image", + Tag: "tag", + ImageDigest: "sha256:7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc", + }, + }, + { + title: "URI with missing tag", + in: "registry.example.com/repository/image", + out: containerattr.ImageInfo{ + Registry: "registry.example.com", + Repository: "repository/image", + }, + }, + { + title: "URI with docker.io (special case in certain libraries)", + in: "docker.io/php:fpm-stretch", + out: containerattr.ImageInfo{ + Registry: "docker.io", + Repository: "php", + Tag: "fpm-stretch", + }, + }, + { + title: "URI with missing registry", + in: "php:fpm-stretch", + out: containerattr.ImageInfo{ + Repository: "php", + Tag: "fpm-stretch", + }, + }, + } + for _, test := range testCases { + t.Run(test.title, func(t *testing.T) { + imageInfo := createImageInfo(test.in) + + require.Equal(t, test.out, imageInfo) + }) + } +} diff --git a/pkg/webhook/mutation/pod/v2/common/arg/arg.go b/pkg/webhook/mutation/pod/v2/common/arg/arg.go new file mode 100644 index 0000000000..0098e23e99 --- /dev/null +++ b/pkg/webhook/mutation/pod/v2/common/arg/arg.go @@ -0,0 +1,25 @@ +package arg + +import "fmt" + +type Arg struct { + Name string + Value string +} + +func (a Arg) String() string { + if a.Value == "" { + return "--" + a.Name + } + + return fmt.Sprintf("--%s=%s", a.Name, a.Value) +} + +func ConvertArgsToStrings(args []Arg) []string { + convertedArgs := make([]string, len(args)) + for i, arg := range args { + convertedArgs[i] = arg.String() + } + + return convertedArgs +} diff --git a/pkg/webhook/mutation/pod/v2/common/arg/arg_test.go b/pkg/webhook/mutation/pod/v2/common/arg/arg_test.go new file mode 100644 index 0000000000..e1c7a7c05b --- /dev/null +++ b/pkg/webhook/mutation/pod/v2/common/arg/arg_test.go @@ -0,0 +1,36 @@ +package arg + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestArgFunctions(t *testing.T) { + t.Run("ArgStringFormat", func(t *testing.T) { + arg := Arg{Name: "key", Value: "value"} + expected := "--key=value" + assert.Equal(t, expected, arg.String()) + }) + + t.Run("ConvertArgsToStringsEmpty", func(t *testing.T) { + args := []Arg{} + expected := []string{} + assert.Equal(t, expected, ConvertArgsToStrings(args)) + }) + + t.Run("ConvertArgsToStringsSingleArg", func(t *testing.T) { + args := []Arg{{Name: "key", Value: "value"}} + expected := []string{"--key=value"} + assert.Equal(t, expected, ConvertArgsToStrings(args)) + }) + + t.Run("ConvertArgsToStringsMultipleArgs", func(t *testing.T) { + args := []Arg{ + {Name: "key1", Value: "value1"}, + {Name: "key2", Value: "value2"}, + } + expected := []string{"--key1=value1", "--key2=value2"} + assert.Equal(t, expected, ConvertArgsToStrings(args)) + }) +} diff --git a/pkg/webhook/mutation/pod/v2/common/volumes/volumes.go b/pkg/webhook/mutation/pod/v2/common/volumes/volumes.go new file mode 100644 index 0000000000..065bde0d18 --- /dev/null +++ b/pkg/webhook/mutation/pod/v2/common/volumes/volumes.go @@ -0,0 +1,94 @@ +package volumes + +import ( + "path/filepath" + + "github.com/Dynatrace/dynatrace-operator/pkg/consts" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/mounts" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/volumes" + corev1 "k8s.io/api/core/v1" +) + +const ( + ConfigVolumeName = "dynatrace-config" + InitConfigMountPath = "/mnt/config" + InitConfigSubPath = "config" + ConfigMountPath = "/var/lib/dynatrace" + + InputVolumeName = "dynatrace-input" + InitInputMountPath = "/mnt/input" +) + +func AddConfigVolume(pod *corev1.Pod) { + if volumes.IsIn(pod.Spec.Volumes, ConfigVolumeName) { + return + } + + pod.Spec.Volumes = append(pod.Spec.Volumes, + corev1.Volume{ + Name: ConfigVolumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + ) +} + +func AddConfigVolumeMount(container *corev1.Container) { + if mounts.IsPathIn(container.VolumeMounts, ConfigMountPath) { + return + } + + container.VolumeMounts = append(container.VolumeMounts, + corev1.VolumeMount{ + Name: ConfigVolumeName, + MountPath: ConfigMountPath, + SubPath: filepath.Join(InitConfigSubPath, container.Name), + }, + ) +} + +func AddInitConfigVolumeMount(container *corev1.Container) { + if mounts.IsPathIn(container.VolumeMounts, InitConfigMountPath) { + return + } + + container.VolumeMounts = append(container.VolumeMounts, + corev1.VolumeMount{ + Name: ConfigVolumeName, + MountPath: InitConfigMountPath, + SubPath: InitConfigSubPath, + }, + ) +} + +func AddInputVolume(pod *corev1.Pod) { + if volumes.IsIn(pod.Spec.Volumes, InputVolumeName) { + return + } + + pod.Spec.Volumes = append(pod.Spec.Volumes, + corev1.Volume{ + Name: InputVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: consts.BootstrapperInitSecretName, + }, + }, + }, + ) +} + +func AddInitInputVolumeMount(container *corev1.Container) { + if mounts.IsPathIn(container.VolumeMounts, InitInputMountPath) { + return + } + + container.VolumeMounts = append(container.VolumeMounts, + corev1.VolumeMount{ + Name: InputVolumeName, + MountPath: InitInputMountPath, + ReadOnly: true, + }, + ) +} diff --git a/pkg/webhook/mutation/pod/v2/config.go b/pkg/webhook/mutation/pod/v2/config.go new file mode 100644 index 0000000000..467e240ed8 --- /dev/null +++ b/pkg/webhook/mutation/pod/v2/config.go @@ -0,0 +1,13 @@ +package v2 + +import "github.com/Dynatrace/dynatrace-operator/pkg/logd" + +var ( + log = logd.Get().WithName("v2-pod-mutation") +) + +const ( + NoBootstrapperConfigReason = "NoBootstrapperConfig" + NoCodeModulesImageReason = "NoCodeModulesImage" + NoMutationNeededReason = "NoMutationNeeded" +) diff --git a/pkg/webhook/mutation/pod/v2/init.go b/pkg/webhook/mutation/pod/v2/init.go new file mode 100644 index 0000000000..bea039e770 --- /dev/null +++ b/pkg/webhook/mutation/pod/v2/init.go @@ -0,0 +1,104 @@ +package v2 + +import ( + "github.com/Dynatrace/dynatrace-bootstrapper/cmd" + "github.com/Dynatrace/dynatrace-bootstrapper/cmd/configure" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + maputils "github.com/Dynatrace/dynatrace-operator/pkg/util/map" + dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" + oacommon "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/common/oneagent" + "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/v2/common/arg" + "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/v2/common/volumes" + corev1 "k8s.io/api/core/v1" + "k8s.io/utils/ptr" +) + +func createInitContainerBase(pod *corev1.Pod, dk dynakube.DynaKube) *corev1.Container { + args := []arg.Arg{ + { + Name: configure.ConfigFolderFlag, + Value: volumes.InitConfigMountPath, + }, + { + Name: configure.InputFolderFlag, + Value: volumes.InitInputMountPath, + }, + } + + if areErrorsSuppressed(pod, dk) { + args = append(args, arg.Arg{Name: cmd.SuppressErrorsFlag}) + } + + initContainer := &corev1.Container{ + Name: dtwebhook.InstallContainerName, + Image: dk.OneAgent().GetCustomCodeModulesImage(), + ImagePullPolicy: corev1.PullIfNotPresent, + SecurityContext: securityContextForInitContainer(pod, dk), + Resources: initContainerResources(dk), + Args: arg.ConvertArgsToStrings(args), + } + + return initContainer +} + +func areErrorsSuppressed(pod *corev1.Pod, dk dynakube.DynaKube) bool { + return maputils.GetField(pod.Annotations, dtwebhook.AnnotationFailurePolicy, dk.FeatureInjectionFailurePolicy()) != "fail" // safer than == silent +} + +func addInitContainerToPod(pod *corev1.Pod, initContainer *corev1.Container) { + volumes.AddInitConfigVolumeMount(initContainer) + volumes.AddInitInputVolumeMount(initContainer) + volumes.AddInputVolume(pod) + volumes.AddConfigVolume(pod) + pod.Spec.InitContainers = append(pod.Spec.InitContainers, *initContainer) +} + +func initContainerResources(dk dynakube.DynaKube) corev1.ResourceRequirements { + customInitResources := dk.OneAgent().GetInitResources() + if customInitResources != nil { + return *customInitResources + } + + return corev1.ResourceRequirements{} +} + +func securityContextForInitContainer(pod *corev1.Pod, dk dynakube.DynaKube) *corev1.SecurityContext { + initSecurityCtx := corev1.SecurityContext{ + ReadOnlyRootFilesystem: ptr.To(true), + AllowPrivilegeEscalation: ptr.To(false), + Privileged: ptr.To(false), + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{ + "ALL", + }, + }, + RunAsUser: ptr.To(oacommon.DefaultUser), + RunAsGroup: ptr.To(oacommon.DefaultGroup), + } + + addSeccompProfile(&initSecurityCtx, dk) + + return combineSecurityContexts(initSecurityCtx, *pod) +} + +func combineSecurityContexts(baseSecurityCtx corev1.SecurityContext, pod corev1.Pod) *corev1.SecurityContext { + podSecurityCtx := pod.Spec.SecurityContext + + if oacommon.HasPodUserSet(podSecurityCtx) { + baseSecurityCtx.RunAsUser = podSecurityCtx.RunAsUser + } + + if oacommon.HasPodGroupSet(podSecurityCtx) { + baseSecurityCtx.RunAsGroup = podSecurityCtx.RunAsGroup + } + + baseSecurityCtx.RunAsNonRoot = ptr.To(oacommon.IsNonRoot(&baseSecurityCtx)) + + return &baseSecurityCtx +} + +func addSeccompProfile(ctx *corev1.SecurityContext, dk dynakube.DynaKube) { + if dk.FeatureInitContainerSeccomp() { + ctx.SeccompProfile = &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeRuntimeDefault} + } +} diff --git a/pkg/webhook/mutation/pod/v2/init_test.go b/pkg/webhook/mutation/pod/v2/init_test.go new file mode 100644 index 0000000000..719abcb76d --- /dev/null +++ b/pkg/webhook/mutation/pod/v2/init_test.go @@ -0,0 +1,208 @@ +package v2 + +import ( + "testing" + + "github.com/Dynatrace/dynatrace-bootstrapper/cmd" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/mounts" + volumeutils "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/volumes" + dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" + oacommon "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/common/oneagent" + "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/v2/common/volumes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + "k8s.io/utils/ptr" +) + +func TestCreateInitContainerBase(t *testing.T) { + t.Run("should create the init container with set container sec ctx but without user and group", func(t *testing.T) { + dk := getTestDynakubeNoInitLimits() + pod := getTestPod() + pod.Spec.Containers[0].SecurityContext.RunAsUser = nil + pod.Spec.Containers[0].SecurityContext.RunAsGroup = nil + + initContainer := createInitContainerBase(pod, *dk) + + require.NotNil(t, initContainer) + assert.Equal(t, dtwebhook.InstallContainerName, initContainer.Name) + assert.Equal(t, customImage, initContainer.Image) + assert.Empty(t, initContainer.Resources) + + require.NotNil(t, initContainer.SecurityContext.AllowPrivilegeEscalation) + assert.False(t, *initContainer.SecurityContext.AllowPrivilegeEscalation) + + require.NotNil(t, initContainer.SecurityContext.Privileged) + assert.False(t, *initContainer.SecurityContext.Privileged) + + require.NotNil(t, initContainer.SecurityContext.ReadOnlyRootFilesystem) + assert.True(t, *initContainer.SecurityContext.ReadOnlyRootFilesystem) + + require.NotNil(t, initContainer.SecurityContext.RunAsNonRoot) + assert.True(t, *initContainer.SecurityContext.RunAsNonRoot) + + require.NotNil(t, initContainer.SecurityContext.RunAsUser) + assert.Equal(t, oacommon.DefaultUser, *initContainer.SecurityContext.RunAsUser) + + require.NotNil(t, initContainer.SecurityContext.RunAsGroup) + assert.Equal(t, oacommon.DefaultGroup, *initContainer.SecurityContext.RunAsGroup) + + assert.Nil(t, initContainer.SecurityContext.SeccompProfile) + }) + t.Run("do not take security context from user container", func(t *testing.T) { + dk := getTestDynakube() + pod := getTestPod() + testUser := ptr.To(int64(420)) + pod.Spec.Containers[0].SecurityContext.RunAsUser = testUser + pod.Spec.Containers[0].SecurityContext.RunAsGroup = testUser + + initContainer := createInitContainerBase(pod, *dk) + + require.NotNil(t, initContainer.SecurityContext.RunAsNonRoot) + assert.True(t, *initContainer.SecurityContext.RunAsNonRoot) + + require.NotNil(t, *initContainer.SecurityContext.RunAsUser) + assert.Equal(t, oacommon.DefaultUser, *initContainer.SecurityContext.RunAsUser) + + require.NotNil(t, *initContainer.SecurityContext.RunAsGroup) + assert.Equal(t, oacommon.DefaultGroup, *initContainer.SecurityContext.RunAsGroup) + }) + t.Run("PodSecurityContext overrules defaults", func(t *testing.T) { + dk := getTestDynakube() + testUser := ptr.To(int64(420)) + pod := getTestPod() + pod.Spec.Containers[0].SecurityContext = nil + pod.Spec.SecurityContext = &corev1.PodSecurityContext{} + pod.Spec.SecurityContext.RunAsUser = testUser + pod.Spec.SecurityContext.RunAsGroup = testUser + + initContainer := createInitContainerBase(pod, *dk) + + require.NotNil(t, initContainer.SecurityContext.RunAsNonRoot) + assert.True(t, *initContainer.SecurityContext.RunAsNonRoot) + + require.NotNil(t, initContainer.SecurityContext.RunAsUser) + assert.Equal(t, *testUser, *initContainer.SecurityContext.RunAsUser) + + require.NotNil(t, initContainer.SecurityContext.RunAsGroup) + assert.Equal(t, *testUser, *initContainer.SecurityContext.RunAsGroup) + }) + t.Run("should set RunAsNonRoot if root user is used", func(t *testing.T) { + dk := getTestDynakube() + pod := getTestPod() + pod.Spec.SecurityContext = &corev1.PodSecurityContext{} + pod.Spec.SecurityContext.RunAsUser = ptr.To(oacommon.RootUserGroup) + pod.Spec.SecurityContext.RunAsGroup = ptr.To(oacommon.RootUserGroup) + + initContainer := createInitContainerBase(pod, *dk) + + assert.NotNil(t, initContainer.SecurityContext.RunAsNonRoot) + assert.False(t, *initContainer.SecurityContext.RunAsNonRoot) + + require.NotNil(t, *initContainer.SecurityContext.RunAsUser) + assert.Equal(t, oacommon.RootUserGroup, *initContainer.SecurityContext.RunAsUser) + + require.NotNil(t, *initContainer.SecurityContext.RunAsGroup) + assert.Equal(t, oacommon.RootUserGroup, *initContainer.SecurityContext.RunAsGroup) + }) + t.Run("should set seccomp profile if feature flag is enabled", func(t *testing.T) { + dk := getTestDynakube() + dk.Annotations = map[string]string{dynakube.AnnotationFeatureInitContainerSeccomp: "true"} + pod := getTestPod() + pod.Annotations = map[string]string{} + + initContainer := createInitContainerBase(pod, *dk) + + assert.Equal(t, corev1.SeccompProfileTypeRuntimeDefault, initContainer.SecurityContext.SeccompProfile.Type) + }) + + t.Run("should not set suppress-error arg - according to dk", func(t *testing.T) { + dk := getTestDynakube() + dk.Annotations = map[string]string{dynakube.AnnotationInjectionFailurePolicy: "fail"} + pod := getTestPod() + pod.Annotations = map[string]string{} + + initContainer := createInitContainerBase(pod, *dk) + + assert.NotContains(t, initContainer.Args, "--"+cmd.SuppressErrorsFlag) + }) + + t.Run("should not set suppress-error arg - according to pod", func(t *testing.T) { + dk := getTestDynakube() + dk.Annotations = map[string]string{dynakube.AnnotationInjectionFailurePolicy: "silent"} + pod := getTestPod() + pod.Annotations = map[string]string{dtwebhook.AnnotationFailurePolicy: "fail"} + + initContainer := createInitContainerBase(pod, *dk) + + assert.NotContains(t, initContainer.Args, "--"+cmd.SuppressErrorsFlag) + }) + + t.Run("should set suppress-error arg - default", func(t *testing.T) { + dk := getTestDynakube() + dk.Annotations = map[string]string{} + pod := getTestPod() + pod.Annotations = map[string]string{} + + initContainer := createInitContainerBase(pod, *dk) + + assert.Contains(t, initContainer.Args, "--"+cmd.SuppressErrorsFlag) + }) + + t.Run("should set suppress-error arg - unknown value", func(t *testing.T) { + dk := getTestDynakube() + dk.Annotations = map[string]string{dynakube.AnnotationInjectionFailurePolicy: "asd"} + pod := getTestPod() + pod.Annotations = map[string]string{} + + initContainer := createInitContainerBase(pod, *dk) + + assert.Contains(t, initContainer.Args, "--"+cmd.SuppressErrorsFlag) + + dk = getTestDynakube() + dk.Annotations = map[string]string{} + pod = getTestPod() + pod.Annotations = map[string]string{dtwebhook.AnnotationFailurePolicy: "asd"} + + initContainer = createInitContainerBase(pod, *dk) + + assert.Contains(t, initContainer.Args, "--"+cmd.SuppressErrorsFlag) + }) +} + +func TestAddInitContainerToPod(t *testing.T) { + t.Run("adds common volumes/mounts", func(t *testing.T) { + pod := corev1.Pod{} + initContainer := corev1.Container{} + + addInitContainerToPod(&pod, &initContainer) + + assert.Contains(t, pod.Spec.InitContainers, initContainer) + require.Len(t, pod.Spec.Volumes, 2) + assert.True(t, volumeutils.IsIn(pod.Spec.Volumes, volumes.ConfigVolumeName)) + assert.True(t, volumeutils.IsIn(pod.Spec.Volumes, volumes.InputVolumeName)) + require.Len(t, initContainer.VolumeMounts, 2) + assert.True(t, mounts.IsPathIn(initContainer.VolumeMounts, volumes.InitConfigMountPath)) + assert.True(t, mounts.IsPathIn(initContainer.VolumeMounts, volumes.InitInputMountPath)) + }) +} + +func TestInitContainerResources(t *testing.T) { + t.Run("should return nothing per default", func(t *testing.T) { + dk := getTestDynakubeNoInitLimits() + + initResources := initContainerResources(*dk) + + require.Empty(t, initResources) + }) + + t.Run("should return custom if set in dynakube", func(t *testing.T) { + dk := getTestDynakube() + + initResources := initContainerResources(*dk) + + require.NotNil(t, initResources) + assert.Equal(t, testResourceRequirements, initResources) + }) +} diff --git a/pkg/webhook/mutation/pod/v2/metadata/config.go b/pkg/webhook/mutation/pod/v2/metadata/config.go new file mode 100644 index 0000000000..8a678f8d8e --- /dev/null +++ b/pkg/webhook/mutation/pod/v2/metadata/config.go @@ -0,0 +1,9 @@ +package metadata + +import ( + "github.com/Dynatrace/dynatrace-operator/pkg/logd" +) + +var ( + log = logd.Get().WithName("v2-pod-mutation-metadata-enrichment") +) diff --git a/pkg/webhook/mutation/pod/v2/metadata/containers.go b/pkg/webhook/mutation/pod/v2/metadata/containers.go new file mode 100644 index 0000000000..badf586f64 --- /dev/null +++ b/pkg/webhook/mutation/pod/v2/metadata/containers.go @@ -0,0 +1,54 @@ +package metadata + +import ( + "maps" + "strings" + + podattr "github.com/Dynatrace/dynatrace-bootstrapper/cmd/configure/attributes/pod" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" + metacommon "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/common/metadata" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func Mutate(metaClient client.Client, request *dtwebhook.MutationRequest, attributes *podattr.Attributes) error { + if !metacommon.IsEnabled(request.BaseRequest) { + return nil + } + + log.Info("adding metadata-enrichment to pod", "name", request.PodName()) + + workloadInfo, err := metacommon.RetrieveWorkload(metaClient, request) + if err != nil { + return err + } + + attributes.WorkloadInfo = podattr.WorkloadInfo{ + WorkloadKind: workloadInfo.Kind, + WorkloadName: workloadInfo.Name, + } + + addMetadataToInitArgs(request, attributes) + + metacommon.SetInjectedAnnotation(request.Pod) + metacommon.SetWorkloadAnnotations(request.Pod, workloadInfo) + + return nil +} + +func addMetadataToInitArgs(request *dtwebhook.MutationRequest, attributes *podattr.Attributes) { + metacommon.CopyMetadataFromNamespace(request.Pod, request.Namespace, request.DynaKube) + + metadataAnnotations := map[string]string{} + + for key, value := range request.Pod.Annotations { + if !strings.HasPrefix(key, dynakube.MetadataPrefix) { + continue + } + + split := strings.Split(key, dynakube.MetadataPrefix) + metadataAnnotations[split[1]] = value + } + + maps.Copy(attributes.UserDefined, metadataAnnotations) +} diff --git a/pkg/webhook/mutation/pod/v2/oneagent/config.go b/pkg/webhook/mutation/pod/v2/oneagent/config.go new file mode 100644 index 0000000000..85905db2ad --- /dev/null +++ b/pkg/webhook/mutation/pod/v2/oneagent/config.go @@ -0,0 +1,9 @@ +package oneagent + +import ( + "github.com/Dynatrace/dynatrace-operator/pkg/logd" +) + +var ( + log = logd.Get().WithName("v2-pod-mutation-oneagent") +) diff --git a/pkg/webhook/mutation/pod/v2/oneagent/containers.go b/pkg/webhook/mutation/pod/v2/oneagent/containers.go new file mode 100644 index 0000000000..5020f5be19 --- /dev/null +++ b/pkg/webhook/mutation/pod/v2/oneagent/containers.go @@ -0,0 +1,68 @@ +package oneagent + +import ( + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/env" + maputils "github.com/Dynatrace/dynatrace-operator/pkg/util/map" + dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" + oacommon "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/common/oneagent" + corev1 "k8s.io/api/core/v1" +) + +const ( + isInjectedEnv = "DT_CM_INJECTED" +) + +func Mutate(request *dtwebhook.MutationRequest) bool { + installPath := maputils.GetField(request.Pod.Annotations, oacommon.AnnotationInstallPath, oacommon.DefaultInstallPath) + mutateInitContainer(request, installPath) + + return mutateUserContainers(request.BaseRequest, installPath) +} + +func Reinvoke(request *dtwebhook.BaseRequest) bool { + installPath := maputils.GetField(request.Pod.Annotations, oacommon.AnnotationInstallPath, oacommon.DefaultInstallPath) + + return mutateUserContainers(request, installPath) +} + +func containerIsInjected(container corev1.Container) bool { + return env.IsIn(container.Env, isInjectedEnv) +} + +func mutateUserContainers(request *dtwebhook.BaseRequest, installPath string) bool { + newContainers := request.NewContainers(containerIsInjected) + for i := range newContainers { + container := newContainers[i] + addOneAgentToContainer(request.DynaKube, container, request.Namespace, installPath) + } + + return len(newContainers) > 0 +} + +func addOneAgentToContainer(dk dynakube.DynaKube, container *corev1.Container, namespace corev1.Namespace, installPath string) { + log.Info("adding OneAgent to container", "name", container.Name) + + addVolumeMounts(container, installPath) + oacommon.AddDeploymentMetadataEnv(container, dk) + oacommon.AddPreloadEnv(container, installPath) + + if dk.Spec.NetworkZone != "" { + oacommon.AddNetworkZoneEnv(container, dk.Spec.NetworkZone) + } + + if dk.FeatureLabelVersionDetection() { + oacommon.AddVersionDetectionEnvs(container, namespace) + } + + setIsInjectedEnv(container) +} + +func setIsInjectedEnv(container *corev1.Container) { + container.Env = append(container.Env, + corev1.EnvVar{ + Name: isInjectedEnv, + Value: "true", + }, + ) +} diff --git a/pkg/webhook/mutation/pod/v2/oneagent/containers_test.go b/pkg/webhook/mutation/pod/v2/oneagent/containers_test.go new file mode 100644 index 0000000000..1b5bba10ac --- /dev/null +++ b/pkg/webhook/mutation/pod/v2/oneagent/containers_test.go @@ -0,0 +1,210 @@ +package oneagent + +import ( + "testing" + + "github.com/Dynatrace/dynatrace-bootstrapper/cmd/configure" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/env" + dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" + oacommon "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/common/oneagent" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" +) + +func TestContainerIsInjected(t *testing.T) { + t.Run("is injected", func(t *testing.T) { + request := createTestMutationRequestWithoutInjectedContainers() + + for _, c := range request.Pod.Spec.Containers { + container := &c + assert.False(t, containerIsInjected(*container)) + setIsInjectedEnv(container) + assert.True(t, containerIsInjected(*container)) + } + }) +} + +func TestMutate(t *testing.T) { + t.Run("success", func(t *testing.T) { + request := createTestMutationRequestWithoutInjectedContainers() + + original := createTestMutationRequestWithoutInjectedContainers() + updated := Mutate(request) + require.True(t, updated) + // update install container + assert.NotEqual(t, original.InstallContainer, request.InstallContainer) + + for i := range request.Pod.Spec.Containers { + // update each container + assert.NotEqual(t, original.Pod.Spec.Containers[i], request.Pod.Spec.Containers[i]) + + assert.True(t, containerIsInjected(request.Pod.Spec.Containers[i])) + } + }) + t.Run("install-path respected", func(t *testing.T) { + expectedInstallPath := "my-install" + request := createTestMutationRequestWithoutInjectedContainers() + request.Pod.Annotations = map[string]string{ + oacommon.AnnotationInstallPath: expectedInstallPath, + } + + updated := Mutate(request) + require.True(t, updated) + + assert.Contains(t, request.InstallContainer.Args, "--"+configure.InstallPathFlag+"="+expectedInstallPath) + + for _, c := range request.Pod.Spec.Containers { + preload := env.FindEnvVar(c.Env, oacommon.PreloadEnv) + require.NotNil(t, preload) + assert.Contains(t, preload.Value, expectedInstallPath) + } + }) + t.Run("no change => no update", func(t *testing.T) { + request := createTestMutationRequestWithoutInjectedContainers() + updateContainer := []corev1.Container{} + + for _, c := range request.Pod.Spec.Containers { + container := &c + setIsInjectedEnv(container) + updateContainer = append(updateContainer, *container) + } + + request.Pod.Spec.Containers = updateContainer + + updated := Mutate(request) + require.False(t, updated) + }) +} + +func TestReinvoke(t *testing.T) { + t.Run("success", func(t *testing.T) { + request := createTestMutationRequestWithInjectedContainers() + + original := createTestMutationRequestWithInjectedContainers() + updated := Reinvoke(request.BaseRequest) + require.True(t, updated) + + // no update to install container + assert.Equal(t, original.InstallContainer, request.InstallContainer) + + for i := range request.Pod.Spec.Containers { + // only update not-injected + if containerIsInjected(original.Pod.Spec.Containers[i]) { + assert.Equal(t, original.Pod.Spec.Containers[i], request.Pod.Spec.Containers[i]) + } else { + assert.NotEqual(t, original.Pod.Spec.Containers[i], request.Pod.Spec.Containers[i]) + } + + assert.True(t, containerIsInjected(request.Pod.Spec.Containers[i])) + } + }) + + t.Run("install-path respected", func(t *testing.T) { + expectedInstallPath := "my-install" + request := createTestMutationRequestWithoutInjectedContainers() + request.Pod.Annotations = map[string]string{ + oacommon.AnnotationInstallPath: expectedInstallPath, + } + + updated := Reinvoke(request.BaseRequest) + require.True(t, updated) + + for _, c := range request.Pod.Spec.Containers { + preload := env.FindEnvVar(c.Env, oacommon.PreloadEnv) + require.NotNil(t, preload) + assert.Contains(t, preload.Value, expectedInstallPath) + } + }) + + t.Run("no change => no update", func(t *testing.T) { + request := createTestMutationRequestWithoutInjectedContainers() + updateContainer := []corev1.Container{} + + for _, c := range request.Pod.Spec.Containers { + container := &c + setIsInjectedEnv(container) + updateContainer = append(updateContainer, *container) + } + + request.Pod.Spec.Containers = updateContainer + + updated := Reinvoke(request.BaseRequest) + require.False(t, updated) + }) +} + +func TestAddOneAgentToContainer(t *testing.T) { + kubeSystemUUID := "my uuid" + networkZone := "my zone" + installPath := "install/path" + + t.Run("add everything", func(t *testing.T) { + container := corev1.Container{} + dk := dynakube.DynaKube{ + Spec: dynakube.DynaKubeSpec{ + OneAgent: oneagent.Spec{ApplicationMonitoring: &oneagent.ApplicationMonitoringSpec{}}, + NetworkZone: networkZone, + }, + Status: dynakube.DynaKubeStatus{ + KubeSystemUUID: kubeSystemUUID, + }, + } + + addOneAgentToContainer(dk, &container, corev1.Namespace{}, installPath) + + assert.Len(t, container.VolumeMounts, 3) // preload,bin,config + + dtMetaEnv := env.FindEnvVar(container.Env, oacommon.DynatraceMetadataEnv) + require.NotNil(t, dtMetaEnv) + assert.Contains(t, dtMetaEnv.Value, kubeSystemUUID) + + dtZoneEnv := env.FindEnvVar(container.Env, oacommon.NetworkZoneEnv) + require.NotNil(t, dtZoneEnv) + assert.Equal(t, networkZone, dtZoneEnv.Value) + + preload := env.FindEnvVar(container.Env, oacommon.PreloadEnv) + require.NotNil(t, preload) + assert.Contains(t, preload.Value, installPath) + + assert.True(t, containerIsInjected(container)) + }) +} + +func createTestMutationRequestWithoutInjectedContainers() *dtwebhook.MutationRequest { + return &dtwebhook.MutationRequest{ + InstallContainer: &corev1.Container{ + Name: dtwebhook.InstallContainerName, + }, + BaseRequest: &dtwebhook.BaseRequest{ + Pod: &corev1.Pod{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "sample-container-1", + Image: "sample-image-1", + }, + { + Name: "sample-container-2", + Image: "sample-image-2", + }, + }, + }, + }, + }, + } +} + +func createTestMutationRequestWithInjectedContainers() *dtwebhook.MutationRequest { + request := createTestMutationRequestWithoutInjectedContainers() + + i := 0 + request.Pod.Spec.Containers[i].Env = append(request.Pod.Spec.Containers[i].Env, corev1.EnvVar{ + Name: isInjectedEnv, + Value: "true", + }) + + return request +} diff --git a/pkg/webhook/mutation/pod/v2/oneagent/init.go b/pkg/webhook/mutation/pod/v2/oneagent/init.go new file mode 100644 index 0000000000..fc2e391989 --- /dev/null +++ b/pkg/webhook/mutation/pod/v2/oneagent/init.go @@ -0,0 +1,49 @@ +package oneagent + +import ( + "github.com/Dynatrace/dynatrace-bootstrapper/cmd" + "github.com/Dynatrace/dynatrace-bootstrapper/cmd/configure" + "github.com/Dynatrace/dynatrace-bootstrapper/cmd/move" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/consts" + dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" + oacommon "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/common/oneagent" + "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/v2/common/arg" + corev1 "k8s.io/api/core/v1" +) + +func mutateInitContainer(mutationRequest *dtwebhook.MutationRequest, installPath string) { + addInitArgs(*mutationRequest.Pod, mutationRequest.InstallContainer, mutationRequest.DynaKube, installPath) + addInitVolumeMounts(mutationRequest.InstallContainer) +} + +func addInitArgs(pod corev1.Pod, initContainer *corev1.Container, dk dynakube.DynaKube, installPath string) { + args := []arg.Arg{ + {Name: cmd.SourceFolderFlag, Value: consts.AgentCodeModuleSource}, + {Name: cmd.TargetFolderFlag, Value: binInitMountPath}, + {Name: configure.InstallPathFlag, Value: installPath}, + } + + if technology := getTechnology(pod, dk); technology != "" { + args = append(args, arg.Arg{Name: move.TechnologyFlag, Value: technology}) + } + + if initContainer.Args == nil { + initContainer.Args = []string{} + } + + initContainer.Args = append(initContainer.Args, arg.ConvertArgsToStrings(args)...) +} + +func getTechnology(pod corev1.Pod, dk dynakube.DynaKube) string { + if technology, ok := pod.Annotations[oacommon.AnnotationTechnologies]; ok { + return technology + } + + technology := dk.FeatureNodeImagePullTechnology() + if technology != "" { + return technology + } + + return "" +} diff --git a/pkg/webhook/mutation/pod/v2/oneagent/init_test.go b/pkg/webhook/mutation/pod/v2/oneagent/init_test.go new file mode 100644 index 0000000000..4ac6ae1967 --- /dev/null +++ b/pkg/webhook/mutation/pod/v2/oneagent/init_test.go @@ -0,0 +1,66 @@ +package oneagent + +import ( + "testing" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + oacommon "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/common/oneagent" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestAddInitArgs(t *testing.T) { + t.Run("WithTechnologyAnnotation", func(t *testing.T) { + pod := corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + oacommon.AnnotationTechnologies: "java", + }, + }, + } + initContainer := &corev1.Container{} + dk := dynakube.DynaKube{} + + addInitArgs(pod, initContainer, dk, oacommon.DefaultInstallPath) + + require.Contains(t, initContainer.Args, "--technology=java") + }) + + t.Run("WithTechnologyFeature", func(t *testing.T) { + pod := corev1.Pod{} + initContainer := &corev1.Container{} + dk := dynakube.DynaKube{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + dynakube.AnnotationTechnologies: "nodejs", + }, + }, + } + + addInitArgs(pod, initContainer, dk, oacommon.DefaultInstallPath) + + require.Contains(t, initContainer.Args, "--technology=nodejs") + }) + + t.Run("WithoutTechnology", func(t *testing.T) { + pod := corev1.Pod{} + initContainer := &corev1.Container{} + dk := dynakube.DynaKube{} + + addInitArgs(pod, initContainer, dk, oacommon.DefaultInstallPath) + + require.NotContains(t, initContainer.Args, "--technology=") + }) + + t.Run("WithDefaultArgs", func(t *testing.T) { + pod := corev1.Pod{} + initContainer := &corev1.Container{} + dk := dynakube.DynaKube{} + + addInitArgs(pod, initContainer, dk, oacommon.DefaultInstallPath) + + require.Contains(t, initContainer.Args, "--source=/opt/dynatrace/oneagent") + require.Contains(t, initContainer.Args, "--target=/mnt/bin") + }) +} diff --git a/pkg/webhook/mutation/pod/v2/oneagent/volumes.go b/pkg/webhook/mutation/pod/v2/oneagent/volumes.go new file mode 100644 index 0000000000..d2b7c41337 --- /dev/null +++ b/pkg/webhook/mutation/pod/v2/oneagent/volumes.go @@ -0,0 +1,44 @@ +package oneagent + +import ( + "path/filepath" + + "github.com/Dynatrace/dynatrace-bootstrapper/pkg/configure/oneagent/preload" + "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/v2/common/volumes" + corev1 "k8s.io/api/core/v1" +) + +const ( + binSubPath = "bin" + binInitMountPath = "/mnt/bin" + + ldPreloadPath = "/etc/ld.so.preload" + ldPreloadSubPath = preload.ConfigPath +) + +func addVolumeMounts(container *corev1.Container, installPath string) { + container.VolumeMounts = append(container.VolumeMounts, + corev1.VolumeMount{ + Name: volumes.ConfigVolumeName, + MountPath: installPath, + SubPath: binSubPath, + }, + corev1.VolumeMount{ + Name: volumes.ConfigVolumeName, + MountPath: ldPreloadPath, + SubPath: filepath.Join(volumes.InitConfigSubPath, ldPreloadSubPath), + }, + ) + + volumes.AddConfigVolumeMount(container) +} + +func addInitVolumeMounts(initContainer *corev1.Container) { + initContainer.VolumeMounts = append(initContainer.VolumeMounts, + corev1.VolumeMount{ + Name: volumes.ConfigVolumeName, + MountPath: binInitMountPath, + SubPath: binSubPath, + }, + ) +} diff --git a/pkg/webhook/mutation/pod/v2/oneagent/volumes_test.go b/pkg/webhook/mutation/pod/v2/oneagent/volumes_test.go new file mode 100644 index 0000000000..67acb038a8 --- /dev/null +++ b/pkg/webhook/mutation/pod/v2/oneagent/volumes_test.go @@ -0,0 +1,46 @@ +package oneagent + +import ( + "path/filepath" + "testing" + + "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/v2/common/volumes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" +) + +func TestAddVolumeMounts(t *testing.T) { + t.Run("should add oneagent volume mounts", func(t *testing.T) { + container := &corev1.Container{ + Name: "test-container", + } + installPath := "test/path" + + addVolumeMounts(container, installPath) + require.Len(t, container.VolumeMounts, 3) + assert.Equal(t, volumes.ConfigVolumeName, container.VolumeMounts[0].Name) + assert.Equal(t, installPath, container.VolumeMounts[0].MountPath) + assert.Equal(t, binSubPath, container.VolumeMounts[0].SubPath) + + assert.Equal(t, volumes.ConfigVolumeName, container.VolumeMounts[1].Name) + assert.Equal(t, ldPreloadPath, container.VolumeMounts[1].MountPath) + assert.Equal(t, filepath.Join(volumes.InitConfigSubPath, ldPreloadSubPath), container.VolumeMounts[1].SubPath) + + assert.Equal(t, volumes.ConfigVolumeName, container.VolumeMounts[2].Name) + assert.Equal(t, filepath.Join(volumes.InitConfigSubPath, container.Name), container.VolumeMounts[2].SubPath) + assert.Equal(t, volumes.ConfigMountPath, container.VolumeMounts[2].MountPath) + }) +} + +func TestAddInitVolumeMounts(t *testing.T) { + t.Run("should add init volume mounts", func(t *testing.T) { + container := &corev1.Container{} + + addInitVolumeMounts(container) + require.Len(t, container.VolumeMounts, 1) + assert.Equal(t, volumes.ConfigVolumeName, container.VolumeMounts[0].Name) + assert.Equal(t, binInitMountPath, container.VolumeMounts[0].MountPath) + assert.Equal(t, binSubPath, container.VolumeMounts[0].SubPath) + }) +} diff --git a/pkg/webhook/mutation/pod/v2/webhook.go b/pkg/webhook/mutation/pod/v2/webhook.go new file mode 100644 index 0000000000..f9698687dd --- /dev/null +++ b/pkg/webhook/mutation/pod/v2/webhook.go @@ -0,0 +1,196 @@ +package v2 + +import ( + "context" + + "github.com/Dynatrace/dynatrace-operator/pkg/consts" + "github.com/Dynatrace/dynatrace-operator/pkg/injection/namespace/bootstrapperconfig" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/container" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/secret" + maputils "github.com/Dynatrace/dynatrace-operator/pkg/util/map" + dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" + "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/common/events" + oacommon "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/common/oneagent" + oamutation "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/v2/oneagent" + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type Injector struct { + recorder events.EventRecorder + kubeClient client.Client + apiReader client.Reader + metaClient client.Client +} + +func IsEnabled(mutationRequest *dtwebhook.MutationRequest) bool { + ffEnabled := mutationRequest.DynaKube.FeatureNodeImagePull() + oaEnabled := oacommon.IsEnabled(mutationRequest.BaseRequest) + + defaultVolumeType := oacommon.EphemeralVolumeType + if mutationRequest.DynaKube.OneAgent().IsCSIAvailable() { + defaultVolumeType = oacommon.CSIVolumeType + } + + correctVolumeType := maputils.GetField(mutationRequest.Pod.Annotations, oacommon.AnnotationVolumeType, defaultVolumeType) == oacommon.EphemeralVolumeType + + return ffEnabled && oaEnabled && correctVolumeType +} + +var _ dtwebhook.PodInjector = &Injector{} + +func NewInjector(kubeClient client.Client, apiReader client.Reader, metaClient client.Client, recorder events.EventRecorder) *Injector { + return &Injector{ + recorder: recorder, + kubeClient: kubeClient, + apiReader: apiReader, + metaClient: metaClient, + } +} + +func (wh *Injector) Handle(_ context.Context, mutationRequest *dtwebhook.MutationRequest) error { + wh.recorder.Setup(mutationRequest) + + if !wh.isInputSecretPresent(mutationRequest) { + return nil + } + + if !isCustomImageSet(mutationRequest) { + return nil + } + + if wh.isInjected(mutationRequest) { + if wh.handlePodReinvocation(mutationRequest) { + log.Info("reinvocation policy applied", "podName", mutationRequest.PodName()) + wh.recorder.SendPodUpdateEvent() + + return nil + } + + log.Info("no change, all containers already injected", "podName", mutationRequest.PodName()) + } else { + if err := wh.handlePodMutation(mutationRequest); err != nil { + return err + } + } + + setDynatraceInjectedAnnotation(mutationRequest) + + log.Info("injection finished for pod", "podName", mutationRequest.PodName(), "namespace", mutationRequest.Namespace.Name) + + return nil +} + +func (wh *Injector) isInjected(mutationRequest *dtwebhook.MutationRequest) bool { + installContainer := container.FindInitContainerInPodSpec(&mutationRequest.Pod.Spec, dtwebhook.InstallContainerName) + if installContainer != nil { + log.Info("Dynatrace init-container already present, skipping mutation, doing reinvocation", "containerName", dtwebhook.InstallContainerName) + + return true + } + + return false +} + +func (wh *Injector) handlePodMutation(mutationRequest *dtwebhook.MutationRequest) error { + mutationRequest.InstallContainer = createInitContainerBase(mutationRequest.Pod, mutationRequest.DynaKube) + + err := addContainerAttributes(mutationRequest) + if err != nil { + return err + } + + updated := oamutation.Mutate(mutationRequest) + if !updated { + oacommon.SetNotInjectedAnnotations(mutationRequest.Pod, NoMutationNeededReason) + + return nil + } + + err = wh.addPodAttributes(mutationRequest) + if err != nil { + log.Info("failed to add pod attributes to init-container") + + return err + } + + oacommon.SetInjectedAnnotation(mutationRequest.Pod) + + addInitContainerToPod(mutationRequest.Pod, mutationRequest.InstallContainer) + wh.recorder.SendPodInjectEvent() + + return nil +} + +func (wh *Injector) handlePodReinvocation(mutationRequest *dtwebhook.MutationRequest) bool { + mutationRequest.InstallContainer = container.FindInitContainerInPodSpec(&mutationRequest.Pod.Spec, dtwebhook.InstallContainerName) + + err := addContainerAttributes(mutationRequest) + if err != nil { + log.Error(err, "error during reinvocation for updating the init-container, failed to update container-attributes on the init container") + + return false + } + + updated := oamutation.Reinvoke(mutationRequest.BaseRequest) + + return updated +} + +func isCustomImageSet(mutationRequest *dtwebhook.MutationRequest) bool { + customImage := mutationRequest.DynaKube.OneAgent().GetCustomCodeModulesImage() + if customImage == "" { + oacommon.SetNotInjectedAnnotations(mutationRequest.Pod, NoCodeModulesImageReason) + + return false + } + + return true +} + +func (wh *Injector) isInputSecretPresent(mutationRequest *dtwebhook.MutationRequest) bool { + err := wh.replicateInputSecret(mutationRequest) + + if k8serrors.IsNotFound(err) { + log.Info("unable to copy source of dynatrace-bootstrapper-config as it is not available, injection not possible", "pod", mutationRequest.PodName()) + + oacommon.SetNotInjectedAnnotations(mutationRequest.Pod, NoBootstrapperConfigReason) + + return false + } + + if err != nil { + log.Error(err, "unable to verify, if dynatrace-bootstrapper-config is available, injection not possible") + + oacommon.SetNotInjectedAnnotations(mutationRequest.Pod, NoBootstrapperConfigReason) + + return false + } + + return true +} + +func (wh *Injector) replicateInputSecret(mutationRequest *dtwebhook.MutationRequest) error { + var initSecret corev1.Secret + + secretObjectKey := client.ObjectKey{Name: consts.BootstrapperInitSecretName, Namespace: mutationRequest.Namespace.Name} + err := wh.apiReader.Get(mutationRequest.Context, secretObjectKey, &initSecret) + + if k8serrors.IsNotFound(err) { + log.Info("dynatrace-bootstrapper-config is not available, trying to replicate", "pod", mutationRequest.PodName()) + + return bootstrapperconfig.Replicate(mutationRequest.Context, mutationRequest.DynaKube, secret.Query(wh.kubeClient, wh.apiReader, log), mutationRequest.Namespace.Name) + } + + return nil +} + +func setDynatraceInjectedAnnotation(mutationRequest *dtwebhook.MutationRequest) { + if mutationRequest.Pod.Annotations == nil { + mutationRequest.Pod.Annotations = make(map[string]string) + } + + mutationRequest.Pod.Annotations[dtwebhook.AnnotationDynatraceInjected] = "true" + delete(mutationRequest.Pod.Annotations, dtwebhook.AnnotationDynatraceReason) +} diff --git a/pkg/webhook/mutation/pod/v2/webhook_test.go b/pkg/webhook/mutation/pod/v2/webhook_test.go new file mode 100644 index 0000000000..963c89db80 --- /dev/null +++ b/pkg/webhook/mutation/pod/v2/webhook_test.go @@ -0,0 +1,441 @@ +package v2 + +import ( + "context" + "testing" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" + "github.com/Dynatrace/dynatrace-operator/pkg/consts" + "github.com/Dynatrace/dynatrace-operator/pkg/injection/namespace/bootstrapperconfig" + "github.com/Dynatrace/dynatrace-operator/pkg/util/installconfig" + "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/container" + dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" + "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/common/events" + oacommon "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/common/oneagent" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/record" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + testNamespaceName = "test-namespace" + testPodName = "test-pod" + testDynakubeName = "test-dynakube" + customImage = "custom-image" +) + +func TestIsEnabled(t *testing.T) { + type testCase struct { + title string + podMods func(*corev1.Pod) + dkMods func(*dynakube.DynaKube) + withCSI bool + withoutCSI bool + } + + cases := []testCase{ + { + title: "nothing enabled => not enabled", + podMods: func(p *corev1.Pod) {}, + dkMods: func(dk *dynakube.DynaKube) {}, + withCSI: false, + withoutCSI: false, + }, + + { + title: "only OA enabled, without FF => not enabled", + podMods: func(p *corev1.Pod) {}, + dkMods: func(dk *dynakube.DynaKube) { + dk.Spec.OneAgent.ApplicationMonitoring = &oneagent.ApplicationMonitoringSpec{} + }, + withCSI: false, + withoutCSI: false, + }, + + { + title: "OA + FF enabled => enabled with no csi", + podMods: func(p *corev1.Pod) {}, + dkMods: func(dk *dynakube.DynaKube) { + dk.Spec.OneAgent.ApplicationMonitoring = &oneagent.ApplicationMonitoringSpec{} + dk.Annotations = map[string]string{dynakube.AnnotationFeatureNodeImagePull: "true"} + }, + withCSI: false, + withoutCSI: true, + }, + { + title: "OA + FF enabled + correct Volume-Type => enabled", + podMods: func(p *corev1.Pod) { + p.Annotations = map[string]string{oacommon.AnnotationVolumeType: oacommon.EphemeralVolumeType} + }, + dkMods: func(dk *dynakube.DynaKube) { + dk.Spec.OneAgent.ApplicationMonitoring = &oneagent.ApplicationMonitoringSpec{} + dk.Annotations = map[string]string{dynakube.AnnotationFeatureNodeImagePull: "true"} + }, + withCSI: true, + withoutCSI: true, + }, + { + title: "OA + FF enabled + incorrect Volume-Type => disabled", + podMods: func(p *corev1.Pod) { + p.Annotations = map[string]string{oacommon.AnnotationVolumeType: oacommon.CSIVolumeType} + }, + dkMods: func(dk *dynakube.DynaKube) { + dk.Spec.OneAgent.ApplicationMonitoring = &oneagent.ApplicationMonitoringSpec{} + dk.Annotations = map[string]string{dynakube.AnnotationFeatureNodeImagePull: "true"} + }, + withCSI: false, + withoutCSI: false, + }, + } + for _, test := range cases { + t.Run(test.title, func(t *testing.T) { + pod := &corev1.Pod{} + test.podMods(pod) + + dk := &dynakube.DynaKube{} + test.dkMods(dk) + + req := &dtwebhook.MutationRequest{BaseRequest: &dtwebhook.BaseRequest{Pod: pod, DynaKube: *dk}} + + assert.Equal(t, test.withCSI, IsEnabled(req)) + + installconfig.SetModulesOverride(t, installconfig.Modules{CSIDriver: false}) + + assert.Equal(t, test.withoutCSI, IsEnabled(req)) + }) + } +} + +func TestHandle(t *testing.T) { + ctx := context.Background() + + initSecret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: consts.BootstrapperInitSecretName, + Namespace: testNamespaceName, + }, + } + + t.Run("no init secret + no init secret source => no injection + only annotation", func(t *testing.T) { + injector := createTestInjectorBase() + clt := fake.NewClient() + injector.apiReader = clt + + request := createTestMutationRequest(getTestDynakube()) + + err := injector.Handle(ctx, request) + require.NoError(t, err) + + isInjected, ok := request.Pod.Annotations[oacommon.AnnotationInjected] + require.True(t, ok) + assert.Equal(t, "false", isInjected) + + reason, ok := request.Pod.Annotations[oacommon.AnnotationReason] + require.True(t, ok) + assert.Equal(t, NoBootstrapperConfigReason, reason) + }) + + t.Run("no init secret + source => replicate + inject", func(t *testing.T) { + injector := createTestInjectorBase() + request := createTestMutationRequest(getTestDynakube()) + + source := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: bootstrapperconfig.GetSourceSecretName(request.DynaKube.Name), + Namespace: request.DynaKube.Namespace, + }, + Data: map[string][]byte{"data": []byte("beep")}, + } + clt := fake.NewClient(&source) + injector.kubeClient = clt + injector.apiReader = clt + + err := injector.Handle(ctx, request) + require.NoError(t, err) + + var replicated corev1.Secret + err = clt.Get(context.Background(), client.ObjectKey{Name: consts.BootstrapperInitSecretName, Namespace: request.Namespace.Name}, &replicated) + require.NoError(t, err) + assert.Equal(t, source.Data, replicated.Data) + + isInjected, ok := request.Pod.Annotations[oacommon.AnnotationInjected] + require.True(t, ok) + assert.Equal(t, "true", isInjected) + + _, ok = request.Pod.Annotations[oacommon.AnnotationReason] + require.False(t, ok) + }) + + t.Run("no codeModulesImage => no injection + only annotation", func(t *testing.T) { + injector := createTestInjectorBase() + injector.apiReader = fake.NewClient(&initSecret) + + request := createTestMutationRequest(&dynakube.DynaKube{}) + + err := injector.Handle(ctx, request) + require.NoError(t, err) + + isInjected, ok := request.Pod.Annotations[oacommon.AnnotationInjected] + require.True(t, ok) + assert.Equal(t, "false", isInjected) + + reason, ok := request.Pod.Annotations[oacommon.AnnotationReason] + require.True(t, ok) + assert.Equal(t, NoCodeModulesImageReason, reason) + }) + + t.Run("happy path", func(t *testing.T) { + injector := createTestInjectorBase() + injector.apiReader = fake.NewClient(&initSecret) + + request := createTestMutationRequest(getTestDynakube()) + + err := injector.Handle(ctx, request) + require.NoError(t, err) + + isInjected, ok := request.Pod.Annotations[oacommon.AnnotationInjected] + require.True(t, ok) + assert.Equal(t, "true", isInjected) + + _, ok = request.Pod.Annotations[oacommon.AnnotationReason] + require.False(t, ok) + + installContainer := container.FindInitContainerInPodSpec(&request.Pod.Spec, dtwebhook.InstallContainerName) + require.NotNil(t, installContainer) + assert.Len(t, installContainer.Env, 3) + assert.Len(t, installContainer.Args, 14) + }) +} + +func TestIsInjected(t *testing.T) { + t.Run("init-container present == injected", func(t *testing.T) { + injector := createTestInjectorBase() + + assert.True(t, injector.isInjected(createTestMutationRequestWithInjectedPod(getTestDynakube()))) + }) + + t.Run("init-container NOT present != injected", func(t *testing.T) { + injector := createTestInjectorBase() + + assert.False(t, injector.isInjected(createTestMutationRequest(getTestDynakube()))) + }) +} + +func createTestInjectorBase() *Injector { + return &Injector{ + recorder: events.NewRecorder(record.NewFakeRecorder(10)), + } +} + +func getTestDynakube() *dynakube.DynaKube { + return &dynakube.DynaKube{ + ObjectMeta: getTestDynakubeMeta(), + Spec: dynakube.DynaKubeSpec{ + OneAgent: getAppMonSpec(&testResourceRequirements), + }, + Status: dynakube.DynaKubeStatus{ + KubernetesClusterMEID: "meid", + KubeSystemUUID: "systemuuid", + KubernetesClusterName: "meidname", + }, + } +} + +var testResourceRequirements = corev1.ResourceRequirements{ + Limits: map[corev1.ResourceName]resource.Quantity{ + corev1.ResourceCPU: resource.MustParse("100m"), + corev1.ResourceMemory: resource.MustParse("100Mi"), + }, +} + +func getTestDynakubeNoInitLimits() *dynakube.DynaKube { + return &dynakube.DynaKube{ + ObjectMeta: getTestDynakubeMeta(), + Spec: dynakube.DynaKubeSpec{ + OneAgent: getAppMonSpec(nil), + }, + } +} + +func getTestDynakubeMeta() metav1.ObjectMeta { + return metav1.ObjectMeta{ + Name: testDynakubeName, + Namespace: testNamespaceName, + Annotations: map[string]string{ + dynakube.AnnotationFeatureNodeImagePull: "true", + }, + } +} + +func getAppMonSpec(initResources *corev1.ResourceRequirements) oneagent.Spec { + return oneagent.Spec{ + ApplicationMonitoring: &oneagent.ApplicationMonitoringSpec{ + AppInjectionSpec: oneagent.AppInjectionSpec{ + InitResources: initResources, + CodeModulesImage: customImage, + }}, + } +} + +func getTestPod() *corev1.Pod { + return &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: testPodName, + Namespace: testNamespaceName, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "container", + Image: "docker.io/php:fpm-stretch", + SecurityContext: getTestSecurityContext(), + }, + }, + InitContainers: []corev1.Container{ + { + Name: "init-container", + Image: "alpine", + }, + }, + Volumes: []corev1.Volume{ + { + Name: "volume", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + }, + }, + } +} + +const testUser int64 = 420 + +func getTestSecurityContext() *corev1.SecurityContext { + return &corev1.SecurityContext{ + RunAsUser: ptr.To(testUser), + RunAsGroup: ptr.To(testUser), + } +} + +func createTestMutationRequest(dk *dynakube.DynaKube) *dtwebhook.MutationRequest { + return dtwebhook.NewMutationRequest(context.Background(), *getTestNamespace(), nil, getTestPod(), *dk) +} + +func createTestMutationRequestWithInjectedPod(dk *dynakube.DynaKube) *dtwebhook.MutationRequest { + return dtwebhook.NewMutationRequest(context.Background(), *getTestNamespace(), nil, getInjectedPod(), *dk) +} + +func getInjectedPod() *corev1.Pod { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: testPodName, + Namespace: testNamespaceName, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "container", + Image: "alpine", + SecurityContext: getTestSecurityContext(), + }, + }, + InitContainers: []corev1.Container{ + { + Name: "init-container", + Image: "alpine", + }, + }, + Volumes: []corev1.Volume{ + { + Name: "volume", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + }, + }, + } + installContainer := createInitContainerBase(pod, *getTestDynakube()) + pod.Spec.InitContainers = append(pod.Spec.InitContainers, *installContainer) + + return pod +} + +func getTestNamespace() *corev1.Namespace { + return &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: testNamespaceName, + Labels: map[string]string{ + dtwebhook.InjectionInstanceLabel: testDynakubeName, + }, + }, + } +} + +func TestIsCustomImageSet(t *testing.T) { + t.Run("true", func(t *testing.T) { + request := dtwebhook.MutationRequest{ + BaseRequest: &dtwebhook.BaseRequest{ + DynaKube: *getTestDynakube(), + }, + } + + assert.True(t, isCustomImageSet(&request)) + }) + t.Run("false, set annotations", func(t *testing.T) { + request := dtwebhook.MutationRequest{ + BaseRequest: &dtwebhook.BaseRequest{ + DynaKube: *getTestDynakube(), + Pod: &corev1.Pod{}, + }, + } + + request.DynaKube.Spec.OneAgent.ApplicationMonitoring.CodeModulesImage = "" + + assert.False(t, isCustomImageSet(&request)) + assert.Equal(t, NoCodeModulesImageReason, request.Pod.Annotations[oacommon.AnnotationReason]) + assert.Equal(t, "false", request.Pod.Annotations[oacommon.AnnotationInjected]) + }) +} + +func TestSetDynatraceInjectedAnnotation(t *testing.T) { + t.Run("add annotation", func(t *testing.T) { + request := dtwebhook.MutationRequest{ + BaseRequest: &dtwebhook.BaseRequest{ + Pod: &corev1.Pod{}, + }, + } + + setDynatraceInjectedAnnotation(&request) + + require.Len(t, request.Pod.Annotations, 1) + assert.Equal(t, "true", request.Pod.Annotations[dtwebhook.AnnotationDynatraceInjected]) + }) + + t.Run("remove reason annotation", func(t *testing.T) { + request := dtwebhook.MutationRequest{ + BaseRequest: &dtwebhook.BaseRequest{ + Pod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + dtwebhook.AnnotationDynatraceReason: "beep", + }, + }, + }, + }, + } + + setDynatraceInjectedAnnotation(&request) + + require.Len(t, request.Pod.Annotations, 1) + assert.Equal(t, "true", request.Pod.Annotations[dtwebhook.AnnotationDynatraceInjected]) + }) +} diff --git a/pkg/webhook/mutation/pod/webhook.go b/pkg/webhook/mutation/pod/webhook.go index 51cef79242..05e8d40a65 100644 --- a/pkg/webhook/mutation/pod/webhook.go +++ b/pkg/webhook/mutation/pod/webhook.go @@ -6,11 +6,12 @@ import ( "fmt" "os" - "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/container" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/env" k8spod "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/pod" maputils "github.com/Dynatrace/dynatrace-operator/pkg/util/map" dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" + "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/common/events" + podv2 "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/v2" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/manager" @@ -38,18 +39,16 @@ func AddWebhookToManager(ctx context.Context, mgr manager.Manager, ns string) er } type webhook struct { + v1 dtwebhook.PodInjector + v2 dtwebhook.PodInjector + + recorder events.EventRecorder decoder admission.Decoder - recorder eventRecorder apiReader client.Reader - webhookImage string webhookNamespace string - clusterID string - - mutators []dtwebhook.PodMutator - apmExists bool - deployedViaOLM bool + deployedViaOLM bool } func (wh *webhook) Handle(ctx context.Context, request admission.Request) admission.Response { @@ -75,23 +74,18 @@ func (wh *webhook) Handle(ctx context.Context, request admission.Request) admiss return emptyPatch } - wh.setupEventRecorder(mutationRequest) + wh.recorder.Setup(mutationRequest) - if wh.isInjected(mutationRequest) { - if wh.handlePodReinvocation(mutationRequest) { - log.Info("reinvocation policy applied", "podName", podName) - wh.recorder.sendPodUpdateEvent() - - return createResponseForPod(mutationRequest.Pod, request) + if podv2.IsEnabled(mutationRequest) { + err := wh.v2.Handle(ctx, mutationRequest) + if err != nil { + return silentErrorResponse(mutationRequest.Pod, err) + } + } else { + err := wh.v1.Handle(ctx, mutationRequest) + if err != nil { + return silentErrorResponse(mutationRequest.Pod, err) } - - log.Info("no change, all containers already injected", "podName", podName) - - return emptyPatch - } - - if err := wh.handlePodMutation(ctx, mutationRequest); err != nil { - return silentErrorResponse(mutationRequest.Pod, err) } log.Info("injection finished for pod", "podName", podName, "namespace", request.Namespace) @@ -104,29 +98,14 @@ func mutationRequired(mutationRequest *dtwebhook.MutationRequest) bool { return false } - return maputils.GetFieldBool(mutationRequest.Pod.Annotations, dtwebhook.AnnotationDynatraceInject, true) -} - -func (wh *webhook) setupEventRecorder(mutationRequest *dtwebhook.MutationRequest) { - wh.recorder.dk = &mutationRequest.DynaKube - wh.recorder.pod = mutationRequest.Pod -} - -func (wh *webhook) isInjected(mutationRequest *dtwebhook.MutationRequest) bool { - for _, mutator := range wh.mutators { - if mutator.Injected(mutationRequest.BaseRequest) { - return true - } - } - - installContainer := container.FindInitContainerInPodSpec(&mutationRequest.Pod.Spec, dtwebhook.InstallContainerName) - if installContainer != nil { - log.Info("Dynatrace init-container already present, skipping mutation, doing reinvocation", "containerName", dtwebhook.InstallContainerName) + enabledOnPod := maputils.GetFieldBool(mutationRequest.Pod.Annotations, dtwebhook.AnnotationDynatraceInject, true) - return true + enabledOnContainers := false + for _, container := range mutationRequest.Pod.Spec.Containers { + enabledOnContainers = enabledOnContainers || !dtwebhook.IsContainerExcludedFromInjection(mutationRequest.DynaKube.Annotations, mutationRequest.Pod.Annotations, container.Name) } - return false + return enabledOnPod && enabledOnContainers } func (wh *webhook) isOcDebugPod(pod *corev1.Pod) bool { @@ -141,83 +120,6 @@ func (wh *webhook) isOcDebugPod(pod *corev1.Pod) bool { return true } -func podNeedsInjection(mutationRequest *dtwebhook.MutationRequest) bool { - needsInjection := false - for _, container := range mutationRequest.Pod.Spec.Containers { - needsInjection = needsInjection || !dtwebhook.IsContainerExcludedFromInjection(mutationRequest.DynaKube.Annotations, mutationRequest.Pod.Annotations, container.Name) - } - - return needsInjection -} - -func (wh *webhook) handlePodMutation(ctx context.Context, mutationRequest *dtwebhook.MutationRequest) error { - if !podNeedsInjection(mutationRequest) { - log.Info("no mutation is needed, all containers are excluded from injection.") - - return nil - } - - mutationRequest.InstallContainer = createInstallInitContainerBase(wh.webhookImage, wh.clusterID, mutationRequest.Pod, mutationRequest.DynaKube) - - _ = updateContainerInfo(mutationRequest.BaseRequest, mutationRequest.InstallContainer) - - var isMutated bool - - for _, mutator := range wh.mutators { - if !mutator.Enabled(mutationRequest.BaseRequest) { - continue - } - - if err := mutator.Mutate(ctx, mutationRequest); err != nil { - return err - } - - isMutated = true - } - - if !isMutated { - log.Info("no mutation is enabled") - - return nil - } - - addInitContainerToPod(mutationRequest.Pod, mutationRequest.InstallContainer) - wh.recorder.sendPodInjectEvent() - setDynatraceInjectedAnnotation(mutationRequest) - - return nil -} - -func (wh *webhook) handlePodReinvocation(mutationRequest *dtwebhook.MutationRequest) bool { - var needsUpdate bool - - reinvocationRequest := mutationRequest.ToReinvocationRequest() - - isMutated := updateContainerInfo(reinvocationRequest.BaseRequest, nil) - - if !isMutated { // == no new containers were detected, we only mutate new containers during reinvoke - return false - } - - for _, mutator := range wh.mutators { - if mutator.Enabled(mutationRequest.BaseRequest) { - if update := mutator.Reinvoke(reinvocationRequest); update { - needsUpdate = true - } - } - } - - return needsUpdate -} - -func setDynatraceInjectedAnnotation(mutationRequest *dtwebhook.MutationRequest) { - if mutationRequest.Pod.Annotations == nil { - mutationRequest.Pod.Annotations = make(map[string]string) - } - - mutationRequest.Pod.Annotations[dtwebhook.AnnotationDynatraceInjected] = "true" -} - // createResponseForPod tries to format pod as json func createResponseForPod(pod *corev1.Pod, req admission.Request) admission.Response { marshaledPod, err := json.MarshalIndent(pod, "", " ") diff --git a/pkg/webhook/mutation/pod/webhook_test.go b/pkg/webhook/mutation/pod/webhook_test.go index 693af2004c..89e26d66a4 100644 --- a/pkg/webhook/mutation/pod/webhook_test.go +++ b/pkg/webhook/mutation/pod/webhook_test.go @@ -2,27 +2,24 @@ package pod import ( "context" - "encoding/json" - "fmt" + "errors" "testing" "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme" "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/consts" - "github.com/Dynatrace/dynatrace-operator/pkg/injection/startup" - "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/env" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" + "github.com/Dynatrace/dynatrace-operator/pkg/util/installconfig" dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" - "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/metadata" - oamutation "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/oneagent" + "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/common/events" webhookmock "github.com/Dynatrace/dynatrace-operator/test/mocks/pkg/webhook" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/tools/record" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" ) @@ -35,323 +32,281 @@ const ( testDynakubeName = "test-dynakube" ) -var testResourceRequirements = corev1.ResourceRequirements{ - Limits: map[corev1.ResourceName]resource.Quantity{ - corev1.ResourceCPU: resource.MustParse("100m"), - corev1.ResourceMemory: resource.MustParse("100Mi"), - }, -} +func TestHandle(t *testing.T) { + ctx := context.Background() -type mutatorTest struct { - name string - mutators []dtwebhook.PodMutator - testPod *corev1.Pod - objects []client.Object - expectedResult func(t *testing.T, response *admission.Response, mutators []dtwebhook.PodMutator) -} + t.Run("can't get NS ==> no inject, err in message", func(t *testing.T) { + wh := createTestWebhook( + webhookmock.NewPodInjector(t), + webhookmock.NewPodInjector(t), + []client.Object{}, + ) -func TestMutator(t *testing.T) { - tests := []mutatorTest{ - { - name: "happy path", - mutators: []dtwebhook.PodMutator{createSimplePodMutatorMock(t), createSimplePodMutatorMock(t)}, - testPod: getTestPod(), - objects: []client.Object{getTestDynakube(), getTestNamespace()}, - expectedResult: func(t *testing.T, response *admission.Response, mutators []dtwebhook.PodMutator) { - require.NotNil(t, response) - assert.True(t, response.Allowed) - assert.Nil(t, response.Result) - assert.NotNil(t, response.Patches) - - for _, mutator := range mutators { - assertPodMutatorCalls(t, mutator, 1) - } - }, - }, - { - name: "disable all mutators with dynatrace.com/inject", - mutators: []dtwebhook.PodMutator{createSimplePodMutatorMock(t), createSimplePodMutatorMock(t)}, - testPod: getTestPodWithInjectionDisabled(), - objects: []client.Object{getTestDynakube(), getTestNamespace()}, - expectedResult: func(t *testing.T, response *admission.Response, mutators []dtwebhook.PodMutator) { - require.NotNil(t, response) - assert.True(t, response.Allowed) - assert.NotNil(t, response.Result) - assert.Nil(t, response.Patches) - - for _, mutator := range mutators { - assertPodMutatorCalls(t, mutator, 0) - } - }, - }, - { - name: "sad path", - mutators: []dtwebhook.PodMutator{createFailPodMutatorMock(t)}, - testPod: getTestPod(), - objects: []client.Object{getTestDynakube(), getTestNamespace()}, - expectedResult: func(t *testing.T, response *admission.Response, mutators []dtwebhook.PodMutator) { - require.NotNil(t, response) - assert.True(t, response.Allowed) - assert.Contains(t, response.Result.Message, "Failed") - assert.Nil(t, response.Patches) - - for _, mutator := range mutators { - assertPodMutatorCalls(t, mutator, 1) - } - - // Logging newline so go test can parse the output correctly - log.Info("") - }, - }, - { - name: "oc debug pod", - mutators: []dtwebhook.PodMutator{createSimplePodMutatorMock(t)}, - testPod: getTestPodWithOcDebugPodAnnotations(), - objects: []client.Object{getTestDynakube(), getTestNamespace()}, - expectedResult: func(t *testing.T, response *admission.Response, mutators []dtwebhook.PodMutator) { - require.NotNil(t, response) - assert.True(t, response.Allowed) - assert.NotNil(t, response.Result) - assert.Nil(t, response.Patches) - assert.Nil(t, response.Patch) - - for _, mutator := range mutators { - assertPodMutatorCalls(t, mutator, 0) - } - }, - }, - } + request := createTestAdmissionRequest(getTestPodWithInjectionDisabled()) - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - ctx := context.Background() - request := createTestAdmissionRequest(test.testPod) - // merge test objects with the test pod - objects := test.objects - objects = append(objects, test.testPod) - podWebhook := createTestWebhook(test.mutators, objects) - - response := podWebhook.Handle(ctx, *request) - test.expectedResult(t, &response, test.mutators) - }) - } -} + resp := wh.Handle(ctx, *request) + require.NotNil(t, resp) + assert.True(t, resp.Allowed) + assert.NotEmpty(t, resp.Result.Message) + assert.Contains(t, resp.Result.Message, "err") + }) -// TestDoubleInjection is special test case for making sure that we do not inject the init-container 2 times incase 1 of the mutators are skipped. -// The mutators are intentionally NOT mocked, as to mock them properly for this scenario you would need to basically reimplement them in the mock. -// This test is necessary as the current interface is not ready to handle the scenario properly. -// Scenario: OneAgent mutation is Enabled however needs to be skipped due to not meeting the requirements, so it needs to annotate but not fully inject -func TestDoubleInjection(t *testing.T) { - noCommunicationHostDK := getTestDynakube() - fakeClient := fake.NewClient(noCommunicationHostDK, getTestNamespace()) - podWebhook := &webhook{ - apiReader: fakeClient, - decoder: admission.NewDecoder(scheme.Scheme), - recorder: eventRecorder{recorder: record.NewFakeRecorder(10), pod: &corev1.Pod{}, dk: noCommunicationHostDK}, - webhookImage: testImage, - webhookNamespace: testNamespaceName, - clusterID: testClusterID, - apmExists: false, - mutators: []dtwebhook.PodMutator{ - oamutation.NewMutator( - testImage, - testClusterID, - testNamespaceName, - fakeClient, - fakeClient, - ), - metadata.NewMutator( - testNamespaceName, - fakeClient, - fakeClient, - fakeClient, - ), - }, - } + t.Run("can't get DK ==> no inject, err in message", func(t *testing.T) { + wh := createTestWebhook( + webhookmock.NewPodInjector(t), + webhookmock.NewPodInjector(t), + []client.Object{getTestNamespace()}, + ) - pod := getTestPod() + request := createTestAdmissionRequest(getTestPodWithInjectionDisabled()) - request := createTestAdmissionRequest(pod) + resp := wh.Handle(ctx, *request) + require.NotNil(t, resp) + assert.True(t, resp.Allowed) + assert.NotEmpty(t, resp.Result.Message) + assert.Contains(t, resp.Result.Message, "err") + }) - response := podWebhook.Handle(context.Background(), *request) - require.NotNil(t, response) - assert.True(t, response.Allowed) - assert.Nil(t, response.Result) - require.Len(t, response.Patches, 2) + t.Run("DK name missing from NS but OLM ==> no inject, no err in message", func(t *testing.T) { + ns := getTestNamespace() + ns.Labels = map[string]string{} + wh := createTestWebhook( + webhookmock.NewPodInjector(t), + webhookmock.NewPodInjector(t), + []client.Object{ns}, + ) + wh.deployedViaOLM = true + + request := createTestAdmissionRequest(getTestPodWithInjectionDisabled()) + + resp := wh.Handle(ctx, *request) + require.NotNil(t, resp) + assert.True(t, resp.Allowed) + assert.NotEmpty(t, resp.Result.Message) + assert.NotContains(t, resp.Result.Message, "err") + }) - allowedPatchPaths := []string{ - "/spec/initContainers/1", - "/metadata/annotations", - } - alreadySeenPaths := []string{} + t.Run("DK name missing from NS ==> no inject, err in message", func(t *testing.T) { + ns := getTestNamespace() + ns.Labels = map[string]string{} + wh := createTestWebhook( + webhookmock.NewPodInjector(t), + webhookmock.NewPodInjector(t), + []client.Object{ns}, + ) + + request := createTestAdmissionRequest(getTestPodWithInjectionDisabled()) + + resp := wh.Handle(ctx, *request) + require.NotNil(t, resp) + assert.True(t, resp.Allowed) + assert.NotEmpty(t, resp.Result.Message) + assert.Contains(t, resp.Result.Message, "err") + }) - for _, patch := range response.Patches { - path := patch.Path - assert.NotContains(t, alreadySeenPaths, path) - assert.Contains(t, allowedPatchPaths, path) - alreadySeenPaths = append(alreadySeenPaths, path) - } + t.Run("no inject annotation ==> no inject, empty patch", func(t *testing.T) { + wh := createTestWebhook( + webhookmock.NewPodInjector(t), + webhookmock.NewPodInjector(t), + []client.Object{ + getTestNamespace(), + getTestDynakube(), + }, + ) - // simulate initial mutation, annotations + init-container <== skip in case on communication hosts - pod.Annotations = map[string]string{ - dtwebhook.AnnotationOneAgentInjected: "false", - dtwebhook.AnnotationOneAgentReason: oamutation.EmptyConnectionInfoReason, - } - pod.Spec.InitContainers = append(pod.Spec.InitContainers, corev1.Container{Name: dtwebhook.InstallContainerName}) + request := createTestAdmissionRequest(getTestPodWithInjectionDisabled()) - // adding communicationHost to the dynakube to make the scenario more complicated - // it shouldn't try to mutate the pod because now it could be enabled, that is just asking for trouble. - communicationHostDK := getTestDynakube() - communicationHostDK.Status.OneAgent.ConnectionInfoStatus.CommunicationHosts = []dynakube.CommunicationHostStatus{{Host: "test"}} - fakeClient = fake.NewClient(communicationHostDK, getTestNamespace()) - podWebhook.apiReader = fakeClient + resp := wh.Handle(ctx, *request) + require.NotNil(t, resp) + assert.True(t, resp.Allowed) + assert.Equal(t, admission.Patched(""), resp) + }) - // simulate a Reinvocation - request = createTestAdmissionRequest(pod) - response = podWebhook.Handle(context.Background(), *request) + t.Run("no inject annotation (per container) ==> no inject, empty patch", func(t *testing.T) { + wh := createTestWebhook( + webhookmock.NewPodInjector(t), + webhookmock.NewPodInjector(t), + []client.Object{ + getTestNamespace(), + getTestDynakube(), + }, + ) - require.NotNil(t, response) - assert.Equal(t, admission.Patched(""), response) -} + request := createTestAdmissionRequest(getTestPodWithInjectionDisabledOnContainer()) + + resp := wh.Handle(ctx, *request) + require.NotNil(t, resp) + assert.True(t, resp.Allowed) + assert.Equal(t, admission.Patched(""), resp) + }) -func TestHandlePodMutation(t *testing.T) { - t.Run("should call both mutators, initContainer and annotation added, no error", func(t *testing.T) { - mutator1 := createSimplePodMutatorMock(t) - mutator2 := createSimplePodMutatorMock(t) - dk := getTestDynakube() - podWebhook := createTestWebhook([]dtwebhook.PodMutator{mutator1, mutator2}, nil) - mutationRequest := createTestMutationRequest(dk) + t.Run("OC debug pod ==> no inject", func(t *testing.T) { + wh := createTestWebhook( + webhookmock.NewPodInjector(t), + webhookmock.NewPodInjector(t), + []client.Object{ + getTestNamespace(), + getTestDynakube(), + }, + ) + + request := createTestAdmissionRequest(getTestPodWithOcDebugPodAnnotations()) - err := podWebhook.handlePodMutation(context.Background(), mutationRequest) - require.NoError(t, err) - assert.NotNil(t, mutationRequest.InstallContainer) + resp := wh.Handle(ctx, *request) + require.NotNil(t, resp) + assert.True(t, resp.Allowed) + assert.Equal(t, admission.Patched(""), resp) + }) + + t.Run("no FF appmon-dk ==> v1 injector", func(t *testing.T) { + v1Injector := webhookmock.NewPodInjector(t) + v1Injector.On("Handle", mock.Anything, mock.Anything).Return(nil) + wh := createTestWebhook( + v1Injector, + webhookmock.NewPodInjector(t), + []client.Object{ + getTestNamespace(), + getTestDynakubeDefaultAppMon(), + }, + ) - require.Len(t, mutationRequest.Pod.Spec.InitContainers, 2) + installconfig.SetModulesOverride(t, installconfig.Modules{CSIDriver: false}) - assertContainersInfo(t, mutationRequest.ToReinvocationRequest(), &mutationRequest.Pod.Spec.InitContainers[1]) + request := createTestAdmissionRequest(getTestPod()) - initSecurityContext := mutationRequest.Pod.Spec.InitContainers[1].SecurityContext - require.NotNil(t, initSecurityContext) + resp := wh.Handle(ctx, *request) + require.NotNil(t, resp) + assert.NotEqual(t, admission.Patched(""), resp) + }) - require.NotNil(t, initSecurityContext.Privileged) - assert.False(t, *initSecurityContext.Privileged) + t.Run("FF appmon-dk WITHOUT CSI ==> v2 injector", func(t *testing.T) { + dk := getTestDynakubeDefaultAppMon() + dk.Annotations = map[string]string{ + dynakube.AnnotationFeatureNodeImagePull: "true", + } - require.NotNil(t, initSecurityContext.AllowPrivilegeEscalation) - assert.False(t, *initSecurityContext.AllowPrivilegeEscalation) + v2Injector := webhookmock.NewPodInjector(t) + v2Injector.On("Handle", mock.Anything, mock.Anything).Return(nil) + wh := createTestWebhook( + webhookmock.NewPodInjector(t), + v2Injector, + []client.Object{ + getTestNamespace(), + dk, + }, + ) - require.NotNil(t, initSecurityContext.ReadOnlyRootFilesystem) - assert.True(t, *initSecurityContext.ReadOnlyRootFilesystem) + installconfig.SetModulesOverride(t, installconfig.Modules{CSIDriver: false}) - assert.NotNil(t, initSecurityContext.RunAsNonRoot) - assert.True(t, *initSecurityContext.RunAsNonRoot) + request := createTestAdmissionRequest(getTestPod()) - assert.Equal(t, mutationRequest.Pod.Spec.InitContainers[1].Resources, testResourceRequirements) - assert.Equal(t, "true", mutationRequest.Pod.Annotations[dtwebhook.AnnotationDynatraceInjected]) - mutator1.AssertCalled(t, "Enabled", mutationRequest.BaseRequest) - mutator1.AssertCalled(t, "Mutate", mock.Anything, mutationRequest) - mutator2.AssertCalled(t, "Enabled", mutationRequest.BaseRequest) - mutator2.AssertCalled(t, "Mutate", mock.Anything, mutationRequest) + resp := wh.Handle(ctx, *request) + require.NotNil(t, resp) + assert.NotEqual(t, admission.Patched(""), resp) }) - t.Run("should call 1 webhook, 1 error, no initContainer and annotation", func(t *testing.T) { - sadMutator := createFailPodMutatorMock(t) - happyMutator := createSimplePodMutatorMock(t) - dk := getTestDynakube() - podWebhook := createTestWebhook([]dtwebhook.PodMutator{sadMutator, happyMutator}, nil) - mutationRequest := createTestMutationRequest(dk) - - err := podWebhook.handlePodMutation(context.Background(), mutationRequest) - require.Error(t, err) - assert.NotNil(t, mutationRequest.InstallContainer) - assert.Len(t, mutationRequest.Pod.Spec.InitContainers, 1) - assert.NotEqual(t, "true", mutationRequest.Pod.Annotations[dtwebhook.AnnotationDynatraceInjected]) - sadMutator.AssertCalled(t, "Enabled", mutationRequest.BaseRequest) - sadMutator.AssertCalled(t, "Mutate", mock.Anything, mutationRequest) - happyMutator.AssertNotCalled(t, "Enabled", mock.Anything) - happyMutator.AssertNotCalled(t, "Mutate", mock.Anything, mock.Anything) - }) -} -func TestHandlePodReinvocation(t *testing.T) { - t.Run("should call both mutators, updated == true", func(t *testing.T) { - mutator1 := createAlreadyInjectedPodMutatorMock(t) - mutator2 := createAlreadyInjectedPodMutatorMock(t) - dk := getTestDynakube() - podWebhook := createTestWebhook([]dtwebhook.PodMutator{mutator1, mutator2}, nil) - mutationRequest := createTestMutationRequestWithInjectedPod(dk) + t.Run("FF metadata-dk WITHOUT CSI ==> v1 injector", func(t *testing.T) { + dk := getTestMetadataDynakube() + dk.Annotations = map[string]string{ + dynakube.AnnotationFeatureNodeImagePull: "true", + } - updated := podWebhook.handlePodReinvocation(mutationRequest) - require.True(t, updated) + v1Injector := webhookmock.NewPodInjector(t) + v1Injector.On("Handle", mock.Anything, mock.Anything).Return(nil) + wh := createTestWebhook( + v1Injector, + webhookmock.NewPodInjector(t), + []client.Object{ + getTestNamespace(), + dk, + }, + ) - require.Len(t, mutationRequest.Pod.Spec.InitContainers, 2) - assertContainersInfo(t, mutationRequest.ToReinvocationRequest(), &mutationRequest.Pod.Spec.InitContainers[1]) + installconfig.SetModulesOverride(t, installconfig.Modules{CSIDriver: false}) - mutator1.AssertCalled(t, "Enabled", mutationRequest.BaseRequest) - mutator1.AssertCalled(t, "Reinvoke", mutationRequest.ToReinvocationRequest()) - mutator2.AssertCalled(t, "Enabled", mutationRequest.BaseRequest) - mutator2.AssertCalled(t, "Reinvoke", mutationRequest.ToReinvocationRequest()) - }) - t.Run("should call both webhook, only 1 update, updated == true", func(t *testing.T) { - failingMutator := createFailPodMutatorMock(t) - workingMutator := createAlreadyInjectedPodMutatorMock(t) - dk := getTestDynakube() - podWebhook := createTestWebhook([]dtwebhook.PodMutator{failingMutator, workingMutator}, nil) - mutationRequest := createTestMutationRequestWithInjectedPod(dk) - - updated := podWebhook.handlePodReinvocation(mutationRequest) - require.True(t, updated) - failingMutator.AssertCalled(t, "Enabled", mutationRequest.BaseRequest) - failingMutator.AssertCalled(t, "Reinvoke", mutationRequest.ToReinvocationRequest()) - workingMutator.AssertCalled(t, "Enabled", mutationRequest.BaseRequest) - workingMutator.AssertCalled(t, "Reinvoke", mutationRequest.ToReinvocationRequest()) - }) - t.Run("should call webhook, no update", func(t *testing.T) { - failingMutator := createFailPodMutatorMock(t) - dk := getTestDynakube() - podWebhook := createTestWebhook([]dtwebhook.PodMutator{failingMutator}, nil) - mutationRequest := createTestMutationRequestWithInjectedPod(dk) - - updated := podWebhook.handlePodReinvocation(mutationRequest) - require.False(t, updated) - failingMutator.AssertCalled(t, "Enabled", mutationRequest.BaseRequest) - failingMutator.AssertCalled(t, "Reinvoke", mutationRequest.ToReinvocationRequest()) - failingMutator.AssertNotCalled(t, "Injected", mock.Anything) - failingMutator.AssertNotCalled(t, "Mutated", mock.Anything, mock.Anything) + request := createTestAdmissionRequest(getTestPod()) + + resp := wh.Handle(ctx, *request) + require.NotNil(t, resp) + assert.NotEqual(t, admission.Patched(""), resp) }) -} -func assertContainersInfo(t *testing.T, request *dtwebhook.ReinvocationRequest, installContainer *corev1.Container) { - rawContainerInfo := env.FindEnvVar(installContainer.Env, consts.ContainerInfoEnv) - require.NotNil(t, rawContainerInfo) + t.Run("FF appmon-dk WITH CSI ==> v1 injector", func(t *testing.T) { + dk := getTestDynakubeDefaultAppMon() + dk.Annotations = map[string]string{ + dynakube.AnnotationFeatureNodeImagePull: "true", + } - var containerInfo []startup.ContainerInfo - err := json.Unmarshal([]byte(rawContainerInfo.Value), &containerInfo) - require.NoError(t, err) + v1Injector := webhookmock.NewPodInjector(t) + v1Injector.On("Handle", mock.Anything, mock.Anything).Return(nil) + wh := createTestWebhook( + v1Injector, + webhookmock.NewPodInjector(t), + []client.Object{ + getTestNamespace(), + dk, + }, + ) - for _, container := range request.Pod.Spec.Containers { - found := false + installconfig.SetModulesOverride(t, installconfig.Modules{CSIDriver: true}) - for _, info := range containerInfo { - if container.Name == info.Name { - assert.Equal(t, container.Image, info.Image) + request := createTestAdmissionRequest(getTestPod()) - found = true + resp := wh.Handle(ctx, *request) + require.NotNil(t, resp) + assert.NotEqual(t, admission.Patched(""), resp) + }) + + t.Run("v1 injector error => silent error", func(t *testing.T) { + v1Injector := webhookmock.NewPodInjector(t) + v1Injector.On("Handle", mock.Anything, mock.Anything).Return(errors.New("BOOM")) + wh := createTestWebhook( + v1Injector, + webhookmock.NewPodInjector(t), + []client.Object{ + getTestNamespace(), + getTestDynakubeDefaultAppMon(), + }, + ) - break - } + installconfig.SetModulesOverride(t, installconfig.Modules{CSIDriver: false}) + + request := createTestAdmissionRequest(getTestPod()) + + resp := wh.Handle(ctx, *request) + require.NotNil(t, resp) + assert.True(t, resp.Allowed) + assert.NotEmpty(t, resp.Result.Message) + assert.Contains(t, resp.Result.Message, "BOOM") + }) + + t.Run("v2 injector error => silent error", func(t *testing.T) { + dk := getTestDynakubeDefaultAppMon() + dk.Annotations = map[string]string{ + dynakube.AnnotationFeatureNodeImagePull: "true", } - require.True(t, found) - } -} + v2Injector := webhookmock.NewPodInjector(t) + v2Injector.On("Handle", mock.Anything, mock.Anything).Return(errors.New("BOOM")) + wh := createTestWebhook( + webhookmock.NewPodInjector(t), + v2Injector, + []client.Object{ + getTestNamespace(), + dk, + }, + ) -func assertPodMutatorCalls(t *testing.T, mutator dtwebhook.PodMutator, expectedCalls int) { - mock, ok := mutator.(*webhookmock.PodMutator) - if !ok { - t.Fatalf("assertPodMutatorCalls: webhook is not a mock") - } + installconfig.SetModulesOverride(t, installconfig.Modules{CSIDriver: false}) - mock.AssertNumberOfCalls(t, "Enabled", expectedCalls) - mock.AssertNumberOfCalls(t, "Mutate", expectedCalls) + request := createTestAdmissionRequest(getTestPod()) + + resp := wh.Handle(ctx, *request) + require.NotNil(t, resp) + assert.True(t, resp.Allowed) + assert.NotEmpty(t, resp.Result.Message) + assert.Contains(t, resp.Result.Message, "BOOM") + }) } func getTestPodWithInjectionDisabled() *corev1.Pod { @@ -373,75 +328,56 @@ func getTestPodWithOcDebugPodAnnotations() *corev1.Pod { return pod } -func createTestWebhook(mutators []dtwebhook.PodMutator, objects []client.Object) *webhook { +func getTestPodWithInjectionDisabledOnContainer() *corev1.Pod { + pod := getTestPod() + pod.Annotations = map[string]string{} + + for _, c := range pod.Spec.Containers { + pod.Annotations[dtwebhook.AnnotationContainerInjection+"/"+c.Name] = "false" + } + + return pod +} + +func createTestWebhook(v1, v2 dtwebhook.PodInjector, objects []client.Object) *webhook { decoder := admission.NewDecoder(scheme.Scheme) return &webhook{ + v1: v1, + v2: v2, apiReader: fake.NewClient(objects...), decoder: decoder, - recorder: eventRecorder{recorder: record.NewFakeRecorder(10), pod: &corev1.Pod{}, dk: getTestDynakube()}, - webhookImage: testImage, webhookNamespace: testNamespaceName, - clusterID: testClusterID, - apmExists: false, - mutators: mutators, + recorder: events.NewRecorder(record.NewFakeRecorder(10)), } } -func createSimplePodMutatorMock(t *testing.T) *webhookmock.PodMutator { - mutator := webhookmock.NewPodMutator(t) - mutator.On("Enabled", mock.Anything).Return(true).Maybe() - mutator.On("Injected", mock.Anything).Return(false).Maybe() - mutator.On("Mutate", mock.Anything, mock.Anything).Return(nil).Maybe() - mutator.On("Reinvoke", mock.Anything).Return(true).Maybe() - - return mutator -} - -func createAlreadyInjectedPodMutatorMock(t *testing.T) *webhookmock.PodMutator { - mutator := webhookmock.NewPodMutator(t) - mutator.On("Enabled", mock.Anything).Return(true).Maybe() - mutator.On("Injected", mock.Anything).Return(true).Maybe() - mutator.On("Mutate", mock.Anything, mock.Anything).Return(nil).Maybe() - mutator.On("Reinvoke", mock.Anything).Return(true).Maybe() - - return mutator -} - -func createFailPodMutatorMock(t *testing.T) *webhookmock.PodMutator { - mutator := webhookmock.NewPodMutator(t) - mutator.On("Enabled", mock.Anything).Return(true).Maybe() - mutator.On("Injected", mock.Anything).Return(false).Maybe() - mutator.On("Mutate", mock.Anything, mock.Anything).Return(fmt.Errorf("BOOM")).Maybe() - mutator.On("Reinvoke", mock.Anything).Return(false).Maybe() - - return mutator -} - func getTestDynakube() *dynakube.DynaKube { return &dynakube.DynaKube{ ObjectMeta: getTestDynakubeMeta(), Spec: dynakube.DynaKubeSpec{ - OneAgent: getCloudNativeSpec(&testResourceRequirements), + OneAgent: getCloudNativeSpec(), }, } } -func getTestDynakubeNoInitLimits() *dynakube.DynaKube { +func getTestDynakubeDefaultAppMon() *dynakube.DynaKube { return &dynakube.DynaKube{ ObjectMeta: getTestDynakubeMeta(), Spec: dynakube.DynaKubeSpec{ - OneAgent: getCloudNativeSpec(nil), + OneAgent: oneagent.Spec{ + ApplicationMonitoring: &oneagent.ApplicationMonitoringSpec{}, + }, }, } } -func getTestDynakubeDefaultAppMon() *dynakube.DynaKube { +func getTestMetadataDynakube() *dynakube.DynaKube { return &dynakube.DynaKube{ ObjectMeta: getTestDynakubeMeta(), Spec: dynakube.DynaKubeSpec{ - OneAgent: dynakube.OneAgentSpec{ - ApplicationMonitoring: &dynakube.ApplicationMonitoringSpec{}, + MetadataEnrichment: dynakube.MetadataEnrichment{ + Enabled: ptr.To(true), }, }, } @@ -454,12 +390,10 @@ func getTestDynakubeMeta() metav1.ObjectMeta { } } -func getCloudNativeSpec(initResources *corev1.ResourceRequirements) dynakube.OneAgentSpec { - return dynakube.OneAgentSpec{ - CloudNativeFullStack: &dynakube.CloudNativeFullStackSpec{ - AppInjectionSpec: dynakube.AppInjectionSpec{ - InitResources: initResources, - }, +func getCloudNativeSpec() oneagent.Spec { + return oneagent.Spec{ + CloudNativeFullStack: &oneagent.CloudNativeFullStackSpec{ + AppInjectionSpec: oneagent.AppInjectionSpec{}, }, } } diff --git a/pkg/webhook/mutator.go b/pkg/webhook/request.go similarity index 71% rename from pkg/webhook/mutator.go rename to pkg/webhook/request.go index 0cffc2dc32..44cc68ff15 100644 --- a/pkg/webhook/mutator.go +++ b/pkg/webhook/request.go @@ -3,26 +3,47 @@ package webhook import ( "context" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/pod" corev1 "k8s.io/api/core/v1" ) -type PodMutator interface { - // Enabled returns true if the mutator needs to be executed for the given request. - // This is used to filter out mutators that are not needed for the given request. - Enabled(request *BaseRequest) bool +func NewMutationRequest(ctx context.Context, namespace corev1.Namespace, installContainer *corev1.Container, pod *corev1.Pod, dk dynakube.DynaKube) *MutationRequest { + return &MutationRequest{ + BaseRequest: newBaseRequest(pod, namespace, dk), + Context: ctx, + InstallContainer: installContainer, + } +} + +// MutationRequest contains all the information needed to mutate a pod +// It is meant to be passed into each mutator, so that they can mutate the elements in the way they need to, +// and after passing it in to all the mutator the request will have the final state which can be used to mutate the pod. +type MutationRequest struct { + *BaseRequest + Context context.Context + InstallContainer *corev1.Container +} - // Injected returns true if the mutator has already injected into the pod of the given request. - // This is used during reinvocation to prevent multiple injections. - Injected(request *BaseRequest) bool +func (request *MutationRequest) ToReinvocationRequest() *ReinvocationRequest { + return &ReinvocationRequest{ + BaseRequest: request.BaseRequest, + } +} - // Mutate mutates the elements of the given MutationRequest, specifically the pod and installContainer. - Mutate(ctx context.Context, request *MutationRequest) error +// ReinvocationRequest contains all the information needed to reinvoke a pod +// It is meant to be passed into each mutator, so that they can mutate the elements in the way they need to, +// and after passing it in to all the mutator the request will have the final state which can be used to mutate the pod. +type ReinvocationRequest struct { + *BaseRequest +} - // Reinvocation mutates the pod of the given ReinvocationRequest. - // It only mutates the parts of the pod that haven't been mutated yet. (example: another webhook mutated the pod after our webhook was executed) - Reinvoke(request *ReinvocationRequest) bool +func newBaseRequest(pod *corev1.Pod, namespace corev1.Namespace, dk dynakube.DynaKube) *BaseRequest { + return &BaseRequest{ + Pod: pod, + DynaKube: dk, + Namespace: namespace, + } } // BaseRequest is the base request for all mutation requests @@ -58,41 +79,3 @@ func (req *BaseRequest) NewContainers(isInjected func(corev1.Container) bool) (n return } - -// MutationRequest contains all the information needed to mutate a pod -// It is meant to be passed into each mutator, so that they can mutate the elements in the way they need to, -// and after passing it in to all the mutator the request will have the final state which can be used to mutate the pod. -type MutationRequest struct { - *BaseRequest - Context context.Context - InstallContainer *corev1.Container -} - -// ReinvocationRequest contains all the information needed to reinvoke a pod -// It is meant to be passed into each mutator, so that they can mutate the elements in the way they need to, -// and after passing it in to all the mutator the request will have the final state which can be used to mutate the pod. -type ReinvocationRequest struct { - *BaseRequest -} - -func newBaseRequest(pod *corev1.Pod, namespace corev1.Namespace, dk dynakube.DynaKube) *BaseRequest { - return &BaseRequest{ - Pod: pod, - DynaKube: dk, - Namespace: namespace, - } -} - -func NewMutationRequest(ctx context.Context, namespace corev1.Namespace, installContainer *corev1.Container, pod *corev1.Pod, dk dynakube.DynaKube) *MutationRequest { - return &MutationRequest{ - BaseRequest: newBaseRequest(pod, namespace, dk), - Context: ctx, - InstallContainer: installContainer, - } -} - -func (request *MutationRequest) ToReinvocationRequest() *ReinvocationRequest { - return &ReinvocationRequest{ - BaseRequest: request.BaseRequest, - } -} diff --git a/test/features/activegate/activegate.go b/test/features/activegate/activegate.go index a887bcf145..4fbbe3066c 100644 --- a/test/features/activegate/activegate.go +++ b/test/features/activegate/activegate.go @@ -9,7 +9,7 @@ import ( "testing" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/value" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/consts" "github.com/Dynatrace/dynatrace-operator/test/helpers" "github.com/Dynatrace/dynatrace-operator/test/helpers/components/activegate" @@ -192,6 +192,7 @@ func assertExpectedModulesAreActive(t *testing.T, log string) { "kubernetes_monitoring", "odin_collector", "metrics_ingest", + "debugging", } head := strings.SplitAfter(log, "[, ModulesManager] Modules:") diff --git a/test/features/activegate/curl.go b/test/features/activegate/curl.go index 7eedb99d4e..a8e16fd957 100644 --- a/test/features/activegate/curl.go +++ b/test/features/activegate/curl.go @@ -7,7 +7,7 @@ import ( "fmt" "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/capability" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/consts" "github.com/Dynatrace/dynatrace-operator/test/helpers/curl" diff --git a/test/features/applicationmonitoring/label_version_detection.go b/test/features/applicationmonitoring/label_version_detection.go index 97b8565f79..c8d0c85612 100644 --- a/test/features/applicationmonitoring/label_version_detection.go +++ b/test/features/applicationmonitoring/label_version_detection.go @@ -8,7 +8,8 @@ import ( "strings" "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" "github.com/Dynatrace/dynatrace-operator/test/helpers" dynakubeComponents "github.com/Dynatrace/dynatrace-operator/test/helpers/components/dynakube" "github.com/Dynatrace/dynatrace-operator/test/helpers/kubeobjects/namespace" @@ -102,7 +103,7 @@ func LabelVersionDetection(t *testing.T) features.Feature { defaultDynakube := *dynakubeComponents.New( dynakubeComponents.WithName("dynakube-components-default"), dynakubeComponents.WithApiUrl(secretConfig.ApiUrl), - dynakubeComponents.WithApplicationMonitoringSpec(&dynakube.ApplicationMonitoringSpec{}), + dynakubeComponents.WithApplicationMonitoringSpec(&oneagent.ApplicationMonitoringSpec{}), dynakubeComponents.WithNameBasedOneAgentNamespaceSelector(), ) @@ -110,7 +111,7 @@ func LabelVersionDetection(t *testing.T) features.Feature { dynakubeComponents.WithName("dynakube-components-labels"), dynakubeComponents.WithAnnotations(map[string]string{dynakube.AnnotationFeatureLabelVersionDetection: "true"}), dynakubeComponents.WithApiUrl(secretConfig.ApiUrl), - dynakubeComponents.WithApplicationMonitoringSpec(&dynakube.ApplicationMonitoringSpec{}), + dynakubeComponents.WithApplicationMonitoringSpec(&oneagent.ApplicationMonitoringSpec{}), dynakubeComponents.WithNameBasedOneAgentNamespaceSelector(), ) @@ -216,12 +217,12 @@ func assertValue(ctx context.Context, t *testing.T, resource *resources.Resource require.NoError(t, err) stdOut := strings.TrimSpace(executionResult.StdOut.String()) - assert.Zero(t, executionResult.StdErr.Len()) + assert.Empty(t, executionResult.StdErr.String()) assert.Equal(t, expectedValue, stdOut, "%s:%s pod - %s variable has invalid value", podItem.Namespace, podItem.Name, variableName) } func buildDisabledBuildLabelNamespace(dk dynakube.DynaKube) corev1.Namespace { - return *namespace.New(disabledBuildLabelsNamespace, namespace.WithLabels(dk.OneAgentNamespaceSelector().MatchLabels)) + return *namespace.New(disabledBuildLabelsNamespace, namespace.WithLabels(dk.OneAgent().GetNamespaceSelector().MatchLabels)) } func buildDisabledBuildLabelSampleApp(t *testing.T, dk dynakube.DynaKube) *sample.App { @@ -229,7 +230,7 @@ func buildDisabledBuildLabelSampleApp(t *testing.T, dk dynakube.DynaKube) *sampl } func buildDefaultBuildLabelNamespace(dk dynakube.DynaKube) corev1.Namespace { - return *namespace.New(defaultBuildLabelsNamespace, namespace.WithLabels(dk.OneAgentNamespaceSelector().MatchLabels)) + return *namespace.New(defaultBuildLabelsNamespace, namespace.WithLabels(dk.OneAgent().GetNamespaceSelector().MatchLabels)) } func buildDefaultBuildLabelSampleApp(t *testing.T, dk dynakube.DynaKube) *sample.App { @@ -252,7 +253,7 @@ func buildDefaultBuildLabelSampleApp(t *testing.T, dk dynakube.DynaKube) *sample func buildCustomBuildLabelNamespace(dk dynakube.DynaKube) corev1.Namespace { return *namespace.New( customBuildLabelsNamespace, - namespace.WithLabels(dk.OneAgentNamespaceSelector().MatchLabels), + namespace.WithLabels(dk.OneAgent().GetNamespaceSelector().MatchLabels), namespace.WithAnnotation(map[string]string{ "mapping.release.dynatrace.com/version": "metadata.labels['my.domain/version']", "mapping.release.dynatrace.com/product": "metadata.labels['my.domain/product']", @@ -282,7 +283,7 @@ func buildCustomBuildLabelSampleApp(t *testing.T, dk dynakube.DynaKube) *sample. func buildPreservedBuildLabelNamespace(dk dynakube.DynaKube) corev1.Namespace { return *namespace.New( preservedBuildLabelsNamespace, - namespace.WithLabels(dk.OneAgentNamespaceSelector().MatchLabels), + namespace.WithLabels(dk.OneAgent().GetNamespaceSelector().MatchLabels), namespace.WithAnnotation(map[string]string{ "mapping.release.dynatrace.com/version": "metadata.labels['my.domain/version']", "mapping.release.dynatrace.com/product": "metadata.labels['my.domain/product']", @@ -350,7 +351,7 @@ func buildPreservedBuildLabelSampleApp(t *testing.T, dk dynakube.DynaKube) *samp func buildInvalidBuildLabelNamespace(dk dynakube.DynaKube) corev1.Namespace { return *namespace.New( invalidBuildLabelsNamespace, - namespace.WithLabels(dk.OneAgentNamespaceSelector().MatchLabels), + namespace.WithLabels(dk.OneAgent().GetNamespaceSelector().MatchLabels), namespace.WithAnnotation(map[string]string{ "mapping.release.dynatrace.com/stage": "metadata.labels['my.domain/invalid-stage']", "mapping.release.dynatrace.com/build-version": "metadata.labels['my.domain/invalid-build-version']", diff --git a/test/features/applicationmonitoring/metadata_enrichment.go b/test/features/applicationmonitoring/metadata_enrichment.go index 0dca2e818a..6c6ba5d716 100644 --- a/test/features/applicationmonitoring/metadata_enrichment.go +++ b/test/features/applicationmonitoring/metadata_enrichment.go @@ -8,11 +8,12 @@ import ( "fmt" "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" "github.com/Dynatrace/dynatrace-operator/pkg/consts" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/env" maputil "github.com/Dynatrace/dynatrace-operator/pkg/util/map" - "github.com/Dynatrace/dynatrace-operator/pkg/webhook" + metacommon "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/common/metadata" + oacommon "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/common/oneagent" "github.com/Dynatrace/dynatrace-operator/test/helpers" dynakubeComponents "github.com/Dynatrace/dynatrace-operator/test/helpers/components/dynakube" "github.com/Dynatrace/dynatrace-operator/test/helpers/kubeobjects/deployment" @@ -47,7 +48,7 @@ func MetadataEnrichment(t *testing.T) features.Feature { testDynakube := *dynakubeComponents.New( dynakubeComponents.WithApiUrl(secretConfig.ApiUrl), dynakubeComponents.WithMetadataEnrichment(), - dynakubeComponents.WithApplicationMonitoringSpec(&dynakube.ApplicationMonitoringSpec{}), + dynakubeComponents.WithApplicationMonitoringSpec(&oneagent.ApplicationMonitoringSpec{}), dynakubeComponents.WithNameBasedMetadataEnrichmentNamespaceSelector(), dynakubeComponents.WithNameBasedOneAgentNamespaceSelector(), ) @@ -59,7 +60,7 @@ func MetadataEnrichment(t *testing.T) features.Feature { } injectEverythingLabels := maputil.MergeMap( - testDynakube.OneAgentNamespaceSelector().MatchLabels, + testDynakube.OneAgent().GetNamespaceSelector().MatchLabels, testDynakube.MetadataEnrichmentNamespaceSelector().MatchLabels, ) @@ -71,8 +72,8 @@ func MetadataEnrichment(t *testing.T) features.Feature { sample.AsDeployment(), sample.WithNamespaceLabels(injectEverythingLabels), sample.WithAnnotations(map[string]string{ - webhook.AnnotationOneAgentInject: "false", - webhook.AnnotationMetadataEnrichmentInject: "true", + oacommon.AnnotationInject: "false", + metacommon.AnnotationInject: "true", })), assess: deploymentPodsHaveOnlyMetadataEnrichmentInitContainer, }, @@ -82,8 +83,8 @@ func MetadataEnrichment(t *testing.T) features.Feature { sample.WithName("pod-metadata-annotation"), sample.WithNamespaceLabels(injectEverythingLabels), sample.WithAnnotations(map[string]string{ - webhook.AnnotationOneAgentInject: "false", - webhook.AnnotationMetadataEnrichmentInject: "true", + oacommon.AnnotationInject: "false", + metacommon.AnnotationInject: "true", })), assess: podHasOnlyMetadataEnrichmentInitContainer, }, @@ -110,8 +111,8 @@ func MetadataEnrichment(t *testing.T) features.Feature { sample.WithName("pod-oa-annotation"), sample.WithNamespaceLabels(injectEverythingLabels), sample.WithAnnotations(map[string]string{ - webhook.AnnotationOneAgentInject: "true", - webhook.AnnotationMetadataEnrichmentInject: "false", + oacommon.AnnotationInject: "true", + metacommon.AnnotationInject: "false", })), assess: podHasOnlyOneAgentInitContainer, }, @@ -119,7 +120,7 @@ func MetadataEnrichment(t *testing.T) features.Feature { name: "control oneagent-injection with namespace-selector - pod", app: sample.NewApp(t, &testDynakube, sample.WithName("pod-oa-label"), - sample.WithNamespaceLabels(testDynakube.OneAgentNamespaceSelector().MatchLabels), + sample.WithNamespaceLabels(testDynakube.OneAgent().GetNamespaceSelector().MatchLabels), ), assess: podHasOnlyOneAgentInitContainer, }, @@ -162,7 +163,7 @@ func podHasOnlyMetadataEnrichmentInitContainer(samplePod *sample.App) features.F func assessPodHasMetadataEnrichmentFile(ctx context.Context, t *testing.T, resource *resources.Resources, testPod corev1.Pod) { enrichmentMetadata := getMetadataEnrichmentMetadataFromPod(ctx, t, resource, testPod) - assert.Equal(t, "Pod", enrichmentMetadata.WorkloadKind) + assert.Equal(t, "pod", enrichmentMetadata.WorkloadKind) assert.Equal(t, testPod.Name, enrichmentMetadata.WorkloadName) } @@ -201,8 +202,8 @@ func podHasCompleteInitContainer(samplePod *sample.App) features.Func { //nolint assert.True(t, env.IsIn(envVars, consts.AgentInjectedEnv)) - assert.Contains(t, testPod.Annotations, webhook.AnnotationWorkloadKind) - assert.Contains(t, testPod.Annotations, webhook.AnnotationWorkloadName) + assert.Contains(t, testPod.Annotations, metacommon.AnnotationWorkloadKind) + assert.Contains(t, testPod.Annotations, metacommon.AnnotationWorkloadName) return ctx } @@ -224,8 +225,8 @@ func podHasOnlyOneAgentInitContainer(samplePod *sample.App) features.Func { //no assert.True(t, env.IsIn(envVars, consts.AgentInjectedEnv)) - assert.NotContains(t, testPod.Annotations, webhook.AnnotationWorkloadKind) - assert.NotContains(t, testPod.Annotations, webhook.AnnotationWorkloadName) + assert.NotContains(t, testPod.Annotations, metacommon.AnnotationWorkloadKind) + assert.NotContains(t, testPod.Annotations, metacommon.AnnotationWorkloadName) return ctx } @@ -235,7 +236,7 @@ func assessDeploymentHasMetadataEnrichmentFile(ctx context.Context, t *testing.T return func(pod corev1.Pod) { enrichmentMetadata := getMetadataEnrichmentMetadataFromPod(ctx, t, resource, pod) - assert.Equal(t, "Deployment", enrichmentMetadata.WorkloadKind) + assert.Equal(t, "deployment", enrichmentMetadata.WorkloadKind) assert.Equal(t, deploymentName, enrichmentMetadata.WorkloadName) } } @@ -274,7 +275,7 @@ func assessOnlyMetadataEnrichmentIsInjected(t *testing.T) deployment.PodConsumer assert.False(t, env.IsIn(envVars, consts.AgentInjectedEnv)) - assert.Contains(t, pod.Annotations, webhook.AnnotationWorkloadKind) - assert.Contains(t, pod.Annotations, webhook.AnnotationWorkloadName) + assert.Contains(t, pod.Annotations, metacommon.AnnotationWorkloadKind) + assert.Contains(t, pod.Annotations, metacommon.AnnotationWorkloadName) } } diff --git a/test/features/applicationmonitoring/read_only_csi_volume.go b/test/features/applicationmonitoring/read_only_csi_volume.go index 677978a75e..491c712c34 100644 --- a/test/features/applicationmonitoring/read_only_csi_volume.go +++ b/test/features/applicationmonitoring/read_only_csi_volume.go @@ -7,8 +7,9 @@ import ( "fmt" "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - oamutation "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/oneagent" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" + oamutation "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/v1/oneagent" "github.com/Dynatrace/dynatrace-operator/test/helpers" "github.com/Dynatrace/dynatrace-operator/test/helpers/components/codemodules" dynakubeComponents "github.com/Dynatrace/dynatrace-operator/test/helpers/components/dynakube" @@ -33,7 +34,7 @@ func ReadOnlyCSIVolume(t *testing.T) features.Feature { testDynakube := *dynakubeComponents.New( dynakubeComponents.WithAnnotations(readOnlyInjection), dynakubeComponents.WithApiUrl(secretConfig.ApiUrl), - dynakubeComponents.WithApplicationMonitoringSpec(&dynakube.ApplicationMonitoringSpec{}), + dynakubeComponents.WithApplicationMonitoringSpec(&oneagent.ApplicationMonitoringSpec{}), ) sampleDeployment := sample.NewApp(t, &testDynakube, sample.AsDeployment()) builder.Assess("install sample deployment namespace", sampleDeployment.InstallNamespace()) diff --git a/test/features/applicationmonitoring/without_csi.go b/test/features/applicationmonitoring/without_csi.go index 8f36d510c7..5b1e0d4706 100644 --- a/test/features/applicationmonitoring/without_csi.go +++ b/test/features/applicationmonitoring/without_csi.go @@ -6,8 +6,7 @@ import ( "context" "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/util/address" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" "github.com/Dynatrace/dynatrace-operator/pkg/webhook" "github.com/Dynatrace/dynatrace-operator/test/helpers" dynakubeComponents "github.com/Dynatrace/dynatrace-operator/test/helpers/components/dynakube" @@ -15,6 +14,7 @@ import ( "github.com/Dynatrace/dynatrace-operator/test/helpers/tenant" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" + "k8s.io/utils/ptr" "sigs.k8s.io/e2e-framework/pkg/envconf" "sigs.k8s.io/e2e-framework/pkg/features" ) @@ -25,7 +25,7 @@ func WithoutCSI(t *testing.T) features.Feature { secretConfig := tenant.GetSingleTenantSecret(t) appOnlyDynakube := *dynakubeComponents.New( dynakubeComponents.WithApiUrl(secretConfig.ApiUrl), - dynakubeComponents.WithApplicationMonitoringSpec(&dynakube.ApplicationMonitoringSpec{}), + dynakubeComponents.WithApplicationMonitoringSpec(&oneagent.ApplicationMonitoringSpec{}), ) dynakubeComponents.Install(builder, helpers.LevelAssess, &secretConfig, appOnlyDynakube) @@ -52,8 +52,8 @@ func WithoutCSI(t *testing.T) features.Feature { sample.WithName("random-user"), sample.AsDeployment(), sample.WithSecurityContext(corev1.PodSecurityContext{ - RunAsUser: address.Of[int64](1234), - RunAsGroup: address.Of[int64](1234), + RunAsUser: ptr.To[int64](1234), + RunAsGroup: ptr.To[int64](1234), }), ) builder.Assess("install sample app with random users set", randomUserSample.Install()) diff --git a/test/features/bootstrapper/node_image_pull_csi.go b/test/features/bootstrapper/node_image_pull_csi.go new file mode 100644 index 0000000000..f285068780 --- /dev/null +++ b/test/features/bootstrapper/node_image_pull_csi.go @@ -0,0 +1,89 @@ +//go:build e2e + +package bootstrapper + +import ( + "context" + "testing" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" + "github.com/Dynatrace/dynatrace-operator/test/features/cloudnative/codemodules" + "github.com/Dynatrace/dynatrace-operator/test/helpers" + dynakubeComponents "github.com/Dynatrace/dynatrace-operator/test/helpers/components/dynakube" + "github.com/Dynatrace/dynatrace-operator/test/helpers/kubeobjects/job" + "github.com/Dynatrace/dynatrace-operator/test/helpers/kubeobjects/namespace" + "github.com/Dynatrace/dynatrace-operator/test/helpers/kubeobjects/pod" + "github.com/Dynatrace/dynatrace-operator/test/helpers/sample" + "github.com/Dynatrace/dynatrace-operator/test/helpers/tenant" + "github.com/stretchr/testify/require" + "sigs.k8s.io/e2e-framework/pkg/envconf" + "sigs.k8s.io/e2e-framework/pkg/features" +) + +const ( + bootstrapperImage = "quay.io/dynatrace/dynatrace-bootstrapper:snapshot" +) + +func InstallWithCSI(t *testing.T) features.Feature { + builder := features.New("node-image-pull-with-csi") + secretConfig := tenant.GetSingleTenantSecret(t) + + appMonDynakube := *dynakubeComponents.New( + dynakubeComponents.WithName("app-codemodules"), + dynakubeComponents.WithApplicationMonitoringSpec(&oneagent.ApplicationMonitoringSpec{AppInjectionSpec: oneagent.AppInjectionSpec{CodeModulesImage: bootstrapperImage}}), + dynakubeComponents.WithAnnotations(map[string]string{dynakube.AnnotationFeatureNodeImagePull: "true"}), + dynakubeComponents.WithApiUrl(secretConfig.ApiUrl), + ) + + sampleNamespace := *namespace.New("codemodules-sample-node-image-pull") + sampleApp := sample.NewApp(t, &appMonDynakube, + sample.AsDeployment(), + sample.WithNamespace(sampleNamespace), + ) + + builder.Assess("create sample namespace", sampleApp.InstallNamespace()) + + dynakubeComponents.Install(builder, helpers.LevelAssess, &secretConfig, appMonDynakube) + + builder.Assess("check if jobs completed", jobsAreCompleted(appMonDynakube)) + + builder.Assess("check if jobs got cleaned up", jobsAreCleanedUp(appMonDynakube)) + + builder.Assess("install sample app", sampleApp.Install()) + + builder.Assess("codemodules have been downloaded", codemodules.ImageHasBeenDownloaded(appMonDynakube)) + builder.Assess("volumes are mounted correctly", codemodules.VolumesAreMountedCorrectly(*sampleApp)) + + builder.Teardown(sampleApp.Uninstall()) + dynakubeComponents.Delete(builder, helpers.LevelTeardown, appMonDynakube) + + return builder.Feature() +} + +func jobsAreCompleted(dk dynakube.DynaKube) features.Func { + return func(ctx context.Context, t *testing.T, envConfig *envconf.Config) context.Context { + resource := envConfig.Client().Resources() + + jobList := job.GetJobsForOwner(ctx, t, resource, dk.Name, dk.Namespace) + require.NotEmpty(t, jobList.Items) + + for _, job := range jobList.Items { + t.Logf("waiting for job to be completed: %s", job.Name) + ctx = pod.WaitForPodsDeletionWithOwner(job.Name, job.Namespace)(ctx, t, envConfig) + } + + return ctx + } +} + +func jobsAreCleanedUp(dk dynakube.DynaKube) features.Func { + return func(ctx context.Context, t *testing.T, envConfig *envconf.Config) context.Context { + resource := envConfig.Client().Resources() + + jobList := job.GetJobsForOwner(ctx, t, resource, dk.Name, dk.Namespace) + require.Empty(t, jobList.Items) + + return ctx + } +} diff --git a/test/features/bootstrapper/without_csi.go b/test/features/bootstrapper/without_csi.go new file mode 100644 index 0000000000..f7cb9d6bf7 --- /dev/null +++ b/test/features/bootstrapper/without_csi.go @@ -0,0 +1,118 @@ +//go:build e2e + +package bootstrapper + +import ( + "context" + "testing" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" + "github.com/Dynatrace/dynatrace-operator/pkg/consts" + "github.com/Dynatrace/dynatrace-operator/pkg/webhook" + "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/v2/common/volumes" + "github.com/Dynatrace/dynatrace-operator/test/helpers" + dynakubeComponents "github.com/Dynatrace/dynatrace-operator/test/helpers/components/dynakube" + "github.com/Dynatrace/dynatrace-operator/test/helpers/sample" + "github.com/Dynatrace/dynatrace-operator/test/helpers/tenant" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + "k8s.io/utils/ptr" + "sigs.k8s.io/e2e-framework/pkg/envconf" + "sigs.k8s.io/e2e-framework/pkg/features" +) + +func NoCSI(t *testing.T) features.Feature { + builder := features.New("node-image-pull-with-no-csi") + secretConfig := tenant.GetSingleTenantSecret(t) + dk := *dynakubeComponents.New( + dynakubeComponents.WithApiUrl(secretConfig.ApiUrl), + dynakubeComponents.WithApplicationMonitoringSpec(&oneagent.ApplicationMonitoringSpec{AppInjectionSpec: oneagent.AppInjectionSpec{CodeModulesImage: bootstrapperImage}}), + dynakubeComponents.WithAnnotations(map[string]string{ + dynakube.AnnotationFeatureNodeImagePull: "true", + dynakube.AnnotationTechnologies: "php", + }), + ) + + dynakubeComponents.Install(builder, helpers.LevelAssess, &secretConfig, dk) + + sampleApp := sample.NewApp(t, &dk, sample.AsDeployment()) + builder.Assess("install sample app", sampleApp.Install()) + builder.Assess("check injection of sample app", checkInjection(sampleApp)) + + podSample := sample.NewApp(t, &dk, + sample.WithName("only-pod-sample"), + ) + builder.Assess("install additional pod", podSample.Install()) + builder.Assess("check injection of additional pod", checkInjection(podSample)) + + randomUserSample := sample.NewApp(t, &dk, + sample.WithName("random-user"), + sample.AsDeployment(), + sample.WithSecurityContext(corev1.PodSecurityContext{ + RunAsUser: ptr.To[int64](1234), + RunAsGroup: ptr.To[int64](1234), + }), + ) + builder.Assess("install sample app with random users set", randomUserSample.Install()) + builder.Assess("check injection of pods with random user", checkInjection(randomUserSample)) + + builder.Teardown(sampleApp.Uninstall()) + builder.Teardown(podSample.Uninstall()) + builder.Teardown(randomUserSample.Uninstall()) + dynakubeComponents.Delete(builder, helpers.LevelTeardown, dk) + + return builder.Feature() +} + +func checkInjection(deployment *sample.App) features.Func { + return func(ctx context.Context, t *testing.T, envConfig *envconf.Config) context.Context { + resource := envConfig.Client().Resources() + samplePods := deployment.GetPods(ctx, t, resource) + + require.NotNil(t, samplePods) + + for _, item := range samplePods.Items { + require.NotNil(t, item.Spec.InitContainers) + require.Equal(t, webhook.InstallContainerName, item.Spec.InitContainers[0].Name) + + args := item.Spec.InitContainers[0].Args + // TODO use bootstrapper repo consts in the future + require.Contains(t, args, "--source=/opt/dynatrace/oneagent") + require.Contains(t, args, "--target=/mnt/bin") + require.Contains(t, args, "--config-directory=/mnt/config") + require.Contains(t, args, "--input-directory=/mnt/input") + require.NotContains(t, args, "--work=") + require.NotContains(t, args, "--debug") + require.Contains(t, args, "--technology=php") + require.Contains(t, args, "--suppress-error") + + expectedVolume := corev1.Volume{ + Name: volumes.InputVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: consts.BootstrapperInitSecretName, + }, + }, + } + + // require.Contains doesn't work, I tried + found := false + for _, v := range item.Spec.Volumes { + if v.Name == expectedVolume.Name { + require.NotNil(t, v.Secret) + require.Equal(t, expectedVolume.Secret.SecretName, v.Secret.SecretName) + found = true + } + } + require.True(t, found) + + if item.Spec.SecurityContext != nil { + item.Spec.InitContainers[0].SecurityContext.RunAsUser = item.Spec.SecurityContext.RunAsUser + item.Spec.InitContainers[0].SecurityContext.RunAsGroup = item.Spec.SecurityContext.RunAsGroup + } + } + + return ctx + } +} diff --git a/test/features/classic/classic.go b/test/features/classic/classic.go index 6a7bfacdda..39cad81f04 100644 --- a/test/features/classic/classic.go +++ b/test/features/classic/classic.go @@ -5,7 +5,7 @@ package classic import ( "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" "github.com/Dynatrace/dynatrace-operator/test/helpers" dynakubeComponents "github.com/Dynatrace/dynatrace-operator/test/helpers/components/dynakube" "github.com/Dynatrace/dynatrace-operator/test/helpers/tenant" @@ -22,7 +22,7 @@ func Feature(t *testing.T) features.Feature { secretConfig := tenant.GetSingleTenantSecret(t) testDynakube := *dynakubeComponents.New( dynakubeComponents.WithApiUrl(secretConfig.ApiUrl), - dynakubeComponents.WithClassicFullstackSpec(&dynakube.HostInjectSpec{}), + dynakubeComponents.WithClassicFullstackSpec(&oneagent.HostInjectSpec{}), ) // check if oneAgent pods startup and report as ready diff --git a/test/features/classic/switch_modes/switch_modes.go b/test/features/classic/switch_modes/switch_modes.go index af59b2afad..d2f5562258 100644 --- a/test/features/classic/switch_modes/switch_modes.go +++ b/test/features/classic/switch_modes/switch_modes.go @@ -5,12 +5,12 @@ package switch_modes import ( "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" "github.com/Dynatrace/dynatrace-operator/test/features/cloudnative" "github.com/Dynatrace/dynatrace-operator/test/helpers" dynakubeComponents "github.com/Dynatrace/dynatrace-operator/test/helpers/components/dynakube" - "github.com/Dynatrace/dynatrace-operator/test/helpers/components/oneagent" + oneagenthelper "github.com/Dynatrace/dynatrace-operator/test/helpers/components/oneagent" "github.com/Dynatrace/dynatrace-operator/test/helpers/sample" "github.com/Dynatrace/dynatrace-operator/test/helpers/tenant" "sigs.k8s.io/e2e-framework/pkg/features" @@ -32,7 +32,7 @@ func Feature(t *testing.T) features.Feature { } dynakubeClassicFullStack := *dynakubeComponents.New( - append(commonOptions, dynakubeComponents.WithClassicFullstackSpec(&dynakube.HostInjectSpec{}))..., + append(commonOptions, dynakubeComponents.WithClassicFullstackSpec(&oneagent.HostInjectSpec{}))..., ) sampleAppClassic := sample.NewApp(t, &dynakubeClassicFullStack, @@ -48,7 +48,7 @@ func Feature(t *testing.T) features.Feature { ) dynakubeComponents.Delete(builder, helpers.LevelAssess, dynakubeClassicFullStack) - oneagent.RunClassicUninstall(builder, helpers.LevelAssess, dynakubeClassicFullStack) + oneagenthelper.RunClassicUninstall(builder, helpers.LevelAssess, dynakubeClassicFullStack) sampleAppCloudNative := sample.NewApp(t, &dynakubeCloudNative, sample.AsDeployment(), sample.WithName(sampleAppsCloudNativeName), diff --git a/test/features/cloudnative/codemodules/codemodules.go b/test/features/cloudnative/codemodules/codemodules.go index 29524dbed3..1d652c1926 100644 --- a/test/features/cloudnative/codemodules/codemodules.go +++ b/test/features/cloudnative/codemodules/codemodules.go @@ -14,11 +14,12 @@ import ( "time" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/value" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" dtcsi "github.com/Dynatrace/dynatrace-operator/pkg/controllers/csi" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/env" - "github.com/Dynatrace/dynatrace-operator/pkg/webhook" - oamutation "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/oneagent" + oacommon "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/common/oneagent" + oamutation "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/v1/oneagent" "github.com/Dynatrace/dynatrace-operator/test/features/cloudnative" "github.com/Dynatrace/dynatrace-operator/test/helpers" "github.com/Dynatrace/dynatrace-operator/test/helpers/components/csi" @@ -39,6 +40,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/e2e-framework/klient/k8s/resources" @@ -79,13 +81,13 @@ func InstallFromImage(t *testing.T) features.Feature { appDynakube := *dynakubeComponents.New( dynakubeComponents.WithName("app-codemodules"), - dynakubeComponents.WithApplicationMonitoringSpec(&dynakube.ApplicationMonitoringSpec{AppInjectionSpec: *codeModulesAppInjectSpec(t)}), + dynakubeComponents.WithApplicationMonitoringSpec(&oneagent.ApplicationMonitoringSpec{AppInjectionSpec: *codeModulesAppInjectSpec(t)}), dynakubeComponents.WithNameBasedOneAgentNamespaceSelector(), dynakubeComponents.WithNameBasedMetadataEnrichmentNamespaceSelector(), dynakubeComponents.WithApiUrl(secretConfigs[1].ApiUrl), ) - labels := cloudNativeDynakube.OneAgentNamespaceSelector().MatchLabels + labels := cloudNativeDynakube.OneAgent().GetNamespaceSelector().MatchLabels sampleNamespace := *namespace.New("codemodules-sample", namespace.WithLabels(labels)) sampleApp := sample.NewApp(t, &cloudNativeDynakube, @@ -104,11 +106,11 @@ func InstallFromImage(t *testing.T) features.Feature { // Register actual test cloudnative.AssessSampleInitContainers(builder, sampleApp) - builder.Assess("codemodules have been downloaded", imageHasBeenDownloaded(cloudNativeDynakube)) + builder.Assess("codemodules have been downloaded", ImageHasBeenDownloaded(cloudNativeDynakube)) builder.Assess("checking storage used", measureDiskUsage(appDynakube.Namespace, storageMap)) dynakubeComponents.Install(builder, helpers.LevelAssess, &secretConfigs[1], appDynakube) builder.Assess("storage size has not increased", diskUsageDoesNotIncrease(appDynakube.Namespace, storageMap)) - builder.Assess("volumes are mounted correctly", volumesAreMountedCorrectly(*sampleApp)) + builder.Assess("volumes are mounted correctly", VolumesAreMountedCorrectly(*sampleApp)) // Register sample, dynakubeComponents and operator uninstall builder.Teardown(sampleApp.Uninstall()) @@ -133,7 +135,7 @@ const ( // the local cluster. Sample application is installed. The test checks if DT_PROXY environment // variable is defined in the *dynakubeComponents-oneagent* container and the *application container*. func WithProxy(t *testing.T, proxySpec *value.Source) features.Feature { - builder := features.New("codemodules-with-proxy") + builder := features.New("codemodules-with-proxy-no-certs") secretConfigs := tenant.GetMultiTenantSecret(t) require.Len(t, secretConfigs, 2) @@ -144,6 +146,9 @@ func WithProxy(t *testing.T, proxySpec *value.Source) features.Feature { dynakubeComponents.WithActiveGate(), dynakubeComponents.WithIstioIntegration(), dynakubeComponents.WithProxy(proxySpec), + dynakubeComponents.WithAnnotations(map[string]string{ + dynakube.AnnotationFeatureActiveGateAutomaticTLSCertificate: "false", + }), ) sampleNamespace := *namespace.New("codemodules-sample-with-proxy", namespace.WithIstio()) @@ -168,13 +173,13 @@ func WithProxy(t *testing.T, proxySpec *value.Source) features.Feature { // Register actual test cloudnative.AssessSampleInitContainers(builder, sampleApp) istio.AssessIstio(builder, cloudNativeDynakube, *sampleApp) - builder.Assess("codemodules have been downloaded", imageHasBeenDownloaded(cloudNativeDynakube)) + builder.Assess("codemodules have been downloaded", ImageHasBeenDownloaded(cloudNativeDynakube)) builder.Assess("check env variables of oneagent pods", checkOneAgentEnvVars(cloudNativeDynakube)) builder.Assess("check proxy settings in ruxitagentproc.conf", proxy.CheckRuxitAgentProcFileHasProxySetting(*sampleApp, proxySpec)) - cloudnative.AssessSampleContainer(builder, sampleApp, nil, nil) - cloudnative.AssessOneAgentContainer(builder, nil, nil) + cloudnative.AssessSampleContainer(builder, sampleApp, func() []byte { return nil }, nil) + cloudnative.AssessOneAgentContainer(builder, func() []byte { return nil }, nil) cloudnative.AssessActiveGateContainer(builder, &cloudNativeDynakube, nil) // Register sample, dynakubeComponents and operator uninstall @@ -186,18 +191,29 @@ func WithProxy(t *testing.T, proxySpec *value.Source) features.Feature { return builder.Feature() } -func WithProxyCA(t *testing.T, proxySpec *value.Source) features.Feature { - const configMapName = "proxy-ca" - builder := features.New("codemodules-with-proxy-custom-ca") +func getAgTlsSecret(secret *corev1.Secret) features.Func { + return func(ctx context.Context, t *testing.T, envConfig *envconf.Config) context.Context { + err := envConfig.Client().Resources().Get(ctx, secret.Name, secret.Namespace, secret) + require.NoError(t, err) + + _, ok := secret.Data[dynakube.TLSCertKey] + require.True(t, ok) + + return ctx + } +} + +func WithProxyAndAGCert(t *testing.T, proxySpec *value.Source) features.Feature { + builder := features.New("codemodules-with-proxy-and-ag-cert") secretConfigs := tenant.GetMultiTenantSecret(t) require.Len(t, secretConfigs, 2) cloudNativeDynakube := *dynakubeComponents.New( - dynakubeComponents.WithName("codemodules-with-proxy-custom-ca"), + dynakubeComponents.WithName("codemodules-with-proxy-and-ag-cert"), dynakubeComponents.WithApiUrl(secretConfigs[0].ApiUrl), dynakubeComponents.WithCloudNativeSpec(codeModulesCloudNativeSpec(t)), - dynakubeComponents.WithCustomCAs(configMapName), dynakubeComponents.WithActiveGate(), + dynakubeComponents.WithActiveGateTLSSecret(agSecretName), dynakubeComponents.WithIstioIntegration(), dynakubeComponents.WithProxy(proxySpec), ) @@ -210,14 +226,20 @@ func WithProxyCA(t *testing.T, proxySpec *value.Source) features.Feature { builder.Assess("create sample namespace", sampleApp.InstallNamespace()) - // Add customCA config map - trustedCa, _ := os.ReadFile(path.Join(project.TestDataDir(), proxyCertificate)) - caConfigMap := configmap.New(configMapName, cloudNativeDynakube.Namespace, - map[string]string{dynakube.TrustedCAKey: string(trustedCa)}) - builder.Assess("create trusted CAs config map", configmap.Create(caConfigMap)) + // Add ActiveGate TLS secret + // public certificate for OneAgents + agCrt, _ := os.ReadFile(path.Join(project.TestDataDir(), agCertificate)) + // public certificate and private key for ActiveGate server + agP12, _ := os.ReadFile(path.Join(project.TestDataDir(), agCertificateAndPrivateKey)) + agSecret := secret.New(agSecretName, cloudNativeDynakube.Namespace, + map[string][]byte{ + dynakube.TLSCertKey: agCrt, + agCertificateAndPrivateKeyField: agP12, + }) + builder.Assess("create AG TLS secret", secret.Create(agSecret)) // Register proxy create and delete - proxy.SetupProxyWithCustomCAandTeardown(t, builder, cloudNativeDynakube) + proxy.SetupProxyWithTeardown(t, builder, cloudNativeDynakube) proxy.CutOffDynatraceNamespace(builder, proxySpec) proxy.IsDynatraceNamespaceCutOff(builder, cloudNativeDynakube) @@ -231,38 +253,36 @@ func WithProxyCA(t *testing.T, proxySpec *value.Source) features.Feature { cloudnative.AssessSampleInitContainers(builder, sampleApp) istio.AssessIstio(builder, cloudNativeDynakube, *sampleApp) - builder.Assess("codemodules have been downloaded", imageHasBeenDownloaded(cloudNativeDynakube)) + builder.Assess("codemodules have been downloaded", ImageHasBeenDownloaded(cloudNativeDynakube)) - cloudnative.AssessSampleContainer(builder, sampleApp, nil, trustedCa) - cloudnative.AssessOneAgentContainer(builder, nil, trustedCa) - cloudnative.AssessActiveGateContainer(builder, &cloudNativeDynakube, trustedCa) + cloudnative.AssessSampleContainer(builder, sampleApp, func() []byte { return agCrt }, nil) + cloudnative.AssessOneAgentContainer(builder, func() []byte { return agCrt }, nil) + cloudnative.AssessActiveGateContainer(builder, &cloudNativeDynakube, nil) // Register sample, dynakubeComponents and operator uninstall builder.Teardown(sampleApp.Uninstall()) dynakubeComponents.Delete(builder, helpers.LevelTeardown, cloudNativeDynakube) builder.WithTeardown("deleted tenant secret", tenant.DeleteTenantSecret(cloudNativeDynakube.Name, cloudNativeDynakube.Namespace)) - builder.WithTeardown("deleted trusted CAs config map", configmap.Delete(caConfigMap)) return builder.Feature() } -func WithProxyAndAGCert(t *testing.T, proxySpec *value.Source) features.Feature { - builder := features.New("codemodules-with-proxy-and-ag-cert") +func WithProxyAndAutomaticAGCert(t *testing.T, proxySpec *value.Source) features.Feature { + builder := features.New("codemodules-with-proxy-and-auto-ag-cert") secretConfigs := tenant.GetMultiTenantSecret(t) require.Len(t, secretConfigs, 2) cloudNativeDynakube := *dynakubeComponents.New( - dynakubeComponents.WithName("codemodules-with-proxy-and-ag-cert"), + dynakubeComponents.WithName("codemodules-with-proxy"), dynakubeComponents.WithApiUrl(secretConfigs[0].ApiUrl), dynakubeComponents.WithCloudNativeSpec(codeModulesCloudNativeSpec(t)), dynakubeComponents.WithActiveGate(), - dynakubeComponents.WithActiveGateTLSSecret(agSecretName), dynakubeComponents.WithIstioIntegration(), dynakubeComponents.WithProxy(proxySpec), ) - sampleNamespace := *namespace.New("codemodules-sample-with-proxy-custom-ca", namespace.WithIstio()) + sampleNamespace := *namespace.New("codemodules-sample-with-proxy", namespace.WithIstio()) sampleApp := sample.NewApp(t, &cloudNativeDynakube, sample.AsDeployment(), sample.WithNamespace(sampleNamespace), @@ -270,18 +290,6 @@ func WithProxyAndAGCert(t *testing.T, proxySpec *value.Source) features.Feature builder.Assess("create sample namespace", sampleApp.InstallNamespace()) - // Add ActiveGate TLS secret - // public certificate for OneAgents - agCrt, _ := os.ReadFile(path.Join(project.TestDataDir(), agCertificate)) - // public certificate and private key for ActiveGate server - agP12, _ := os.ReadFile(path.Join(project.TestDataDir(), agCertificateAndPrivateKey)) - agSecret := secret.New(agSecretName, cloudNativeDynakube.Namespace, - map[string][]byte{ - dynakube.TLSCertKey: agCrt, - agCertificateAndPrivateKeyField: agP12, - }) - builder.Assess("create AG TLS secret", secret.Create(agSecret)) - // Register proxy create and delete proxy.SetupProxyWithTeardown(t, builder, cloudNativeDynakube) proxy.CutOffDynatraceNamespace(builder, proxySpec) @@ -296,11 +304,21 @@ func WithProxyAndAGCert(t *testing.T, proxySpec *value.Source) features.Feature // Register actual test cloudnative.AssessSampleInitContainers(builder, sampleApp) istio.AssessIstio(builder, cloudNativeDynakube, *sampleApp) + builder.Assess("codemodules have been downloaded", ImageHasBeenDownloaded(cloudNativeDynakube)) - builder.Assess("codemodules have been downloaded", imageHasBeenDownloaded(cloudNativeDynakube)) + builder.Assess("check env variables of oneagent pods", checkOneAgentEnvVars(cloudNativeDynakube)) + builder.Assess("check proxy settings in ruxitagentproc.conf", proxy.CheckRuxitAgentProcFileHasProxySetting(*sampleApp, proxySpec)) - cloudnative.AssessSampleContainer(builder, sampleApp, agCrt, nil) - cloudnative.AssessOneAgentContainer(builder, agCrt, nil) + agTlsSecret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: cloudNativeDynakube.ActiveGate().GetTLSSecretName(), + Namespace: cloudNativeDynakube.Namespace, + }, + } + builder.Assess("read AG TLS secret", getAgTlsSecret(&agTlsSecret)) + + cloudnative.AssessSampleContainer(builder, sampleApp, func() []byte { return agTlsSecret.Data[dynakube.TLSCertKey] }, nil) + cloudnative.AssessOneAgentContainer(builder, func() []byte { return agTlsSecret.Data[dynakube.TLSCertKey] }, nil) cloudnative.AssessActiveGateContainer(builder, &cloudNativeDynakube, nil) // Register sample, dynakubeComponents and operator uninstall @@ -369,10 +387,79 @@ func WithProxyCAAndAGCert(t *testing.T, proxySpec *value.Source) features.Featur cloudnative.AssessSampleInitContainers(builder, sampleApp) istio.AssessIstio(builder, cloudNativeDynakube, *sampleApp) - builder.Assess("codemodules have been downloaded", imageHasBeenDownloaded(cloudNativeDynakube)) + builder.Assess("codemodules have been downloaded", ImageHasBeenDownloaded(cloudNativeDynakube)) + + cloudnative.AssessSampleContainer(builder, sampleApp, func() []byte { return agCrt }, trustedCa) + cloudnative.AssessOneAgentContainer(builder, func() []byte { return agCrt }, trustedCa) + cloudnative.AssessActiveGateContainer(builder, &cloudNativeDynakube, trustedCa) + + // Register sample, dynakubeComponents and operator uninstall + builder.Teardown(sampleApp.Uninstall()) + dynakubeComponents.Delete(builder, helpers.LevelTeardown, cloudNativeDynakube) + + builder.WithTeardown("deleted tenant secret", tenant.DeleteTenantSecret(cloudNativeDynakube.Name, cloudNativeDynakube.Namespace)) + builder.WithTeardown("deleted trusted CAs config map", configmap.Delete(caConfigMap)) + + return builder.Feature() +} + +func WithProxyCAAndAutomaticAGCert(t *testing.T, proxySpec *value.Source) features.Feature { + builder := features.New("codemodules-with-proxy-custom-ca-auto-ag-cert") + secretConfigs := tenant.GetMultiTenantSecret(t) + require.Len(t, secretConfigs, 2) + + cloudNativeDynakube := *dynakubeComponents.New( + dynakubeComponents.WithName("codemodules-with-proxy-custom-ca-ag-cert"), + dynakubeComponents.WithApiUrl(secretConfigs[0].ApiUrl), + dynakubeComponents.WithCloudNativeSpec(codeModulesCloudNativeSpec(t)), + dynakubeComponents.WithCustomCAs(configMapName), + dynakubeComponents.WithActiveGate(), + dynakubeComponents.WithActiveGateTLSSecret(agSecretName), + dynakubeComponents.WithIstioIntegration(), + dynakubeComponents.WithProxy(proxySpec), + ) + + sampleNamespace := *namespace.New("codemodules-sample-with-proxy-custom-ca", namespace.WithIstio()) + sampleApp := sample.NewApp(t, &cloudNativeDynakube, + sample.AsDeployment(), + sample.WithNamespace(sampleNamespace), + ) + + builder.Assess("create sample namespace", sampleApp.InstallNamespace()) + + // Add customCA config map + trustedCa, _ := os.ReadFile(path.Join(project.TestDataDir(), proxyCertificate)) + caConfigMap := configmap.New(configMapName, cloudNativeDynakube.Namespace, + map[string]string{dynakube.TrustedCAKey: string(trustedCa)}) + builder.Assess("create trusted CAs config map", configmap.Create(caConfigMap)) + + // Register proxy create and delete + proxy.SetupProxyWithCustomCAandTeardown(t, builder, cloudNativeDynakube) + proxy.CutOffDynatraceNamespace(builder, proxySpec) + proxy.IsDynatraceNamespaceCutOff(builder, cloudNativeDynakube) + + // Register dynakubeComponents install + dynakubeComponents.Install(builder, helpers.LevelAssess, &secretConfigs[0], cloudNativeDynakube) + + // Register sample app install + builder.Assess("install sample app", sampleApp.Install()) + + // Register actual test + cloudnative.AssessSampleInitContainers(builder, sampleApp) + istio.AssessIstio(builder, cloudNativeDynakube, *sampleApp) + + builder.Assess("codemodules have been downloaded", ImageHasBeenDownloaded(cloudNativeDynakube)) + + agTlsSecret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: cloudNativeDynakube.ActiveGate().GetTLSSecretName(), + Namespace: cloudNativeDynakube.Namespace, + }, + } + builder.Assess("read AG TLS secret", getAgTlsSecret(&agTlsSecret)) - cloudnative.AssessSampleContainer(builder, sampleApp, agCrt, trustedCa) - cloudnative.AssessOneAgentContainer(builder, agCrt, trustedCa) + cloudnative.AssessSampleContainer(builder, sampleApp, func() []byte { return agTlsSecret.Data[dynakube.TLSCertKey] }, trustedCa) + cloudnative.AssessOneAgentContainer(builder, func() []byte { return agTlsSecret.Data[dynakube.TLSCertKey] }, trustedCa) cloudnative.AssessActiveGateContainer(builder, &cloudNativeDynakube, trustedCa) // Register sample, dynakubeComponents and operator uninstall @@ -385,19 +472,19 @@ func WithProxyCAAndAGCert(t *testing.T, proxySpec *value.Source) features.Featur return builder.Feature() } -func codeModulesCloudNativeSpec(t *testing.T) *dynakube.CloudNativeFullStackSpec { - return &dynakube.CloudNativeFullStackSpec{ +func codeModulesCloudNativeSpec(t *testing.T) *oneagent.CloudNativeFullStackSpec { + return &oneagent.CloudNativeFullStackSpec{ AppInjectionSpec: *codeModulesAppInjectSpec(t), } } -func codeModulesAppInjectSpec(t *testing.T) *dynakube.AppInjectionSpec { - return &dynakube.AppInjectionSpec{ +func codeModulesAppInjectSpec(t *testing.T) *oneagent.AppInjectionSpec { + return &oneagent.AppInjectionSpec{ CodeModulesImage: registry.GetLatestCodeModulesImageURI(t), } } -func imageHasBeenDownloaded(dk dynakube.DynaKube) features.Func { +func ImageHasBeenDownloaded(dk dynakube.DynaKube) features.Func { return func(ctx context.Context, t *testing.T, envConfig *envconf.Config) context.Context { resource := envConfig.Client().Resources() clientset, err := kubernetes.NewForConfig(resource.GetConfig()) @@ -414,7 +501,7 @@ func imageHasBeenDownloaded(dk dynakube.DynaKube) features.Func { require.NoError(t, err) buffer := new(bytes.Buffer) _, err = io.Copy(buffer, logStream) - isNew := strings.Contains(buffer.String(), "Installed agent version: "+dk.CustomCodeModulesImage()) + isNew := strings.Contains(buffer.String(), "Installed agent version: "+dk.OneAgent().GetCustomCodeModulesImage()) isOld := strings.Contains(buffer.String(), "agent already installed") t.Logf("wait for Installed agent version in %s", podItem.Name) @@ -483,7 +570,7 @@ func getDiskUsage(ctx context.Context, t *testing.T, resource *resources.Resourc return diskUsage } -func volumesAreMountedCorrectly(sampleApp sample.App) features.Func { +func VolumesAreMountedCorrectly(sampleApp sample.App) features.Func { return func(ctx context.Context, t *testing.T, envConfig *envconf.Config) context.Context { resource := envConfig.Client().Resources() err := deployment.NewQuery(ctx, resource, client.ObjectKey{ @@ -496,13 +583,13 @@ func volumesAreMountedCorrectly(sampleApp sample.App) features.Func { assert.True(t, isVolumeAttached(t, volumes, oamutation.OneAgentBinVolumeName)) assert.True(t, isVolumeMounted(t, volumeMounts, oamutation.OneAgentBinVolumeName)) - listCommand := shell.ListDirectory(webhook.DefaultInstallPath) + listCommand := shell.ListDirectory(oacommon.DefaultInstallPath) executionResult, err := pod.Exec(ctx, resource, podItem, sampleApp.ContainerName(), listCommand...) require.NoError(t, err) assert.NotEmpty(t, executionResult.StdOut.String()) - diskUsage := getDiskUsage(ctx, t, envConfig.Client().Resources(), podItem, sampleApp.ContainerName(), webhook.DefaultInstallPath) + diskUsage := getDiskUsage(ctx, t, envConfig.Client().Resources(), podItem, sampleApp.ContainerName(), oacommon.DefaultInstallPath) assert.Positive(t, diskUsage) }) @@ -518,7 +605,7 @@ func isVolumeMounted(t *testing.T, volumeMounts []corev1.VolumeMount, volumeMoun if volumeMount.Name == volumeMountName { result = true - assert.Equal(t, webhook.DefaultInstallPath, volumeMount.MountPath) + assert.Equal(t, oacommon.DefaultInstallPath, volumeMount.MountPath) assert.False(t, volumeMount.ReadOnly) } } @@ -548,13 +635,13 @@ func checkOneAgentEnvVars(dk dynakube.DynaKube) features.Func { return func(ctx context.Context, t *testing.T, envConfig *envconf.Config) context.Context { resources := envConfig.Client().Resources() err := daemonset.NewQuery(ctx, resources, client.ObjectKey{ - Name: dk.OneAgentDaemonsetName(), + Name: dk.OneAgent().GetDaemonsetName(), Namespace: dk.Namespace, }).ForEachPod(func(podItem corev1.Pod) { require.NotNil(t, podItem) require.NotNil(t, podItem.Spec) - checkEnvVarsInContainer(t, podItem, dk.OneAgentDaemonsetName(), httpsProxy) + checkEnvVarsInContainer(t, podItem, dk.OneAgent().GetDaemonsetName(), httpsProxy) }) require.NoError(t, err) diff --git a/test/features/cloudnative/container.go b/test/features/cloudnative/container.go index 542fefb8fa..57f3f84ea0 100644 --- a/test/features/cloudnative/container.go +++ b/test/features/cloudnative/container.go @@ -5,7 +5,7 @@ package cloudnative import ( "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/test/helpers/components/activegate" "github.com/Dynatrace/dynatrace-operator/test/helpers/kubeobjects/pod" "github.com/Dynatrace/dynatrace-operator/test/helpers/sample" @@ -25,11 +25,11 @@ const ( activeGateRootCAPath = "/var/lib/dynatrace/secrets/rootca/rootca.pem" ) -func AssessSampleContainer(builder *features.FeatureBuilder, sampleApp *sample.App, agCrt []byte, trustedCAs []byte) { - builder.Assess("certificates are propagated to sample apps containers", checkSampleContainer(sampleApp, agCrt, trustedCAs)) +func AssessSampleContainer(builder *features.FeatureBuilder, sampleApp *sample.App, agCrtFunc func() []byte, trustedCAs []byte) { + builder.Assess("certificates are propagated to sample apps containers", checkSampleContainer(sampleApp, agCrtFunc, trustedCAs)) } -func checkSampleContainer(sampleApp *sample.App, agCrt []byte, trustedCAs []byte) features.Func { +func checkSampleContainer(sampleApp *sample.App, agCrtFunc func() []byte, trustedCAs []byte) features.Func { return func(ctx context.Context, t *testing.T, envConfig *envconf.Config) context.Context { resources := envConfig.Client().Resources() @@ -45,9 +45,9 @@ func checkSampleContainer(sampleApp *sample.App, agCrt []byte, trustedCAs []byte require.NotNil(t, podItem.Spec) require.NotEmpty(t, podItem.Spec.Containers) - certs := string(agCrt) + "\n" + string(trustedCAs) + certs := string(agCrtFunc()) + "\n" + string(trustedCAs) - if string(agCrt) == "" && string(trustedCAs) == "" { + if string(agCrtFunc()) == "" && string(trustedCAs) == "" { checkFileNotFound(ctx, t, resources, podItem, sampleApp.ContainerName(), oneAgentCustomPemPath) } else { checkFileContents(ctx, t, resources, podItem, sampleApp.ContainerName(), oneAgentCustomPemPath, certs) @@ -63,11 +63,11 @@ func checkSampleContainer(sampleApp *sample.App, agCrt []byte, trustedCAs []byte } } -func AssessOneAgentContainer(builder *features.FeatureBuilder, agCrt []byte, trustedCAs []byte) { - builder.Assess("certificates are propagated to OneAgent containers", checkOneAgentContainer(agCrt, trustedCAs)) +func AssessOneAgentContainer(builder *features.FeatureBuilder, agCrtFunc func() []byte, trustedCAs []byte) { + builder.Assess("certificates are propagated to OneAgent containers", checkOneAgentContainer(agCrtFunc, trustedCAs)) } -func checkOneAgentContainer(agCrt []byte, trustedCAs []byte) features.Func { +func checkOneAgentContainer(agCrtFunc func() []byte, trustedCAs []byte) features.Func { return func(ctx context.Context, t *testing.T, envConfig *envconf.Config) context.Context { // TODO: when OneAgent ticket is done, probably the same pem files as in case of sample container diff --git a/test/features/cloudnative/default/default.go b/test/features/cloudnative/default/default.go index af0af50a9b..a81cabc454 100644 --- a/test/features/cloudnative/default/default.go +++ b/test/features/cloudnative/default/default.go @@ -47,7 +47,7 @@ import ( // // Sample application Deployment is installed and restarted to check if OneAgent is // injected and VERSION environment variable is correct. -func Feature(t *testing.T, istioEnabled bool) features.Feature { +func Feature(t *testing.T, istioEnabled bool, withCSI bool) features.Feature { builder := features.New("cloudnative") t.Logf("istio enabled: %v", istioEnabled) secretConfig := tenant.GetSingleTenantSecret(t) @@ -80,7 +80,9 @@ func Feature(t *testing.T, istioEnabled bool) features.Feature { istio.AssessIstio(builder, testDynakube, *sampleApp) } - builder.Assess(fmt.Sprintf("check %s has no conn info", codemodules.RuxitAgentProcFile), codemodules.CheckRuxitAgentProcFileHasNoConnInfo(testDynakube)) + if withCSI { + builder.Assess(fmt.Sprintf("check %s has no conn info", codemodules.RuxitAgentProcFile), codemodules.CheckRuxitAgentProcFileHasNoConnInfo(testDynakube)) + } // Register sample, dynakube and operator uninstall builder.Teardown(sampleApp.Uninstall()) diff --git a/test/features/cloudnative/disabled_auto_injection/disabled_auto_injection.go b/test/features/cloudnative/disabled_auto_injection/disabled_auto_injection.go index 3b6bbd2e82..79b01c9217 100644 --- a/test/features/cloudnative/disabled_auto_injection/disabled_auto_injection.go +++ b/test/features/cloudnative/disabled_auto_injection/disabled_auto_injection.go @@ -5,7 +5,7 @@ package disabled_auto_injection import ( "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/test/features/cloudnative" "github.com/Dynatrace/dynatrace-operator/test/helpers" dynakubeComponents "github.com/Dynatrace/dynatrace-operator/test/helpers/components/dynakube" diff --git a/test/features/cloudnative/init_containers.go b/test/features/cloudnative/init_containers.go index d126ba83a5..68a3abb96d 100644 --- a/test/features/cloudnative/init_containers.go +++ b/test/features/cloudnative/init_containers.go @@ -6,7 +6,7 @@ import ( "context" "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" "github.com/Dynatrace/dynatrace-operator/pkg/webhook" "github.com/Dynatrace/dynatrace-operator/test/helpers/kubeobjects/pod" "github.com/Dynatrace/dynatrace-operator/test/helpers/logs" @@ -81,8 +81,8 @@ func checkInitContainers(sampleApp *sample.App) features.Func { } } -func DefaultCloudNativeSpec() *dynakube.CloudNativeFullStackSpec { - return &dynakube.CloudNativeFullStackSpec{ - HostInjectSpec: dynakube.HostInjectSpec{}, +func DefaultCloudNativeSpec() *oneagent.CloudNativeFullStackSpec { + return &oneagent.CloudNativeFullStackSpec{ + HostInjectSpec: oneagent.HostInjectSpec{}, } } diff --git a/test/features/cloudnative/network_problems/network_problems.go b/test/features/cloudnative/network_problems/network_problems.go index 15abd79ce6..146e329e78 100644 --- a/test/features/cloudnative/network_problems/network_problems.go +++ b/test/features/cloudnative/network_problems/network_problems.go @@ -56,7 +56,7 @@ func ResilienceFeature(t *testing.T) features.Feature { dynakube.WithApiUrl(secretConfig.ApiUrl), dynakube.WithCloudNativeSpec(cloudnative.DefaultCloudNativeSpec()), dynakube.WithAnnotations(map[string]string{ - "feature.dynatrace.com/max-csi-mount-attempts": "2", + "feature.dynatrace.com/max-csi-mount-timeout": "1m", }), ) diff --git a/test/features/cloudnative/switch_modes/switch_modes.go b/test/features/cloudnative/switch_modes/switch_modes.go index bb8b071b45..73e0256686 100644 --- a/test/features/cloudnative/switch_modes/switch_modes.go +++ b/test/features/cloudnative/switch_modes/switch_modes.go @@ -5,7 +5,7 @@ package switch_modes import ( "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" "github.com/Dynatrace/dynatrace-operator/test/features/cloudnative" "github.com/Dynatrace/dynatrace-operator/test/helpers" dynakubeComponents "github.com/Dynatrace/dynatrace-operator/test/helpers/components/dynakube" @@ -27,7 +27,7 @@ func Feature(t *testing.T) features.Feature { commonOptions := []dynakubeComponents.Option{ dynakubeComponents.WithApiUrl(secretConfig.ApiUrl), } - dynakubeCloudNative := *dynakubeComponents.New(append(commonOptions, dynakubeComponents.WithCloudNativeSpec(&dynakube.CloudNativeFullStackSpec{}))...) + dynakubeCloudNative := *dynakubeComponents.New(append(commonOptions, dynakubeComponents.WithCloudNativeSpec(&oneagent.CloudNativeFullStackSpec{}))...) sampleAppCloudNative := sample.NewApp(t, &dynakubeCloudNative, sample.AsDeployment(), sample.WithName(sampleAppsCloudNativeName), @@ -45,7 +45,7 @@ func Feature(t *testing.T) features.Feature { cloudnative.AssessSampleInitContainers(builder, sampleAppCloudNative) // switch to classic full stack - dynakubeClassicFullStack := *dynakubeComponents.New(append(commonOptions, dynakubeComponents.WithClassicFullstackSpec(&dynakube.HostInjectSpec{}))...) + dynakubeClassicFullStack := *dynakubeComponents.New(append(commonOptions, dynakubeComponents.WithClassicFullstackSpec(&oneagent.HostInjectSpec{}))...) sampleAppClassicFullStack := sample.NewApp(t, &dynakubeClassicFullStack, sample.AsDeployment(), sample.WithName(sampleAppsClassicName), diff --git a/test/features/cloudnative/upgrade/upgrade.go b/test/features/cloudnative/upgrade/upgrade.go index e4eebd31d6..8dc4e881dc 100644 --- a/test/features/cloudnative/upgrade/upgrade.go +++ b/test/features/cloudnative/upgrade/upgrade.go @@ -5,7 +5,7 @@ package upgrade import ( "testing" - dynakubev1beta1 "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta1/dynakube" //nolint:staticcheck + dynakubev1beta3 "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" "github.com/Dynatrace/dynatrace-operator/test/features/cloudnative" "github.com/Dynatrace/dynatrace-operator/test/helpers" "github.com/Dynatrace/dynatrace-operator/test/helpers/components/dynakube" @@ -31,7 +31,7 @@ func Feature(t *testing.T) features.Feature { ) builder.Assess("create sample namespace", sampleApp.InstallNamespace()) - previousVersionDynakube := &dynakubev1beta1.DynaKube{} + previousVersionDynakube := &dynakubev1beta3.DynaKube{} previousVersionDynakube.ConvertFrom(&testDynakube) dynakube.InstallPreviousVersion(builder, helpers.LevelAssess, &secretConfig, *previousVersionDynakube) diff --git a/test/features/consts/consts.go b/test/features/consts/consts.go index 4d5aff03c8..8a2bd0c1ba 100644 --- a/test/features/consts/consts.go +++ b/test/features/consts/consts.go @@ -5,7 +5,11 @@ const ( AgCertificateAndPrivateKey = "custom-cas/agcrtkey.p12" AgCertificateAndPrivateKeyField = "server.p12" AgSecretName = "ag-ca" - DevRegistryPullSecretName = "devregistry" - EecImageRepo = "478983378254.dkr.ecr.us-east-1.amazonaws.com/dynatrace/dynatrace-eec" - EecImageTag = "1.303.0.20240930-183404" + TelemetryIngestTLSSecretName = "telemetry-ingest-tls" + + DevRegistryPullSecretName = "devregistry" + EecImageRepo = "478983378254.dkr.ecr.us-east-1.amazonaws.com/dynatrace/dynatrace-eec" + EecImageTag = "1.303.0.20240930-183404" + LogMonitoringImageRepo = "public.ecr.aws/dynatrace/dynatrace-logmodule" + LogMonitoringImageTag = "1.309.59.20250319-140247" ) diff --git a/test/features/extensions/extensions.go b/test/features/extensions/extensions.go index 5e7e43bd2a..6cb602b612 100644 --- a/test/features/extensions/extensions.go +++ b/test/features/extensions/extensions.go @@ -8,7 +8,7 @@ import ( "path" "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/test/features/consts" "github.com/Dynatrace/dynatrace-operator/test/helpers" "github.com/Dynatrace/dynatrace-operator/test/helpers/components/activegate" @@ -30,10 +30,11 @@ func Feature(t *testing.T) features.Feature { options := []componentDynakube.Option{ componentDynakube.WithApiUrl(secretConfig.ApiUrl), - componentDynakube.WithActiveGateTLSSecret(consts.AgSecretName), componentDynakube.WithCustomPullSecret(consts.DevRegistryPullSecretName), componentDynakube.WithExtensionsEnabledSpec(true), componentDynakube.WithExtensionsEECImageRefSpec(consts.EecImageRepo, consts.EecImageTag), + componentDynakube.WithActiveGate(), + componentDynakube.WithActiveGateTLSSecret(consts.AgSecretName), } testDynakube := *componentDynakube.New(options...) @@ -57,7 +58,7 @@ func Feature(t *testing.T) features.Feature { builder.Assess("extensions execution controller started", statefulset.WaitFor(testDynakube.ExtensionsExecutionControllerStatefulsetName(), testDynakube.Namespace)) - builder.Assess("extension collector started", statefulset.WaitFor(testDynakube.ExtensionsCollectorStatefulsetName(), testDynakube.Namespace)) + builder.Assess("extension collector started", statefulset.WaitFor(testDynakube.OtelCollectorStatefulsetName(), testDynakube.Namespace)) componentDynakube.Delete(builder, helpers.LevelTeardown, testDynakube) diff --git a/test/features/hostmonitoring/without_csi.go b/test/features/hostmonitoring/without_csi.go new file mode 100644 index 0000000000..4813642336 --- /dev/null +++ b/test/features/hostmonitoring/without_csi.go @@ -0,0 +1,38 @@ +//go:build e2e + +package hostmonitoring + +import ( + "testing" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" + "github.com/Dynatrace/dynatrace-operator/test/helpers" + "github.com/Dynatrace/dynatrace-operator/test/helpers/components/dynakube" + "github.com/Dynatrace/dynatrace-operator/test/helpers/kubeobjects/daemonset" + "github.com/Dynatrace/dynatrace-operator/test/helpers/tenant" + "sigs.k8s.io/e2e-framework/pkg/features" +) + +// ApplicationMonitoring deployment without CSI driver +func WithoutCSI(t *testing.T) features.Feature { + builder := features.New("host-monitoring-without-csi") + secretConfig := tenant.GetSingleTenantSecret(t) + + options := []dynakube.Option{ + dynakube.WithApiUrl(secretConfig.ApiUrl), + dynakube.WithHostMonitoringSpec(&oneagent.HostInjectSpec{}), + } + testDynakube := *dynakube.New(options...) + + // Register dynakube install + dynakube.Install(builder, helpers.LevelAssess, &secretConfig, testDynakube) + + builder.Assess("one agent started", daemonset.WaitForDaemonset(testDynakube.OneAgent().GetDaemonsetName(), testDynakube.Namespace)) + + // Register sample, dynakube and operator uninstall + dynakube.Delete(builder, helpers.LevelTeardown, testDynakube) + + builder.WithTeardown("deleted tenant secret", tenant.DeleteTenantSecret(testDynakube.Name, testDynakube.Namespace)) + + return builder.Feature() +} diff --git a/test/features/logmonitoring/logmonitoring.go b/test/features/logmonitoring/logmonitoring.go new file mode 100644 index 0000000000..d69180a514 --- /dev/null +++ b/test/features/logmonitoring/logmonitoring.go @@ -0,0 +1,125 @@ +//go:build e2e + +package logmonitoring + +import ( + "os" + "path" + "testing" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/logmonitoring/configsecret" + lmdaemonset "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/logmonitoring/daemonset" + "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/logmonitoring/logmonsettings" + "github.com/Dynatrace/dynatrace-operator/pkg/util/conditions" + "github.com/Dynatrace/dynatrace-operator/test/features/consts" + "github.com/Dynatrace/dynatrace-operator/test/helpers" + "github.com/Dynatrace/dynatrace-operator/test/helpers/components/activegate" + componentDynakube "github.com/Dynatrace/dynatrace-operator/test/helpers/components/dynakube" + "github.com/Dynatrace/dynatrace-operator/test/helpers/kubeobjects/daemonset" + "github.com/Dynatrace/dynatrace-operator/test/helpers/kubeobjects/secret" + "github.com/Dynatrace/dynatrace-operator/test/helpers/platform" + "github.com/Dynatrace/dynatrace-operator/test/helpers/tenant" + "github.com/Dynatrace/dynatrace-operator/test/project" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/net/context" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/e2e-framework/pkg/envconf" + "sigs.k8s.io/e2e-framework/pkg/features" +) + +func Feature(t *testing.T) features.Feature { + builder := features.New("logmonitoring-components-rollout") + + secretConfig := tenant.GetSingleTenantSecret(t) + + options := []componentDynakube.Option{ + componentDynakube.WithApiUrl(secretConfig.ApiUrl), + componentDynakube.WithCustomPullSecret(consts.DevRegistryPullSecretName), + componentDynakube.WithLogMonitoring(), + componentDynakube.WithLogMonitoringImageRefSpec(consts.LogMonitoringImageRepo, consts.LogMonitoringImageTag), + componentDynakube.WithActiveGate(), + componentDynakube.WithActiveGateTLSSecret(consts.AgSecretName), + } + + isOpenshift, err := platform.NewResolver().IsOpenshift() + require.NoError(t, err) + if isOpenshift { + options = append(options, componentDynakube.WithAnnotations(map[string]string{ + dynakube.AnnotationFeatureRunOneAgentContainerPrivileged: "true", + })) + } + + testDynakube := *componentDynakube.New(options...) + + agCrt, err := os.ReadFile(path.Join(project.TestDataDir(), consts.AgCertificate)) + require.NoError(t, err) + + agP12, err := os.ReadFile(path.Join(project.TestDataDir(), consts.AgCertificateAndPrivateKey)) + require.NoError(t, err) + + agSecret := secret.New(consts.AgSecretName, testDynakube.Namespace, + map[string][]byte{ + dynakube.TLSCertKey: agCrt, + consts.AgCertificateAndPrivateKeyField: agP12, + }) + builder.Assess("create AG TLS secret", secret.Create(agSecret)) + + componentDynakube.Install(builder, helpers.LevelAssess, &secretConfig, testDynakube) + + builder.Assess("active gate pod is running", checkActiveGateContainer(&testDynakube)) + + builder.Assess("log agent started", daemonset.WaitForDaemonset(testDynakube.LogMonitoring().GetDaemonSetName(), testDynakube.Namespace)) + + builder.Assess("log monitoring conditions", checkConditions(testDynakube.Name, testDynakube.Namespace)) + + componentDynakube.Delete(builder, helpers.LevelTeardown, testDynakube) + + builder.WithTeardown("deleted tenant secret", tenant.DeleteTenantSecret(testDynakube.Name, testDynakube.Namespace)) + + builder.WithTeardown("deleted ag secret", secret.Delete(agSecret)) + + return builder.Feature() +} + +func checkActiveGateContainer(dk *dynakube.DynaKube) features.Func { + return func(ctx context.Context, t *testing.T, envConfig *envconf.Config) context.Context { + resources := envConfig.Client().Resources() + + var activeGatePod corev1.Pod + require.NoError(t, resources.WithNamespace(dk.Namespace).Get(ctx, activegate.GetActiveGatePodName(dk, "activegate"), dk.Namespace, &activeGatePod)) + + require.NotNil(t, activeGatePod.Spec) + require.NotEmpty(t, activeGatePod.Spec.Containers) + + return ctx + } +} + +func checkConditions(name string, namespace string) features.Func { + return func(ctx context.Context, t *testing.T, envConfig *envconf.Config) context.Context { + dk := &dynakube.DynaKube{} + err := envConfig.Client().Resources().Get(ctx, name, namespace, dk) + require.NoError(t, err) + + condition := meta.FindStatusCondition(*dk.Conditions(), configsecret.LmcConditionType) + require.NotNil(t, condition) + assert.Equal(t, metav1.ConditionTrue, condition.Status) + assert.Equal(t, conditions.SecretCreatedReason, condition.Reason) + + condition = meta.FindStatusCondition(*dk.Conditions(), lmdaemonset.ConditionType) + require.NotNil(t, condition) + assert.Equal(t, metav1.ConditionTrue, condition.Status) + assert.Equal(t, conditions.DaemonSetSetCreatedReason, condition.Reason) + + condition = meta.FindStatusCondition(*dk.Conditions(), logmonsettings.ConditionType) + if condition != nil { + assert.NotEqual(t, metav1.ConditionFalse, condition.Status) + } + + return ctx + } +} diff --git a/test/features/support_archive/files.go b/test/features/support_archive/files.go index 50821f8cc7..85425df731 100644 --- a/test/features/support_archive/files.go +++ b/test/features/support_archive/files.go @@ -10,19 +10,21 @@ import ( "github.com/Dynatrace/dynatrace-operator/cmd/support_archive" "github.com/Dynatrace/dynatrace-operator/pkg/api/v1alpha2/edgeconnect" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/util/functional" "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/labels" "github.com/Dynatrace/dynatrace-operator/test/helpers/components/csi" "github.com/Dynatrace/dynatrace-operator/test/helpers/components/oneagent" "github.com/Dynatrace/dynatrace-operator/test/helpers/components/operator" e2ewebhook "github.com/Dynatrace/dynatrace-operator/test/helpers/components/webhook" + "github.com/Dynatrace/dynatrace-operator/test/helpers/kubeobjects/event" "github.com/Dynatrace/dynatrace-operator/test/helpers/kubeobjects/pod" "github.com/Dynatrace/dynatrace-operator/test/helpers/kubeobjects/replicaset" "github.com/Dynatrace/dynatrace-operator/test/helpers/kubeobjects/service" "github.com/Dynatrace/dynatrace-operator/test/helpers/kubeobjects/statefulset" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/e2e-framework/klient/k8s/resources" ) @@ -71,6 +73,7 @@ func (r requiredFiles) collectRequiredFiles() []string { requiredFiles = append(requiredFiles, r.getRequiredWebhookConfigurationFiles()...) requiredFiles = append(requiredFiles, r.getRequiredCRDFiles()...) requiredFiles = append(requiredFiles, r.getRequiredConfigMapFiles()...) + requiredFiles = append(requiredFiles, r.getRequiredEventFiles()...) return requiredFiles } @@ -348,3 +351,24 @@ func (r requiredFiles) getRequiredConfigMapFiles() []string { return requiredFiles } + +func (r requiredFiles) getRequiredEventFiles() []string { + optFunc := func(options *metav1.ListOptions) { + options.Limit = int64(support_archive.NumEventsFlagValue) + options.FieldSelector = fmt.Sprint(support_archive.DefaultEventFieldSelector) + } + events := event.List(r.t, r.ctx, r.resources, r.dk.Namespace, optFunc) + requiredFiles := make([]string, 0) + + for _, requiredEvent := range events.Items { + requiredFiles = append(requiredFiles, + fmt.Sprintf("%s/%s/%s/%s%s", + support_archive.ManifestsDirectoryName, + requiredEvent.Namespace, + "event", + requiredEvent.Name, + support_archive.ManifestsFileExtension)) + } + + return requiredFiles +} diff --git a/test/features/support_archive/support_archive.go b/test/features/support_archive/support_archive.go index ad716ca7d5..af498ff9da 100644 --- a/test/features/support_archive/support_archive.go +++ b/test/features/support_archive/support_archive.go @@ -13,7 +13,8 @@ import ( "testing" "github.com/Dynatrace/dynatrace-operator/pkg/api/v1alpha2/edgeconnect" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" agconsts "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/consts" "github.com/Dynatrace/dynatrace-operator/pkg/util/functional" "github.com/Dynatrace/dynatrace-operator/test/features/consts" @@ -66,7 +67,7 @@ func Feature(t *testing.T) features.Feature { MatchLabels: injectLabels, }), dynakubeComponents.WithApiUrl(secretConfig.ApiUrl), - dynakubeComponents.WithCloudNativeSpec(&dynakube.CloudNativeFullStackSpec{}), + dynakubeComponents.WithCloudNativeSpec(&oneagent.CloudNativeFullStackSpec{}), dynakubeComponents.WithActiveGate(), dynakubeComponents.WithActiveGateTLSSecret(consts.AgSecretName), dynakubeComponents.WithCustomPullSecret(consts.DevRegistryPullSecretName), @@ -114,7 +115,7 @@ func Feature(t *testing.T) features.Feature { // check if components are running builder.Assess("active gate pod is running", statefulset.WaitFor(testDynakube.Name+"-"+agconsts.MultiActiveGateName, testDynakube.Namespace)) builder.Assess("extensions execution controller started", statefulset.WaitFor(testDynakube.ExtensionsExecutionControllerStatefulsetName(), testDynakube.Namespace)) - builder.Assess("extension collector started", statefulset.WaitFor(testDynakube.ExtensionsCollectorStatefulsetName(), testDynakube.Namespace)) + builder.Assess("extension collector started", statefulset.WaitFor(testDynakube.OtelCollectorStatefulsetName(), testDynakube.Namespace)) // Register actual test builder.Assess("support archive subcommand can be executed correctly with managed logs", testSupportArchiveCommand(testDynakube, testEdgeConnect, true)) diff --git a/test/features/telemetryingest/telemetryingest.go b/test/features/telemetryingest/telemetryingest.go new file mode 100644 index 0000000000..08a30b0104 --- /dev/null +++ b/test/features/telemetryingest/telemetryingest.go @@ -0,0 +1,426 @@ +//go:build e2e + +package telemetryingest + +import ( + "context" + "os" + "path" + "strings" + "testing" + "time" + + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" + otelcconsts "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/otelc/consts" + "github.com/Dynatrace/dynatrace-operator/test/features/consts" + "github.com/Dynatrace/dynatrace-operator/test/helpers" + componentActiveGate "github.com/Dynatrace/dynatrace-operator/test/helpers/components/activegate" + componentDynakube "github.com/Dynatrace/dynatrace-operator/test/helpers/components/dynakube" + "github.com/Dynatrace/dynatrace-operator/test/helpers/kubeobjects/pod" + "github.com/Dynatrace/dynatrace-operator/test/helpers/kubeobjects/secret" + "github.com/Dynatrace/dynatrace-operator/test/helpers/kubeobjects/statefulset" + "github.com/Dynatrace/dynatrace-operator/test/helpers/logs" + "github.com/Dynatrace/dynatrace-operator/test/helpers/tenant" + "github.com/Dynatrace/dynatrace-operator/test/helpers/tls" + "github.com/Dynatrace/dynatrace-operator/test/project" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/e2e-framework/klient/wait" + "sigs.k8s.io/e2e-framework/klient/wait/conditions" + "sigs.k8s.io/e2e-framework/pkg/envconf" + "sigs.k8s.io/e2e-framework/pkg/features" +) + +const ( + activeGateComponent = "activegate" + TelemetryIngestTLSCrt = "custom-cas/tls-telemetry-ingest.crt" + TelemetryIngestTLSKey = "custom-cas/tls-telemetry-ingest.key" +) + +// Rollout of OTel collector when no ActiveGate is configured in the Dynakube +func WithPublicActiveGate(t *testing.T) features.Feature { + builder := features.New("telemetryingest-with-public-ag-components-rollout") + + secretConfig := tenant.GetSingleTenantSecret(t) + + options := []componentDynakube.Option{ + componentDynakube.WithApiUrl(secretConfig.ApiUrl), + componentDynakube.WithTelemetryIngestEnabled(true), + } + + testDynakube := *componentDynakube.New(options...) + + componentDynakube.Install(builder, helpers.LevelAssess, &secretConfig, testDynakube) + + builder.Assess("otel collector started", statefulset.WaitFor(testDynakube.OtelCollectorStatefulsetName(), testDynakube.Namespace)) + builder.Assess("otel collector config created", checkOtelCollectorConfig(&testDynakube)) + builder.Assess("otel collector service created", checkOtelCollectorService(&testDynakube)) + + componentDynakube.Delete(builder, helpers.LevelTeardown, testDynakube) + + builder.WithTeardown("deleted tenant secret", tenant.DeleteTenantSecret(testDynakube.Name, testDynakube.Namespace)) + + return builder.Feature() +} + +// Rollout of OTel collector and a local in-cluster ActiveGate. Make sure that components are cleaned up after telemetryIngest gets disabled. +func WithLocalActiveGateAndCleanup(t *testing.T) features.Feature { + builder := features.New("telemetryingest-with-local-active-gate-component-rollout-and-cleanup-after-disable") + + secretConfig := tenant.GetSingleTenantSecret(t) + + optionsTelemetryIngestEnabled := []componentDynakube.Option{ + componentDynakube.WithApiUrl(secretConfig.ApiUrl), + componentDynakube.WithTelemetryIngestEnabled(true, "zipkin"), + componentDynakube.WithActiveGateModules(activegate.KubeMonCapability.DisplayName), + componentDynakube.WithActiveGateTLSSecret(consts.AgSecretName), + } + + testDynakube := *componentDynakube.New(optionsTelemetryIngestEnabled...) + + agSecret, err := createAgTlsSecret(testDynakube.Namespace) + require.NoError(t, err, "failed to create ag-tls secret") + builder.Assess("create AG TLS secret", secret.Create(agSecret)) + + componentDynakube.Install(builder, helpers.LevelAssess, &secretConfig, testDynakube) + builder.Assess("active gate pod is running", checkActiveGateContainer(&testDynakube)) + + builder.Assess("otel collector started", statefulset.WaitFor(testDynakube.OtelCollectorStatefulsetName(), testDynakube.Namespace)) + builder.Assess("otel collector config created", checkOtelCollectorConfig(&testDynakube)) + builder.Assess("otel collector service created", checkOtelCollectorService(&testDynakube)) + builder.Assess("otel collector endpoint configmap created", checkOtelCollectorEndpointConfigMap(&testDynakube)) + + optionsTelemetryIngestDisabled := []componentDynakube.Option{ + componentDynakube.WithApiUrl(secretConfig.ApiUrl), + componentDynakube.WithTelemetryIngestEnabled(false), + componentDynakube.WithActiveGateModules(activegate.KubeMonCapability.DisplayName), + componentDynakube.WithActiveGateTLSSecret(consts.AgSecretName), + } + + testDynakubeNoTelemetryIngest := *componentDynakube.New(optionsTelemetryIngestDisabled...) + componentDynakube.Update(builder, helpers.LevelAssess, testDynakubeNoTelemetryIngest) + + builder.Assess("otel collector shutdown", waitForShutdown(testDynakubeNoTelemetryIngest.OtelCollectorStatefulsetName(), testDynakubeNoTelemetryIngest.Namespace)) + builder.Assess("otel collector config removed", checkOtelCollectorConfigRemoved(&testDynakubeNoTelemetryIngest)) + builder.Assess("otel collector service removed", checkOtelCollectorServiceRemoved(&testDynakubeNoTelemetryIngest)) + builder.Assess("otel collector endpoint configmap removed", checkOtelCollectorEndpointConfigMapRemoved(&testDynakubeNoTelemetryIngest)) + + componentDynakube.Delete(builder, helpers.LevelTeardown, testDynakubeNoTelemetryIngest) + + builder.WithTeardown("deleted tenant secret", tenant.DeleteTenantSecret(testDynakubeNoTelemetryIngest.Name, testDynakubeNoTelemetryIngest.Namespace)) + + return builder.Feature() +} + +// Rollout of OTel collector with TLS secret to secure the telemetryIngest endpoints +func WithTelemetryIngestEndpointTLS(t *testing.T) features.Feature { + builder := features.New("telemetryingest-with-otel-collector-endpoint-tls") + + secretConfig := tenant.GetSingleTenantSecret(t) + + options := []componentDynakube.Option{ + componentDynakube.WithApiUrl(secretConfig.ApiUrl), + componentDynakube.WithTelemetryIngestEnabled(true), + componentDynakube.WithTelemetryIngestEndpointTLS(consts.TelemetryIngestTLSSecretName), + } + + testDynakube := *componentDynakube.New(options...) + + tlsSecret, err := tls.CreateTestdataTLSSecret(testDynakube.Namespace, consts.TelemetryIngestTLSSecretName, TelemetryIngestTLSKey, TelemetryIngestTLSCrt) + require.NoError(t, err, "failed to create TLS secret for otel collector endpoints") + + builder.Assess("create OTel collector endpoint TLS secret", secret.Create(tlsSecret)) + + componentDynakube.Install(builder, helpers.LevelAssess, &secretConfig, testDynakube) + + builder.Assess("otel collector started", statefulset.WaitFor(testDynakube.OtelCollectorStatefulsetName(), testDynakube.Namespace)) + builder.Assess("otel collector config created", checkOtelCollectorConfig(&testDynakube)) + builder.Assess("otel collector service created", checkOtelCollectorService(&testDynakube)) + builder.Assess("otel collector endpoint configmap created", checkOtelCollectorEndpointConfigMap(&testDynakube)) + + componentDynakube.Delete(builder, helpers.LevelTeardown, testDynakube) + builder.WithTeardown("deleted tenant secret", tenant.DeleteTenantSecret(testDynakube.Name, testDynakube.Namespace)) + builder.WithTeardown("deleted OTel collector endpoint TLS secret", secret.Delete(tlsSecret)) + + return builder.Feature() +} + +// Make sure the Otel collector configuration is updated and pods are restarted when protocols for telemetryIngest change +func OtelCollectorConfigUpdate(t *testing.T) features.Feature { + builder := features.New("telemetryingest-configuration-update") + + secretConfig := tenant.GetSingleTenantSecret(t) + + optionsZipkin := []componentDynakube.Option{ + componentDynakube.WithApiUrl(secretConfig.ApiUrl), + componentDynakube.WithTelemetryIngestEnabled(true, "zipkin"), + } + + testDynakubeZipkin := *componentDynakube.New(optionsZipkin...) + + componentDynakube.Install(builder, helpers.LevelAssess, &secretConfig, testDynakubeZipkin) + + builder.Assess("otel collector started", statefulset.WaitFor(testDynakubeZipkin.OtelCollectorStatefulsetName(), testDynakubeZipkin.Namespace)) + builder.Assess("otel collector config created", checkOtelCollectorConfig(&testDynakubeZipkin)) + builder.Assess("otel collector service created", checkOtelCollectorService(&testDynakubeZipkin)) + builder.Assess("otel collector endpoint configmap created", checkOtelCollectorEndpointConfigMap(&testDynakubeZipkin)) + + var zipkinConfigResourceVersion string + builder.Assess("otel collector configuration timestamp", getOtelCollectorConfigResourceVersion(&testDynakubeZipkin, &zipkinConfigResourceVersion)) + + var zipkinPodStartTs time.Time + builder.Assess("otel collector pod creation timestamp", getOtelCollectorPodTimestamp(&testDynakubeZipkin, &zipkinPodStartTs)) + + optionsJaeger := []componentDynakube.Option{ + componentDynakube.WithApiUrl(secretConfig.ApiUrl), + componentDynakube.WithTelemetryIngestEnabled(true, "jaeger"), + } + + testDynakubeJaeger := *componentDynakube.New(optionsJaeger...) + componentDynakube.Update(builder, helpers.LevelAssess, testDynakubeJaeger) + + builder.Assess("otel collector started", statefulset.WaitFor(testDynakubeJaeger.OtelCollectorStatefulsetName(), testDynakubeJaeger.Namespace)) + builder.Assess("otel collector config created", checkOtelCollectorConfig(&testDynakubeJaeger)) + builder.Assess("otel collector service created", checkOtelCollectorService(&testDynakubeJaeger)) + + var jaegerConfigResourceVersion string + builder.Assess("otel collector configuration timestamp", getOtelCollectorConfigResourceVersion(&testDynakubeJaeger, &jaegerConfigResourceVersion)) + builder.Assess("otel collector configuration updated", func(ctx context.Context, t *testing.T, config *envconf.Config) context.Context { + assert.NotEqual(t, jaegerConfigResourceVersion, zipkinConfigResourceVersion) + + return ctx + }) + + var jaegerPodStartTs time.Time + builder.Assess("otel collector pod creation timestamp", getOtelCollectorPodTimestamp(&testDynakubeJaeger, &jaegerPodStartTs)) + builder.Assess("otel collector pod restarted", func(ctx context.Context, t *testing.T, config *envconf.Config) context.Context { + assert.Greater(t, jaegerPodStartTs, zipkinPodStartTs) + + return ctx + }) + + componentDynakube.Delete(builder, helpers.LevelTeardown, testDynakubeJaeger) + + builder.WithTeardown("deleted tenant secret", tenant.DeleteTenantSecret(testDynakubeJaeger.Name, testDynakubeJaeger.Namespace)) + + return builder.Feature() +} + +func checkActiveGateContainer(dk *dynakube.DynaKube) features.Func { + return func(ctx context.Context, t *testing.T, envConfig *envconf.Config) context.Context { + resources := envConfig.Client().Resources() + + var activeGatePod corev1.Pod + require.NoError(t, resources.WithNamespace(dk.Namespace).Get(ctx, componentActiveGate.GetActiveGatePodName(dk, activeGateComponent), dk.Namespace, &activeGatePod)) + + require.NotNil(t, activeGatePod.Spec) + require.NotEmpty(t, activeGatePod.Spec.Containers) + + assertTelemetryIngestActiveGateModulesAreActive(ctx, t, envConfig, dk) + + return ctx + } +} + +func assertTelemetryIngestActiveGateModulesAreActive(ctx context.Context, t *testing.T, envConfig *envconf.Config, dk *dynakube.DynaKube) { + var expectedModules = []string{"log_analytics_collector", "otlp_ingest"} + var expectedServices = []string{"generic_ingest"} + + log := componentActiveGate.ReadActiveGateLog(ctx, t, envConfig, dk, activeGateComponent) + + /* componentActiveGate 2025-03-24 15:08:02 UTC INFO [] [, ServicesManager] Services active: [generic_filecache, local_support_archive, generic_ingest] */ + servicesLog := logs.FindLineContainingText(log, "Services active:") + for _, service := range expectedServices { + assert.Contains(t, servicesLog, service, "ActiveGate services is not active: '"+service+"'") + } + + head := strings.SplitAfter(log, "[, ModulesManager] Modules:") + require.Len(t, head, 2, "list of AG active modules not found") + + tail := strings.SplitAfter(head[1], "Lifecycle listeners:") + require.Len(t, head, 2, "list of AG active modules not found") + + /* + Expected log messages of the Gateway process: + `Active: + log_analytics_collector" + generic_ingest" + otlp_ingest" + Lifecycle listeners:` + + Warning: modules are printed in random order. + */ + for _, module := range expectedModules { + assert.Contains(t, tail[0], module, "ActiveGate module is not active: '"+module+"'") + } +} + +func checkOtelCollectorConfig(dk *dynakube.DynaKube) features.Func { + return func(ctx context.Context, t *testing.T, envConfig *envconf.Config) context.Context { + otelCollectorConfig, err := getOtelCollectorConfigMap(dk, ctx, envConfig) + require.NoError(t, err, "failed to get otel collector config") + + require.NotNil(t, otelCollectorConfig.Data) + + return ctx + } +} + +func checkOtelCollectorConfigRemoved(dk *dynakube.DynaKube) features.Func { + return func(ctx context.Context, t *testing.T, envConfig *envconf.Config) context.Context { + _, err := getOtelCollectorConfigMap(dk, ctx, envConfig) + require.Error(t, err) + assert.True(t, k8serrors.IsNotFound(err), "ConfigMap still exists") + + return ctx + } +} + +func getOtelCollectorConfigResourceVersion(dk *dynakube.DynaKube, resourceVersion *string) features.Func { + return func(ctx context.Context, t *testing.T, envConfig *envconf.Config) context.Context { + otelCollectorConfig, err := getOtelCollectorConfigMap(dk, ctx, envConfig) + require.NoError(t, err, "failed to get otel collector config") + + *resourceVersion = otelCollectorConfig.ResourceVersion + + return ctx + } +} + +func checkOtelCollectorService(dk *dynakube.DynaKube) features.Func { + return func(ctx context.Context, t *testing.T, envConfig *envconf.Config) context.Context { + otelCollectorService, err := getOtelCollectorService(dk, ctx, envConfig) + require.NoError(t, err) + require.NotEmpty(t, otelCollectorService.Spec.Ports) + + return ctx + } +} + +func checkOtelCollectorEndpointConfigMap(dk *dynakube.DynaKube) features.Func { + return func(ctx context.Context, t *testing.T, envConfig *envconf.Config) context.Context { + cm, err := getOtelCollectorEndpointConfigMap(dk, ctx, envConfig) + require.NoError(t, err) + assert.NotNil(t, cm) + + return ctx + } +} + +func checkOtelCollectorServiceRemoved(dk *dynakube.DynaKube) features.Func { + return func(ctx context.Context, t *testing.T, envConfig *envconf.Config) context.Context { + _, err := getOtelCollectorService(dk, ctx, envConfig) + require.Error(t, err) + assert.True(t, k8serrors.IsNotFound(err), "Service still exists") + + return ctx + } +} + +func checkOtelCollectorEndpointConfigMapRemoved(dk *dynakube.DynaKube) features.Func { + return func(ctx context.Context, t *testing.T, envConfig *envconf.Config) context.Context { + _, err := getOtelCollectorEndpointConfigMap(dk, ctx, envConfig) + require.Error(t, err) + assert.True(t, k8serrors.IsNotFound(err), "Service still exists") + + return ctx + } +} + +func getOtelCollectorPodTimestamp(dk *dynakube.DynaKube, startTimestamp *time.Time) features.Func { + return func(ctx context.Context, t *testing.T, envConfig *envconf.Config) context.Context { + resources := envConfig.Client().Resources() + + podList := pod.GetPodsForOwner(ctx, t, resources, dk.OtelCollectorStatefulsetName(), dk.Namespace) + + expectedPodCount := 1 + if dk.Spec.Templates.OpenTelemetryCollector.Replicas != nil && *dk.Spec.Templates.OpenTelemetryCollector.Replicas >= 1 { + expectedPodCount = int(*dk.Spec.Templates.OpenTelemetryCollector.Replicas) + } + assert.Len(t, podList.Items, expectedPodCount) + + require.NotEmpty(t, podList.Items) + *startTimestamp = podList.Items[0].Status.StartTime.Time + + return ctx + } +} + +func getOtelCollectorConfigMap(dk *dynakube.DynaKube, ctx context.Context, envConfig *envconf.Config) (*corev1.ConfigMap, error) { + resources := envConfig.Client().Resources() + + var otelCollectorConfig corev1.ConfigMap + err := resources.WithNamespace(dk.Namespace).Get(ctx, dk.Name+otelcconsts.TelemetryCollectorConfigmapSuffix, dk.Namespace, &otelCollectorConfig) + + if err != nil { + return nil, err + } + + return &otelCollectorConfig, nil +} + +func getOtelCollectorService(dk *dynakube.DynaKube, ctx context.Context, envConfig *envconf.Config) (*corev1.Service, error) { + resources := envConfig.Client().Resources() + + var otelCollectorService corev1.Service + err := resources.WithNamespace(dk.Namespace).Get(ctx, dk.TelemetryIngest().GetServiceName(), dk.Namespace, &otelCollectorService) + + if err != nil { + return nil, err + } + + return &otelCollectorService, nil +} + +func getOtelCollectorEndpointConfigMap(dk *dynakube.DynaKube, ctx context.Context, envConfig *envconf.Config) (*corev1.ConfigMap, error) { + resources := envConfig.Client().Resources() + + var otelCollectorEndpointConfigMap corev1.ConfigMap + err := resources.WithNamespace(dk.Namespace).Get(ctx, otelcconsts.OtlpApiEndpointConfigMapName, dk.Namespace, &otelCollectorEndpointConfigMap) + + if err != nil { + return nil, err + } + + return &otelCollectorEndpointConfigMap, nil +} + +func waitForShutdown(name string, namespace string) features.Func { + return func(ctx context.Context, t *testing.T, envConfig *envconf.Config) context.Context { + resources := envConfig.Client().Resources() + + err := wait.For(conditions.New(resources).ResourceDeleted(&appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + }), wait.WithTimeout(10*time.Minute)) + + require.NoError(t, err) + + return ctx + } +} + +func createAgTlsSecret(namespace string) (corev1.Secret, error) { + agCrt, err := os.ReadFile(path.Join(project.TestDataDir(), consts.AgCertificate)) + if err != nil { + return corev1.Secret{}, err + } + + agP12, err := os.ReadFile(path.Join(project.TestDataDir(), consts.AgCertificateAndPrivateKey)) + if err != nil { + return corev1.Secret{}, err + } + + return secret.New(consts.AgSecretName, namespace, + map[string][]byte{ + dynakube.TLSCertKey: agCrt, + consts.AgCertificateAndPrivateKeyField: agP12, + }), nil +} diff --git a/test/helpers/components/activegate/installation.go b/test/helpers/components/activegate/installation.go index c34226e711..50bd8f6110 100644 --- a/test/helpers/components/activegate/installation.go +++ b/test/helpers/components/activegate/installation.go @@ -7,7 +7,7 @@ import ( "fmt" "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/consts" "github.com/Dynatrace/dynatrace-operator/test/helpers/kubeobjects/statefulset" "github.com/Dynatrace/dynatrace-operator/test/helpers/logs" diff --git a/test/helpers/components/codemodules/codemodules.go b/test/helpers/components/codemodules/codemodules.go index a1da4eff62..902eae0aec 100644 --- a/test/helpers/components/codemodules/codemodules.go +++ b/test/helpers/components/codemodules/codemodules.go @@ -7,7 +7,7 @@ import ( "path/filepath" "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/test/helpers/components/csi" "github.com/Dynatrace/dynatrace-operator/test/helpers/kubeobjects/daemonset" "github.com/Dynatrace/dynatrace-operator/test/helpers/kubeobjects/pod" @@ -36,7 +36,7 @@ func CheckRuxitAgentProcFileHasNoConnInfo(testDynakube dynakube.DynaKube) featur Namespace: testDynakube.Namespace, }).ForEachPod(func(podItem corev1.Pod) { // /data/codemodules/1.273.0.20230719-145632/agent/conf/ruxitagentproc.conf - dir := filepath.Join("/data", "codemodules", dk.CodeModulesVersion(), "agent", "conf", RuxitAgentProcFile) + dir := filepath.Join("/data", "codemodules", dk.OneAgent().GetCodeModulesVersion(), "agent", "conf", RuxitAgentProcFile) readFileCommand := shell.ReadFile(dir) result, err := pod.Exec(ctx, resources, podItem, "provisioner", readFileCommand...) require.NoError(t, err) diff --git a/test/helpers/components/dynakube/dynakube.go b/test/helpers/components/dynakube/dynakube.go index e60b16619e..6d5137a802 100644 --- a/test/helpers/components/dynakube/dynakube.go +++ b/test/helpers/components/dynakube/dynakube.go @@ -9,8 +9,8 @@ import ( "time" "github.com/Dynatrace/dynatrace-operator/pkg/api/status" - prevDynakube "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta1/dynakube" //nolint:staticcheck - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + prevDynakube "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/test/helpers/components/oneagent" "github.com/Dynatrace/dynatrace-operator/test/helpers/tenant" "github.com/stretchr/testify/require" @@ -62,8 +62,8 @@ func CreatePreviousVersion(builder *features.FeatureBuilder, level features.Leve } func VerifyStartupPreviousVersion(builder *features.FeatureBuilder, level features.Level, prevDk prevDynakube.DynaKube) { - if prevDk.NeedsOneAgent() { - builder.WithStep("oneagent started", level, oneagent.WaitFromDaemonSetPrevDk(prevDk)) + if prevDk.OneAgent().IsDaemonsetRequired() { + builder.WithStep("oneagent started", level, oneagent.WaitForDaemonset(prevDk.OneAgent().GetDaemonsetName(), prevDk.Namespace)) } builder.WithStep( fmt.Sprintf("'%s' dynakube phase changes to 'Running'", prevDk.Name), @@ -73,17 +73,17 @@ func VerifyStartupPreviousVersion(builder *features.FeatureBuilder, level featur func Delete(builder *features.FeatureBuilder, level features.Level, dk dynakube.DynaKube) { builder.WithStep("dynakube deleted", level, remove(dk)) - if dk.NeedsOneAgent() { - builder.WithStep("oneagent pods stopped", level, oneagent.WaitForDaemonSetPodsDeletion(dk)) + if dk.OneAgent().IsDaemonsetRequired() { + builder.WithStep("oneagent pods stopped", level, oneagent.WaitForDaemonSetPodsDeletion(dk.OneAgent().GetDaemonsetName(), dk.Namespace)) } - if dk.ClassicFullStackMode() { + if dk.OneAgent().IsClassicFullStackMode() { oneagent.RunClassicUninstall(builder, level, dk) } } func VerifyStartup(builder *features.FeatureBuilder, level features.Level, dk dynakube.DynaKube) { - if dk.NeedsOneAgent() { - builder.WithStep("oneagent started", level, oneagent.WaitForDaemonset(dk)) + if dk.OneAgent().IsDaemonsetRequired() { + builder.WithStep("oneagent started", level, oneagent.WaitForDaemonset(dk.OneAgent().GetDaemonsetName(), dk.Namespace)) } builder.WithStep( fmt.Sprintf("'%s' dynakube phase changes to 'Running'", dk.Name), diff --git a/test/helpers/components/dynakube/options.go b/test/helpers/components/dynakube/options.go index b8effdc108..aef24f2776 100644 --- a/test/helpers/components/dynakube/options.go +++ b/test/helpers/components/dynakube/options.go @@ -5,11 +5,14 @@ package dynakube import ( "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/image" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/value" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate" - "github.com/Dynatrace/dynatrace-operator/pkg/util/address" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/activegate" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/logmonitoring" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/oneagent" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/telemetryingest" "github.com/Dynatrace/dynatrace-operator/test/helpers/components/operator" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" ) type Option func(dk *dynakube.DynaKube) @@ -64,14 +67,24 @@ func WithActiveGate() Option { activegate.DynatraceApiCapability.DisplayName, activegate.RoutingCapability.DisplayName, activegate.MetricsIngestCapability.DisplayName, + activegate.DebuggingCapability.DisplayName, }, } } } +func WithActiveGateModules(capabilities ...activegate.CapabilityDisplayName) Option { + return func(dk *dynakube.DynaKube) { + dk.Spec.ActiveGate = activegate.Spec{ + Capabilities: []activegate.CapabilityDisplayName{}, + } + dk.Spec.ActiveGate.Capabilities = append(dk.Spec.ActiveGate.Capabilities, capabilities...) + } +} + func WithMetadataEnrichment() Option { return func(dk *dynakube.DynaKube) { - dk.Spec.MetadataEnrichment.Enabled = address.Of(true) + dk.Spec.MetadataEnrichment.Enabled = ptr.To(true) } } @@ -95,9 +108,9 @@ func WithNameBasedOneAgentNamespaceSelector() Option { }, } switch { - case dk.CloudNativeFullstackMode(): + case dk.OneAgent().IsCloudNativeFullstackMode(): dk.Spec.OneAgent.CloudNativeFullStack.NamespaceSelector = namespaceSelector - case dk.ApplicationMonitoringMode(): + case dk.OneAgent().IsApplicationMonitoringMode(): dk.Spec.OneAgent.ApplicationMonitoring.NamespaceSelector = namespaceSelector } } @@ -117,9 +130,9 @@ func WithNameBasedMetadataEnrichmentNamespaceSelector() Option { func WithOneAgentNamespaceSelector(selector metav1.LabelSelector) Option { return func(dk *dynakube.DynaKube) { switch { - case dk.CloudNativeFullstackMode(): + case dk.OneAgent().IsCloudNativeFullstackMode(): dk.Spec.OneAgent.CloudNativeFullStack.NamespaceSelector = selector - case dk.ApplicationMonitoringMode(): + case dk.OneAgent().IsApplicationMonitoringMode(): dk.Spec.OneAgent.ApplicationMonitoring.NamespaceSelector = selector } } @@ -137,19 +150,25 @@ func WithIstioIntegration() Option { } } -func WithClassicFullstackSpec(classicFullStackSpec *dynakube.HostInjectSpec) Option { +func WithClassicFullstackSpec(classicFullStackSpec *oneagent.HostInjectSpec) Option { return func(dk *dynakube.DynaKube) { dk.Spec.OneAgent.ClassicFullStack = classicFullStackSpec } } -func WithCloudNativeSpec(cloudNativeFullStackSpec *dynakube.CloudNativeFullStackSpec) Option { +func WithCloudNativeSpec(cloudNativeFullStackSpec *oneagent.CloudNativeFullStackSpec) Option { return func(dk *dynakube.DynaKube) { dk.Spec.OneAgent.CloudNativeFullStack = cloudNativeFullStackSpec } } -func WithApplicationMonitoringSpec(applicationMonitoringSpec *dynakube.ApplicationMonitoringSpec) Option { +func WithHostMonitoringSpec(hostInjectSpec *oneagent.HostInjectSpec) Option { + return func(dk *dynakube.DynaKube) { + dk.Spec.OneAgent.HostMonitoring = hostInjectSpec + } +} + +func WithApplicationMonitoringSpec(applicationMonitoringSpec *oneagent.ApplicationMonitoringSpec) Option { return func(dk *dynakube.DynaKube) { dk.Spec.OneAgent.ApplicationMonitoring = applicationMonitoringSpec } @@ -180,3 +199,40 @@ func WithCustomPullSecret(secretName string) Option { dk.Spec.CustomPullSecret = secretName } } + +func WithLogMonitoring() Option { + return func(dk *dynakube.DynaKube) { + dk.Spec.LogMonitoring = &logmonitoring.Spec{} + } +} + +func WithLogMonitoringImageRefSpec(repo, tag string) Option { + return func(dk *dynakube.DynaKube) { + dk.Spec.Templates.LogMonitoring = &logmonitoring.TemplateSpec{ + ImageRef: image.Ref{ + Repository: repo, + Tag: tag, + }, + } + } +} + +func WithTelemetryIngestEnabled(enabled bool, protocols ...string) Option { + return func(dk *dynakube.DynaKube) { + if enabled { + dk.Spec.TelemetryIngest = &telemetryingest.Spec{} + dk.Spec.TelemetryIngest.Protocols = append(dk.Spec.TelemetryIngest.Protocols, protocols...) + } else { + dk.Spec.TelemetryIngest = nil + } + } +} + +func WithTelemetryIngestEndpointTLS(secretName string) Option { + return func(dk *dynakube.DynaKube) { + if dk.Spec.TelemetryIngest == nil { + dk.Spec.TelemetryIngest = &telemetryingest.Spec{} + } + dk.Spec.TelemetryIngest.TlsRefName = secretName + } +} diff --git a/test/helpers/components/oneagent/daemonset.go b/test/helpers/components/oneagent/daemonset.go index c5e94a3ee5..efa48a56dc 100644 --- a/test/helpers/components/oneagent/daemonset.go +++ b/test/helpers/components/oneagent/daemonset.go @@ -5,8 +5,7 @@ package oneagent import ( "context" - dynakubev1beta1 "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta1/dynakube" //nolint - dynakubev1beta3 "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/test/helpers" "github.com/Dynatrace/dynatrace-operator/test/helpers/kubeobjects/daemonset" "github.com/Dynatrace/dynatrace-operator/test/helpers/kubeobjects/pod" @@ -16,21 +15,17 @@ import ( "sigs.k8s.io/e2e-framework/pkg/features" ) -func WaitForDaemonset(dk dynakubev1beta3.DynaKube) features.Func { - return helpers.ToFeatureFunc(daemonset.WaitFor(dk.OneAgentDaemonsetName(), dk.Namespace), true) +func WaitForDaemonset(dsName, namespace string) features.Func { + return helpers.ToFeatureFunc(daemonset.WaitFor(dsName, namespace), true) } -func WaitFromDaemonSetPrevDk(prevDk dynakubev1beta1.DynaKube) features.Func { - return helpers.ToFeatureFunc(daemonset.WaitFor(prevDk.OneAgentDaemonsetName(), prevDk.Namespace), true) +func WaitForDaemonSetPodsDeletion(dsName, namespace string) features.Func { + return pod.WaitForPodsDeletionWithOwner(dsName, namespace) } -func WaitForDaemonSetPodsDeletion(dk dynakubev1beta3.DynaKube) features.Func { - return pod.WaitForPodsDeletionWithOwner(dk.OneAgentDaemonsetName(), dk.Namespace) -} - -func Get(ctx context.Context, resource *resources.Resources, dk dynakubev1beta3.DynaKube) (appsv1.DaemonSet, error) { +func Get(ctx context.Context, resource *resources.Resources, dk dynakube.DynaKube) (appsv1.DaemonSet, error) { return daemonset.NewQuery(ctx, resource, client.ObjectKey{ - Name: dk.OneAgentDaemonsetName(), + Name: dk.OneAgent().GetDaemonsetName(), Namespace: dk.Namespace, }).Get() } diff --git a/test/helpers/components/oneagent/uninstall.go b/test/helpers/components/oneagent/uninstall.go index 5092da8f17..45b25a2a3b 100644 --- a/test/helpers/components/oneagent/uninstall.go +++ b/test/helpers/components/oneagent/uninstall.go @@ -8,7 +8,7 @@ import ( "path" "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/test/helpers" "github.com/Dynatrace/dynatrace-operator/test/helpers/kubeobjects/daemonset" "github.com/Dynatrace/dynatrace-operator/test/helpers/kubeobjects/manifests" diff --git a/test/helpers/components/operator/installation.go b/test/helpers/components/operator/installation.go index da3e9b59d4..d95eb7dfef 100644 --- a/test/helpers/components/operator/installation.go +++ b/test/helpers/components/operator/installation.go @@ -25,24 +25,24 @@ const ( func InstallViaMake(withCSI bool) env.Func { return func(ctx context.Context, envConfig *envconf.Config) (context.Context, error) { rootDir := project.RootDir() - err := execMakeCommand(rootDir, "deploy/helm", fmt.Sprintf("ENABLE_CSI=%t", withCSI)) + err := execMakeCommand(rootDir, "deploy", fmt.Sprintf("ENABLE_CSI=%t", withCSI)) if err != nil { return ctx, err } - ctx, err = VerifyInstall(ctx, envConfig) + ctx, err = VerifyInstall(ctx, envConfig, withCSI) return ctx, err } } -func InstallViaHelm(releaseTag string, withCsi bool, namespace string) env.Func { +func InstallViaHelm(releaseTag string, withCSI bool, namespace string) env.Func { return func(ctx context.Context, envConfig *envconf.Config) (context.Context, error) { - err := installViaHelm(releaseTag, withCsi, namespace) + err := installViaHelm(releaseTag, withCSI, namespace) if err != nil { return ctx, err } - return VerifyInstall(ctx, envConfig) + return VerifyInstall(ctx, envConfig, withCSI) } } @@ -56,11 +56,11 @@ func UninstallViaMake(withCSI bool) env.Func { } } - return ctx, execMakeCommand(rootDir, "undeploy/helm", fmt.Sprintf("ENABLE_CSI=%t", withCSI)) + return ctx, execMakeCommand(rootDir, "undeploy", fmt.Sprintf("ENABLE_CSI=%t", withCSI)) } } -func VerifyInstall(ctx context.Context, envConfig *envconf.Config) (context.Context, error) { +func VerifyInstall(ctx context.Context, envConfig *envconf.Config, withCSI bool) (context.Context, error) { ctx, err := WaitForDeployment(DefaultNamespace)(ctx, envConfig) if err != nil { return ctx, err @@ -69,9 +69,12 @@ func VerifyInstall(ctx context.Context, envConfig *envconf.Config) (context.Cont if err != nil { return ctx, err } - ctx, err = csi.WaitForDaemonset(DefaultNamespace)(ctx, envConfig) - if err != nil { - return ctx, err + + if withCSI { + ctx, err = csi.WaitForDaemonset(DefaultNamespace)(ctx, envConfig) + if err != nil { + return ctx, err + } } return ctx, nil diff --git a/test/helpers/curl/curl.go b/test/helpers/curl/curl.go index eada3b552b..d8f64f2fd6 100644 --- a/test/helpers/curl/curl.go +++ b/test/helpers/curl/curl.go @@ -7,7 +7,7 @@ import ( "io" "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" dtwebhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" diff --git a/test/helpers/feature.go b/test/helpers/feature.go index eceba634ed..8bbe9f0421 100644 --- a/test/helpers/feature.go +++ b/test/helpers/feature.go @@ -1,3 +1,5 @@ +//go:build e2e + package helpers import ( diff --git a/test/helpers/istio/install.go b/test/helpers/istio/install.go index dd72d2fcc6..ccb74420f5 100644 --- a/test/helpers/istio/install.go +++ b/test/helpers/istio/install.go @@ -8,7 +8,7 @@ import ( "strings" "testing" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/istio" "github.com/Dynatrace/dynatrace-operator/test/helpers/platform" @@ -104,7 +104,7 @@ func assertIstioInitContainer(t *testing.T, pods corev1.PodList, testDynakube dy require.NotNil(t, podItem) require.NotNil(t, podItem.Spec) - if strings.HasPrefix(podItem.Name, testDynakube.OneAgentDaemonsetName()) { + if strings.HasPrefix(podItem.Name, testDynakube.OneAgent().GetDaemonsetName()) { continue } diff --git a/test/helpers/kubeobjects/daemonset/wait.go b/test/helpers/kubeobjects/daemonset/wait.go index e3dce7825a..339e2b1b33 100644 --- a/test/helpers/kubeobjects/daemonset/wait.go +++ b/test/helpers/kubeobjects/daemonset/wait.go @@ -6,6 +6,7 @@ import ( "context" "time" + "github.com/Dynatrace/dynatrace-operator/test/helpers" appsv1 "k8s.io/api/apps/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/e2e-framework/klient/k8s" @@ -13,6 +14,7 @@ import ( "sigs.k8s.io/e2e-framework/klient/wait/conditions" "sigs.k8s.io/e2e-framework/pkg/env" "sigs.k8s.io/e2e-framework/pkg/envconf" + "sigs.k8s.io/e2e-framework/pkg/features" ) func WaitFor(name string, namespace string) env.Func { @@ -33,3 +35,7 @@ func WaitFor(name string, namespace string) env.Func { return ctx, err } } + +func WaitForDaemonset(name string, namespace string) features.Func { + return helpers.ToFeatureFunc(WaitFor(name, namespace), true) +} diff --git a/test/helpers/kubeobjects/event/list.go b/test/helpers/kubeobjects/event/list.go new file mode 100644 index 0000000000..76a4de105d --- /dev/null +++ b/test/helpers/kubeobjects/event/list.go @@ -0,0 +1,20 @@ +//go:build e2e + +package event + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/e2e-framework/klient/k8s/resources" +) + +func List(t *testing.T, ctx context.Context, resource *resources.Resources, namespace string, listOpt resources.ListOption) corev1.EventList { + var events corev1.EventList + + require.NoError(t, resource.WithNamespace(namespace).List(ctx, &events, listOpt)) + + return events +} diff --git a/test/helpers/kubeobjects/job/get.go b/test/helpers/kubeobjects/job/get.go new file mode 100644 index 0000000000..f483be173a --- /dev/null +++ b/test/helpers/kubeobjects/job/get.go @@ -0,0 +1,44 @@ +//go:build e2e + +package job + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + batchv1 "k8s.io/api/batch/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "sigs.k8s.io/e2e-framework/klient/k8s/resources" +) + +func GetJobsForOwner(ctx context.Context, t *testing.T, resource *resources.Resources, ownerName, namespace string) batchv1.JobList { + jobs := GetJobsForNamespace(ctx, t, resource, namespace) + + var targetJobs batchv1.JobList + for _, pod := range jobs.Items { + if len(pod.ObjectMeta.OwnerReferences) < 1 { + continue + } + + if pod.ObjectMeta.OwnerReferences[0].Name == ownerName { + targetJobs.Items = append(targetJobs.Items, pod) + } + } + + return targetJobs +} + +func GetJobsForNamespace(ctx context.Context, t *testing.T, resource *resources.Resources, namespace string) batchv1.JobList { + var jobs batchv1.JobList + err := resource.WithNamespace(namespace).List(ctx, &jobs) + + if err != nil { + if k8serrors.IsNotFound(err) { + err = nil + } + require.NoError(t, err) + } + + return jobs +} diff --git a/test/helpers/kubeobjects/namespace/namespace.go b/test/helpers/kubeobjects/namespace/namespace.go index a1af00e080..49e178f7fa 100644 --- a/test/helpers/kubeobjects/namespace/namespace.go +++ b/test/helpers/kubeobjects/namespace/namespace.go @@ -7,12 +7,12 @@ import ( "testing" "time" - "github.com/Dynatrace/dynatrace-operator/pkg/util/address" "github.com/Dynatrace/dynatrace-operator/test/helpers" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" "sigs.k8s.io/e2e-framework/klient/wait" "sigs.k8s.io/e2e-framework/klient/wait/conditions" "sigs.k8s.io/e2e-framework/pkg/env" @@ -65,7 +65,7 @@ func Delete(namespaceName string) features.Func { } err := envConfig.Client().Resources().Delete(ctx, &namespace, func(options *metav1.DeleteOptions) { - options.GracePeriodSeconds = address.Of[int64](0) + options.GracePeriodSeconds = ptr.To[int64](0) }) if err != nil { diff --git a/test/helpers/kubeobjects/statefulset/get.go b/test/helpers/kubeobjects/statefulset/get.go index b955763097..4691491fb3 100644 --- a/test/helpers/kubeobjects/statefulset/get.go +++ b/test/helpers/kubeobjects/statefulset/get.go @@ -1,3 +1,5 @@ +//go:build e2e + package statefulset import ( diff --git a/test/helpers/logs/logs.go b/test/helpers/logs/logs.go index 666a95219f..fe418b40dc 100644 --- a/test/helpers/logs/logs.go +++ b/test/helpers/logs/logs.go @@ -6,6 +6,7 @@ import ( "bytes" "context" "io" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -44,3 +45,14 @@ func AssertContains(t *testing.T, logStream io.ReadCloser, contains string) { require.Equal(t, int64(buffer.Len()), copied) assert.Contains(t, buffer.String(), contains) } + +func FindLineContainingText(log, searchText string) string { + lines := strings.Split(log, "\n") + for _, line := range lines { + if strings.Contains(line, searchText) { + return line + } + } + + return "" +} diff --git a/test/helpers/platform/platform.go b/test/helpers/platform/platform.go index e3adf95dbb..5a4fab7a58 100644 --- a/test/helpers/platform/platform.go +++ b/test/helpers/platform/platform.go @@ -1,9 +1,11 @@ +//go:build e2e + package platform import ( - "github.com/Dynatrace/dynatrace-operator/cmd/config" k8serrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/client-go/discovery" + "sigs.k8s.io/controller-runtime/pkg/client/config" ) const ( @@ -48,8 +50,7 @@ func (p *Resolver) GetPlatform() (string, error) { } func getDiscoveryClient() (discovery.DiscoveryInterface, error) { - kubeconfigProvider := config.KubeConfigProvider{} - kubeconfig, err := kubeconfigProvider.GetConfig() + kubeconfig, err := config.GetConfig() if err != nil { return nil, err } diff --git a/test/helpers/proxy/proxy.go b/test/helpers/proxy/proxy.go index 5d790fdecd..2b39ab7c21 100644 --- a/test/helpers/proxy/proxy.go +++ b/test/helpers/proxy/proxy.go @@ -10,10 +10,10 @@ import ( "testing" "github.com/Dynatrace/dynatrace-operator/pkg/api/shared/value" - "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "github.com/Dynatrace/dynatrace-operator/pkg/injection/codemodule/installer/common" "github.com/Dynatrace/dynatrace-operator/pkg/webhook" - oamutation "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/oneagent" + oamutation "github.com/Dynatrace/dynatrace-operator/pkg/webhook/mutation/pod/v1/oneagent" "github.com/Dynatrace/dynatrace-operator/test/helpers" "github.com/Dynatrace/dynatrace-operator/test/helpers/curl" "github.com/Dynatrace/dynatrace-operator/test/helpers/kubeobjects/deployment" diff --git a/test/helpers/registry/registry.go b/test/helpers/registry/registry.go index 659f4f954d..18ac5c51c0 100644 --- a/test/helpers/registry/registry.go +++ b/test/helpers/registry/registry.go @@ -1,3 +1,5 @@ +//go:build e2e + package registry import ( diff --git a/test/helpers/sample/app.go b/test/helpers/sample/app.go index 8e5819b9ec..829239e151 100644 --- a/test/helpers/sample/app.go +++ b/test/helpers/sample/app.go @@ -9,7 +9,6 @@ import ( "testing" "time" - "github.com/Dynatrace/dynatrace-operator/pkg/util/address" "github.com/Dynatrace/dynatrace-operator/test/helpers" "github.com/Dynatrace/dynatrace-operator/test/helpers/kubeobjects/deployment" "github.com/Dynatrace/dynatrace-operator/test/helpers/kubeobjects/manifests" @@ -22,6 +21,7 @@ import ( appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" "sigs.k8s.io/e2e-framework/klient/k8s" "sigs.k8s.io/e2e-framework/klient/k8s/resources" "sigs.k8s.io/e2e-framework/klient/wait" @@ -215,7 +215,7 @@ func (app *App) asDeployment() *appsv1.Deployment { return &appsv1.Deployment{ ObjectMeta: app.base.ObjectMeta, Spec: appsv1.DeploymentSpec{ - Replicas: address.Of(int32(2)), + Replicas: ptr.To(int32(2)), Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{ selectorKey: selectorValue, diff --git a/test/helpers/scheme.go b/test/helpers/scheme.go index 69b3f85b74..c7acc87f92 100644 --- a/test/helpers/scheme.go +++ b/test/helpers/scheme.go @@ -15,11 +15,17 @@ import ( _ "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta2/dynakube" //nolint:staticcheck "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3" _ "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4" + _ "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" "sigs.k8s.io/e2e-framework/pkg/envconf" ) func SetScheme(ctx context.Context, envConfig *envconf.Config) (context.Context, error) { - err := v1beta3.AddToScheme(envConfig.Client().Resources().GetScheme()) + err := v1beta4.AddToScheme(envConfig.Client().Resources().GetScheme()) + if err != nil { + return ctx, err + } + err = v1beta3.AddToScheme(envConfig.Client().Resources().GetScheme()) if err != nil { return ctx, err } diff --git a/test/helpers/tenant/secrets.go b/test/helpers/tenant/secrets.go index cb3468b0c0..22f80541cd 100644 --- a/test/helpers/tenant/secrets.go +++ b/test/helpers/tenant/secrets.go @@ -30,9 +30,10 @@ type Secrets struct { } type Secret struct { - TenantUid string `yaml:"tenantUid"` - ApiUrl string `yaml:"apiUrl"` - ApiToken string `yaml:"apiToken"` + TenantUid string `yaml:"tenantUid"` + ApiUrl string `yaml:"apiUrl"` + ApiToken string `yaml:"apiToken"` + DataIngestToken string `yaml:"dataIngestToken"` } type EdgeConnectSecret struct { @@ -117,6 +118,10 @@ func CreateTenantSecret(secretConfig Secret, name, namespace string) features.Fu }, } + if secretConfig.DataIngestToken != "" { + defaultSecret.Data["dataIngestToken"] = []byte(secretConfig.DataIngestToken) + } + err := envConfig.Client().Resources().Create(ctx, &defaultSecret) if k8serrors.IsAlreadyExists(err) { diff --git a/test/helpers/tls/tls.go b/test/helpers/tls/tls.go new file mode 100644 index 0000000000..d75c0ba123 --- /dev/null +++ b/test/helpers/tls/tls.go @@ -0,0 +1,34 @@ +//go:build e2e + +package tls + +import ( + "os" + "path" + + operatorconsts "github.com/Dynatrace/dynatrace-operator/pkg/consts" + "github.com/Dynatrace/dynatrace-operator/test/helpers/kubeobjects/secret" + "github.com/Dynatrace/dynatrace-operator/test/project" + corev1 "k8s.io/api/core/v1" +) + +func CreateTestdataTLSSecret(namespace string, name string, keyFile string, crtFile string) (corev1.Secret, error) { + tlsCrt, err := os.ReadFile(path.Join(project.TestDataDir(), crtFile)) + if err != nil { + return corev1.Secret{}, err + } + + tlsKey, err := os.ReadFile(path.Join(project.TestDataDir(), keyFile)) + if err != nil { + return corev1.Secret{}, err + } + + tlsSecret := secret.New(name, namespace, + map[string][]byte{ + operatorconsts.TLSCrtDataName: tlsCrt, + operatorconsts.TLSKeyDataName: tlsKey, + }) + tlsSecret.Type = corev1.SecretTypeTLS + + return tlsSecret, nil +} diff --git a/cmd/integration/integration_test.go b/test/integration/integration_test.go similarity index 100% rename from cmd/integration/integration_test.go rename to test/integration/integration_test.go diff --git a/test/mocks/cmd/config/provider.go b/test/mocks/cmd/config/provider.go index d232aff85a..7ee35117e2 100644 --- a/test/mocks/cmd/config/provider.go +++ b/test/mocks/cmd/config/provider.go @@ -20,7 +20,7 @@ func (_m *Provider) EXPECT() *Provider_Expecter { return &Provider_Expecter{mock: &_m.Mock} } -// GetConfig provides a mock function with given fields: +// GetConfig provides a mock function with no fields func (_m *Provider) GetConfig() (*rest.Config, error) { ret := _m.Called() diff --git a/pkg/util/testing/mocks/k8s.io/client-go/kubernetes/typed/core/v1/pod_interface.go b/test/mocks/k8s.io/client-go/kubernetes/typed/core/v1/pod_interface.go similarity index 76% rename from pkg/util/testing/mocks/k8s.io/client-go/kubernetes/typed/core/v1/pod_interface.go rename to test/mocks/k8s.io/client-go/kubernetes/typed/core/v1/pod_interface.go index d72ccef08e..709b2d7259 100644 --- a/pkg/util/testing/mocks/k8s.io/client-go/kubernetes/typed/core/v1/pod_interface.go +++ b/test/mocks/k8s.io/client-go/kubernetes/typed/core/v1/pod_interface.go @@ -1,19 +1,27 @@ -// Code generated by mockery v2.15.0. DO NOT EDIT. +// Code generated by mockery. DO NOT EDIT. package mocks import ( context "context" - mock "github.com/stretchr/testify/mock" corev1 "k8s.io/api/core/v1" - policyv1 "k8s.io/api/policy/v1" - v1beta1 "k8s.io/api/policy/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + mock "github.com/stretchr/testify/mock" + + policyv1 "k8s.io/api/policy/v1" + + rest "k8s.io/client-go/rest" + types "k8s.io/apimachinery/pkg/types" - watch "k8s.io/apimachinery/pkg/watch" + v1 "k8s.io/client-go/applyconfigurations/core/v1" - rest "k8s.io/client-go/rest" + + v1beta1 "k8s.io/api/policy/v1beta1" + + watch "k8s.io/apimachinery/pkg/watch" ) // PodInterface is an autogenerated mock type for the PodInterface type @@ -33,7 +41,15 @@ func (_m *PodInterface) EXPECT() *PodInterface_Expecter { func (_m *PodInterface) Apply(ctx context.Context, pod *v1.PodApplyConfiguration, opts metav1.ApplyOptions) (*corev1.Pod, error) { ret := _m.Called(ctx, pod, opts) + if len(ret) == 0 { + panic("no return value specified for Apply") + } + var r0 *corev1.Pod + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *v1.PodApplyConfiguration, metav1.ApplyOptions) (*corev1.Pod, error)); ok { + return rf(ctx, pod, opts) + } if rf, ok := ret.Get(0).(func(context.Context, *v1.PodApplyConfiguration, metav1.ApplyOptions) *corev1.Pod); ok { r0 = rf(ctx, pod, opts) } else { @@ -42,7 +58,6 @@ func (_m *PodInterface) Apply(ctx context.Context, pod *v1.PodApplyConfiguration } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, *v1.PodApplyConfiguration, metav1.ApplyOptions) error); ok { r1 = rf(ctx, pod, opts) } else { @@ -69,7 +84,6 @@ func (_c *PodInterface_Apply_Call) Run(run func(ctx context.Context, pod *v1.Pod _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(*v1.PodApplyConfiguration), args[2].(metav1.ApplyOptions)) }) - return _c } @@ -78,11 +92,24 @@ func (_c *PodInterface_Apply_Call) Return(result *corev1.Pod, err error) *PodInt return _c } +func (_c *PodInterface_Apply_Call) RunAndReturn(run func(context.Context, *v1.PodApplyConfiguration, metav1.ApplyOptions) (*corev1.Pod, error)) *PodInterface_Apply_Call { + _c.Call.Return(run) + return _c +} + // ApplyStatus provides a mock function with given fields: ctx, pod, opts func (_m *PodInterface) ApplyStatus(ctx context.Context, pod *v1.PodApplyConfiguration, opts metav1.ApplyOptions) (*corev1.Pod, error) { ret := _m.Called(ctx, pod, opts) + if len(ret) == 0 { + panic("no return value specified for ApplyStatus") + } + var r0 *corev1.Pod + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *v1.PodApplyConfiguration, metav1.ApplyOptions) (*corev1.Pod, error)); ok { + return rf(ctx, pod, opts) + } if rf, ok := ret.Get(0).(func(context.Context, *v1.PodApplyConfiguration, metav1.ApplyOptions) *corev1.Pod); ok { r0 = rf(ctx, pod, opts) } else { @@ -91,7 +118,6 @@ func (_m *PodInterface) ApplyStatus(ctx context.Context, pod *v1.PodApplyConfigu } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, *v1.PodApplyConfiguration, metav1.ApplyOptions) error); ok { r1 = rf(ctx, pod, opts) } else { @@ -118,7 +144,6 @@ func (_c *PodInterface_ApplyStatus_Call) Run(run func(ctx context.Context, pod * _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(*v1.PodApplyConfiguration), args[2].(metav1.ApplyOptions)) }) - return _c } @@ -127,10 +152,19 @@ func (_c *PodInterface_ApplyStatus_Call) Return(result *corev1.Pod, err error) * return _c } +func (_c *PodInterface_ApplyStatus_Call) RunAndReturn(run func(context.Context, *v1.PodApplyConfiguration, metav1.ApplyOptions) (*corev1.Pod, error)) *PodInterface_ApplyStatus_Call { + _c.Call.Return(run) + return _c +} + // Bind provides a mock function with given fields: ctx, binding, opts func (_m *PodInterface) Bind(ctx context.Context, binding *corev1.Binding, opts metav1.CreateOptions) error { ret := _m.Called(ctx, binding, opts) + if len(ret) == 0 { + panic("no return value specified for Bind") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, *corev1.Binding, metav1.CreateOptions) error); ok { r0 = rf(ctx, binding, opts) @@ -158,7 +192,6 @@ func (_c *PodInterface_Bind_Call) Run(run func(ctx context.Context, binding *cor _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(*corev1.Binding), args[2].(metav1.CreateOptions)) }) - return _c } @@ -167,11 +200,24 @@ func (_c *PodInterface_Bind_Call) Return(_a0 error) *PodInterface_Bind_Call { return _c } +func (_c *PodInterface_Bind_Call) RunAndReturn(run func(context.Context, *corev1.Binding, metav1.CreateOptions) error) *PodInterface_Bind_Call { + _c.Call.Return(run) + return _c +} + // Create provides a mock function with given fields: ctx, pod, opts func (_m *PodInterface) Create(ctx context.Context, pod *corev1.Pod, opts metav1.CreateOptions) (*corev1.Pod, error) { ret := _m.Called(ctx, pod, opts) + if len(ret) == 0 { + panic("no return value specified for Create") + } + var r0 *corev1.Pod + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *corev1.Pod, metav1.CreateOptions) (*corev1.Pod, error)); ok { + return rf(ctx, pod, opts) + } if rf, ok := ret.Get(0).(func(context.Context, *corev1.Pod, metav1.CreateOptions) *corev1.Pod); ok { r0 = rf(ctx, pod, opts) } else { @@ -180,7 +226,6 @@ func (_m *PodInterface) Create(ctx context.Context, pod *corev1.Pod, opts metav1 } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, *corev1.Pod, metav1.CreateOptions) error); ok { r1 = rf(ctx, pod, opts) } else { @@ -207,7 +252,6 @@ func (_c *PodInterface_Create_Call) Run(run func(ctx context.Context, pod *corev _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(*corev1.Pod), args[2].(metav1.CreateOptions)) }) - return _c } @@ -216,10 +260,19 @@ func (_c *PodInterface_Create_Call) Return(_a0 *corev1.Pod, _a1 error) *PodInter return _c } +func (_c *PodInterface_Create_Call) RunAndReturn(run func(context.Context, *corev1.Pod, metav1.CreateOptions) (*corev1.Pod, error)) *PodInterface_Create_Call { + _c.Call.Return(run) + return _c +} + // Delete provides a mock function with given fields: ctx, name, opts func (_m *PodInterface) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { ret := _m.Called(ctx, name, opts) + if len(ret) == 0 { + panic("no return value specified for Delete") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, string, metav1.DeleteOptions) error); ok { r0 = rf(ctx, name, opts) @@ -247,7 +300,6 @@ func (_c *PodInterface_Delete_Call) Run(run func(ctx context.Context, name strin _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(string), args[2].(metav1.DeleteOptions)) }) - return _c } @@ -256,10 +308,19 @@ func (_c *PodInterface_Delete_Call) Return(_a0 error) *PodInterface_Delete_Call return _c } +func (_c *PodInterface_Delete_Call) RunAndReturn(run func(context.Context, string, metav1.DeleteOptions) error) *PodInterface_Delete_Call { + _c.Call.Return(run) + return _c +} + // DeleteCollection provides a mock function with given fields: ctx, opts, listOpts func (_m *PodInterface) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error { ret := _m.Called(ctx, opts, listOpts) + if len(ret) == 0 { + panic("no return value specified for DeleteCollection") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, metav1.DeleteOptions, metav1.ListOptions) error); ok { r0 = rf(ctx, opts, listOpts) @@ -287,7 +348,6 @@ func (_c *PodInterface_DeleteCollection_Call) Run(run func(ctx context.Context, _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(metav1.DeleteOptions), args[2].(metav1.ListOptions)) }) - return _c } @@ -296,10 +356,19 @@ func (_c *PodInterface_DeleteCollection_Call) Return(_a0 error) *PodInterface_De return _c } +func (_c *PodInterface_DeleteCollection_Call) RunAndReturn(run func(context.Context, metav1.DeleteOptions, metav1.ListOptions) error) *PodInterface_DeleteCollection_Call { + _c.Call.Return(run) + return _c +} + // Evict provides a mock function with given fields: ctx, eviction func (_m *PodInterface) Evict(ctx context.Context, eviction *v1beta1.Eviction) error { ret := _m.Called(ctx, eviction) + if len(ret) == 0 { + panic("no return value specified for Evict") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, *v1beta1.Eviction) error); ok { r0 = rf(ctx, eviction) @@ -326,7 +395,6 @@ func (_c *PodInterface_Evict_Call) Run(run func(ctx context.Context, eviction *v _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(*v1beta1.Eviction)) }) - return _c } @@ -335,10 +403,19 @@ func (_c *PodInterface_Evict_Call) Return(_a0 error) *PodInterface_Evict_Call { return _c } +func (_c *PodInterface_Evict_Call) RunAndReturn(run func(context.Context, *v1beta1.Eviction) error) *PodInterface_Evict_Call { + _c.Call.Return(run) + return _c +} + // EvictV1 provides a mock function with given fields: ctx, eviction func (_m *PodInterface) EvictV1(ctx context.Context, eviction *policyv1.Eviction) error { ret := _m.Called(ctx, eviction) + if len(ret) == 0 { + panic("no return value specified for EvictV1") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, *policyv1.Eviction) error); ok { r0 = rf(ctx, eviction) @@ -365,7 +442,6 @@ func (_c *PodInterface_EvictV1_Call) Run(run func(ctx context.Context, eviction _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(*policyv1.Eviction)) }) - return _c } @@ -374,10 +450,19 @@ func (_c *PodInterface_EvictV1_Call) Return(_a0 error) *PodInterface_EvictV1_Cal return _c } +func (_c *PodInterface_EvictV1_Call) RunAndReturn(run func(context.Context, *policyv1.Eviction) error) *PodInterface_EvictV1_Call { + _c.Call.Return(run) + return _c +} + // EvictV1beta1 provides a mock function with given fields: ctx, eviction func (_m *PodInterface) EvictV1beta1(ctx context.Context, eviction *v1beta1.Eviction) error { ret := _m.Called(ctx, eviction) + if len(ret) == 0 { + panic("no return value specified for EvictV1beta1") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, *v1beta1.Eviction) error); ok { r0 = rf(ctx, eviction) @@ -404,7 +489,6 @@ func (_c *PodInterface_EvictV1beta1_Call) Run(run func(ctx context.Context, evic _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(*v1beta1.Eviction)) }) - return _c } @@ -413,11 +497,24 @@ func (_c *PodInterface_EvictV1beta1_Call) Return(_a0 error) *PodInterface_EvictV return _c } +func (_c *PodInterface_EvictV1beta1_Call) RunAndReturn(run func(context.Context, *v1beta1.Eviction) error) *PodInterface_EvictV1beta1_Call { + _c.Call.Return(run) + return _c +} + // Get provides a mock function with given fields: ctx, name, opts func (_m *PodInterface) Get(ctx context.Context, name string, opts metav1.GetOptions) (*corev1.Pod, error) { ret := _m.Called(ctx, name, opts) + if len(ret) == 0 { + panic("no return value specified for Get") + } + var r0 *corev1.Pod + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, metav1.GetOptions) (*corev1.Pod, error)); ok { + return rf(ctx, name, opts) + } if rf, ok := ret.Get(0).(func(context.Context, string, metav1.GetOptions) *corev1.Pod); ok { r0 = rf(ctx, name, opts) } else { @@ -426,7 +523,6 @@ func (_m *PodInterface) Get(ctx context.Context, name string, opts metav1.GetOpt } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, string, metav1.GetOptions) error); ok { r1 = rf(ctx, name, opts) } else { @@ -453,7 +549,6 @@ func (_c *PodInterface_Get_Call) Run(run func(ctx context.Context, name string, _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(string), args[2].(metav1.GetOptions)) }) - return _c } @@ -462,10 +557,19 @@ func (_c *PodInterface_Get_Call) Return(_a0 *corev1.Pod, _a1 error) *PodInterfac return _c } +func (_c *PodInterface_Get_Call) RunAndReturn(run func(context.Context, string, metav1.GetOptions) (*corev1.Pod, error)) *PodInterface_Get_Call { + _c.Call.Return(run) + return _c +} + // GetLogs provides a mock function with given fields: name, opts func (_m *PodInterface) GetLogs(name string, opts *corev1.PodLogOptions) *rest.Request { ret := _m.Called(name, opts) + if len(ret) == 0 { + panic("no return value specified for GetLogs") + } + var r0 *rest.Request if rf, ok := ret.Get(0).(func(string, *corev1.PodLogOptions) *rest.Request); ok { r0 = rf(name, opts) @@ -494,7 +598,6 @@ func (_c *PodInterface_GetLogs_Call) Run(run func(name string, opts *corev1.PodL _c.Call.Run(func(args mock.Arguments) { run(args[0].(string), args[1].(*corev1.PodLogOptions)) }) - return _c } @@ -503,11 +606,24 @@ func (_c *PodInterface_GetLogs_Call) Return(_a0 *rest.Request) *PodInterface_Get return _c } +func (_c *PodInterface_GetLogs_Call) RunAndReturn(run func(string, *corev1.PodLogOptions) *rest.Request) *PodInterface_GetLogs_Call { + _c.Call.Return(run) + return _c +} + // List provides a mock function with given fields: ctx, opts func (_m *PodInterface) List(ctx context.Context, opts metav1.ListOptions) (*corev1.PodList, error) { ret := _m.Called(ctx, opts) + if len(ret) == 0 { + panic("no return value specified for List") + } + var r0 *corev1.PodList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, metav1.ListOptions) (*corev1.PodList, error)); ok { + return rf(ctx, opts) + } if rf, ok := ret.Get(0).(func(context.Context, metav1.ListOptions) *corev1.PodList); ok { r0 = rf(ctx, opts) } else { @@ -516,7 +632,6 @@ func (_m *PodInterface) List(ctx context.Context, opts metav1.ListOptions) (*cor } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, metav1.ListOptions) error); ok { r1 = rf(ctx, opts) } else { @@ -542,7 +657,6 @@ func (_c *PodInterface_List_Call) Run(run func(ctx context.Context, opts metav1. _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(metav1.ListOptions)) }) - return _c } @@ -551,19 +665,31 @@ func (_c *PodInterface_List_Call) Return(_a0 *corev1.PodList, _a1 error) *PodInt return _c } +func (_c *PodInterface_List_Call) RunAndReturn(run func(context.Context, metav1.ListOptions) (*corev1.PodList, error)) *PodInterface_List_Call { + _c.Call.Return(run) + return _c +} + // Patch provides a mock function with given fields: ctx, name, pt, data, opts, subresources func (_m *PodInterface) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (*corev1.Pod, error) { _va := make([]interface{}, len(subresources)) for _i := range subresources { _va[_i] = subresources[_i] } - var _ca []interface{} _ca = append(_ca, ctx, name, pt, data, opts) _ca = append(_ca, _va...) ret := _m.Called(_ca...) + if len(ret) == 0 { + panic("no return value specified for Patch") + } + var r0 *corev1.Pod + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, types.PatchType, []byte, metav1.PatchOptions, ...string) (*corev1.Pod, error)); ok { + return rf(ctx, name, pt, data, opts, subresources...) + } if rf, ok := ret.Get(0).(func(context.Context, string, types.PatchType, []byte, metav1.PatchOptions, ...string) *corev1.Pod); ok { r0 = rf(ctx, name, pt, data, opts, subresources...) } else { @@ -572,7 +698,6 @@ func (_m *PodInterface) Patch(ctx context.Context, name string, pt types.PatchTy } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, string, types.PatchType, []byte, metav1.PatchOptions, ...string) error); ok { r1 = rf(ctx, name, pt, data, opts, subresources...) } else { @@ -602,16 +727,13 @@ func (_e *PodInterface_Expecter) Patch(ctx interface{}, name interface{}, pt int func (_c *PodInterface_Patch_Call) Run(run func(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string)) *PodInterface_Patch_Call { _c.Call.Run(func(args mock.Arguments) { variadicArgs := make([]string, len(args)-5) - for i, a := range args[5:] { if a != nil { variadicArgs[i] = a.(string) } } - run(args[0].(context.Context), args[1].(string), args[2].(types.PatchType), args[3].([]byte), args[4].(metav1.PatchOptions), variadicArgs...) }) - return _c } @@ -620,10 +742,19 @@ func (_c *PodInterface_Patch_Call) Return(result *corev1.Pod, err error) *PodInt return _c } +func (_c *PodInterface_Patch_Call) RunAndReturn(run func(context.Context, string, types.PatchType, []byte, metav1.PatchOptions, ...string) (*corev1.Pod, error)) *PodInterface_Patch_Call { + _c.Call.Return(run) + return _c +} + // ProxyGet provides a mock function with given fields: scheme, name, port, path, params func (_m *PodInterface) ProxyGet(scheme string, name string, port string, path string, params map[string]string) rest.ResponseWrapper { ret := _m.Called(scheme, name, port, path, params) + if len(ret) == 0 { + panic("no return value specified for ProxyGet") + } + var r0 rest.ResponseWrapper if rf, ok := ret.Get(0).(func(string, string, string, string, map[string]string) rest.ResponseWrapper); ok { r0 = rf(scheme, name, port, path, params) @@ -655,7 +786,6 @@ func (_c *PodInterface_ProxyGet_Call) Run(run func(scheme string, name string, p _c.Call.Run(func(args mock.Arguments) { run(args[0].(string), args[1].(string), args[2].(string), args[3].(string), args[4].(map[string]string)) }) - return _c } @@ -664,11 +794,24 @@ func (_c *PodInterface_ProxyGet_Call) Return(_a0 rest.ResponseWrapper) *PodInter return _c } +func (_c *PodInterface_ProxyGet_Call) RunAndReturn(run func(string, string, string, string, map[string]string) rest.ResponseWrapper) *PodInterface_ProxyGet_Call { + _c.Call.Return(run) + return _c +} + // Update provides a mock function with given fields: ctx, pod, opts func (_m *PodInterface) Update(ctx context.Context, pod *corev1.Pod, opts metav1.UpdateOptions) (*corev1.Pod, error) { ret := _m.Called(ctx, pod, opts) + if len(ret) == 0 { + panic("no return value specified for Update") + } + var r0 *corev1.Pod + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *corev1.Pod, metav1.UpdateOptions) (*corev1.Pod, error)); ok { + return rf(ctx, pod, opts) + } if rf, ok := ret.Get(0).(func(context.Context, *corev1.Pod, metav1.UpdateOptions) *corev1.Pod); ok { r0 = rf(ctx, pod, opts) } else { @@ -677,7 +820,6 @@ func (_m *PodInterface) Update(ctx context.Context, pod *corev1.Pod, opts metav1 } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, *corev1.Pod, metav1.UpdateOptions) error); ok { r1 = rf(ctx, pod, opts) } else { @@ -704,7 +846,6 @@ func (_c *PodInterface_Update_Call) Run(run func(ctx context.Context, pod *corev _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(*corev1.Pod), args[2].(metav1.UpdateOptions)) }) - return _c } @@ -713,11 +854,24 @@ func (_c *PodInterface_Update_Call) Return(_a0 *corev1.Pod, _a1 error) *PodInter return _c } +func (_c *PodInterface_Update_Call) RunAndReturn(run func(context.Context, *corev1.Pod, metav1.UpdateOptions) (*corev1.Pod, error)) *PodInterface_Update_Call { + _c.Call.Return(run) + return _c +} + // UpdateEphemeralContainers provides a mock function with given fields: ctx, podName, pod, opts func (_m *PodInterface) UpdateEphemeralContainers(ctx context.Context, podName string, pod *corev1.Pod, opts metav1.UpdateOptions) (*corev1.Pod, error) { ret := _m.Called(ctx, podName, pod, opts) + if len(ret) == 0 { + panic("no return value specified for UpdateEphemeralContainers") + } + var r0 *corev1.Pod + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, *corev1.Pod, metav1.UpdateOptions) (*corev1.Pod, error)); ok { + return rf(ctx, podName, pod, opts) + } if rf, ok := ret.Get(0).(func(context.Context, string, *corev1.Pod, metav1.UpdateOptions) *corev1.Pod); ok { r0 = rf(ctx, podName, pod, opts) } else { @@ -726,7 +880,6 @@ func (_m *PodInterface) UpdateEphemeralContainers(ctx context.Context, podName s } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, string, *corev1.Pod, metav1.UpdateOptions) error); ok { r1 = rf(ctx, podName, pod, opts) } else { @@ -754,7 +907,6 @@ func (_c *PodInterface_UpdateEphemeralContainers_Call) Run(run func(ctx context. _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(string), args[2].(*corev1.Pod), args[3].(metav1.UpdateOptions)) }) - return _c } @@ -763,11 +915,85 @@ func (_c *PodInterface_UpdateEphemeralContainers_Call) Return(_a0 *corev1.Pod, _ return _c } +func (_c *PodInterface_UpdateEphemeralContainers_Call) RunAndReturn(run func(context.Context, string, *corev1.Pod, metav1.UpdateOptions) (*corev1.Pod, error)) *PodInterface_UpdateEphemeralContainers_Call { + _c.Call.Return(run) + return _c +} + +// UpdateResize provides a mock function with given fields: ctx, podName, pod, opts +func (_m *PodInterface) UpdateResize(ctx context.Context, podName string, pod *corev1.Pod, opts metav1.UpdateOptions) (*corev1.Pod, error) { + ret := _m.Called(ctx, podName, pod, opts) + + if len(ret) == 0 { + panic("no return value specified for UpdateResize") + } + + var r0 *corev1.Pod + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, *corev1.Pod, metav1.UpdateOptions) (*corev1.Pod, error)); ok { + return rf(ctx, podName, pod, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, string, *corev1.Pod, metav1.UpdateOptions) *corev1.Pod); ok { + r0 = rf(ctx, podName, pod, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*corev1.Pod) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, *corev1.Pod, metav1.UpdateOptions) error); ok { + r1 = rf(ctx, podName, pod, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// PodInterface_UpdateResize_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateResize' +type PodInterface_UpdateResize_Call struct { + *mock.Call +} + +// UpdateResize is a helper method to define mock.On call +// - ctx context.Context +// - podName string +// - pod *corev1.Pod +// - opts metav1.UpdateOptions +func (_e *PodInterface_Expecter) UpdateResize(ctx interface{}, podName interface{}, pod interface{}, opts interface{}) *PodInterface_UpdateResize_Call { + return &PodInterface_UpdateResize_Call{Call: _e.mock.On("UpdateResize", ctx, podName, pod, opts)} +} + +func (_c *PodInterface_UpdateResize_Call) Run(run func(ctx context.Context, podName string, pod *corev1.Pod, opts metav1.UpdateOptions)) *PodInterface_UpdateResize_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(*corev1.Pod), args[3].(metav1.UpdateOptions)) + }) + return _c +} + +func (_c *PodInterface_UpdateResize_Call) Return(_a0 *corev1.Pod, _a1 error) *PodInterface_UpdateResize_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *PodInterface_UpdateResize_Call) RunAndReturn(run func(context.Context, string, *corev1.Pod, metav1.UpdateOptions) (*corev1.Pod, error)) *PodInterface_UpdateResize_Call { + _c.Call.Return(run) + return _c +} + // UpdateStatus provides a mock function with given fields: ctx, pod, opts func (_m *PodInterface) UpdateStatus(ctx context.Context, pod *corev1.Pod, opts metav1.UpdateOptions) (*corev1.Pod, error) { ret := _m.Called(ctx, pod, opts) + if len(ret) == 0 { + panic("no return value specified for UpdateStatus") + } + var r0 *corev1.Pod + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *corev1.Pod, metav1.UpdateOptions) (*corev1.Pod, error)); ok { + return rf(ctx, pod, opts) + } if rf, ok := ret.Get(0).(func(context.Context, *corev1.Pod, metav1.UpdateOptions) *corev1.Pod); ok { r0 = rf(ctx, pod, opts) } else { @@ -776,7 +1002,6 @@ func (_m *PodInterface) UpdateStatus(ctx context.Context, pod *corev1.Pod, opts } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, *corev1.Pod, metav1.UpdateOptions) error); ok { r1 = rf(ctx, pod, opts) } else { @@ -803,7 +1028,6 @@ func (_c *PodInterface_UpdateStatus_Call) Run(run func(ctx context.Context, pod _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(*corev1.Pod), args[2].(metav1.UpdateOptions)) }) - return _c } @@ -812,11 +1036,24 @@ func (_c *PodInterface_UpdateStatus_Call) Return(_a0 *corev1.Pod, _a1 error) *Po return _c } +func (_c *PodInterface_UpdateStatus_Call) RunAndReturn(run func(context.Context, *corev1.Pod, metav1.UpdateOptions) (*corev1.Pod, error)) *PodInterface_UpdateStatus_Call { + _c.Call.Return(run) + return _c +} + // Watch provides a mock function with given fields: ctx, opts func (_m *PodInterface) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { ret := _m.Called(ctx, opts) + if len(ret) == 0 { + panic("no return value specified for Watch") + } + var r0 watch.Interface + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, metav1.ListOptions) (watch.Interface, error)); ok { + return rf(ctx, opts) + } if rf, ok := ret.Get(0).(func(context.Context, metav1.ListOptions) watch.Interface); ok { r0 = rf(ctx, opts) } else { @@ -825,7 +1062,6 @@ func (_m *PodInterface) Watch(ctx context.Context, opts metav1.ListOptions) (wat } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, metav1.ListOptions) error); ok { r1 = rf(ctx, opts) } else { @@ -851,7 +1087,6 @@ func (_c *PodInterface_Watch_Call) Run(run func(ctx context.Context, opts metav1 _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(metav1.ListOptions)) }) - return _c } @@ -860,13 +1095,17 @@ func (_c *PodInterface_Watch_Call) Return(_a0 watch.Interface, _a1 error) *PodIn return _c } -type mockConstructorTestingTNewPodInterface interface { - mock.TestingT - Cleanup(func()) +func (_c *PodInterface_Watch_Call) RunAndReturn(run func(context.Context, metav1.ListOptions) (watch.Interface, error)) *PodInterface_Watch_Call { + _c.Call.Return(run) + return _c } // NewPodInterface creates a new instance of PodInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewPodInterface(t mockConstructorTestingTNewPodInterface) *PodInterface { +// The first argument is typically a *testing.T value. +func NewPodInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *PodInterface { mock := &PodInterface{} mock.Mock.Test(t) diff --git a/test/mocks/pkg/clients/dynatrace/client.go b/test/mocks/pkg/clients/dynatrace/client.go index 0bb92f83d0..16d1b1b69a 100644 --- a/test/mocks/pkg/clients/dynatrace/client.go +++ b/test/mocks/pkg/clients/dynatrace/client.go @@ -8,7 +8,7 @@ import ( dynatrace "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" - logmonitoring "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/logmonitoring" + logmonitoring "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube/logmonitoring" mock "github.com/stretchr/testify/mock" ) @@ -370,9 +370,9 @@ func (_c *Client_GetAgent_Call) RunAndReturn(run func(context.Context, string, s return _c } -// GetAgentVersions provides a mock function with given fields: ctx, os, installerType, flavor, arch -func (_m *Client) GetAgentVersions(ctx context.Context, os string, installerType string, flavor string, arch string) ([]string, error) { - ret := _m.Called(ctx, os, installerType, flavor, arch) +// GetAgentVersions provides a mock function with given fields: ctx, os, installerType, flavor +func (_m *Client) GetAgentVersions(ctx context.Context, os string, installerType string, flavor string) ([]string, error) { + ret := _m.Called(ctx, os, installerType, flavor) if len(ret) == 0 { panic("no return value specified for GetAgentVersions") @@ -380,19 +380,19 @@ func (_m *Client) GetAgentVersions(ctx context.Context, os string, installerType var r0 []string var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) ([]string, error)); ok { - return rf(ctx, os, installerType, flavor, arch) + if rf, ok := ret.Get(0).(func(context.Context, string, string, string) ([]string, error)); ok { + return rf(ctx, os, installerType, flavor) } - if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) []string); ok { - r0 = rf(ctx, os, installerType, flavor, arch) + if rf, ok := ret.Get(0).(func(context.Context, string, string, string) []string); ok { + r0 = rf(ctx, os, installerType, flavor) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]string) } } - if rf, ok := ret.Get(1).(func(context.Context, string, string, string, string) error); ok { - r1 = rf(ctx, os, installerType, flavor, arch) + if rf, ok := ret.Get(1).(func(context.Context, string, string, string) error); ok { + r1 = rf(ctx, os, installerType, flavor) } else { r1 = ret.Error(1) } @@ -410,14 +410,13 @@ type Client_GetAgentVersions_Call struct { // - os string // - installerType string // - flavor string -// - arch string -func (_e *Client_Expecter) GetAgentVersions(ctx interface{}, os interface{}, installerType interface{}, flavor interface{}, arch interface{}) *Client_GetAgentVersions_Call { - return &Client_GetAgentVersions_Call{Call: _e.mock.On("GetAgentVersions", ctx, os, installerType, flavor, arch)} +func (_e *Client_Expecter) GetAgentVersions(ctx interface{}, os interface{}, installerType interface{}, flavor interface{}) *Client_GetAgentVersions_Call { + return &Client_GetAgentVersions_Call{Call: _e.mock.On("GetAgentVersions", ctx, os, installerType, flavor)} } -func (_c *Client_GetAgentVersions_Call) Run(run func(ctx context.Context, os string, installerType string, flavor string, arch string)) *Client_GetAgentVersions_Call { +func (_c *Client_GetAgentVersions_Call) Run(run func(ctx context.Context, os string, installerType string, flavor string)) *Client_GetAgentVersions_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string), args[4].(string)) + run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string)) }) return _c } @@ -427,7 +426,7 @@ func (_c *Client_GetAgentVersions_Call) Return(_a0 []string, _a1 error) *Client_ return _c } -func (_c *Client_GetAgentVersions_Call) RunAndReturn(run func(context.Context, string, string, string, string) ([]string, error)) *Client_GetAgentVersions_Call { +func (_c *Client_GetAgentVersions_Call) RunAndReturn(run func(context.Context, string, string, string) ([]string, error)) *Client_GetAgentVersions_Call { _c.Call.Return(run) return _c } @@ -480,7 +479,7 @@ func (_c *Client_GetAgentViaInstallerUrl_Call) RunAndReturn(run func(context.Con return _c } -// GetCommunicationHostForClient provides a mock function with given fields: +// GetCommunicationHostForClient provides a mock function with no fields func (_m *Client) GetCommunicationHostForClient() (dynatrace.CommunicationHost, error) { ret := _m.Called() diff --git a/test/mocks/pkg/clients/edgeconnect/client.go b/test/mocks/pkg/clients/edgeconnect/client.go index 050448688a..e662ca436f 100644 --- a/test/mocks/pkg/clients/edgeconnect/client.go +++ b/test/mocks/pkg/clients/edgeconnect/client.go @@ -214,7 +214,7 @@ func (_c *Client_DeleteEdgeConnect_Call) RunAndReturn(run func(string) error) *C return _c } -// GetConnectionSettings provides a mock function with given fields: +// GetConnectionSettings provides a mock function with no fields func (_m *Client) GetConnectionSettings() ([]edgeconnect.EnvironmentSetting, error) { ret := _m.Called() diff --git a/test/mocks/pkg/controllers/dynakube/dynatraceclient/builder.go b/test/mocks/pkg/controllers/dynakube/dynatraceclient/builder.go index 28de949c0c..7956a95671 100644 --- a/test/mocks/pkg/controllers/dynakube/dynatraceclient/builder.go +++ b/test/mocks/pkg/controllers/dynakube/dynatraceclient/builder.go @@ -5,7 +5,7 @@ package mocks import ( context "context" - dynakube "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + dynakube "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" dynatrace "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace" dynatraceclient "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/dynatraceclient" @@ -28,7 +28,7 @@ func (_m *Builder) EXPECT() *Builder_Expecter { return &Builder_Expecter{mock: &_m.Mock} } -// Build provides a mock function with given fields: +// Build provides a mock function with no fields func (_m *Builder) Build() (dynatrace.Client, error) { ret := _m.Called() diff --git a/test/mocks/pkg/controllers/dynakube/istio/reconciler.go b/test/mocks/pkg/controllers/dynakube/istio/reconciler.go index 18d001080c..32b6069701 100644 --- a/test/mocks/pkg/controllers/dynakube/istio/reconciler.go +++ b/test/mocks/pkg/controllers/dynakube/istio/reconciler.go @@ -5,7 +5,7 @@ package mocks import ( context "context" - dynakube "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + dynakube "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" mock "github.com/stretchr/testify/mock" ) diff --git a/test/mocks/pkg/controllers/dynakube/version/reconciler.go b/test/mocks/pkg/controllers/dynakube/version/reconciler.go index d3ab34f29d..beb55e5707 100644 --- a/test/mocks/pkg/controllers/dynakube/version/reconciler.go +++ b/test/mocks/pkg/controllers/dynakube/version/reconciler.go @@ -5,7 +5,7 @@ package mocks import ( context "context" - dynakube "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube" + dynakube "github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube" mock "github.com/stretchr/testify/mock" ) diff --git a/test/mocks/pkg/controllers/dynakube/version/status_updater.go b/test/mocks/pkg/controllers/dynakube/version/status_updater.go index 9876280843..907c7de660 100644 --- a/test/mocks/pkg/controllers/dynakube/version/status_updater.go +++ b/test/mocks/pkg/controllers/dynakube/version/status_updater.go @@ -80,7 +80,7 @@ func (_c *StatusUpdater_CheckForDowngrade_Call) RunAndReturn(run func(string) (b return _c } -// CustomImage provides a mock function with given fields: +// CustomImage provides a mock function with no fields func (_m *StatusUpdater) CustomImage() string { ret := _m.Called() @@ -125,7 +125,7 @@ func (_c *StatusUpdater_CustomImage_Call) RunAndReturn(run func() string) *Statu return _c } -// CustomVersion provides a mock function with given fields: +// CustomVersion provides a mock function with no fields func (_m *StatusUpdater) CustomVersion() string { ret := _m.Called() @@ -170,7 +170,7 @@ func (_c *StatusUpdater_CustomVersion_Call) RunAndReturn(run func() string) *Sta return _c } -// IsAutoUpdateEnabled provides a mock function with given fields: +// IsAutoUpdateEnabled provides a mock function with no fields func (_m *StatusUpdater) IsAutoUpdateEnabled() bool { ret := _m.Called() @@ -215,7 +215,7 @@ func (_c *StatusUpdater_IsAutoUpdateEnabled_Call) RunAndReturn(run func() bool) return _c } -// IsEnabled provides a mock function with given fields: +// IsEnabled provides a mock function with no fields func (_m *StatusUpdater) IsEnabled() bool { ret := _m.Called() @@ -260,7 +260,7 @@ func (_c *StatusUpdater_IsEnabled_Call) RunAndReturn(run func() bool) *StatusUpd return _c } -// IsPublicRegistryEnabled provides a mock function with given fields: +// IsPublicRegistryEnabled provides a mock function with no fields func (_m *StatusUpdater) IsPublicRegistryEnabled() bool { ret := _m.Called() @@ -363,7 +363,7 @@ func (_c *StatusUpdater_LatestImageInfo_Call) RunAndReturn(run func(context.Cont return _c } -// Name provides a mock function with given fields: +// Name provides a mock function with no fields func (_m *StatusUpdater) Name() string { ret := _m.Called() @@ -408,7 +408,7 @@ func (_c *StatusUpdater_Name_Call) RunAndReturn(run func() string) *StatusUpdate return _c } -// Target provides a mock function with given fields: +// Target provides a mock function with no fields func (_m *StatusUpdater) Target() *status.VersionStatus { ret := _m.Called() @@ -501,7 +501,7 @@ func (_c *StatusUpdater_UseTenantRegistry_Call) RunAndReturn(run func(context.Co return _c } -// ValidateStatus provides a mock function with given fields: +// ValidateStatus provides a mock function with no fields func (_m *StatusUpdater) ValidateStatus() error { ret := _m.Called() diff --git a/test/mocks/pkg/util/builder/modifier.go b/test/mocks/pkg/util/builder/modifier.go index 6086bedf5b..122a1b58eb 100644 --- a/test/mocks/pkg/util/builder/modifier.go +++ b/test/mocks/pkg/util/builder/modifier.go @@ -5,11 +5,11 @@ package mocks import mock "github.com/stretchr/testify/mock" // Modifier is an autogenerated mock type for the Modifier type -type Modifier[T any] struct { +type Modifier[T interface{}] struct { mock.Mock } -type Modifier_Expecter[T any] struct { +type Modifier_Expecter[T interface{}] struct { mock *mock.Mock } @@ -17,7 +17,7 @@ func (_m *Modifier[T]) EXPECT() *Modifier_Expecter[T] { return &Modifier_Expecter[T]{mock: &_m.Mock} } -// Enabled provides a mock function with given fields: +// Enabled provides a mock function with no fields func (_m *Modifier[T]) Enabled() bool { ret := _m.Called() @@ -36,7 +36,7 @@ func (_m *Modifier[T]) Enabled() bool { } // Modifier_Enabled_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Enabled' -type Modifier_Enabled_Call[T any] struct { +type Modifier_Enabled_Call[T interface{}] struct { *mock.Call } @@ -81,7 +81,7 @@ func (_m *Modifier[T]) Modify(_a0 *T) error { } // Modifier_Modify_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Modify' -type Modifier_Modify_Call[T any] struct { +type Modifier_Modify_Call[T interface{}] struct { *mock.Call } @@ -110,7 +110,7 @@ func (_c *Modifier_Modify_Call[T]) RunAndReturn(run func(*T) error) *Modifier_Mo // NewModifier creates a new instance of Modifier. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. -func NewModifier[T any](t interface { +func NewModifier[T interface{}](t interface { mock.TestingT Cleanup(func()) }) *Modifier[T] { diff --git a/test/mocks/pkg/webhook/pod_injector.go b/test/mocks/pkg/webhook/pod_injector.go new file mode 100644 index 0000000000..9f81946381 --- /dev/null +++ b/test/mocks/pkg/webhook/pod_injector.go @@ -0,0 +1,84 @@ +// Code generated by mockery. DO NOT EDIT. + +package mocks + +import ( + context "context" + + webhook "github.com/Dynatrace/dynatrace-operator/pkg/webhook" + mock "github.com/stretchr/testify/mock" +) + +// PodInjector is an autogenerated mock type for the PodInjector type +type PodInjector struct { + mock.Mock +} + +type PodInjector_Expecter struct { + mock *mock.Mock +} + +func (_m *PodInjector) EXPECT() *PodInjector_Expecter { + return &PodInjector_Expecter{mock: &_m.Mock} +} + +// Handle provides a mock function with given fields: _a0, _a1 +func (_m *PodInjector) Handle(_a0 context.Context, _a1 *webhook.MutationRequest) error { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for Handle") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *webhook.MutationRequest) error); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// PodInjector_Handle_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Handle' +type PodInjector_Handle_Call struct { + *mock.Call +} + +// Handle is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 *webhook.MutationRequest +func (_e *PodInjector_Expecter) Handle(_a0 interface{}, _a1 interface{}) *PodInjector_Handle_Call { + return &PodInjector_Handle_Call{Call: _e.mock.On("Handle", _a0, _a1)} +} + +func (_c *PodInjector_Handle_Call) Run(run func(_a0 context.Context, _a1 *webhook.MutationRequest)) *PodInjector_Handle_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*webhook.MutationRequest)) + }) + return _c +} + +func (_c *PodInjector_Handle_Call) Return(_a0 error) *PodInjector_Handle_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *PodInjector_Handle_Call) RunAndReturn(run func(context.Context, *webhook.MutationRequest) error) *PodInjector_Handle_Call { + _c.Call.Return(run) + return _c +} + +// NewPodInjector creates a new instance of PodInjector. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewPodInjector(t interface { + mock.TestingT + Cleanup(func()) +}) *PodInjector { + mock := &PodInjector{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/test/mocks/sigs.k8s.io/controller-runtime/pkg/manager/manager.go b/test/mocks/sigs.k8s.io/controller-runtime/pkg/manager/manager.go index 8b8ee50c94..6c3dfa9eff 100644 --- a/test/mocks/sigs.k8s.io/controller-runtime/pkg/manager/manager.go +++ b/test/mocks/sigs.k8s.io/controller-runtime/pkg/manager/manager.go @@ -231,7 +231,7 @@ func (_c *Manager_AddReadyzCheck_Call) RunAndReturn(run func(string, healthz.Che return _c } -// Elected provides a mock function with given fields: +// Elected provides a mock function with no fields func (_m *Manager) Elected() <-chan struct{} { ret := _m.Called() @@ -278,7 +278,7 @@ func (_c *Manager_Elected_Call) RunAndReturn(run func() <-chan struct{}) *Manage return _c } -// GetAPIReader provides a mock function with given fields: +// GetAPIReader provides a mock function with no fields func (_m *Manager) GetAPIReader() client.Reader { ret := _m.Called() @@ -325,7 +325,7 @@ func (_c *Manager_GetAPIReader_Call) RunAndReturn(run func() client.Reader) *Man return _c } -// GetCache provides a mock function with given fields: +// GetCache provides a mock function with no fields func (_m *Manager) GetCache() cache.Cache { ret := _m.Called() @@ -372,7 +372,7 @@ func (_c *Manager_GetCache_Call) RunAndReturn(run func() cache.Cache) *Manager_G return _c } -// GetClient provides a mock function with given fields: +// GetClient provides a mock function with no fields func (_m *Manager) GetClient() client.Client { ret := _m.Called() @@ -419,7 +419,7 @@ func (_c *Manager_GetClient_Call) RunAndReturn(run func() client.Client) *Manage return _c } -// GetConfig provides a mock function with given fields: +// GetConfig provides a mock function with no fields func (_m *Manager) GetConfig() *rest.Config { ret := _m.Called() @@ -466,7 +466,7 @@ func (_c *Manager_GetConfig_Call) RunAndReturn(run func() *rest.Config) *Manager return _c } -// GetControllerOptions provides a mock function with given fields: +// GetControllerOptions provides a mock function with no fields func (_m *Manager) GetControllerOptions() config.Controller { ret := _m.Called() @@ -559,7 +559,7 @@ func (_c *Manager_GetEventRecorderFor_Call) RunAndReturn(run func(string) record return _c } -// GetFieldIndexer provides a mock function with given fields: +// GetFieldIndexer provides a mock function with no fields func (_m *Manager) GetFieldIndexer() client.FieldIndexer { ret := _m.Called() @@ -606,7 +606,7 @@ func (_c *Manager_GetFieldIndexer_Call) RunAndReturn(run func() client.FieldInde return _c } -// GetHTTPClient provides a mock function with given fields: +// GetHTTPClient provides a mock function with no fields func (_m *Manager) GetHTTPClient() *http.Client { ret := _m.Called() @@ -653,7 +653,7 @@ func (_c *Manager_GetHTTPClient_Call) RunAndReturn(run func() *http.Client) *Man return _c } -// GetLogger provides a mock function with given fields: +// GetLogger provides a mock function with no fields func (_m *Manager) GetLogger() logr.Logger { ret := _m.Called() @@ -698,7 +698,7 @@ func (_c *Manager_GetLogger_Call) RunAndReturn(run func() logr.Logger) *Manager_ return _c } -// GetRESTMapper provides a mock function with given fields: +// GetRESTMapper provides a mock function with no fields func (_m *Manager) GetRESTMapper() meta.RESTMapper { ret := _m.Called() @@ -745,7 +745,7 @@ func (_c *Manager_GetRESTMapper_Call) RunAndReturn(run func() meta.RESTMapper) * return _c } -// GetScheme provides a mock function with given fields: +// GetScheme provides a mock function with no fields func (_m *Manager) GetScheme() *runtime.Scheme { ret := _m.Called() @@ -792,7 +792,7 @@ func (_c *Manager_GetScheme_Call) RunAndReturn(run func() *runtime.Scheme) *Mana return _c } -// GetWebhookServer provides a mock function with given fields: +// GetWebhookServer provides a mock function with no fields func (_m *Manager) GetWebhookServer() webhook.Server { ret := _m.Called() diff --git a/test/mocks/sigs.k8s.io/controller-runtime/pkg/reconcile/reconciler.go b/test/mocks/sigs.k8s.io/controller-runtime/pkg/reconcile/reconciler.go index 35e031341c..1d39ff638d 100644 --- a/test/mocks/sigs.k8s.io/controller-runtime/pkg/reconcile/reconciler.go +++ b/test/mocks/sigs.k8s.io/controller-runtime/pkg/reconcile/reconciler.go @@ -10,20 +10,20 @@ import ( ) // Reconciler is an autogenerated mock type for the Reconciler type -type Reconciler struct { +type Reconciler[request comparable] struct { mock.Mock } -type Reconciler_Expecter struct { +type Reconciler_Expecter[request comparable] struct { mock *mock.Mock } -func (_m *Reconciler) EXPECT() *Reconciler_Expecter { - return &Reconciler_Expecter{mock: &_m.Mock} +func (_m *Reconciler[request]) EXPECT() *Reconciler_Expecter[request] { + return &Reconciler_Expecter[request]{mock: &_m.Mock} } // Reconcile provides a mock function with given fields: _a0, _a1 -func (_m *Reconciler) Reconcile(_a0 context.Context, _a1 reconcile.Request) (reconcile.Result, error) { +func (_m *Reconciler[request]) Reconcile(_a0 context.Context, _a1 reconcile.Request) (reconcile.Result, error) { ret := _m.Called(_a0, _a1) if len(ret) == 0 { @@ -51,41 +51,41 @@ func (_m *Reconciler) Reconcile(_a0 context.Context, _a1 reconcile.Request) (rec } // Reconciler_Reconcile_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Reconcile' -type Reconciler_Reconcile_Call struct { +type Reconciler_Reconcile_Call[request comparable] struct { *mock.Call } // Reconcile is a helper method to define mock.On call // - _a0 context.Context // - _a1 reconcile.Request -func (_e *Reconciler_Expecter) Reconcile(_a0 interface{}, _a1 interface{}) *Reconciler_Reconcile_Call { - return &Reconciler_Reconcile_Call{Call: _e.mock.On("Reconcile", _a0, _a1)} +func (_e *Reconciler_Expecter[request]) Reconcile(_a0 interface{}, _a1 interface{}) *Reconciler_Reconcile_Call[request] { + return &Reconciler_Reconcile_Call[request]{Call: _e.mock.On("Reconcile", _a0, _a1)} } -func (_c *Reconciler_Reconcile_Call) Run(run func(_a0 context.Context, _a1 reconcile.Request)) *Reconciler_Reconcile_Call { +func (_c *Reconciler_Reconcile_Call[request]) Run(run func(_a0 context.Context, _a1 reconcile.Request)) *Reconciler_Reconcile_Call[request] { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(reconcile.Request)) }) return _c } -func (_c *Reconciler_Reconcile_Call) Return(_a0 reconcile.Result, _a1 error) *Reconciler_Reconcile_Call { +func (_c *Reconciler_Reconcile_Call[request]) Return(_a0 reconcile.Result, _a1 error) *Reconciler_Reconcile_Call[request] { _c.Call.Return(_a0, _a1) return _c } -func (_c *Reconciler_Reconcile_Call) RunAndReturn(run func(context.Context, reconcile.Request) (reconcile.Result, error)) *Reconciler_Reconcile_Call { +func (_c *Reconciler_Reconcile_Call[request]) RunAndReturn(run func(context.Context, reconcile.Request) (reconcile.Result, error)) *Reconciler_Reconcile_Call[request] { _c.Call.Return(run) return _c } // NewReconciler creates a new instance of Reconciler. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. -func NewReconciler(t interface { +func NewReconciler[request comparable](t interface { mock.TestingT Cleanup(func()) -}) *Reconciler { - mock := &Reconciler{} +}) *Reconciler[request] { + mock := &Reconciler[request]{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) diff --git a/test/scenarios/filter.go b/test/scenarios/filter.go index e1955a49e1..2e342a63aa 100644 --- a/test/scenarios/filter.go +++ b/test/scenarios/filter.go @@ -1,3 +1,5 @@ +//go:build e2e + package scenarios import ( diff --git a/test/scenarios/istio/istio_test.go b/test/scenarios/istio/istio_test.go index 26b697266e..c6f93279f6 100644 --- a/test/scenarios/istio/istio_test.go +++ b/test/scenarios/istio/istio_test.go @@ -51,11 +51,12 @@ func TestIstio(t *testing.T) { feats := []features.Feature{ networkProblems.ResilienceFeature(t), // TODO: Fix so order do not matter, because its the first feature here for a reason => we don’t want to have any downloaded codemodules in the filesystem of the CSI-driver, and we can't clean the filesystem between features as the operator is not reinstalled and therefore the csi-driver is running, and you would have to mess with the database because removing it just bricks things. activegate.Feature(t, proxy.ProxySpec), - cloudnativeDefault.Feature(t, true), + cloudnativeDefault.Feature(t, true, true), codemodules.WithProxy(t, proxy.ProxySpec), - codemodules.WithProxyCA(t, proxy.HttpsProxySpec), codemodules.WithProxyAndAGCert(t, proxy.ProxySpec), + codemodules.WithProxyAndAutomaticAGCert(t, proxy.ProxySpec), codemodules.WithProxyCAAndAGCert(t, proxy.HttpsProxySpec), + codemodules.WithProxyCAAndAutomaticAGCert(t, proxy.HttpsProxySpec), } testEnv.Test(t, scenarios.FilterFeatures(*cfg, feats)...) diff --git a/test/scenarios/no_csi/no_csi_test.go b/test/scenarios/no_csi/no_csi_test.go new file mode 100644 index 0000000000..59225c84c1 --- /dev/null +++ b/test/scenarios/no_csi/no_csi_test.go @@ -0,0 +1,68 @@ +//go:build e2e + +package no_csi + +import ( + "testing" + + "github.com/Dynatrace/dynatrace-operator/test/features/activegate" + "github.com/Dynatrace/dynatrace-operator/test/features/applicationmonitoring" + "github.com/Dynatrace/dynatrace-operator/test/features/bootstrapper" + "github.com/Dynatrace/dynatrace-operator/test/features/classic" + cloudnativeDefault "github.com/Dynatrace/dynatrace-operator/test/features/cloudnative/default" + "github.com/Dynatrace/dynatrace-operator/test/features/edgeconnect" + "github.com/Dynatrace/dynatrace-operator/test/features/extensions" + "github.com/Dynatrace/dynatrace-operator/test/features/hostmonitoring" + "github.com/Dynatrace/dynatrace-operator/test/features/logmonitoring" + "github.com/Dynatrace/dynatrace-operator/test/features/telemetryingest" + "github.com/Dynatrace/dynatrace-operator/test/helpers" + "github.com/Dynatrace/dynatrace-operator/test/helpers/components/operator" + "github.com/Dynatrace/dynatrace-operator/test/helpers/kubeobjects/environment" + "github.com/Dynatrace/dynatrace-operator/test/scenarios" + "sigs.k8s.io/e2e-framework/pkg/env" + "sigs.k8s.io/e2e-framework/pkg/envconf" + "sigs.k8s.io/e2e-framework/pkg/features" +) + +var ( + testEnv env.Environment + cfg *envconf.Config +) + +func TestMain(m *testing.M) { + cfg = environment.GetStandardKubeClusterEnvConfig() + testEnv = env.NewWithConfig(cfg) + testEnv.Setup( + helpers.SetScheme, + operator.InstallViaMake(false), + ) + // If we cleaned up during a fail-fast (aka.: /debug) it wouldn't be possible to investigate the error. + if !cfg.FailFast() { + testEnv.Finish(operator.UninstallViaMake(false)) + } + testEnv.Run(m) +} + +func TestNoCSI(t *testing.T) { + feats := []features.Feature{ + activegate.Feature(t, nil), + applicationmonitoring.MetadataEnrichment(t), + applicationmonitoring.LabelVersionDetection(t), + applicationmonitoring.WithoutCSI(t), + extensions.Feature(t), + edgeconnect.NormalModeFeature(t), + edgeconnect.ProvisionerModeFeature(t), + edgeconnect.AutomationModeFeature(t), + classic.Feature(t), + bootstrapper.NoCSI(t), + logmonitoring.Feature(t), + hostmonitoring.WithoutCSI(t), + cloudnativeDefault.Feature(t, false, false), + telemetryingest.WithLocalActiveGateAndCleanup(t), + telemetryingest.WithPublicActiveGate(t), + telemetryingest.WithTelemetryIngestEndpointTLS(t), + telemetryingest.OtelCollectorConfigUpdate(t), + } + + testEnv.Test(t, scenarios.FilterFeatures(*cfg, feats)...) +} diff --git a/test/scenarios/release/release_test.go b/test/scenarios/release/release_test.go index c6d29aaf98..bc89973141 100644 --- a/test/scenarios/release/release_test.go +++ b/test/scenarios/release/release_test.go @@ -20,7 +20,7 @@ var ( cfg *envconf.Config ) -const releaseTag = "1.2.2" +const releaseTag = "1.4.1" func TestMain(m *testing.M) { cfg = environment.GetStandardKubeClusterEnvConfig() diff --git a/test/scenarios/standard/standard_test.go b/test/scenarios/standard/standard_test.go index 9f4dc37135..eac133d398 100644 --- a/test/scenarios/standard/standard_test.go +++ b/test/scenarios/standard/standard_test.go @@ -5,16 +5,13 @@ package standard import ( "testing" - "github.com/Dynatrace/dynatrace-operator/test/features/activegate" "github.com/Dynatrace/dynatrace-operator/test/features/applicationmonitoring" - "github.com/Dynatrace/dynatrace-operator/test/features/classic" + "github.com/Dynatrace/dynatrace-operator/test/features/bootstrapper" classicToCloud "github.com/Dynatrace/dynatrace-operator/test/features/classic/switch_modes" "github.com/Dynatrace/dynatrace-operator/test/features/cloudnative/codemodules" cloudnativeDefault "github.com/Dynatrace/dynatrace-operator/test/features/cloudnative/default" disabledAutoInjection "github.com/Dynatrace/dynatrace-operator/test/features/cloudnative/disabled_auto_injection" cloudToClassic "github.com/Dynatrace/dynatrace-operator/test/features/cloudnative/switch_modes" - "github.com/Dynatrace/dynatrace-operator/test/features/edgeconnect" - "github.com/Dynatrace/dynatrace-operator/test/features/extensions" "github.com/Dynatrace/dynatrace-operator/test/features/publicregistry" supportArchive "github.com/Dynatrace/dynatrace-operator/test/features/support_archive" "github.com/Dynatrace/dynatrace-operator/test/helpers" @@ -47,23 +44,15 @@ func TestMain(m *testing.M) { func TestStandard(t *testing.T) { feats := []features.Feature{ - activegate.Feature(t, nil), - cloudnativeDefault.Feature(t, false), - applicationmonitoring.MetadataEnrichment(t), - applicationmonitoring.LabelVersionDetection(t), + cloudnativeDefault.Feature(t, false, true), applicationmonitoring.ReadOnlyCSIVolume(t), - applicationmonitoring.WithoutCSI(t), codemodules.InstallFromImage(t), publicregistry.Feature(t), - extensions.Feature(t), disabledAutoInjection.Feature(t), supportArchive.Feature(t), - edgeconnect.NormalModeFeature(t), - edgeconnect.ProvisionerModeFeature(t), - edgeconnect.AutomationModeFeature(t), - classic.Feature(t), classicToCloud.Feature(t), cloudToClassic.Feature(t), + bootstrapper.InstallWithCSI(t), } testEnv.Test(t, scenarios.FilterFeatures(*cfg, feats)...) diff --git a/test/testdata/custom-cas/README.md b/test/testdata/custom-cas/README.md new file mode 100644 index 0000000000..ce79b56553 --- /dev/null +++ b/test/testdata/custom-cas/README.md @@ -0,0 +1,41 @@ +## Create Squid proxy certificate + +> openssl req -nodes -x509 -newkey rsa:4096 -keyout spkey.pem -out custom.pem -days 365 -subj "/CN=squid.proxy" -addext="subjectAltName=DNS:squid.proxy,DNS:squid.proxy.svc,DNS:squid.proxy.svc.cluster.local" -addext="basicConstraints=CA:TRUE" +> +> cat custom.pem spkey.pem | base64 -w0 + +(!) Copy encoded files to proxy-ssl.yaml:squid-ca-cert.pem field. + +## Create ActiveGate TLS certificate + +create a private key +> openssl genrsa -out agkey.pem 2048 + +create a certificate signing request +> openssl req -key agkey.pem -new -out ag.csr -subj '/CN=dynakube-activegate.dynatrace' + +create a self-signed root CA +> openssl req -x509 -nodes -sha256 -days 1825 -newkey rsa:2048 -keyout root.pem -out root.crt -subj '/CN=dynakube-activegate.issuer' + +sign certificate signing request with root CA +> openssl x509 -req -CA root.crt -CAkey root.pem -in ag.csr -out agcrt.pem -days 365 -CAcreateserial -extfile ag.ext + +convert to p12 +> openssl pkcs12 -export -out agcrtkey.p12 -inkey agkey.pem -in agcrt.pem -certfile root.crt + +append root certificate to agcrt.pem +> cat root.crt >> agcrt.pem + +(!) Use empty password. + +## Print the certificate in text form + +> openssl x509 -text -noout -in agcrt.pem +> +> openssl pkcs12 -info -in agcrtkey.p12 -nodes + +## Create telemetry ingest TLS certificate + +> openssl genpkey -algorithm RSA -out tls-telemetry-ingest.key -pkeyopt rsa_keygen_bits:2048 +> openssl req -new -key tls-telemetry-ingest.key -out tls-telemetry-ingest.csr +> openssl x509 -req -in tls-telemetry-ingest.csr -signkey tls-telemetry-ingest.key -out tls-telemetry-ingest.crt -days 36500 -subj '/CN=dynakube-telemetry-ingest.dynatrace' diff --git a/test/testdata/custom-cas/ag.ext b/test/testdata/custom-cas/ag.ext new file mode 100644 index 0000000000..36a87ec5c7 --- /dev/null +++ b/test/testdata/custom-cas/ag.ext @@ -0,0 +1,7 @@ +authorityKeyIdentifier=keyid,issuer +basicConstraints=CA:FALSE +subjectAltName = @alt_names +[alt_names] +DNS.1 = dynakube-activegate.dynatrace +DNS.2 = dynakube-activegate.dynatrace.svc +DNS.3 = dynakube-activegate.dynatrace.svc.cluster.local diff --git a/test/testdata/custom-cas/agcrt.pem b/test/testdata/custom-cas/agcrt.pem index 2e5bf5676b..66bc0c2b0b 100644 --- a/test/testdata/custom-cas/agcrt.pem +++ b/test/testdata/custom-cas/agcrt.pem @@ -1,41 +1,41 @@ -----BEGIN CERTIFICATE----- -MIIDqDCCApCgAwIBAgIUTrMdIL1V6mZCfDVzJB36N9Kb/vowDQYJKoZIhvcNAQEL -BQAwJTEjMCEGA1UEAwwaZHluYWt1YmUtYWN0aXZlZ2F0ZS5pc3N1ZXIwHhcNMjQw -MjAzMTYxODM2WhcNMjUwMjAyMTYxODM2WjAoMSYwJAYDVQQDDB1keW5ha3ViZS1h +MIIDqDCCApCgAwIBAgIUXLu0L1BM5PpWjDDFmeNFK+3cEV8wDQYJKoZIhvcNAQEL +BQAwJTEjMCEGA1UEAwwaZHluYWt1YmUtYWN0aXZlZ2F0ZS5pc3N1ZXIwHhcNMjUw +MjAzMDk1OTU4WhcNMjYwMjAzMDk1OTU4WjAoMSYwJAYDVQQDDB1keW5ha3ViZS1h Y3RpdmVnYXRlLmR5bmF0cmFjZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC -ggEBAKiO7ZPq2axVnyXwqerhv3WsRt+GVd4mttvjFuJXoD7b/KuuXypQJbPLnEW0 -Gob0YOzmLyvppfraT+vynHhJjzniEXj6dVuPeUEgoU/8nbRG09lh+E1/IPfgANNx -Sj0emK++3vIE65bFULQ89FFJPfvSAeqjtfl0FczQMzc2COp3iQL+7ZO7Ug/wfWP7 -raVVDsra926IjSlEm7mFN0cKeKjZA91DEJID0Ge50YZ6ZCm1BZGa76mqCmHFmsdY -TpfbMDoWCInc7bY/M6nS3tNQC/cH0QhDoOLAzHj5hO5+Vy3hOXDVstI7JfwCVwyo -29DOzHCGqigYnxv3jHWehk8tPNcCAwEAAaOBzDCByTAfBgNVHSMEGDAWgBSqrFPU -fx0iICfVB9dzmKX/CHqvqzAJBgNVHRMEAjAAMHwGA1UdEQR1MHOCHWR5bmFrdWJl +ggEBAL7HE20SHjkWIiYpNt0QC0v3YS31skD/z1eAWlQZJazIhQOSkcmU5RJShzkJ +oYZPc1zt0WQIUAd1EBFuM7Tc55YBIC9a/PySpvtALMvF11GMt6bRSagUrw5dvJ+T +odUoxhWgveIzRG0f7sPjny0/McFOcRw5cNDr518tZKDm9X0/2yV7Y0RLXKNIRwEv +6pQQl9XXAQ16NxmOAvArdm2LLvLfwrUPNG/XjtOiICmdesWHg6hTMYKXhSkhQYZC +mE9nvMi7fJMWeZFWIZnHSSFThAy5O73yZZ6S0Il+JnvpSf74a8Qh90Rab3nQL6Yj +4aHwWJCVmvP6hEOjJp3UkK75MrUCAwEAAaOBzDCByTAfBgNVHSMEGDAWgBSVn1nI +X+9ryJ9jZwCslweq6OPLZDAJBgNVHRMEAjAAMHwGA1UdEQR1MHOCHWR5bmFrdWJl LWFjdGl2ZWdhdGUuZHluYXRyYWNlgiFkeW5ha3ViZS1hY3RpdmVnYXRlLmR5bmF0 cmFjZS5zdmOCL2R5bmFrdWJlLWFjdGl2ZWdhdGUuZHluYXRyYWNlLnN2Yy5jbHVz -dGVyLmxvY2FsMB0GA1UdDgQWBBSVTtZcoy6Xk65m5qqTSCfK2isTcTANBgkqhkiG -9w0BAQsFAAOCAQEAduSHuFzyxPc2iptS3Zoq82SafgBj/rxdM2CgPdn6nKdtKbot -f0+laaMIoJ0wVxz3jClqMiCPK/l4AFV79bhQwaVRxR8Jp8ebDfKs7WJfxrcoLXvf -TgjyLmBGx/3ucAGh3CY7gelPUXLyzV6R4siWWzu/6Ln8fBwJDwqMDQShZHIkSXXz -xr366z5taXBvIjnwgBW0QXTm09b76PRTMbkM1WYIO54EAjW00xhclzVnn0xwseG2 -fLD9cFcleOovFczlqgVIf2HBe8E+g8WICSW8aeC3CgvpuUtCQZQ1Ha6kEWHc1jop -nWWvK79XYJjoxHnFchyNkcHHRDeujw9rN9P4fg== +dGVyLmxvY2FsMB0GA1UdDgQWBBQWFNPIq2M4ysrDoNXKl/NIPNJR+DANBgkqhkiG +9w0BAQsFAAOCAQEABYm45MXQ20TVWha4dfQxfZ5WCo4IDWNjR/ND/eaRUA704TG/ +u1pV2jflclIvfDw9B55u0bH4b5ZCx57oMPOQpvXgLI2qUONpfLqdmg9hTiGd+P7U +wtm+hOfmCg1YSpVLOdmXF9sglRIUJO9jX7CiFZ1MvFxCYxXkHccBswoSgSTBzhrY +tLAyEpwgh2YwLVSsq+iBXv6tAI3po9Hpr1d266nMQ9J0ry2aiKR8Id0FKhV9LmZl +bCKYedOUqpPgOQ/j3eN+SrH+tI2a6RLbkTrOU934dlyd3AMrr2hGUerVRt8ScC5u +/ryXiTY10wzIkcmjNLvH5q/0TLiyU5SlHzKAEw== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -MIIDKzCCAhOgAwIBAgIUWQkPicmb4jXD6HdHqXh3GKFXF28wDQYJKoZIhvcNAQEL -BQAwJTEjMCEGA1UEAwwaZHluYWt1YmUtYWN0aXZlZ2F0ZS5pc3N1ZXIwHhcNMjQw -MjAzMTYxODM2WhcNMjkwMjAxMTYxODM2WjAlMSMwIQYDVQQDDBpkeW5ha3ViZS1h +MIIDKzCCAhOgAwIBAgIUejLjZYxit5P49BBArjUQ8LUGr3IwDQYJKoZIhvcNAQEL +BQAwJTEjMCEGA1UEAwwaZHluYWt1YmUtYWN0aXZlZ2F0ZS5pc3N1ZXIwHhcNMjUw +MjAzMTEzNjUxWhcNMzAwMjAyMTEzNjUxWjAlMSMwIQYDVQQDDBpkeW5ha3ViZS1h Y3RpdmVnYXRlLmlzc3VlcjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB -AMzNzK3Xpqa3vjRt/kE02gH3o3sWAuGaGwQQDSqhBWTk4w36WQJzzAA/Vi9QiUxU -wFSJ6X4+ctTwcSozBhHlsW796q40X81teDgZyQl0C+cl1x0rO6HrLkBJYQC5A8cy -kj84eKl3wZCk3GKuOH1zvD3Io4dI6SozeSS5cQb9qMIWqomzvRdJDtyUOgT+RdNQ -FL/SKR7/V83T9n0qCGlu0GiZUvoSBG/NI2gFzRppJIiOyDkikRUXutzXTmhHxtD9 -9zDEkgAjpLMtZ5nfNbaDcvo99XQ0Iq85f/3OMDQC1tc4p4spf3s506MrM7o7pPfZ -vgQaiVF9kKO3AQH3uVCUx/sCAwEAAaNTMFEwHQYDVR0OBBYEFKqsU9R/HSIgJ9UH -13OYpf8Ieq+rMB8GA1UdIwQYMBaAFKqsU9R/HSIgJ9UH13OYpf8Ieq+rMA8GA1Ud -EwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAL3drdFOkEBvDANYKs0Vg4mt -2jnEJghSQppY/j7iQWVe5lbVfbvnMOQjio1aeIG2xMu+ZYfEJgrX7x1/h3tni4tI -cMA0FZg37LavIJTd0b09+fT/O6SVvJFCFnAotezVS7z/aA54r3pxkjt1JEsADzzb -Cq42IdXkNLiPgqxpAHjI6m83UoS6g6p5+V/Z4sadR6QeIOBxLjO+cEhNz349tm7d -LDYOO+Ti+Z/yb6d7wyVYNFCLFzHkQPXYvKEwAD0mUOWS9doKQkKgVPj3yK5bBjDW -UNPmUJ412aSo1aRsEJt6zlvHaHMtfhSieG/PwF+1Z82D8dRLamPLGioan8qp9jM= +ANe6fMAThI6gccyf/rT6YonH92kGGrkzBK46Gnq1dG6a9Khn3iCCQuTvHzuI+l0x +vE6nNeEo6gjPjt/7t66rDEmdGwJNvwbF7YxElt/S2X/eTPMMIjdTkfUQTAXbVFWI +6Uy1q3g2thuIUohc+IoIJmyFP4hnnPAhN9KwzCwFTxnaqzoVf0gbwrQ6P19M7cpH +UvH9O/QVbcFBiz88y4eDD+jeXfSYRpP41o3cds1OkueBOWnvPhw91WsVw7BxhlDG +17lBKgEbqJ1/4vBKZklkFU0o9WGDccbyGBw1t2nf7dXkKHcgIjQmk4/sebjhsLZr +i+BVGSKW4GI+t3VvEME2SQMCAwEAAaNTMFEwHQYDVR0OBBYEFP83X0iOEm6+6Az4 +3HSnUG5IerqjMB8GA1UdIwQYMBaAFP83X0iOEm6+6Az43HSnUG5IerqjMA8GA1Ud +EwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAC1TNWD/GLdSuspDrwOn7seC +TQFBRQa6eU8B43ry5+v6YU78lD/CJ5Gm5m5X+u10rEJ4deFCVPm97QEnY6w4QWZR +NKo8RGza8nN5R2Slip6LpFo62UmfeMyzRzYqNkR3wwlHE0u3aggaTv//VdI3bFtR +XOL5McGoybsxKHK1Ts22OZRZO6DefvFbU2gPN/Kq1ZRnz7dRqGUEeyCdLQv7+Jo4 +ZZaVuTvVI5OInPl5nUp43auU6DQeFuYFrB20/G5vx/ycsrnbsClm2y081W7FrSRh +dljXpdkiJc16liykeFDU/YI5WyNwD7k4jq1n+mZgs/2rgOurjS6oA4eBc+ELn7s= -----END CERTIFICATE----- diff --git a/test/testdata/custom-cas/agcrtkey.p12 b/test/testdata/custom-cas/agcrtkey.p12 index 01f80be221..d5b6227920 100644 Binary files a/test/testdata/custom-cas/agcrtkey.p12 and b/test/testdata/custom-cas/agcrtkey.p12 differ diff --git a/test/testdata/custom-cas/custom.pem b/test/testdata/custom-cas/custom.pem index 4af7c55672..599a335e32 100644 --- a/test/testdata/custom-cas/custom.pem +++ b/test/testdata/custom-cas/custom.pem @@ -1,20 +1,31 @@ -----BEGIN CERTIFICATE----- -MIIDWDCCAkCgAwIBAgIUVYmiolB97PjZq5Mp/nRbSnzoqoAwDQYJKoZIhvcNAQEL -BQAwHTEbMBkGA1UEAwwSc3F1aWQucHJveHkuaXNzdWVyMB4XDTI0MDIwMjEzMjQ0 -M1oXDTI1MDIwMTEzMjQ0M1owFjEUMBIGA1UEAwwLc3F1aWQucHJveHkwggEiMA0G -CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9uDub9pA47dVq61IAmvOIlOx2Dcrt -oY9xaf2zw1HBQORIz2ONABiWvq0WK082ZD6sDdwol5YVR8Y6vRhvBiJPMV3WBH8w -+e8SUazrN/aX4uttfx3cE6DGVvuNDzooFuxMr8aitbtEvgG+3fG6bLma4Xb2Xp8q -CcRxww3aOKU84mRsRxQmYl4kBGHRnmnvyBQ8uzM9p31TisUYwH9IRbXU1zXfGZpR -a8XGo61qRo/cUS+KpBEEaLd5CsFEaTmppnVPlmLJj47Ow7enAeucwj4cMOzP9GnP -795EUNOtWYzcuxa3FaLN6uGMeCkI8VXZCsfIZnTIILwmtjehmGy5ixLRAgMBAAGj -gZYwgZMwHwYDVR0jBBgwFoAU7jJjxqWq485VU405vJMQL96JtUEwCQYDVR0TBAIw -ADBGBgNVHREEPzA9ggtzcXVpZC5wcm94eYIPc3F1aWQucHJveHkuc3Zjgh1zcXVp -ZC5wcm94eS5zdmMuY2x1c3Rlci5sb2NhbDAdBgNVHQ4EFgQUGMSDkrUtztRk54bs -84vZJsgyhAkwDQYJKoZIhvcNAQELBQADggEBAFH/Xd50F7kde0zivHxq0Y4MlEr5 -+8TnB+icfTSDYl68lv4s3hPcmFOHSvQbPLk9fbK+Z0WXGb43t4WnzwdLoFO+eumD -NhNcYt/iOpCECoYckjBmtwo4PXLS56UCmNBNWmKAcLSxwpui6HcIPKEecJ5+2Wak -BVMhE0ooUgW0UEIlxf2P1oO4RqsktdTvfJpcrE2eMxCDqxr3MEETLrIuvYPke6aV -mUu0vFhvq+i1AIbBYUKHbI/Y5zKfiWfAqTzpJmmwwAgQesg68xu63GGDvvDfaZim -qFCo5Vr/1iNtCbkGBqQ35c1Ba6Idh68JeHTOF/O4RCaMKsWTJFck2cwnv9s= +MIIFVDCCAzygAwIBAgIUPiRJza3KU2pXzumVpvyHi67nItkwDQYJKoZIhvcNAQEL +BQAwFjEUMBIGA1UEAwwLc3F1aWQucHJveHkwHhcNMjUwMjAzMTIxMjQyWhcNMjYw +MjAzMTIxMjQyWjAWMRQwEgYDVQQDDAtzcXVpZC5wcm94eTCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAMFOnjWspDFGJiszWH8wKneVQPNqLoXK0ewm/Ahy +YLDAW4jiRBeI9J2e6v8JdvZzwiHGYbtJQnYzCdPah7qdeLI3RJ6jLA1iBYjuajI6 +Dodgf/ihT9z0vwS4UpJ8FNB98QDugngw2dnTzNbCF1HxxxXhWmTHRKPv79qWGg2Q +MqK7w3mtKPN6ha5DZhNV6ejdcmJZU4L9mOmCJIZEOkU/KKM3hnWQTwT0IzqxppOg +chCXXtsOZWP1JSWgGebCye2yNQQIksBuHYnjZQc9g/919sowCGZbEZc19kgvaa+I +jNNAAh5PH+UuSzjupd+HJGZtz39U/pvQmz/ImuntwGZEEMLCY1Lw+H3O2W7bKAac +hM4QDah5J5En77+lFk+ce0dnctQ76iL1ww0TDiLS5C7oZ5lGfgidXgIYbpsIVhUH +ee8Ei0Zc6K3smMM4dXsqJtwWERhs0G2u/CvJ1r4gxeIAf5+bihXPGI9lMtPRHa/1 +31Sn6UTyZaoizjSYyYmIuIfYMVC5X+DtFUb3wG+/UyStqDqFXPLDS3yHyhj42z8O +SAx82vZDPO+CIVDzsqhrxg2G9s5gBFzv/xSacElZEwzELa0fcrAra8BLjkFVgdhM +NsNZ4CYOhF2iuDC9aQmsWKB0jNBujdMm/EPJKUDLKg0+J4rDa85YND96NaKLZKU2 +c6NdAgMBAAGjgZkwgZYwHQYDVR0OBBYEFAf8Nh6D5ZGPPhVCf8PX8S4p+GpOMB8G +A1UdIwQYMBaAFAf8Nh6D5ZGPPhVCf8PX8S4p+GpOMEYGA1UdEQQ/MD2CC3NxdWlk +LnByb3h5gg9zcXVpZC5wcm94eS5zdmOCHXNxdWlkLnByb3h5LnN2Yy5jbHVzdGVy +LmxvY2FsMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBACsa8L4boFIm +LZTztUZdQAHZnuJiMHHs2vbvbK3nziaFd9ygf0f/x/xJ4b9Ae5mzAsC+tN65PwRt +L9nkYr8KmWHUhDjcB0ya74YbNYjlbbOfPkDtH21HGuORDTBe0Dw9acMmXy0ardYu +cm4/x0I1iAFcOl1OX61U9lb75G2Fj+WObOzWVXBDCJVDkTZ2DN+m4fS/RIEhSrmk +cCMaIr60sKMOiPXgAIpQGVXEV142Rm8bducoM2xo5ffU+PZ7ZUnO+up35wGSCDpq +boGpJTILqA68XhsPaoHF6OF/pDRO9oxQ5aYHQl7RaxNizsf0TqhUbYxg5/XURvSj +gH17Oc0FQihoqSSmEVeUq9iE18uHLCe27eUcl/Uj+oOBX4qvHysdJdFIcPceNrYq +EdSFFzNirJkT9htnzbXGexWuBBAoiZyFiBjSI/hygkL9C43aHR+ezuOedyUFoW+1 +xh/4gWaEe01Ws5Un8XqGexNTukPr9+2RtGtvwXm9fUlhYhJKHEsq7Yh1TzSeylLK +iHE3gabqy9HHwK2iqhR8345F8irFC+7ARHTq55UIvWNojzaW5nwWJDxay+bmP+6r +EjtP36UUhH8ExAR4d59FAeTi6gH/9NFNf1DWHtd4Cndixs9VEKkQZ1Lx1zbIHFNh +iJVRSZs2HZVjPAxGuydRHp9J9b7Jka6C -----END CERTIFICATE----- diff --git a/test/testdata/custom-cas/tls-telemetry-ingest.crt b/test/testdata/custom-cas/tls-telemetry-ingest.crt new file mode 100644 index 0000000000..0bc1337f48 --- /dev/null +++ b/test/testdata/custom-cas/tls-telemetry-ingest.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDfzCCAmcCFCMpB+z5EszqgCcYvc7IZdKnhy+MMA0GCSqGSIb3DQEBCwUAMHsx +CzAJBgNVBAYTAkFUMRUwEwYDVQQIDAxFMkVUZXN0U3RhdGUxFDASBgNVBAcMC0Uy +RVRlc3RDaXR5MRMwEQYDVQQKDApGb29iYXIgTHRkMRcwFQYDVQQLDA5UZXN0RGVw +YXJ0bWVudDERMA8GA1UEAwwISm9obiBEb2UwIBcNMjUwMzI2MDc1OTA2WhgPMjEy +NTAzMDIwNzU5MDZaMHsxCzAJBgNVBAYTAkFUMRUwEwYDVQQIDAxFMkVUZXN0U3Rh +dGUxFDASBgNVBAcMC0UyRVRlc3RDaXR5MRMwEQYDVQQKDApGb29iYXIgTHRkMRcw +FQYDVQQLDA5UZXN0RGVwYXJ0bWVudDERMA8GA1UEAwwISm9obiBEb2UwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCevAPX5+OrGEL2xdtw5hcMbrN8gaX4 +TOlTFz08Ak+yFQaLbpVgWUsU/Bgn+/75E/I4a2wwFT8K3ooGQAzd/JrIrmILt+9g +vPUsWF7BMbctzDgAi8nlGj9Xu/EZgzrMD6L0p4zbLPQm/fk8VWkokfnahbGN1aLf +Q2O+WCgO15S/2N5OcFcVG40Yi2q1w75R78ytp/OSa62BkKXA2UHvS7NJWdwbRahI +erEEGoozlt85gnhS5+iLeq3K/jn2evrBCWKTPPiOwrg1Q2Mof7q6YsY4dfJKbfw2 +cYj/UH6Yhd7FqjT7Brfyb6VjKFDfWnRVYd/EVEUqWi2VYt2fzV8BLjZvAgMBAAEw +DQYJKoZIhvcNAQELBQADggEBAI92qc9QDbHuWsqSTqjHd1KcZENUE4mrZ6Uc3lB6 +DU9nCozSNDjYIXFT4xusQRjY1sJmT0eYcZWv6VJxWm7tSlMtgfDq/qmV/p47B9NA +FamwxnwAEgdBFfOe5WEgvHLfeNYabsd75cWNor+N3D7J9RVWMGXCg/yuEyCP/PCG +yUX6/0RDq7ICT+Gwl09gDvXSxKAmojBxfw+z5qH4kx0dCteM9xKCFakV/zWfZvZs +Wt2LekBue/532ZH1cvwkcZRQPQ+wSepZsDdWRFlmJwdyzMEuc85gN7TuxTLk788l +ndmghop06v7mUtZj8VKTfCU1UGhed7P39Ihq93g4uZMGl84= +-----END CERTIFICATE----- diff --git a/test/testdata/custom-cas/tls-telemetry-ingest.key b/test/testdata/custom-cas/tls-telemetry-ingest.key new file mode 100644 index 0000000000..b1129e5634 --- /dev/null +++ b/test/testdata/custom-cas/tls-telemetry-ingest.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCevAPX5+OrGEL2 +xdtw5hcMbrN8gaX4TOlTFz08Ak+yFQaLbpVgWUsU/Bgn+/75E/I4a2wwFT8K3ooG +QAzd/JrIrmILt+9gvPUsWF7BMbctzDgAi8nlGj9Xu/EZgzrMD6L0p4zbLPQm/fk8 +VWkokfnahbGN1aLfQ2O+WCgO15S/2N5OcFcVG40Yi2q1w75R78ytp/OSa62BkKXA +2UHvS7NJWdwbRahIerEEGoozlt85gnhS5+iLeq3K/jn2evrBCWKTPPiOwrg1Q2Mo +f7q6YsY4dfJKbfw2cYj/UH6Yhd7FqjT7Brfyb6VjKFDfWnRVYd/EVEUqWi2VYt2f +zV8BLjZvAgMBAAECggEATOe61Pg8jcCLcRQbZW+U6ykXPNNzngFlaiRwPorAIf+d +1CAXrz6T6e7ZpUWqGvNW/47MM9+XU6TOKokNst+X/nK+ff73s17ZSkrmXaPApCe1 +Wk5f2ugEmHUuMrYp3oQU54Pl8qqs/9c80cZv1IAlFYKAq7890f3MbYQ4pDg+kVO1 +oGrEf6/h8kDRln/DyJ3j90KzsYnmoUFhp3Pq2LGPo7RsGOTmLnmgq2xfRmWW0JES +qZ35HJzmw+iCWNsZIKIT9cie7uHoLfiUyY5iN1AnW4cZQr3UWsbE3mxTxm+dExQj +091xNfN3WATw6wX5CrksbalP7QLM9ofkmgp4HJRDwQKBgQDSKc47M9ZcIDJT8LOe +SGkzv7Cs3J4w61w8JiyV/mbhs/dRtB/xxhVT3Kw3fFV5vw2Kf4zXBLXbcn6bBhlJ ++HKBK+cyqwwdlK/X2BJumeeJIgD+iQHHFueZrAD2iVXcPC9uF5IuhIhbkvjzGaP/ +QSR/oy9/9F81yvA+Hpyd+jj0qwKBgQDBWr7WEkE+Xm3TsgZNgazwF4O7rcJ0sTrn +TscZGiNhgTpsXhv7HCpJ9POO3zTG41We2ye0XiZh+vrD4uv2WU0evoB/lrGUoTcX ++J324Gn2n8oeLn/SoINsWUys/td83yAp1M2zAR51IQiHog2g3m3SXj5Eawqwov/J +vImuln/dTQKBgGOtNkX3+QQqtRQAxoAc4eCMWxQxcsnK5y0UAfOF/G+x9mwG3VvD +Uhw7Pmb2jme2yIpWoorcjhAtxoRqKRZfQpenJflvDMj+20OpFFzmm9z7hrMycehm +IaRN4wsK6fERjoFNpqRvcWjVVUOfdpu63r+2uvGaCoot3THpPOjkY50PAoGANb4r +XQUl7VgB3t2UsuZgUzu1+eyKKDU60iArZubE/s0UmBpwXJOvjW7wY73WxZFasxTn +LFMfCAzitp/URlz7+peoz83q/gzxa1BHV994lHxFia4TCVBkNzF7BnqvGp5KKlZj +9mVROe08mbaJYzVwAREA7bNy/TXRMxmci1J5p+ECgYAE8frZjCGisRDb5wZ9Vzni +f8travqyk5VlHLEXqylSfoe4CzV88n2tvF3pypg/li8xG3CICznpw0VSCTR/jR5h +iVh4bPv3gC6KcktKSAEwWblyMa0Egzl2x3ZYFV81Hy2Xi4oZg8JENShPHpKhuGET +9VVS9UFtedFjWJLfxZYFLw== +-----END PRIVATE KEY----- diff --git a/test/testdata/network/proxy-ssl.yaml b/test/testdata/network/proxy-ssl.yaml index 820ae79d25..1eada72f37 100644 --- a/test/testdata/network/proxy-ssl.yaml +++ b/test/testdata/network/proxy-ssl.yaml @@ -20,7 +20,7 @@ metadata: namespace: proxy data: squid-ca-cert.pem: | - LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURXRENDQWtDZ0F3SUJBZ0lVVlltaW9sQjk3UGpacTVNcC9uUmJTbnpvcW9Bd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0hURWJNQmtHQTFVRUF3d1NjM0YxYVdRdWNISnZlSGt1YVhOemRXVnlNQjRYRFRJME1ESXdNakV6TWpRMApNMW9YRFRJMU1ESXdNVEV6TWpRME0xb3dGakVVTUJJR0ExVUVBd3dMYzNGMWFXUXVjSEp2ZUhrd2dnRWlNQTBHCkNTcUdTSWIzRFFFQkFRVUFBNElCRHdBd2dnRUtBb0lCQVFDOXVEdWI5cEE0N2RWcTYxSUFtdk9JbE94MkRjcnQKb1k5eGFmMnp3MUhCUU9SSXoyT05BQmlXdnEwV0swODJaRDZzRGR3b2w1WVZSOFk2dlJodkJpSlBNVjNXQkg4dworZThTVWF6ck4vYVg0dXR0ZngzY0U2REdWdnVORHpvb0Z1eE1yOGFpdGJ0RXZnRyszZkc2YkxtYTRYYjJYcDhxCkNjUnh3dzNhT0tVODRtUnNSeFFtWWw0a0JHSFJubW52eUJROHV6TTlwMzFUaXNVWXdIOUlSYlhVMXpYZkdacFIKYThYR282MXFSby9jVVMrS3BCRUVhTGQ1Q3NGRWFUbXBwblZQbG1MSmo0N093N2VuQWV1Y3dqNGNNT3pQOUduUAo3OTVFVU5PdFdZemN1eGEzRmFMTjZ1R01lQ2tJOFZYWkNzZklablRJSUx3bXRqZWhtR3k1aXhMUkFnTUJBQUdqCmdaWXdnWk13SHdZRFZSMGpCQmd3Rm9BVTdqSmp4cVdxNDg1VlU0MDV2Sk1RTDk2SnRVRXdDUVlEVlIwVEJBSXcKQURCR0JnTlZIUkVFUHpBOWdndHpjWFZwWkM1d2NtOTRlWUlQYzNGMWFXUXVjSEp2ZUhrdWMzWmpnaDF6Y1hWcApaQzV3Y205NGVTNXpkbU11WTJ4MWMzUmxjaTVzYjJOaGJEQWRCZ05WSFE0RUZnUVVHTVNEa3JVdHp0Ums1NGJzCjg0dlpKc2d5aEFrd0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFGSC9YZDUwRjdrZGUweml2SHhxMFk0TWxFcjUKKzhUbkIraWNmVFNEWWw2OGx2NHMzaFBjbUZPSFN2UWJQTGs5ZmJLK1owV1hHYjQzdDRXbnp3ZExvRk8rZXVtRApOaE5jWXQvaU9wQ0VDb1lja2pCbXR3bzRQWExTNTZVQ21OQk5XbUtBY0xTeHdwdWk2SGNJUEtFZWNKNSsyV2FrCkJWTWhFMG9vVWdXMFVFSWx4ZjJQMW9PNFJxc2t0ZFR2ZkpwY3JFMmVNeENEcXhyM01FRVRMckl1dllQa2U2YVYKbVV1MHZGaHZxK2kxQUliQllVS0hiSS9ZNXpLZmlXZkFxVHpwSm1td3dBZ1Flc2c2OHh1NjNHR0R2dkRmYVppbQpxRkNvNVZyLzFpTnRDYmtHQnFRMzVjMUJhNklkaDY4SmVIVE9GL080UkNhTUtzV1RKRmNrMmN3bnY5cz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQotLS0tLUJFR0lOIFBSSVZBVEUgS0VZLS0tLS0KTUlJRXZnSUJBREFOQmdrcWhraUc5dzBCQVFFRkFBU0NCS2d3Z2dTa0FnRUFBb0lCQVFDOXVEdWI5cEE0N2RWcQo2MUlBbXZPSWxPeDJEY3J0b1k5eGFmMnp3MUhCUU9SSXoyT05BQmlXdnEwV0swODJaRDZzRGR3b2w1WVZSOFk2CnZSaHZCaUpQTVYzV0JIOHcrZThTVWF6ck4vYVg0dXR0ZngzY0U2REdWdnVORHpvb0Z1eE1yOGFpdGJ0RXZnRysKM2ZHNmJMbWE0WGIyWHA4cUNjUnh3dzNhT0tVODRtUnNSeFFtWWw0a0JHSFJubW52eUJROHV6TTlwMzFUaXNVWQp3SDlJUmJYVTF6WGZHWnBSYThYR282MXFSby9jVVMrS3BCRUVhTGQ1Q3NGRWFUbXBwblZQbG1MSmo0N093N2VuCkFldWN3ajRjTU96UDlHblA3OTVFVU5PdFdZemN1eGEzRmFMTjZ1R01lQ2tJOFZYWkNzZklablRJSUx3bXRqZWgKbUd5NWl4TFJBZ01CQUFFQ2dnRUFEMHZmWW4vS3VReFQvTDdrYWJBVW1wcUhSaTF1Z3lKWGttbGRUelVjVGx5UgpDMTNOUlAwUS9uWjVOaVJmeXA3aWZyVFBYMlo1YjFYT0ljWkZSZzVVamRZSkhzeFdCUGpoMGJSTUlOcnVRdWpFCjJwczVLUmxjQ1NIU1BsbFA4eE9HRWRqS1J5d240UzhBclFid0NudmhxeDhxbkVxY3dxTHU5ODBtUE1YOGJLQWcKbXhXblh6bDROQ21kZUZqYTlRQ0YvLzNQellUa3hLamNRMGhvSXpBa2N4Q3ZDZFJJVHVZN2Y4a2MwSG1ySWtrego2UGhCNDBKZiswMk9HMzZhK1NGcFZ2em1QZ0FOQUFoT2lPeHE2TlRoRTJaMGwyMGZFMVJxRDB0eTN3R2tMZ3JiCmZwMGg4SGh5bHVnbGtRSlY3TUxuUHVLZE9odnpXWklVVXZJLy94cFZVUUtCZ1FEdVY2OGxiLzRlZkp3b3VMa2MKakV1Z2Z1VXhnc3I1VmRSakZVZmVkdTJsYmRsSDBoanQwUTNQUjZOMUlrRlVqTEgvY1VNS0xBanlFeFFYNisrTAp5SHhYckZqMnQrS3A5bXJSVkE1clVpRUJlZTZ4R29KVkFHRHFzZ0h0UGRBZjYxZ0dXVmwyK2hSS1BiQ3NGTm9VCmJNMStadjg5UHVHbVJjNXhrdnFNY2hrQ1d3S0JnUURMeG1LMjI0OGF3UmZwNzArNloyTWRGMkNVRTJRUFFLK08KTVByaWlIYmxCL3FJazFWemtPajlYL2JqaEYwSjVXZzgrbDAwekNjRzBhTkRLb01PTlE3ZGVSUVZzTnlkaG9Eago4RnlMZUpHa0d3T2pmWXpsc29PazUrUTFrL0hyVDUzbkJGM0VkVmg2UlNhZUNUZ080djUvWEtsOUVwYWI2TWdYCiswNTQ3QTV2UXdLQmdRQ1VIVXVMc1dBelkrN0xZNWd0eElYTzlHekw2dUxtTmM5cHo3Uzg3QjFjKzduV1p4cjAKMTBDRXVwazYxcEhRMENwaGV1cFZiTzRXT1lMNEpyZlRuMENlWDAxZDdRSmY2dkdRcW5MWGdNOWdFbjBoOUQ1ZwpRbjczK3EwMTJINzVCeERKeVViT3FEUnB1cEtMTGQ0a2FVVCtzMVVVbzNvcEVTSnM5QkRkcko0Y0Z3S0JnR252Cjlpdm9VeXB1amxjaEFjci9xc2haK2V5aGRCaDE0WTdEcWZxUlJYWm1Rbm8wVm0xaFBhOVQ3NDl5cGNmYVN1bkEKb3lvcXBITm9Fejk4MzJ0SWJEVDVtRlo2ZndjcUFPSC9lSzFOZmpIWmxYZXVjc3lMbE9MclozbnZNd3JKZG1hKwpuMXplUUtRNFJRNU43cVhXbnNacHp2ZGw3WVNhYlVRQ2MxWnNLa2p2QW9HQkFJbk5zRzAxOUdQUDNpSzdYOWJpCld3MnI4RDdMakpHVlppKzlqM29uOENwMldpYy84dW9TL1ZBSmRsTURHVFFxZ004M2tKUFVhalpNNjI3K1NNT24KTTFiSmMwOGpTSERzcCtMS1hwMEQ4QTVkWnoraUt4TVFyZWxZVG9wcUhUbHh1M1RXSmlTY0FoNHRlWTl0QjZxcQoyc3NkY240eFJZRUFYZnU1aHVad0UyczAKLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLQoK + LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZWRENDQXp5Z0F3SUJBZ0lVUGlSSnphM0tVMnBYenVtVnB2eUhpNjduSXRrd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0ZqRVVNQklHQTFVRUF3d0xjM0YxYVdRdWNISnZlSGt3SGhjTk1qVXdNakF6TVRJeE1qUXlXaGNOTWpZdwpNakF6TVRJeE1qUXlXakFXTVJRd0VnWURWUVFEREF0emNYVnBaQzV3Y205NGVUQ0NBaUl3RFFZSktvWklodmNOCkFRRUJCUUFEZ2dJUEFEQ0NBZ29DZ2dJQkFNRk9ualdzcERGR0ppc3pXSDh3S25lVlFQTnFMb1hLMGV3bS9BaHkKWUxEQVc0amlSQmVJOUoyZTZ2OEpkdlp6d2lIR1lidEpRbll6Q2RQYWg3cWRlTEkzUko2akxBMWlCWWp1YWpJNgpEb2RnZi9paFQ5ejB2d1M0VXBKOEZOQjk4UUR1Z25ndzJkblR6TmJDRjFIeHh4WGhXbVRIUktQdjc5cVdHZzJRCk1xSzd3M210S1BONmhhNURaaE5WNmVqZGNtSlpVNEw5bU9tQ0pJWkVPa1UvS0tNM2huV1FUd1QwSXpxeHBwT2cKY2hDWFh0c09aV1AxSlNXZ0dlYkN5ZTJ5TlFRSWtzQnVIWW5qWlFjOWcvOTE5c293Q0daYkVaYzE5a2d2YWErSQpqTk5BQWg1UEgrVXVTemp1cGQrSEpHWnR6MzlVL3B2UW16L0ltdW50d0daRUVNTENZMUx3K0gzTzJXN2JLQWFjCmhNNFFEYWg1SjVFbjc3K2xGaytjZTBkbmN0UTc2aUwxd3cwVERpTFM1QzdvWjVsR2ZnaWRYZ0lZYnBzSVZoVUgKZWU4RWkwWmM2SzNzbU1NNGRYc3FKdHdXRVJoczBHMnUvQ3ZKMXI0Z3hlSUFmNStiaWhYUEdJOWxNdFBSSGEvMQozMVNuNlVUeVphb2l6alNZeVltSXVJZllNVkM1WCtEdEZVYjN3RysvVXlTdHFEcUZYUExEUzN5SHloajQyejhPClNBeDgydlpEUE8rQ0lWRHpzcWhyeGcyRzlzNWdCRnp2L3hTYWNFbFpFd3pFTGEwZmNyQXJhOEJMamtGVmdkaE0KTnNOWjRDWU9oRjJpdURDOWFRbXNXS0Iwak5CdWpkTW0vRVBKS1VETEtnMCtKNHJEYTg1WU5EOTZOYUtMWktVMgpjNk5kQWdNQkFBR2pnWmt3Z1pZd0hRWURWUjBPQkJZRUZBZjhOaDZENVpHUFBoVkNmOFBYOFM0cCtHcE9NQjhHCkExVWRJd1FZTUJhQUZBZjhOaDZENVpHUFBoVkNmOFBYOFM0cCtHcE9NRVlHQTFVZEVRUS9NRDJDQzNOeGRXbGsKTG5CeWIzaDVnZzl6Y1hWcFpDNXdjbTk0ZVM1emRtT0NIWE54ZFdsa0xuQnliM2g1TG5OMll5NWpiSFZ6ZEdWeQpMbXh2WTJGc01Bd0dBMVVkRXdRRk1BTUJBZjh3RFFZSktvWklodmNOQVFFTEJRQURnZ0lCQUNzYThMNGJvRkltCkxaVHp0VVpkUUFIWm51SmlNSEhzMnZidmJLM256aWFGZDl5Z2YwZi94L3hKNGI5QWU1bXpBc0MrdE42NVB3UnQKTDlua1lyOEttV0hVaERqY0IweWE3NFliTllqbGJiT2ZQa0R0SDIxSEd1T1JEVEJlMER3OWFjTW1YeTBhcmRZdQpjbTQveDBJMWlBRmNPbDFPWDYxVTlsYjc1RzJGaitXT2JPeldWWEJEQ0pWRGtUWjJETittNGZTL1JJRWhTcm1rCmNDTWFJcjYwc0tNT2lQWGdBSXBRR1ZYRVYxNDJSbThiZHVjb00yeG81ZmZVK1BaN1pVbk8rdXAzNXdHU0NEcHEKYm9HcEpUSUxxQTY4WGhzUGFvSEY2T0YvcERSTzlveFE1YVlIUWw3UmF4Tml6c2YwVHFoVWJZeGc1L1hVUnZTagpnSDE3T2MwRlFpaG9xU1NtRVZlVXE5aUUxOHVITENlMjdlVWNsL1VqK29PQlg0cXZIeXNkSmRGSWNQY2VOcllxCkVkU0ZGek5pckprVDlodG56YlhHZXhXdUJCQW9pWnlGaUJqU0kvaHlna0w5QzQzYUhSK2V6dU9lZHlVRm9XKzEKeGgvNGdXYUVlMDFXczVVbjhYcUdleE5UdWtQcjkrMlJ0R3R2d1htOWZVbGhZaEpLSEVzcTdZaDFUelNleWxMSwppSEUzZ2FicXk5SEh3SzJpcWhSODM0NUY4aXJGQys3QVJIVHE1NVVJdldOb2p6YVc1bndXSkR4YXkrYm1QKzZyCkVqdFAzNlVVaEg4RXhBUjRkNTlGQWVUaTZnSC85TkZOZjFEV0h0ZDRDbmRpeHM5VkVLa1FaMUx4MXpiSUhGTmgKaUpWUlNaczJIWlZqUEF4R3V5ZFJIcDlKOWI3SmthNkMKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQotLS0tLUJFR0lOIFBSSVZBVEUgS0VZLS0tLS0KTUlJSlFnSUJBREFOQmdrcWhraUc5dzBCQVFFRkFBU0NDU3d3Z2drb0FnRUFBb0lDQVFEQlRwNDFyS1F4UmlZcgpNMWgvTUNwM2xVRHphaTZGeXRIc0p2d0ljbUN3d0Z1STRrUVhpUFNkbnVyL0NYYjJjOEloeG1HN1NVSjJNd25UCjJvZTZuWGl5TjBTZW95d05ZZ1dJN21veU9nNkhZSC80b1UvYzlMOEV1RktTZkJUUWZmRUE3b0o0TU5uWjA4elcKd2hkUjhjY1Y0VnBreDBTajcrL2FsaG9Oa0RLaXU4TjVyU2p6ZW9XdVEyWVRWZW5vM1hKaVdWT0MvWmpwZ2lTRwpSRHBGUHlpak40WjFrRThFOUNNNnNhYVRvSElRbDE3YkRtVmo5U1Vsb0JubXdzbnRzalVFQ0pMQWJoMko0MlVIClBZUC9kZmJLTUFobVd4R1hOZlpJTDJtdmlJelRRQUllVHgvbExrczQ3cVhmaHlSbWJjOS9WUDZiMEpzL3lKcnAKN2NCbVJCREN3bU5TOFBoOXp0bHUyeWdHbklUT0VBMm9lU2VSSisrL3BSWlBuSHRIWjNMVU8rb2k5Y01ORXc0aQowdVF1NkdlWlJuNEluVjRDR0c2YkNGWVZCM252Qkl0R1hPaXQ3SmpET0hWN0tpYmNGaEVZYk5CdHJ2d3J5ZGErCklNWGlBSCtmbTRvVnp4aVBaVExUMFIydjlkOVVwK2xFOG1XcUlzNDBtTW1KaUxpSDJERlF1Vi9nN1JWRzk4QnYKdjFNa3JhZzZoVnp5dzB0OGg4b1krTnMvRGtnTWZOcjJRenp2Z2lGUTg3S29hOFlOaHZiT1lBUmM3LzhVbW5CSgpXUk1NeEMydEgzS3dLMnZBUzQ1QlZZSFlURGJEV2VBbURvUmRvcmd3dldrSnJGaWdkSXpRYm8zVEp2eER5U2xBCnl5b05QaWVLdzJ2T1dEUS9laldpaTJTbE5uT2pYUUlEQVFBQkFvSUNBQzV6K3hjQTd0QWNnRzJmUUNRSWFod2sKbE9BcDR4WXB3RHFVdjdvejZrSnZaMC9FdUFKRDJpektsTVJHL1B5S290dEU5aFZ3ckhVRkhOWjVUR2F2RXVNWQozdmVVVkxDK25uL2ljMGl3cE84cFpIZFdKSC8vbkt2QXM2OFovRktDQVZsczk1TjBnZFdUelVUS2pab1dsUFlRCkdvM2ZTUUp3VlY1YzlkUE9sQ3lCSEo5djJraHdhQkdSaHNVY3YwSkRmUXBmVnU5Q2krMkpaY2VTTzhLS1EvUzkKRWlYTVVRRHF2bENMZ25FMWZGTzZYSVFkdUlYRjBuQnhRZWd2WlNFbTB4Q3VFcjRGZURtN21IcWQ4TXVDQ0pWYgo1NWpaUjZmand0UmorR3pEVHJ3eFJKRU9DS2NsY2RRem5VN1RCZzlMVWpMU05RRXlweXd6dys1MVFPZ1NjVWhCCkJjWTJzcitXeml3R3lhMC9KdUZGaTFQam9sckpRSmxLRW9sZVVHeGZxUGN3ZWI4OUluOFE3dVlRSHNscmJ2M1AKYzdyWFJQLzkydWd1bS9ocTNJcURWcUhSVUIrUUkwTWdabHlkTi9RbENVaFNUbmxkZUhKbFNrOEwvdXZTTmVhaApXRnV4ZHdBZnBMUUE3ZXo2Z1BSandtRXAzZ3BnVkduRVpNcWIzaGtJbm9ZWnlrVW1TNUI4eHRDV3RXenhITUFDCnl0RU52N1RoeUUwWkdkZkhZRXBmQ2dFYnp5MUZDK0FXZGc3N3VyTE1ORWpvakRKOTZydHd2anUwMk9uRmVDaTAKUm15emEvaEJQanhzRUEzY3JZQXkxQlNMM0l2TGNGUC9nRXM4a0FqaWt3cURsNWNxeVcvYzdGN2VTTXBkeWY5QwplaXMrNHpIRDZORnpFYzBPNWhQaEFvSUJBUUR2Szh1UzBYdXB5aXZreThUVDVveGR5SnlNUk1xZW9DVWRGYndjClg4cUdmZnYxdlo4SkJKRHZGdHFZWnF4YlpTVmVBbTdWeTFCVGs3TlhhbmdoRi9tL05aM0dIRUVmWHZxY3lsdzYKL21QRXRLYXcwRU1DOC9tZFNMNjFramdlT2JiWVl4bW5Hd2QzYXJ4VEtacEJkOWFUcWZGUWdjQTdSZWVWa3hFWAo5N0F0TzZ1Qzk5OTJrRVdWS1dNRGtKbjNGSTBQTWN4WVN2bi9ORi95M1RncHJSTTZ0UDlGUWNtWC9OVElYb2l1CmhoM0x2bEU1MlhKQnpweC84SnhreGpPSkI5QXdLUC85endlVG1ncW1peUVJcWhlM0lUNjMzdDltV2hQR29JZVUKT0FkcFVBNHg1UmlLWGVaelUzZm4wQW83WlZVdjN3UWRkOFVhTThXZVcxVVA3clhaQW9JQkFRRE82S3dEMzJxcgo4bzdYWWhlS0toZHRMUnkzcFpHUlZheVlqWHZObFFQb1hQb1BvNjkxSk9OZWQrQWhrdVNNYzBJdFNBMGtMaXRUCmdyaVhpemhsNHpZMkptdENoRzhDRXRVVXhzbGY3UkY5SlNlN1p3ekhxeWVRVlNvdVF5aFllK1VKTWYwdTR6QTAKMWNTWTlFUGNweG1ja01nZ3dkc0NRbEpNYnJaMWRLeFNBTTBFRXlPcnFXdU9wd1hBWkhLRVdwcXh5M0Z4SGlZVgpXYVZ0K2ZzOERsODhwZU9tMVZaeXk4TllMSHl3VlFzS2s0UFpUL0hwZStMMHhyUGdxNXJyWHBjVWRmL3RmZGVoCjBxc3h4cmRSSmliMUFmdGh2ME5Mb1F0TUxxaXJjU1o4S1hxWFEwVG5PQ0JYTm9XSWNXUmlrM0ZlN1BGYXMrS0EKdkVEVXV5Z3ZMbE1sQW9JQkFBaExpNDczQndQM2lCZ3lYUXhBWmNQbTdrOExINy9xcS83YlB4LzR6b3hsbURTSQp0QmhhK1MvaHFnazVIbWM1RmRleDIrZzhXZmZjR285QW1SUUV3ZHU0MzFUOHErR0xxTU9CWFR1S2tTbEVYcmVwCk1Ybkx2bStQRTFZMjBRMXpVUDBtU3NCNTlvTlV4MTFYQnd1WVBXLzNwKy96NEJmdUw3OEhUOE4yQ3IwMjRaYjAKUStMWDFDSDlRbnJnTEFiZXhwbXRUM29NZDZrN1JzeWtrWXNZZnA4OW9kRGtIRHJTUVFzR0JGV1JQejFPeDRCcgpJMFJYQnlTRTB0Zkg3QWVucHJmVTVEUUlWeW51WU1vdjd5QmV6ZDNESUdxK0p4OWtwbVR3TW1PWW9lRXNMcUhhCllVU0RSemZld0R6aEFVbllGT0ZKS2RwZnlnMURtR29LbnVPamt0a0NnZ0VBWlVzYkN6cFJLcVN3c1ZqZ1ZVK04KOEhEcFlpNjRPUUpNWU5MRERUNHFqNU1WQ0pzRnhyK3NZQThudHNnSEE0dFpsbmx6bFliVXh5bHozUnpYRzJwRQptL1hyQk1GNDV2YjVRaGFmZDByRUNSUXJnMTlMcm1Sb0ZnemJmWko3S2ZaZGhrYm13QkdSQkF5ekZuNWV5cU16CnNxWmVrMHJVUVNMZXozUlQ5dVNMaUFuRVZINWFOQ3ZZOEJsc2cyZXBlSW95dVYvenhZRVErOXJMVmkvUGd2TTIKUkthaDhJYjRyM0o1eTZ6YnppZVVKRFZia3dQRVZwM0Qyamw4emp5MHR3Mnp3TnlUMGx5Tk9EZStmN3ZjK3VsRwpvU083UVhzMUlzMVFqcGM1RTlWdEZkUG9wQ3pXaXF1N2lYYXpvTHlDZkkvYUxMS3E1ZEN5em50YThjbytQZnJiCjBRS0NBUUVBcktXSThDOW1ENnlRdWZIQVBCRDJLTXhnUTJVdjNaUHZqNU5kcUppMjFweThNSjNyNWQ4RFFsQWcKQ2w3RURoNml0alY2cFhCeGdsL0Nha1lMcURtczh4aG1KVkhWSmtlZ0E4OW53QXMxekx3d21tUmpyU3U2R0F3VgphOWpNdEJWK3dhY09kWHQwUVRlbVVhbzdNY1gzVy9PU1l6WXBlS1dEemQwUy9qeDZadHJkM2lGQ20vMTBydkYyCmJ1QTlJTWE2Q041QytHTTFhZ2xrb2s0Z1VWYWtHSk1uMXJRc3F3b0JlVkpxSkFHRVRWcExKY0hhbzNhVFh6ckgKU2gvVGFxUEVZUzlpRnV6NWhsc3ZNWUFFZUtML0VZNGo4Vi9EMlljYTJMTG5HV1BzS2VMQ2FMWkhucU1ieWtHeQo3ZEVQdGVZRTlrVFZqcmtwV2t6SGYyelQ4d3NxNkE9PQotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0tCg== --- apiVersion: v1 kind: ConfigMap