Repository: open-telemetry/opentelemetry-operator

Issue:
Operator does not re-try failed upgrades
### Component(s)

collector

### What happened?

## Description
The operator does not re-try failed upgrades of managed instances. In case an upgrade fails here: https://github.com/open-telemetry/opentelemetry-operator/blob/42a689e93bdeabded2ef4af08a9b5edc9b6e6486/pkg/collector/upgrade/upgrade.go#L86 (for example the Kubernetes API server is temporarily unreachable), an error is printed to the log, and the `status.version` field of the instance is updated  in the reconcile loop here: https://github.com/open-telemetry/opentelemetry-operator/blob/42a689e93bdeabded2ef4af08a9b5edc9b6e6486/internal/status/collector/handle.go#L55-L69 to the latest version regardless (note, the `spec` is not updated, only the `status` subresource). Therefore, any future re-starts of the operator also won't attempt to upgrade this instance.

Related, if the collector instance is moved from `unmanaged` to `managed` state, the upgrade process also doesn't run.

## Expected Result
The upgrade is re-tried.

## Actual Result
The `status.version` field of the instance is updated as part of the reconcile loop, however the `spec` field didn't get upgraded.

## Possible Solutions
Perform the upgrade process in the reconcile loop instead of the operator startup. This resolves the issue of re-trying failed upgrades, and also upgrading instances when they are moved from `unmanaged` to `managed` state.

### Kubernetes Version

1.31.0

### Operator version

0.113.0

### Collector version

0.113.0

### Environment information

_No response_

### Log output

_No response_

### Additional context

_No response_

Discussion:
@jaronoff97: cc @pavolloffay who wrote:
> Proposal is: run upgrade only as part of the reconciliation

I'll let him follow up with more info.

([We discussed this 5th of December](https://docs.google.com/document/d/1Unbs2qp_j5kp8FfL_lRH-ld7i5EOQpsq0I4djkOOSL4/edit?tab=t.0))

Solution patch:
diff --git a/.chloggen/retry_upgrades.yaml b/.chloggen/retry_upgrades.yaml
new file mode 100755
index 00000000..f8a8dc9d
--- /dev/null
+++ b/.chloggen/retry_upgrades.yaml
@@ -0,0 +1,18 @@
+# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
+change_type: enhancement
+
+# The name of the component, or a single word describing the area of concern, (e.g. collector, target allocator, auto-instrumentation, opamp, github action)
+component: collector
+
+# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
+note: Re-try failed upgrades
+
+# One or more tracking issues related to the change
+issues: [3515]
+
+# (Optional) One or more lines of additional information to render under the primary note.
+# These lines will be padded with 2 spaces and then inserted directly into the document.
+# Use pipe (|) for multiline entries.
+subtext: |
+  The upgrade process runs now in the reconcile loop.
+  In case of an error, the operator re-tries the upgrade with exponential backoff.
diff --git a/.github/workflows/auto-update-agent.yaml b/.github/workflows/auto-update-agent.yaml
new file mode 100644
index 00000000..092c5b35
--- /dev/null
+++ b/.github/workflows/auto-update-agent.yaml
@@ -0,0 +1,145 @@
+name: Auto-update agents
+
+on:
+  schedule:
+    # Daily at 01:30 (UTC)
+    - cron: '30 1 * * *'
+  workflow_dispatch:
+  pull_request:
+
+jobs:
+  update-agent:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        agent: [java, dotnet, nodejs, python]
+    steps:
+      - uses: actions/checkout@v4
+
+      - id: check-versions
+        name: Check versions
+        env:
+          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        run: |
+          case ${{ matrix.agent }} in
+            java)
+              current_version=$(grep -Po "[0-9]+.[0-9]+.[0-9]+" autoinstrumentation/java/version.txt)
+              latest_version=$(gh release view \
+                                  --repo open-telemetry/opentelemetry-java-instrumentation \
+                                  --json tagName \
+                                  --jq .tagName \
+                                | sed 's/^v//')
+              ;;
+            dotnet)
+              current_version=$(grep -Po "[0-9]+.[0-9]+.[0-9]+" autoinstrumentation/dotnet/version.txt)
+              latest_version=$(gh release view \
+                                  --repo open-telemetry/opentelemetry-dotnet-instrumentation \
+                                  --json tagName \
+                                  --jq .tagName \
+                                | sed 's/^v//')
+              ;;
+            nodejs)
+              current_version=$(grep -Po '"@opentelemetry/auto-instrumentations-node": "\K[0-9]+\.[0-9]+\.[0-9]+"' autoinstrumentation/nodejs/package.json | tr -d '"')
+              latest_version=$(gh release list \
+                                  --repo open-telemetry/opentelemetry-js-contrib \
+                                  --json tagName \
+                                  --jq '.[] | select(.tagName | startswith("auto-instrumentations-node-v")) | .tagName' \
+                                  | head -1 \
+                                  | sed 's/auto-instrumentations-node-v//')
+              ;;
+            python)
+              current_version=$(grep -Po "^opentelemetry-distro==\K[0-9]+\.[0-9]+(b[0-9]+)?" autoinstrumentation/python/requirements.txt)
+              # Python releases are not tagged with the same version published on PyPI. Instead, we extract the PyPI version from the release name.
+              latest_version=$(gh release view \
+                                  --repo open-telemetry/opentelemetry-python \
+                                  --json name \
+                                  --jq .name \
+                                | sed -E 's/.*\/([0-9]+\.[0-9]+(b[0-9]+)?).*/\1/')
+              ;;
+          esac
+
+          matches=$(gh pr list \
+                        --author opentelemetrybot \
+                        --state open \
+                        --search "in:title \"Update the OpenTelemetry ${{ matrix.agent }} agent version to $latest_version\"")
+          if [ ! -z "$matches" ]
+          then
+            already_opened=true
+          fi
+
+          # Ensure proper format and remove any unintended characters
+          echo "current-version=${current_version}" >> $GITHUB_OUTPUT
+          echo "latest-version=${latest_version}" >> $GITHUB_OUTPUT
+          echo "already-opened=${already_opened}" >> $GITHUB_OUTPUT
+
+          # Debug information
+          echo "Debug information for ${{ matrix.agent }} agent:"
+          echo "Current version detected: ${current_version}"
+          echo "Latest version detected: ${latest_version}"
+          echo "PR already opened for this version: ${already_opened:-false}"
+          echo "Version comparison: Current ${current_version} vs Latest ${latest_version}"
+
+          # Additional validation
+          if [[ -z "$current_version" ]]; then
+            echo "WARNING: Failed to detect current version"
+          fi
+
+          if [[ -z "$latest_version" ]]; then
+            echo "WARNING: Failed to detect latest version"
+          fi
+
+          # Print PR search query details
+          echo "PR search query: \"Update the OpenTelemetry ${{ matrix.agent }} agent version to $latest_version\""
+          echo "PR search results: ${matches:-none}"
+
+      - name: Update version
+        if: |
+          steps.check-versions.outputs.current-version != steps.check-versions.outputs.latest-version &&
+          steps.check-versions.outputs.already-opened != 'true'
+        env:
+          VERSION: ${{ steps.check-versions.outputs.latest-version }}
+        run: |
+          case ${{ matrix.agent }} in
+            java)
+              echo $VERSION > autoinstrumentation/java/version.txt
+              ;;
+            dotnet)
+              echo $VERSION > autoinstrumentation/dotnet/version.txt
+              ;;
+            nodejs)
+              sed -i "s/\"@opentelemetry\/auto-instrumentations-node\": \".*\"/\"@opentelemetry\/auto-instrumentations-node\": \"$VERSION\"/" autoinstrumentation/nodejs/package.json
+              ;;
+            python)
+              sed -i "s/^opentelemetry-distro==.*/opentelemetry-distro==$VERSION/" autoinstrumentation/python/requirements.txt
+              ;;
+          esac
+
+      - name: Use CLA approved github bot
+        run: |
+          git config user.name opentelemetrybot
+          git config user.email 107717825+opentelemetrybot@users.noreply.github.com
+
+      - name: Create pull request against main
+        if: |
+          steps.check-versions.outputs.current-version != steps.check-versions.outputs.latest-version &&
+          steps.check-versions.outputs.already-opened != 'true'
+        env:
+          VERSION: ${{ steps.check-versions.outputs.latest-version }}
+          # not using secrets.GITHUB_TOKEN since pull requests from that token do not run workflows
+          GH_TOKEN: ${{ secrets.OPENTELEMETRYBOT_GITHUB_TOKEN }}
+        run: |
+          message="Update the OpenTelemetry ${{ matrix.agent }} agent version to $VERSION"
+          body="Update the OpenTelemetry ${{ matrix.agent }} agent version to \`$VERSION\`."
+          branch="opentelemetrybot/update-opentelemetry-${{ matrix.agent }}-agent-to-${VERSION}"
+
+          git checkout -b $branch
+          git commit -a -m "$message"
+          # Only push and create PR when not triggered by a pull_request event
+          if [[ "${{ github.event_name }}" != "pull_request" ]]; then
+            git push --set-upstream origin $branch
+            gh pr create --title "$message" \
+            --body "$body" \
+            --base main
+          else
+            git show HEAD
+          fi
\ No newline at end of file
diff --git a/.github/workflows/auto-update-java-agent.yaml b/.github/workflows/auto-update-java-agent.yaml
deleted file mode 100644
index 18b865c5..00000000
--- a/.github/workflows/auto-update-java-agent.yaml
+++ /dev/null
@@ -1,81 +0,0 @@
-name: Auto-update Java agent
-
-on:
-  schedule:
-    # Daily at 01:30 (UTC)
-    - cron: '30 1 * * *'
-  workflow_dispatch:
-
-jobs:
-  check-versions:
-    runs-on: ubuntu-latest
-    outputs:
-      current-version: ${{ steps.check-versions.outputs.current-version }}
-      latest-version: ${{ steps.check-versions.outputs.latest-version }}
-      already-opened: ${{ steps.check-versions.outputs.already-opened }}
-    steps:
-      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
-
-      - id: check-versions
-        name: Check versions
-        env:
-          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-        run: |
-          current_version=$(grep -Po "[0-9]+.[0-9]+.[0-9]+" autoinstrumentation/java/version.txt)
-          latest_version=$(gh release view \
-                               --repo open-telemetry/opentelemetry-java-instrumentation \
-                               --json tagName \
-                               --jq .tagName \
-                             | sed 's/^v//')
-
-          matches=$(gh pr list \
-                        --author opentelemetrybot \
-                        --state open \
-                        --search "in:title \"Update the OpenTelemetry Java agent version to $latest_version\"")
-          if [ ! -z "$matches" ]
-          then
-            already_opened=true
-          fi
-
-          echo "current-version=$current_version" >> $GITHUB_OUTPUT
-          echo "latest-version=$latest_version" >> $GITHUB_OUTPUT
-          echo "already-opened=$already_opened" >> $GITHUB_OUTPUT
-
-  update-java-agent:
-    runs-on: ubuntu-latest
-    if: |
-      needs.check-versions.outputs.current-version != needs.check-versions.outputs.latest-version &&
-      needs.check-versions.outputs.already-opened != 'true'
-    needs:
-      - check-versions
-    steps:
-      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
-
-      - name: Update version
-        env:
-          VERSION: ${{ needs.check-versions.outputs.latest-version }}
-        run: |
-          echo $VERSION > autoinstrumentation/java/version.txt
-
-      - name: Use CLA approved github bot
-        run: |
-          git config user.name opentelemetrybot
-          git config user.email 107717825+opentelemetrybot@users.noreply.github.com
-
-      - name: Create pull request against main
-        if: success() || failure()
-        env:
-          VERSION: ${{ needs.check-versions.outputs.latest-version }}
-          # not using secrets.GITHUB_TOKEN since pull requests from that token do not run workflows
-          GH_TOKEN: ${{ secrets.OPENTELEMETRYBOT_GITHUB_TOKEN }}
-        run: |
-          message="Update the OpenTelemetry Java agent version to $VERSION"
-          body="Update the OpenTelemetry Java agent version to \`$VERSION\`."
-          branch="opentelemetrybot/update-opentelemetry-java-agent-to-${VERSION}"
-
-          git checkout -b $branch
-          git commit -a -m "$message"
-          git push --set-upstream origin $branch
-          gh pr create --title "$message" \
-                       --body "$body" \
-                       --base main
diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml
index bf2ebdc1..7fcd7cc5 100644
--- a/.github/workflows/continuous-integration.yaml
+++ b/.github/workflows/continuous-integration.yaml
@@ -13,7 +13,7 @@ concurrency:
 jobs:
   unit-tests:
     name: Unit tests
-    runs-on: ubuntu-22.04
+    runs-on: ubuntu-latest
     steps:
     - name: Check out code into the Go module directory
       uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -38,7 +38,7 @@ jobs:
 
   lint:
     name: Code standards (linting)
-    runs-on: ubuntu-22.04
+    runs-on: ubuntu-latest
     steps:
     - name: Check out code into the Go module directory
       uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -71,7 +71,7 @@ jobs:
 
   security:
     name: Security
-    runs-on: ubuntu-22.04
+    runs-on: ubuntu-latest
     steps:
     - name: Check out code into the Go module directory
       uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -82,15 +82,15 @@ jobs:
         go-version: "~1.24.2"
 
     - name: Initialize CodeQL
-      uses: github/codeql-action/init@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3.28.13
+      uses: github/codeql-action/init@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15
       with:
         languages: go
 
     - name: Autobuild
-      uses: github/codeql-action/autobuild@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3.28.13
+      uses: github/codeql-action/autobuild@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15
 
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3.28.13
+      uses: github/codeql-action/analyze@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15
 
     - name: Govulncheck
       uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee # v1.0.4
diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml
index 725748a1..d90896dc 100644
--- a/.github/workflows/e2e.yaml
+++ b/.github/workflows/e2e.yaml
@@ -13,9 +13,45 @@ concurrency:
 permissions:
   contents: read
 
+env:
+  IMAGE_ARCHIVE_FILENAME: images.tar
+  VERSION: e2e
+
 jobs:
+  build-test-images:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Check out code into the Go module directory
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+      - name: Set up Go
+        uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0
+        id: setup-go
+        with:
+          go-version: "~1.24.2"
+
+      - name: Cache tools
+        uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
+        with:
+          path: bin
+          key: tools-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('Makefile') }}-${{ steps.setup-go.outputs.go-version }}
+
+      - name: Install tools
+        run: make install-tools
+
+      - name: Build test image archive
+        run: make container-image-archive IMAGE_ARCHIVE=${IMAGE_ARCHIVE_FILENAME} VERSION=${VERSION}
+
+      - name: Upload test image archive
+        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+        with:
+          name: Test image archive
+          path: ${{ env.IMAGE_ARCHIVE_FILENAME }}
+          if-no-files-found: 'error'
+          retention-days: 1
+
   e2e-tests:
-    runs-on: ubuntu-22.04
+    runs-on: ubuntu-latest
+    needs: [build-test-images]
     strategy:
       fail-fast: false
       matrix:
@@ -75,12 +111,17 @@ jobs:
     - name: Install tools
       run: make install-tools
 
+    - name: Download test image archive
+      uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
+      with:
+        name: Test image archive
+
     - name: Prepare e2e tests
       env:
         KUBE_VERSION: ${{ matrix.kube-version }}
       run: |
         set -e
-        make ${{ matrix.setup != '' && matrix.setup || 'prepare-e2e' }} KUBE_VERSION=$KUBE_VERSION VERSION=e2e
+        make ${{ matrix.setup != '' && matrix.setup || 'prepare-e2e' }} KUBE_VERSION=$KUBE_VERSION VERSION=$VERSION IMAGE_ARCHIVE=$IMAGE_ARCHIVE_FILENAME
     - name: Run e2e tests
       env:
         KUBE_VERSION: ${{ matrix.kube-version }}
@@ -96,7 +137,7 @@ jobs:
         make e2e-log-operator KUBE_VERSION=$KUBE_VERSION
 
   e2e-tests-check:
-    runs-on: ubuntu-22.04
+    runs-on: ubuntu-latest
     if: always()
     needs: [e2e-tests]
     steps:
diff --git a/.github/workflows/ossf-scorecard.yml b/.github/workflows/ossf-scorecard.yml
index bad81a7e..6a9f19aa 100644
--- a/.github/workflows/ossf-scorecard.yml
+++ b/.github/workflows/ossf-scorecard.yml
@@ -42,6 +42,6 @@ jobs:
       # Upload the results to GitHub's code scanning dashboard (optional).
       # Commenting out will disable upload of results to your repo's Code Scanning dashboard
       - name: "Upload to code-scanning"
-        uses: github/codeql-action/upload-sarif@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3.28.13
+        uses: github/codeql-action/upload-sarif@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15
         with:
           sarif_file: results.sarif
diff --git a/.github/workflows/publish-autoinstrumentation-apache-httpd.yaml b/.github/workflows/publish-autoinstrumentation-apache-httpd.yaml
index 24b70f09..e55ba115 100644
--- a/.github/workflows/publish-autoinstrumentation-apache-httpd.yaml
+++ b/.github/workflows/publish-autoinstrumentation-apache-httpd.yaml
@@ -25,7 +25,7 @@ permissions:
 
 jobs:
   publish:
-    runs-on: ubuntu-22.04
+    runs-on: ubuntu-latest
 
     steps:
       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
diff --git a/.github/workflows/publish-autoinstrumentation-dotnet.yaml b/.github/workflows/publish-autoinstrumentation-dotnet.yaml
index 950dedf1..375696d7 100644
--- a/.github/workflows/publish-autoinstrumentation-dotnet.yaml
+++ b/.github/workflows/publish-autoinstrumentation-dotnet.yaml
@@ -25,7 +25,7 @@ permissions:
 
 jobs:
   publish:
-    runs-on: ubuntu-22.04
+    runs-on: ubuntu-latest
 
     steps:
       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
diff --git a/.github/workflows/publish-autoinstrumentation-java.yaml b/.github/workflows/publish-autoinstrumentation-java.yaml
index b76bed81..6e2351e2 100644
--- a/.github/workflows/publish-autoinstrumentation-java.yaml
+++ b/.github/workflows/publish-autoinstrumentation-java.yaml
@@ -25,7 +25,7 @@ permissions:
 
 jobs:
   publish:
-    runs-on: ubuntu-22.04
+    runs-on: ubuntu-latest
 
     steps:
       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
diff --git a/.github/workflows/publish-autoinstrumentation-nodejs.yaml b/.github/workflows/publish-autoinstrumentation-nodejs.yaml
index da40b64f..43dc737a 100644
--- a/.github/workflows/publish-autoinstrumentation-nodejs.yaml
+++ b/.github/workflows/publish-autoinstrumentation-nodejs.yaml
@@ -25,7 +25,7 @@ permissions:
 
 jobs:
   publish:
-    runs-on: ubuntu-22.04
+    runs-on: ubuntu-latest
 
     steps:
       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
diff --git a/.github/workflows/publish-autoinstrumentation-php.yaml b/.github/workflows/publish-autoinstrumentation-php.yaml
index ef77ac7a..9ce8e33c 100644
--- a/.github/workflows/publish-autoinstrumentation-php.yaml
+++ b/.github/workflows/publish-autoinstrumentation-php.yaml
@@ -25,7 +25,7 @@ permissions:
 
 jobs:
   publish:
-    runs-on: ubuntu-22.04
+    runs-on: ubuntu-latest
 
     steps:
       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
diff --git a/.github/workflows/publish-autoinstrumentation-python.yaml b/.github/workflows/publish-autoinstrumentation-python.yaml
index ff2cf794..91e2a298 100644
--- a/.github/workflows/publish-autoinstrumentation-python.yaml
+++ b/.github/workflows/publish-autoinstrumentation-python.yaml
@@ -25,7 +25,7 @@ permissions:
 
 jobs:
   publish:
-    runs-on: ubuntu-22.04
+    runs-on: ubuntu-latest
 
     steps:
       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
diff --git a/.github/workflows/publish-images.yaml b/.github/workflows/publish-images.yaml
index 2cd3a661..9cfe647b 100644
--- a/.github/workflows/publish-images.yaml
+++ b/.github/workflows/publish-images.yaml
@@ -19,7 +19,7 @@ permissions:
 jobs:
   publish:
     name: Publish container images
-    runs-on: ubuntu-22.04
+    runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
 
diff --git a/.github/workflows/publish-must-gather.yaml b/.github/workflows/publish-must-gather.yaml
index 7ebef36f..7e77ba29 100644
--- a/.github/workflows/publish-must-gather.yaml
+++ b/.github/workflows/publish-must-gather.yaml
@@ -19,7 +19,7 @@ permissions:
 jobs:
   publish:
     name: Publish must-gather container image
-    runs-on: ubuntu-22.04
+    runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
 
diff --git a/.github/workflows/publish-operator-bundle.yaml b/.github/workflows/publish-operator-bundle.yaml
index b76a5c71..c751e4ee 100644
--- a/.github/workflows/publish-operator-bundle.yaml
+++ b/.github/workflows/publish-operator-bundle.yaml
@@ -19,7 +19,7 @@ permissions:
 
 jobs:
   publish:
-    runs-on: ubuntu-22.04
+    runs-on: ubuntu-latest
 
     steps:
       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
diff --git a/.github/workflows/publish-operator-opamp-bridge.yaml b/.github/workflows/publish-operator-opamp-bridge.yaml
index fad59b35..9fc6c6de 100644
--- a/.github/workflows/publish-operator-opamp-bridge.yaml
+++ b/.github/workflows/publish-operator-opamp-bridge.yaml
@@ -22,7 +22,7 @@ permissions:
 
 jobs:
   publish:
-    runs-on: ubuntu-22.04
+    runs-on: ubuntu-latest
 
     steps:
       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
diff --git a/.github/workflows/publish-target-allocator.yaml b/.github/workflows/publish-target-allocator.yaml
index 5a9ab74d..d134349f 100644
--- a/.github/workflows/publish-target-allocator.yaml
+++ b/.github/workflows/publish-target-allocator.yaml
@@ -22,7 +22,7 @@ permissions:
 
 jobs:
   publish:
-    runs-on: ubuntu-22.04
+    runs-on: ubuntu-latest
 
     steps:
       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index c1761136..a078510a 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -9,7 +9,7 @@ on:
 
 jobs:
   get-versions:
-    runs-on: ubuntu-22.04
+    runs-on: ubuntu-latest
     outputs:
       current_version: ${{ steps.get-versions.outputs.current_version }}
       desired_version: ${{ steps.get-versions.outputs.desired_version }}
@@ -24,7 +24,7 @@ jobs:
           echo "current_version=$(git describe --tags --abbrev=0 | sed 's/^v//')" >> $GITHUB_OUTPUT
 
   release:
-    runs-on: ubuntu-22.04
+    runs-on: ubuntu-latest
     needs: get-versions
     if: needs.get-versions.outputs.desired_version != needs.get-versions.outputs.current_version
     env:
diff --git a/.github/workflows/reusable-publish-test-e2e-images.yaml b/.github/workflows/reusable-publish-test-e2e-images.yaml
index 419dbd16..1ba90e8c 100644
--- a/.github/workflows/reusable-publish-test-e2e-images.yaml
+++ b/.github/workflows/reusable-publish-test-e2e-images.yaml
@@ -15,7 +15,7 @@ permissions:
 
 jobs:
   publish-e2e-image:
-    runs-on: ubuntu-20.04
+    runs-on: ubuntu-latest
 
     steps:
       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
diff --git a/.github/workflows/scorecard.yaml b/.github/workflows/scorecard.yaml
index 473624f6..a61e7ae3 100644
--- a/.github/workflows/scorecard.yaml
+++ b/.github/workflows/scorecard.yaml
@@ -16,7 +16,7 @@ permissions:
 jobs:
   scorecard-tests:
     name: test on k8s
-    runs-on: ubuntu-22.04
+    runs-on: ubuntu-latest
     strategy:
       matrix:
         kube-version:
@@ -55,7 +55,7 @@ jobs:
         run: make scorecard-tests
 
   scorecard-tests-check:
-    runs-on: ubuntu-22.04
+    runs-on: ubuntu-latest
     if: always()
     needs: [scorecard-tests]
     steps:
diff --git a/.github/workflows/shellcheck.yaml b/.github/workflows/shellcheck.yaml
index 51487d6a..10b23b23 100644
--- a/.github/workflows/shellcheck.yaml
+++ b/.github/workflows/shellcheck.yaml
@@ -9,7 +9,7 @@ permissions: {}
 jobs:
   shellcheck:
     name: shellcheck
-    runs-on: ubuntu-24.04
+    runs-on: ubuntu-latest
     env:
       # renovate: datasource=github-releases depName=https://github.com/koalaman/shellcheck
       SHELLCHECK_VERSION: v0.10.0
diff --git a/.gitignore b/.gitignore
index 5ce30788..70cd3593 100644
--- a/.gitignore
+++ b/.gitignore
@@ -44,4 +44,5 @@ config/rbac/certmanager-permissions/
 build
 node_modules
 package-lock.json
+
 .tool-versions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index aaa8629e..fda17931 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,32 @@
 
 <!-- next version -->
 
+## 0.123.0
+
+### 💡 Enhancements 💡
+
+- `opamp`: Enables the OpAMP Bridge to act as a read-only proxy for effective configuration and health reporting. (#3823)
+
+### 🧰 Bug fixes 🧰
+
+- `collector`: Fix the telemetry config to not override the user-defined config. (#3913)
+- `collector`: Fix Jaeger query extension default endpoint (#3906)
+  The operator 0.122.0 introduced a fix to add a default endpoint. However, for Jaeger query extension the endpoint
+  was put in the wrong place, causing collector to fail to start due to an incorrect config.
+  
+
+### Components
+
+* [OpenTelemetry Collector - v0.123.1](https://github.com/open-telemetry/opentelemetry-collector/releases/tag/v0.123.1)
+* [OpenTelemetry Contrib - v0.123.1](https://github.com/open-telemetry/opentelemetry-collector-contrib/releases/tag/v0.123.1)
+* [Java auto-instrumentation - v1.33.6](https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/tag/v1.33.6)
+* [.NET auto-instrumentation - v1.2.0](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/releases/tag/v1.2.0)
+* [Node.JS - v0.57.1](https://github.com/open-telemetry/opentelemetry-js/releases/tag/experimental%2Fv0.57.1)
+* [Python - v0.51b0](https://github.com/open-telemetry/opentelemetry-python-contrib/releases/tag/v0.51b0)
+* [Go - v0.19.0-alpha](https://github.com/open-telemetry/opentelemetry-go-instrumentation/releases/tag/v0.19.0-alpha)
+* [ApacheHTTPD - 1.0.4](https://github.com/open-telemetry/opentelemetry-cpp-contrib/releases/tag/webserver%2Fv1.0.4)
+* [Nginx - 1.0.4](https://github.com/open-telemetry/opentelemetry-cpp-contrib/releases/tag/webserver%2Fv1.0.4)
+
 ## 0.122.0
 
 ### 🛑 Breaking changes 🛑
diff --git a/Makefile b/Makefile
index 215c03d0..bb6d909a 100644
--- a/Makefile
+++ b/Makefile
@@ -370,7 +370,7 @@ e2e-upgrade: undeploy chainsaw
 	$(CHAINSAW) test --test-dir ./tests/e2e-upgrade
 
 .PHONY: prepare-e2e
-prepare-e2e: chainsaw set-image-controller add-image-targetallocator add-image-opampbridge container container-target-allocator container-operator-opamp-bridge container-bridge-test-server start-kind cert-manager install-metrics-server install-targetallocator-prometheus-crds load-image-all deploy
+prepare-e2e: chainsaw set-image-controller add-image-targetallocator add-image-opampbridge start-kind cert-manager install-metrics-server install-targetallocator-prometheus-crds load-image-all deploy
 
 .PHONY: scorecard-tests
 scorecard-tests: operator-sdk
@@ -438,7 +438,13 @@ install-targetallocator-prometheus-crds:
 	./hack/install-targetallocator-prometheus-crds.sh
 
 .PHONY: load-image-all
-load-image-all: load-image-operator load-image-target-allocator load-image-operator-opamp-bridge load-image-bridge-test-server
+load-image-all:
+ifeq ($(IMAGE_ARCHIVE),)
+	@make container container-target-allocator container-operator-opamp-bridge container-bridge-test-server load-image-operator load-image-target-allocator load-image-operator-opamp-bridge load-image-bridge-test-server
+else
+	$(KIND) load --name $(KIND_CLUSTER_NAME) image-archive $(IMAGE_ARCHIVE)
+endif
+
 
 .PHONY: load-image-operator
 load-image-operator: container kind
@@ -500,7 +506,7 @@ KUSTOMIZE_VERSION ?= v5.6.0
 # renovate: datasource=go depName=sigs.k8s.io/controller-tools/cmd/controller-gen
 CONTROLLER_TOOLS_VERSION ?= v0.17.1
 # renovate: datasource=github-releases depName=golangci/golangci-lint
-GOLANGCI_LINT_VERSION ?= v2.0.2
+GOLANGCI_LINT_VERSION ?= v2.1.2
 # renovate: datasource=go depName=sigs.k8s.io/kind
 KIND_VERSION ?= v0.27.0
 # renovate: datasource=go depName=github.com/kyverno/chainsaw
@@ -718,3 +724,15 @@ catalog-build: opm bundle-build bundle-push ## Build a catalog image.
 .PHONY: catalog-push
 catalog-push: ## Push a catalog image.
 	docker push $(CATALOG_IMG)
+
+container-image-archive: IMAGE_LIST_FILE = images-$(VERSION).txt
+container-image-archive: container container-target-allocator container-operator-opamp-bridge container-bridge-test-server
+ifeq ($(IMAGE_ARCHIVE),)
+	$(error "Use make container-image-archive IMAGE_ARCHIVE=<filename>")
+endif
+	@rm -f $(IMAGE_LIST_FILE)
+	@echo "$(IMG)" >>$(IMAGE_LIST_FILE)
+	@echo "$(TARGETALLOCATOR_IMG)" >>$(IMAGE_LIST_FILE)
+	@echo "$(OPERATOROPAMPBRIDGE_IMG)" >>$(IMAGE_LIST_FILE)
+	@echo "$(BRIDGETESTSERVER_IMG)" >>$(IMAGE_LIST_FILE)
+	xargs -x -n 50 docker save -o "$(IMAGE_ARCHIVE)" <$(IMAGE_LIST_FILE)
\ No newline at end of file
diff --git a/RELEASE.md b/RELEASE.md
index fd3a137f..c5fd8918 100644
--- a/RELEASE.md
+++ b/RELEASE.md
@@ -44,10 +44,10 @@ The operator should be released within a week after the [OpenTelemetry collector
 
 | Version  | Release manager |
 |----------|-----------------|
-| v0.123.0 | @jaronoff97     |
 | v0.124.0 | @swiatekm       |
 | v0.125.0 | @iblancasa      |
 | v0.126.0 | @frzifus        |
 | v0.127.0 | @yuriolisa      |
 | v0.128.0 | @pavolloffay    |
 | v0.129.0 | @TylerHelmuth   |
+| v0.130.0 | @jaronoff97     |
diff --git a/apis/v1beta1/config.go b/apis/v1beta1/config.go
index fd84156e..4fdc785f 100644
--- a/apis/v1beta1/config.go
+++ b/apis/v1beta1/config.go
@@ -462,6 +462,9 @@ func (s *Service) ApplyDefaults(logger logr.Logger) error {
 
 	if tel == nil {
 		tel = &Telemetry{}
+		s.Telemetry = &AnyConfig{
+			Object: map[string]interface{}{},
+		}
 	}
 
 	if tel.Metrics.Address != "" || len(tel.Metrics.Readers) != 0 {
@@ -477,11 +480,14 @@ func (s *Service) ApplyDefaults(logger logr.Logger) error {
 	reader := AddPrometheusMetricsEndpoint(host, port)
 	tel.Metrics.Readers = append(tel.Metrics.Readers, reader)
 
-	s.Telemetry, err = tel.ToAnyConfig()
+	telConfig, err := tel.ToAnyConfig()
 	if err != nil {
 		return err
 	}
 
+	if err := mergo.Merge(&s.Telemetry.Object, telConfig.Object); err != nil {
+		return err
+	}
 	return nil
 }
 
diff --git a/autoinstrumentation/nodejs/package.json b/autoinstrumentation/nodejs/package.json
index 14734573..c0547b17 100644
--- a/autoinstrumentation/nodejs/package.json
+++ b/autoinstrumentation/nodejs/package.json
@@ -13,6 +13,6 @@
     },
     "dependencies": {
         "@opentelemetry/api": "1.9.0",
-        "@opentelemetry/auto-instrumentations-node": "0.56.0"
+        "@opentelemetry/auto-instrumentations-node": "0.58.0"
     }
 }
diff --git a/autoinstrumentation/python/requirements.txt b/autoinstrumentation/python/requirements.txt
index 76fdca23..6c6222e0 100644
--- a/autoinstrumentation/python/requirements.txt
+++ b/autoinstrumentation/python/requirements.txt
@@ -1,4 +1,4 @@
-opentelemetry-distro==0.51b0
+opentelemetry-distro==0.53b1
 
 # We add an upper limit to urllib3 version else Python 3.8 support is dropped.
 # TODO: Update autoinstrumentation image builds to support all supported Python versions
@@ -6,63 +6,63 @@ opentelemetry-distro==0.51b0
 urllib3 < 2.3.0
 # We don't use the distro[otlp] option which automatically includes exporters since gRPC is not appropriate for
 # injected auto-instrumentation, where it has a strict dependency on the OS / Python version the artifact is built for.
-opentelemetry-exporter-otlp-proto-http==1.30.0
-opentelemetry-exporter-prometheus==0.51b0
+opentelemetry-exporter-otlp-proto-http==1.32.1
+opentelemetry-exporter-prometheus==0.53b1
 
-opentelemetry-propagator-b3==1.30.0
-opentelemetry-propagator-jaeger==1.30.0
+opentelemetry-propagator-b3==1.32.1
+opentelemetry-propagator-jaeger==1.32.1
 opentelemetry-propagator-aws-xray==1.0.2
 
-opentelemetry-instrumentation==0.51b0
-opentelemetry-propagator-ot-trace==0.51b0
+opentelemetry-instrumentation==0.53b1
+opentelemetry-propagator-ot-trace==0.53b1
 
 # Copied in from https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation
-opentelemetry-instrumentation-aio-pika==0.51b0
-opentelemetry-instrumentation-aiohttp-client==0.51b0
-opentelemetry-instrumentation-aiohttp-server==0.51b0
-opentelemetry-instrumentation-aiokafka==0.51b0
-opentelemetry-instrumentation-aiopg==0.51b0
-opentelemetry-instrumentation-asgi==0.51b0
-opentelemetry-instrumentation-asyncio==0.51b0
-opentelemetry-instrumentation-asyncpg==0.51b0
-opentelemetry-instrumentation-aws-lambda==0.51b0
-opentelemetry-instrumentation-boto==0.51b0
-opentelemetry-instrumentation-boto3sqs==0.51b0
-opentelemetry-instrumentation-botocore==0.51b0
-opentelemetry-instrumentation-cassandra==0.51b0
-opentelemetry-instrumentation-celery==0.51b0
-opentelemetry-instrumentation-click==0.51b0
-opentelemetry-instrumentation-confluent-kafka==0.51b0
-opentelemetry-instrumentation-dbapi==0.51b0
-opentelemetry-instrumentation-django==0.51b0
-opentelemetry-instrumentation-elasticsearch==0.51b0
-opentelemetry-instrumentation-falcon==0.51b0
-opentelemetry-instrumentation-fastapi==0.51b0
-opentelemetry-instrumentation-flask==0.51b0
-opentelemetry-instrumentation-grpc==0.51b0
-opentelemetry-instrumentation-httpx==0.51b0
-opentelemetry-instrumentation-jinja2==0.51b0
-opentelemetry-instrumentation-kafka-python==0.51b0
-opentelemetry-instrumentation-logging==0.51b0
-opentelemetry-instrumentation-mysql==0.51b0
-opentelemetry-instrumentation-mysqlclient==0.51b0
-opentelemetry-instrumentation-pika==0.51b0
-opentelemetry-instrumentation-psycopg==0.51b0
-opentelemetry-instrumentation-psycopg2==0.51b0
-opentelemetry-instrumentation-pymemcache==0.51b0
-opentelemetry-instrumentation-pymongo==0.51b0
-opentelemetry-instrumentation-pymysql==0.51b0
-opentelemetry-instrumentation-pyramid==0.51b0
-opentelemetry-instrumentation-redis==0.51b0
-opentelemetry-instrumentation-remoulade==0.51b0
-opentelemetry-instrumentation-requests==0.51b0
-opentelemetry-instrumentation-sqlalchemy==0.51b0
-opentelemetry-instrumentation-sqlite3==0.51b0
-opentelemetry-instrumentation-starlette==0.51b0
-opentelemetry-instrumentation-system-metrics==0.51b0
-opentelemetry-instrumentation-threading==0.51b0
-opentelemetry-instrumentation-tornado==0.51b0
-opentelemetry-instrumentation-tortoiseorm==0.51b0
-opentelemetry-instrumentation-urllib==0.51b0
-opentelemetry-instrumentation-urllib3==0.51b0
-opentelemetry-instrumentation-wsgi==0.51b0
+opentelemetry-instrumentation-aio-pika==0.53b1
+opentelemetry-instrumentation-aiohttp-client==0.53b1
+opentelemetry-instrumentation-aiohttp-server==0.53b1
+opentelemetry-instrumentation-aiokafka==0.53b1
+opentelemetry-instrumentation-aiopg==0.53b1
+opentelemetry-instrumentation-asgi==0.53b1
+opentelemetry-instrumentation-asyncio==0.53b1
+opentelemetry-instrumentation-asyncpg==0.53b1
+opentelemetry-instrumentation-aws-lambda==0.53b1
+opentelemetry-instrumentation-boto==0.53b1
+opentelemetry-instrumentation-boto3sqs==0.53b1
+opentelemetry-instrumentation-botocore==0.53b1
+opentelemetry-instrumentation-cassandra==0.53b1
+opentelemetry-instrumentation-celery==0.53b1
+opentelemetry-instrumentation-click==0.53b1
+opentelemetry-instrumentation-confluent-kafka==0.53b1
+opentelemetry-instrumentation-dbapi==0.53b1
+opentelemetry-instrumentation-django==0.53b1
+opentelemetry-instrumentation-elasticsearch==0.53b1
+opentelemetry-instrumentation-falcon==0.53b1
+opentelemetry-instrumentation-fastapi==0.53b1
+opentelemetry-instrumentation-flask==0.53b1
+opentelemetry-instrumentation-grpc==0.53b1
+opentelemetry-instrumentation-httpx==0.53b1
+opentelemetry-instrumentation-jinja2==0.53b1
+opentelemetry-instrumentation-kafka-python==0.53b1
+opentelemetry-instrumentation-logging==0.53b1
+opentelemetry-instrumentation-mysql==0.53b1
+opentelemetry-instrumentation-mysqlclient==0.53b1
+opentelemetry-instrumentation-pika==0.53b1
+opentelemetry-instrumentation-psycopg==0.53b1
+opentelemetry-instrumentation-psycopg2==0.53b1
+opentelemetry-instrumentation-pymemcache==0.53b1
+opentelemetry-instrumentation-pymongo==0.53b1
+opentelemetry-instrumentation-pymysql==0.53b1
+opentelemetry-instrumentation-pyramid==0.53b1
+opentelemetry-instrumentation-redis==0.53b1
+opentelemetry-instrumentation-remoulade==0.53b1
+opentelemetry-instrumentation-requests==0.53b1
+opentelemetry-instrumentation-sqlalchemy==0.53b1
+opentelemetry-instrumentation-sqlite3==0.53b1
+opentelemetry-instrumentation-starlette==0.53b1
+opentelemetry-instrumentation-system-metrics==0.53b1
+opentelemetry-instrumentation-threading==0.53b1
+opentelemetry-instrumentation-tornado==0.53b1
+opentelemetry-instrumentation-tortoiseorm==0.53b1
+opentelemetry-instrumentation-urllib==0.53b1
+opentelemetry-instrumentation-urllib3==0.53b1
+opentelemetry-instrumentation-wsgi==0.53b1
diff --git a/bundle/community/manifests/opentelemetry-operator.clusterserviceversion.yaml b/bundle/community/manifests/opentelemetry-operator.clusterserviceversion.yaml
index dd3c08ea..fd1f6255 100644
--- a/bundle/community/manifests/opentelemetry-operator.clusterserviceversion.yaml
+++ b/bundle/community/manifests/opentelemetry-operator.clusterserviceversion.yaml
@@ -99,13 +99,13 @@ metadata:
     categories: Logging & Tracing,Monitoring
     certified: "false"
     containerImage: ghcr.io/open-telemetry/opentelemetry-operator/opentelemetry-operator
-    createdAt: "2025-04-10T17:47:00Z"
+    createdAt: "2025-04-16T17:49:47Z"
     description: Provides the OpenTelemetry components, including the Collector
     operators.operatorframework.io/builder: operator-sdk-v1.29.0
     operators.operatorframework.io/project_layout: go.kubebuilder.io/v3
     repository: github.com/open-telemetry/opentelemetry-operator
     support: OpenTelemetry Community
-  name: opentelemetry-operator.v0.122.0
+  name: opentelemetry-operator.v0.123.0
   namespace: placeholder
 spec:
   apiservicedefinitions: {}
@@ -529,7 +529,7 @@ spec:
                   valueFrom:
                     fieldRef:
                       fieldPath: spec.serviceAccountName
-                image: ghcr.io/open-telemetry/opentelemetry-operator/opentelemetry-operator:0.122.0
+                image: ghcr.io/open-telemetry/opentelemetry-operator/opentelemetry-operator:0.123.0
                 livenessProbe:
                   httpGet:
                     path: /healthz
@@ -637,7 +637,7 @@ spec:
   minKubeVersion: 1.23.0
   provider:
     name: OpenTelemetry Community
-  version: 0.122.0
+  version: 0.123.0
   webhookdefinitions:
   - admissionReviewVersions:
     - v1alpha1
diff --git a/bundle/openshift/manifests/opentelemetry-operator.clusterserviceversion.yaml b/bundle/openshift/manifests/opentelemetry-operator.clusterserviceversion.yaml
index 432679d4..f60600cf 100644
--- a/bundle/openshift/manifests/opentelemetry-operator.clusterserviceversion.yaml
+++ b/bundle/openshift/manifests/opentelemetry-operator.clusterserviceversion.yaml
@@ -99,13 +99,13 @@ metadata:
     categories: Logging & Tracing,Monitoring
     certified: "false"
     containerImage: ghcr.io/open-telemetry/opentelemetry-operator/opentelemetry-operator
-    createdAt: "2025-04-10T17:47:01Z"
+    createdAt: "2025-04-16T17:49:48Z"
     description: Provides the OpenTelemetry components, including the Collector
     operators.operatorframework.io/builder: operator-sdk-v1.29.0
     operators.operatorframework.io/project_layout: go.kubebuilder.io/v3
     repository: github.com/open-telemetry/opentelemetry-operator
     support: OpenTelemetry Community
-  name: opentelemetry-operator.v0.122.0
+  name: opentelemetry-operator.v0.123.0
   namespace: placeholder
 spec:
   apiservicedefinitions: {}
@@ -534,7 +534,7 @@ spec:
                   valueFrom:
                     fieldRef:
                       fieldPath: spec.serviceAccountName
-                image: ghcr.io/open-telemetry/opentelemetry-operator/opentelemetry-operator:0.122.0
+                image: ghcr.io/open-telemetry/opentelemetry-operator/opentelemetry-operator:0.123.0
                 livenessProbe:
                   httpGet:
                     path: /healthz
@@ -653,7 +653,7 @@ spec:
   minKubeVersion: 1.23.0
   provider:
     name: OpenTelemetry Community
-  version: 0.122.0
+  version: 0.123.0
   webhookdefinitions:
   - admissionReviewVersions:
     - v1alpha1
diff --git a/cmd/operator-opamp-bridge/Dockerfile b/cmd/operator-opamp-bridge/Dockerfile
index 89c84f4d..bdc07692 100644
--- a/cmd/operator-opamp-bridge/Dockerfile
+++ b/cmd/operator-opamp-bridge/Dockerfile
@@ -1,5 +1,5 @@
 # Get CA certificates from the Alpine package repo
-FROM alpine:3.21 as certificates
+FROM alpine:3.21 AS certificates
 
 RUN apk --no-cache add ca-certificates
 
diff --git a/cmd/operator-opamp-bridge/agent/agent.go b/cmd/operator-opamp-bridge/internal/agent/agent.go
similarity index 89%
rename from cmd/operator-opamp-bridge/agent/agent.go
rename to cmd/operator-opamp-bridge/internal/agent/agent.go
index 488e3af8..073c5557 100644
--- a/cmd/operator-opamp-bridge/agent/agent.go
+++ b/cmd/operator-opamp-bridge/internal/agent/agent.go
@@ -21,9 +21,15 @@ import (
 	"sigs.k8s.io/yaml"
 
 	"github.com/open-telemetry/opentelemetry-operator/apis/v1beta1"
-	"github.com/open-telemetry/opentelemetry-operator/cmd/operator-opamp-bridge/config"
-	"github.com/open-telemetry/opentelemetry-operator/cmd/operator-opamp-bridge/metrics"
-	"github.com/open-telemetry/opentelemetry-operator/cmd/operator-opamp-bridge/operator"
+	"github.com/open-telemetry/opentelemetry-operator/cmd/operator-opamp-bridge/internal/config"
+	"github.com/open-telemetry/opentelemetry-operator/cmd/operator-opamp-bridge/internal/metrics"
+	"github.com/open-telemetry/opentelemetry-operator/cmd/operator-opamp-bridge/internal/operator"
+	"github.com/open-telemetry/opentelemetry-operator/cmd/operator-opamp-bridge/internal/proxy"
+)
+
+const (
+	// proxyPrefix is included to make clear if a collector configuration is proxied.
+	proxyPrefix = "proxy:"
 )
 
 type Agent struct {
@@ -39,6 +45,7 @@ type Agent struct {
 	remoteConfigStatus *protobufs.RemoteConfigStatus
 
 	opampClient         client.OpAMPClient
+	proxy               proxy.Server
 	metricReporter      *metrics.MetricReporter
 	config              *config.Config
 	applier             operator.ConfigApplier
@@ -48,19 +55,20 @@ type Agent struct {
 	ticker *time.Ticker
 }
 
-func NewAgent(logger logr.Logger, applier operator.ConfigApplier, config *config.Config, opampClient client.OpAMPClient) *Agent {
+func NewAgent(logger logr.Logger, applier operator.ConfigApplier, cfg *config.Config, opampClient client.OpAMPClient, p proxy.Server) *Agent {
 	var t *time.Ticker
-	if config.HeartbeatInterval > 0 {
-		t = time.NewTicker(config.HeartbeatInterval)
+	if cfg.HeartbeatInterval > 0 {
+		t = time.NewTicker(cfg.HeartbeatInterval)
 	}
 	agent := &Agent{
-		config:              config,
+		config:              cfg,
 		applier:             applier,
+		proxy:               p,
 		logger:              logger,
 		appliedKeys:         map[kubeResourceKey]bool{},
-		instanceId:          config.GetInstanceId(),
-		agentDescription:    config.GetDescription(),
-		remoteConfigEnabled: config.RemoteConfigEnabled(),
+		instanceId:          cfg.GetInstanceId(),
+		agentDescription:    cfg.GetDescription(),
+		remoteConfigEnabled: cfg.RemoteConfigEnabled(),
 		opampClient:         opampClient,
 		clock:               clock.RealClock{},
 		done:                make(chan struct{}, 1),
@@ -69,8 +77,8 @@ func NewAgent(logger logr.Logger, applier operator.ConfigApplier, config *config
 
 	agent.logger.V(3).Info("Agent created",
 		"instanceId", agent.instanceId.String(),
-		"agentType", config.GetAgentType(),
-		"agentVersion", config.GetAgentVersion())
+		"agentType", cfg.GetAgentType(),
+		"agentVersion", cfg.GetAgentVersion())
 
 	return agent
 }
@@ -138,6 +146,10 @@ func (agent *Agent) generateCollectorPoolHealth() (map[string]*protobufs.Compone
 			Healthy:            isPoolHealthy,
 		}
 	}
+	// TODO: Figure out how to tie this to the agent health from the proxy.
+	for instance, health := range agent.proxy.GetHealth() {
+		healthMap[instance.String()] = health
+	}
 	return healthMap, nil
 }
 
@@ -260,12 +272,35 @@ func (agent *Agent) Start() error {
 	if agent.config.HeartbeatInterval > 0 {
 		go agent.runHeartbeat()
 	}
+	go agent.checkForProxyUpdates()
 
 	agent.logger.V(3).Info("OpAMP Client started.")
 
 	return nil
 }
 
+func (agent *Agent) checkForProxyUpdates() {
+	for {
+		select {
+		case <-agent.proxy.HasUpdates():
+			agent.logger.Info("new update from agent proxy")
+			err := agent.opampClient.SetHealth(agent.getHealth())
+			if err != nil {
+				agent.logger.Error(err, "failed to update from proxy")
+				return
+			}
+			updateErr := agent.opampClient.UpdateEffectiveConfig(context.Background())
+			if updateErr != nil {
+				agent.logger.Error(updateErr, "failed to update from proxy")
+				return
+			}
+		case <-agent.done:
+			agent.logger.Info("stopping check for proxy updates")
+			return
+		}
+	}
+}
+
 // runHeartbeat sets health on an interval to keep the connection active.
 func (agent *Agent) runHeartbeat() {
 	if agent.ticker == nil {
@@ -320,6 +355,11 @@ func (agent *Agent) getEffectiveConfig(ctx context.Context) (*protobufs.Effectiv
 			ContentType: "yaml",
 		}
 	}
+	for id, instance := range agent.proxy.GetConfigurations() {
+		if cfg, ok := instance.GetConfigMap().GetConfigMap()[""]; ok {
+			instanceMap[proxyPrefix+id.String()] = cfg
+		}
+	}
 	return &protobufs.EffectiveConfig{
 		ConfigMap: &protobufs.AgentConfigMap{
 			ConfigMap: instanceMap,
diff --git a/cmd/operator-opamp-bridge/agent/kube_resource_key.go b/cmd/operator-opamp-bridge/internal/agent/kube_resource_key.go
similarity index 100%
rename from cmd/operator-opamp-bridge/agent/kube_resource_key.go
rename to cmd/operator-opamp-bridge/internal/agent/kube_resource_key.go
diff --git a/cmd/operator-opamp-bridge/config/config.go b/cmd/operator-opamp-bridge/internal/config/config.go
similarity index 99%
rename from cmd/operator-opamp-bridge/config/config.go
rename to cmd/operator-opamp-bridge/internal/config/config.go
index 6dad4517..f1ad91d5 100644
--- a/cmd/operator-opamp-bridge/config/config.go
+++ b/cmd/operator-opamp-bridge/internal/config/config.go
@@ -30,7 +30,7 @@ import (
 
 	"github.com/open-telemetry/opentelemetry-operator/apis/v1alpha1"
 	"github.com/open-telemetry/opentelemetry-operator/apis/v1beta1"
-	"github.com/open-telemetry/opentelemetry-operator/cmd/operator-opamp-bridge/logger"
+	"github.com/open-telemetry/opentelemetry-operator/cmd/operator-opamp-bridge/internal/logger"
 )
 
 const (
diff --git a/cmd/operator-opamp-bridge/config/flags.go b/cmd/operator-opamp-bridge/internal/config/flags.go
similarity index 100%
rename from cmd/operator-opamp-bridge/config/flags.go
rename to cmd/operator-opamp-bridge/internal/config/flags.go
diff --git a/cmd/operator-opamp-bridge/config/headers.go b/cmd/operator-opamp-bridge/internal/config/headers.go
similarity index 100%
rename from cmd/operator-opamp-bridge/config/headers.go
rename to cmd/operator-opamp-bridge/internal/config/headers.go
diff --git a/cmd/operator-opamp-bridge/logger/logger.go b/cmd/operator-opamp-bridge/internal/logger/logger.go
similarity index 100%
rename from cmd/operator-opamp-bridge/logger/logger.go
rename to cmd/operator-opamp-bridge/internal/logger/logger.go
diff --git a/cmd/operator-opamp-bridge/metrics/reporter.go b/cmd/operator-opamp-bridge/internal/metrics/reporter.go
similarity index 100%
rename from cmd/operator-opamp-bridge/metrics/reporter.go
rename to cmd/operator-opamp-bridge/internal/metrics/reporter.go
diff --git a/cmd/operator-opamp-bridge/operator/client.go b/cmd/operator-opamp-bridge/internal/operator/client.go
similarity index 100%
rename from cmd/operator-opamp-bridge/operator/client.go
rename to cmd/operator-opamp-bridge/internal/operator/client.go
diff --git a/cmd/operator-opamp-bridge/internal/proxy/agent.go b/cmd/operator-opamp-bridge/internal/proxy/agent.go
new file mode 100644
index 00000000..73cbcb4e
--- /dev/null
+++ b/cmd/operator-opamp-bridge/internal/proxy/agent.go
@@ -0,0 +1,157 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package proxy
+
+import (
+	"sync"
+
+	"github.com/go-logr/logr"
+	"github.com/google/uuid"
+	"github.com/open-telemetry/opamp-go/protobufs"
+	"github.com/open-telemetry/opamp-go/server/types"
+	"google.golang.org/protobuf/proto"
+)
+
+type Agent struct {
+	// logger for the agentImpl
+	logger logr.Logger
+	// Agent's instance id. This is an immutable field.
+	InstanceId uuid.UUID
+	// Connection to the Agent.
+	conn types.Connection
+	// mutex for the fields that follow it.
+	mux sync.RWMutex
+
+	health          *protobufs.ComponentHealth
+	effectiveConfig *protobufs.EffectiveConfig
+	// Agent's current status.
+	Status *protobufs.AgentToServer
+}
+
+func NewAgent(logger logr.Logger, agentId uuid.UUID, conn types.Connection) *Agent {
+	return &Agent{
+		logger:     logger,
+		InstanceId: agentId,
+		conn:       conn,
+	}
+}
+
+func (a *Agent) hasCapability(capability protobufs.AgentCapabilities) bool {
+	if capability < 0 {
+		return false
+	}
+	//nolint:gosec
+	return a.Status.Capabilities&uint64(capability) != 0
+}
+
+// updateAgentDescription assumes that the status is already non-nil.
+func (a *Agent) updateAgentDescription(newStatus *protobufs.AgentToServer) (agentDescrChanged bool) {
+	prevStatus := a.Status
+
+	// Check what's changed in the AgentDescription.
+	if newStatus.AgentDescription != nil {
+		// If the AgentDescription field is set it means the Agent tells us
+		// something is changed in the field since the last status report
+		// (or this is the first report).
+		// Make full comparison of previous and new descriptions to see if it
+		// really is different.
+		if prevStatus != nil && proto.Equal(prevStatus.AgentDescription, newStatus.AgentDescription) {
+			// Agent description didn't change.
+			agentDescrChanged = false
+		} else {
+			// Yes, the description is different, update it.
+			a.Status.AgentDescription = newStatus.AgentDescription
+			agentDescrChanged = true
+		}
+	} else {
+		// AgentDescription field is not set, which means description didn't change.
+		agentDescrChanged = false
+	}
+	return agentDescrChanged
+}
+
+func (a *Agent) updateStatusField(newStatus *protobufs.AgentToServer) (agentDescrChanged bool) {
+	if a.Status == nil {
+		// First time this Agent reports a status, remember it.
+		a.Status = newStatus
+		agentDescrChanged = true
+	}
+	a.Status.SequenceNum = newStatus.SequenceNum
+	return agentDescrChanged || a.updateAgentDescription(newStatus)
+}
+
+func (a *Agent) UpdateStatus(newStatus *protobufs.AgentToServer, response *protobufs.ServerToAgent) bool {
+	a.mux.Lock()
+	defer a.mux.Unlock()
+	// We don't have any status for this Agent, or we lost the previous status update from the Agent, so our
+	// current status is not up-to-date.
+	lostPreviousUpdate := (a.Status == nil) || (a.Status != nil && a.Status.SequenceNum+1 != newStatus.SequenceNum)
+
+	agentDescrChanged := a.updateStatusField(newStatus)
+	// Check if any fields were omitted in the status report.
+	effectiveConfigOmitted := newStatus.EffectiveConfig == nil &&
+		a.hasCapability(protobufs.AgentCapabilities_AgentCapabilities_ReportsEffectiveConfig)
+
+	packageStatusesOmitted := newStatus.PackageStatuses == nil &&
+		a.hasCapability(protobufs.AgentCapabilities_AgentCapabilities_ReportsPackageStatuses)
+
+	remoteConfigStatusOmitted := newStatus.RemoteConfigStatus == nil &&
+		a.hasCapability(protobufs.AgentCapabilities_AgentCapabilities_ReportsRemoteConfig)
+
+	healthOmitted := newStatus.Health == nil &&
+		a.hasCapability(protobufs.AgentCapabilities_AgentCapabilities_ReportsHealth)
+
+	// True if the status was not fully reported.
+	statusIsCompressed := effectiveConfigOmitted || packageStatusesOmitted || remoteConfigStatusOmitted || healthOmitted
+	if statusIsCompressed && lostPreviousUpdate {
+		// The status message is not fully set in the message that we received, but we lost the previous
+		// status update. Request full status update from the a.
+		response.Flags |= uint64(protobufs.ServerToAgentFlags_ServerToAgentFlags_ReportFullState)
+	}
+
+	if agentDescrChanged {
+		// Agent description is changed.
+		// And set connection settings that are appropriate for the Agent description.
+		a.calcConnectionSettings(response)
+	}
+	if newStatus.Health != nil {
+		a.health = newStatus.Health
+	}
+	if newStatus.EffectiveConfig != nil {
+		a.effectiveConfig = newStatus.EffectiveConfig
+	}
+	if newStatus.CustomMessage != nil {
+		a.logger.V(5).Info("received custom message, not implemented")
+	}
+	return agentDescrChanged
+}
+
+func (a *Agent) calcConnectionSettings(response *protobufs.ServerToAgent) {
+	// Here we can use Agent's description to send the appropriate connection
+	// settings to the Agent.
+	// In this simple example the connection settings do not depend on the
+	// Agent description, so we just set them directly.
+
+	response.ConnectionSettings = &protobufs.ConnectionSettingsOffers{
+		Hash:  nil, // TODO: calc has from settings.
+		Opamp: nil,
+		// TODO: The own settings should be sent to the gateway for leaf nodes automatically.
+		OwnMetrics:       nil,
+		OwnTraces:        nil,
+		OwnLogs:          nil,
+		OtherConnections: nil,
+	}
+}
+
+func (a *Agent) GetHealth() *protobufs.ComponentHealth {
+	a.mux.RLock()
+	defer a.mux.RUnlock()
+	return a.health
+}
+
+func (a *Agent) GetConfiguration() *protobufs.EffectiveConfig {
+	a.mux.RLock()
+	defer a.mux.RUnlock()
+	return a.effectiveConfig
+}
diff --git a/cmd/operator-opamp-bridge/internal/proxy/server.go b/cmd/operator-opamp-bridge/internal/proxy/server.go
new file mode 100644
index 00000000..09230714
--- /dev/null
+++ b/cmd/operator-opamp-bridge/internal/proxy/server.go
@@ -0,0 +1,187 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package proxy
+
+import (
+	"context"
+	"errors"
+	"net/http"
+	"sync"
+
+	"github.com/go-logr/logr"
+	"github.com/google/uuid"
+	"github.com/oklog/ulid"
+	"github.com/open-telemetry/opamp-go/protobufs"
+	"github.com/open-telemetry/opamp-go/server"
+	"github.com/open-telemetry/opamp-go/server/types"
+	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
+
+	"github.com/open-telemetry/opentelemetry-operator/cmd/operator-opamp-bridge/internal/logger"
+)
+
+type Server interface {
+	GetConfigurations() map[uuid.UUID]*protobufs.EffectiveConfig
+	GetHealth() map[uuid.UUID]*protobufs.ComponentHealth
+	HasUpdates() <-chan struct{}
+}
+
+var _ Server = &OpAMPProxy{}
+
+type OpAMPProxy struct {
+	server.OpAMPServer
+
+	// endpoint for service
+	endpoint string
+
+	// logger
+	logger logr.Logger
+
+	// updatesChan indicates if there any new agents
+	updatesChan chan struct{}
+	// internal state
+	mux        sync.RWMutex
+	agentsById map[uuid.UUID]*Agent
+	// connections map is required because that's the only way we know to remove an agent.
+	connections map[types.Connection]map[uuid.UUID]bool
+}
+
+func NewOpAMPProxy(l logr.Logger, endpoint string) *OpAMPProxy {
+	return &OpAMPProxy{
+		logger:      l,
+		OpAMPServer: server.New(logger.NewLogger(l)),
+		endpoint:    endpoint,
+		agentsById:  map[uuid.UUID]*Agent{},
+		connections: map[types.Connection]map[uuid.UUID]bool{},
+		updatesChan: make(chan struct{}, 1),
+	}
+}
+
+func (s *OpAMPProxy) Start() error {
+	settings := server.StartSettings{
+		Settings: server.Settings{
+			CustomCapabilities: []string{},
+			Callbacks: server.CallbacksStruct{
+				OnConnectingFunc: func(request *http.Request) types.ConnectionResponse {
+					return types.ConnectionResponse{
+						Accept: true,
+						ConnectionCallbacks: server.ConnectionCallbacksStruct{
+							OnMessageFunc:         s.onMessage,
+							OnConnectionCloseFunc: s.onDisconnect,
+						},
+					}
+				},
+			},
+		},
+		ListenEndpoint: s.endpoint,
+		HTTPMiddleware: otelhttp.NewMiddleware("/v1/opamp"),
+	}
+	// TODO: In the future we will probably want some TLS configuration.
+	// tlsConfig, err := internal.CreateServerTLSConfig(
+	// 	"../../certs/certs/ca.cert.pem",
+	// 	"../../certs/server_certs/server.cert.pem",
+	// 	"../../certs/server_certs/server.key.pem",
+	// )
+	// if err != nil {
+	// 	srv.logger.Debugf(context.Background(), "Could not load TLS config, working without TLS: %v", err.Error())
+	// }
+	// settings.TLSConfig = tlsConfig
+	s.logger.Info("starting opamp server", "address", s.endpoint)
+	return s.OpAMPServer.Start(settings)
+}
+
+func (s *OpAMPProxy) Stop(ctx context.Context) error {
+	return s.OpAMPServer.Stop(ctx)
+}
+
+func (s *OpAMPProxy) onDisconnect(conn types.Connection) {
+	s.mux.Lock()
+	defer s.mux.Unlock()
+
+	for instanceId := range s.connections[conn] {
+		delete(s.agentsById, instanceId)
+	}
+	delete(s.connections, conn)
+	// Tell listeners to get updates.
+	s.updatesChan <- struct{}{}
+}
+
+func (s *OpAMPProxy) onMessage(ctx context.Context, conn types.Connection, msg *protobufs.AgentToServer) *protobufs.ServerToAgent {
+	// Start building the response.
+	response := &protobufs.ServerToAgent{}
+
+	instanceId, err := getInstanceId(msg)
+	if err != nil {
+		s.logger.Error(err, "failed to get instance id")
+		response.ErrorResponse = &protobufs.ServerErrorResponse{
+			ErrorMessage: err.Error(),
+		}
+		return response
+	}
+	s.logger.V(5).Info("received message", "instance ID", instanceId)
+
+	agentUpdated := false
+	s.mux.Lock()
+	if _, ok := s.agentsById[instanceId]; !ok {
+		s.agentsById[instanceId] = NewAgent(s.logger.WithValues("instanceId", instanceId.String()), instanceId, conn)
+		// Ensure the Agent's instance id is associated with the connection.
+		if s.connections[conn] == nil {
+			s.connections[conn] = map[uuid.UUID]bool{}
+		}
+		s.connections[conn][instanceId] = true
+		agentUpdated = true
+	}
+	agentUpdated = s.agentsById[instanceId].UpdateStatus(msg, response) || agentUpdated
+	s.mux.Unlock()
+	if agentUpdated {
+		s.updatesChan <- struct{}{}
+	}
+	// Send the response back to the Agent.
+	return response
+}
+
+// GetConfigurations implements Server.
+func (s *OpAMPProxy) GetConfigurations() map[uuid.UUID]*protobufs.EffectiveConfig {
+	s.mux.RLock()
+	defer s.mux.RUnlock()
+	toReturn := make(map[uuid.UUID]*protobufs.EffectiveConfig, len(s.agentsById))
+	for i, agent := range s.agentsById {
+		toReturn[i] = agent.GetConfiguration()
+	}
+	return toReturn
+}
+
+// GetHealth implements Server.
+func (s *OpAMPProxy) GetHealth() map[uuid.UUID]*protobufs.ComponentHealth {
+	s.mux.RLock()
+	defer s.mux.RUnlock()
+	toReturn := make(map[uuid.UUID]*protobufs.ComponentHealth, len(s.agentsById))
+	for i, agent := range s.agentsById {
+		toReturn[i] = agent.GetHealth()
+	}
+	return toReturn
+}
+
+// HasUpdates implements Server.
+func (s *OpAMPProxy) HasUpdates() <-chan struct{} {
+	return s.updatesChan
+}
+
+func getInstanceId(msg *protobufs.AgentToServer) (uuid.UUID, error) {
+	var instanceId uuid.UUID
+
+	if len(msg.InstanceUid) == 26 {
+		// This is an old-style ULID.
+		u, err := ulid.Parse(string(msg.InstanceUid))
+		if err != nil {
+			return instanceId, err
+		}
+		instanceId = uuid.UUID(u)
+	} else if len(msg.InstanceUid) == 16 {
+		// This is a 16 byte, new style UID.
+		instanceId = uuid.UUID(msg.InstanceUid)
+	} else {
+		return instanceId, errors.New("invalid length of msg.InstanceUid")
+	}
+	return instanceId, nil
+}
diff --git a/cmd/operator-opamp-bridge/main.go b/cmd/operator-opamp-bridge/main.go
index 9168440c..8058f572 100644
--- a/cmd/operator-opamp-bridge/main.go
+++ b/cmd/operator-opamp-bridge/main.go
@@ -4,14 +4,16 @@
 package main
 
 import (
+	"context"
 	"os"
 	"os/signal"
 
 	"github.com/spf13/pflag"
 
-	"github.com/open-telemetry/opentelemetry-operator/cmd/operator-opamp-bridge/agent"
-	"github.com/open-telemetry/opentelemetry-operator/cmd/operator-opamp-bridge/config"
-	"github.com/open-telemetry/opentelemetry-operator/cmd/operator-opamp-bridge/operator"
+	"github.com/open-telemetry/opentelemetry-operator/cmd/operator-opamp-bridge/internal/agent"
+	"github.com/open-telemetry/opentelemetry-operator/cmd/operator-opamp-bridge/internal/config"
+	"github.com/open-telemetry/opentelemetry-operator/cmd/operator-opamp-bridge/internal/operator"
+	"github.com/open-telemetry/opentelemetry-operator/cmd/operator-opamp-bridge/internal/proxy"
 )
 
 func main() {
@@ -38,15 +40,24 @@ func main() {
 	operatorClient := operator.NewClient(cfg.Name, l.WithName("operator-client"), kubeClient, cfg.GetComponentsAllowed())
 
 	opampClient := cfg.CreateClient()
-	opampAgent := agent.NewAgent(l.WithName("agent"), operatorClient, cfg, opampClient)
+	opampProxy := proxy.NewOpAMPProxy(l.WithName("server"), cfg.ListenAddr)
+	opampAgent := agent.NewAgent(l.WithName("agent"), operatorClient, cfg, opampClient, opampProxy)
 
 	if err := opampAgent.Start(); err != nil {
 		l.Error(err, "Cannot start OpAMP client")
 		os.Exit(1)
 	}
+	if err := opampProxy.Start(); err != nil {
+		l.Error(err, "failed to start OpAMP Server")
+		os.Exit(1)
+	}
 
 	interrupt := make(chan os.Signal, 1)
 	signal.Notify(interrupt, os.Interrupt)
 	<-interrupt
 	opampAgent.Shutdown()
+	proxyStopErr := opampProxy.Stop(context.Background())
+	if proxyStopErr != nil {
+		l.Error(proxyStopErr, "failed to shutdown proxy server")
+	}
 }
diff --git a/controllers/opentelemetrycollector_controller.go b/controllers/opentelemetrycollector_controller.go
index 0ec160bd..3ff4300f 100644
--- a/controllers/opentelemetrycollector_controller.go
+++ b/controllers/opentelemetrycollector_controller.go
@@ -7,6 +7,7 @@ package controllers
 import (
 	"context"
 	"sort"
+	"time"
 
 	"github.com/go-logr/logr"
 	routev1 "github.com/openshift/api/route/v1"
@@ -39,6 +40,8 @@ import (
 	"github.com/open-telemetry/opentelemetry-operator/internal/manifests/manifestutils"
 	internalRbac "github.com/open-telemetry/opentelemetry-operator/internal/rbac"
 	collectorStatus "github.com/open-telemetry/opentelemetry-operator/internal/status/collector"
+	"github.com/open-telemetry/opentelemetry-operator/internal/version"
+	"github.com/open-telemetry/opentelemetry-operator/pkg/collector/upgrade"
 	"github.com/open-telemetry/opentelemetry-operator/pkg/constants"
 	"github.com/open-telemetry/opentelemetry-operator/pkg/featuregate"
 )
@@ -60,6 +63,7 @@ type OpenTelemetryCollectorReconciler struct {
 	log      logr.Logger
 	config   config.Config
 	reviewer *internalRbac.Reviewer
+	upgrade  *upgrade.VersionUpgrade
 }
 
 // Params is the set of options to build a new OpenTelemetryCollectorReconciler.
@@ -70,6 +74,7 @@ type Params struct {
 	Log      logr.Logger
 	Config   config.Config
 	Reviewer *internalRbac.Reviewer
+	Version  version.Version
 }
 
 func (r *OpenTelemetryCollectorReconciler) findOtelOwnedObjects(ctx context.Context, params manifests.Params) (map[types.UID]client.Object, error) {
@@ -187,6 +192,13 @@ func (r *OpenTelemetryCollectorReconciler) getTargetAllocator(ctx context.Contex
 
 // NewReconciler creates a new reconciler for OpenTelemetryCollector objects.
 func NewReconciler(p Params) *OpenTelemetryCollectorReconciler {
+	up := &upgrade.VersionUpgrade{
+		Client:   p.Client,
+		Log:      p.Log.WithName("collector-upgrade"),
+		Recorder: p.Recorder,
+		Version:  p.Version,
+	}
+
 	r := &OpenTelemetryCollectorReconciler{
 		Client:   p.Client,
 		log:      p.Log,
@@ -194,6 +206,7 @@ func NewReconciler(p Params) *OpenTelemetryCollectorReconciler {
 		config:   p.Config,
 		recorder: p.Recorder,
 		reviewer: p.Reviewer,
+		upgrade:  up,
 	}
 	return r
 }
@@ -263,6 +276,15 @@ func (r *OpenTelemetryCollectorReconciler) Reconcile(ctx context.Context, req ct
 		return ctrl.Result{}, nil
 	}
 
+	if r.upgrade.NeedsUpgrade(instance) {
+		err = r.upgrade.Upgrade(ctx, instance)
+		if err != nil {
+			return ctrl.Result{}, err
+		}
+		// if the OpenTelemetryCollector CR was upgraded (modified), return here and re-queue the reconcile event.
+		return ctrl.Result{Requeue: true, RequeueAfter: 1 * time.Second}, nil
+	}
+
 	// Add finalizer for this CR
 	if !controllerutil.ContainsFinalizer(&instance, collectorFinalizer) {
 		if controllerutil.AddFinalizer(&instance, collectorFinalizer) {
diff --git a/docs/compatibility.md b/docs/compatibility.md
index d03b39ba..547f00e7 100644
--- a/docs/compatibility.md
+++ b/docs/compatibility.md
@@ -46,7 +46,8 @@ The OpenTelemetry Operator _might_ work on versions outside of the given range,
 
 | OpenTelemetry Operator | Kubernetes     | Cert-Manager | Prometheus-Operator |
 |------------------------|----------------|--------------|---------------------|
-| v0.122.0               | v1.23 to v1.32 | v1           | v0.81.0             |
+| v0.123.0               | v1.23 to v1.32 | v1           | v0.80.1             |
+| v0.122.0               | v1.23 to v1.32 | v1           | v0.80.1             |
 | v0.121.0               | v1.23 to v1.32 | v1           | v0.78.2             |
 | v0.120.0               | v1.23 to v1.32 | v1           | v0.76.2             |
 | v0.119.0               | v1.23 to v1.32 | v1           | v0.76.2             |
@@ -70,7 +71,6 @@ The OpenTelemetry Operator _might_ work on versions outside of the given range,
 | v0.101.0               | v1.23 to v1.30 | v1           | v0.71.2             |
 | v0.100.0               | v1.23 to v1.29 | v1           | v0.71.2             |
 | v0.99.0                | v1.23 to v1.29 | v1           | v0.71.2             |
-| v0.98.0                | v1.23 to v1.29 | v1           | v0.71.2             |
 
 [kubernetes_releases]: https://kubernetes.io/releases/
 [openshift_support]: https://access.redhat.com/support/policy/updates/openshift
diff --git a/go.mod b/go.mod
index 9169904d..4f3de64d 100644
--- a/go.mod
+++ b/go.mod
@@ -28,13 +28,13 @@ require (
 	github.com/prometheus-operator/prometheus-operator v0.80.1
 	github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.80.1
 	github.com/prometheus-operator/prometheus-operator/pkg/client v0.80.1
-	github.com/prometheus/client_golang v1.21.1
+	github.com/prometheus/client_golang v1.22.0
 	github.com/prometheus/common v0.63.0
 	github.com/prometheus/prometheus v0.301.0
 	github.com/shirou/gopsutil v3.21.11+incompatible
 	github.com/spf13/pflag v1.0.6
 	github.com/stretchr/testify v1.10.0
-	go.opentelemetry.io/collector/featuregate v1.29.0
+	go.opentelemetry.io/collector/featuregate v1.30.0
 	go.opentelemetry.io/otel v1.35.0
 	go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0
 	go.opentelemetry.io/otel/exporters/prometheus v0.57.0
@@ -153,7 +153,6 @@ require (
 	github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 // indirect
 	github.com/josharian/intern v1.0.0 // indirect
 	github.com/jpillora/backoff v1.0.0 // indirect
-	github.com/klauspost/compress v1.18.0 // indirect
 	github.com/klauspost/cpuid/v2 v2.2.8 // indirect
 	github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
 	github.com/kylelemons/godebug v1.1.0 // indirect
@@ -170,7 +169,7 @@ require (
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
 	github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
-	github.com/oklog/ulid v1.3.1 // indirect
+	github.com/oklog/ulid v1.3.1
 	github.com/opencontainers/go-digest v1.0.0 // indirect
 	github.com/opencontainers/image-spec v1.0.2 // indirect
 	github.com/opentracing/opentracing-go v1.2.0 // indirect
@@ -197,7 +196,7 @@ require (
 	github.com/yusufpapurcu/wmi v1.2.4 // indirect
 	go.mongodb.org/mongo-driver v1.17.2 // indirect
 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
-	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
+	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0
 	go.opentelemetry.io/otel/trace v1.35.0 // indirect
 	go.opentelemetry.io/proto/otlp v1.5.0 // indirect
 	go.uber.org/atomic v1.11.0 // indirect
@@ -205,7 +204,7 @@ require (
 	golang.org/x/crypto v0.36.0 // indirect
 	golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
 	golang.org/x/mod v0.22.0 // indirect
-	golang.org/x/net v0.37.0 // indirect
+	golang.org/x/net v0.38.0 // indirect
 	golang.org/x/oauth2 v0.28.0 // indirect
 	golang.org/x/sync v0.12.0 // indirect
 	golang.org/x/sys v0.31.0 // indirect
@@ -218,7 +217,7 @@ require (
 	google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20250227231956-55c901821b1e // indirect
 	google.golang.org/grpc v1.71.0 // indirect
-	google.golang.org/protobuf v1.36.6 // indirect
+	google.golang.org/protobuf v1.36.6
 	gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect
diff --git a/go.sum b/go.sum
index 8625f9fd..6e4e44e2 100644
--- a/go.sum
+++ b/go.sum
@@ -451,8 +451,8 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP
 github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
 github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
 github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
-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_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
+github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
 github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
 github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@@ -533,8 +533,8 @@ go.mongodb.org/mongo-driver v1.17.2 h1:gvZyk8352qSfzyZ2UMWcpDpMSGEr1eqE4T793Sqyh
 go.mongodb.org/mongo-driver v1.17.2/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
 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/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/featuregate v1.30.0 h1:mx7+iP/FQnY7KO8qw/xE3Qd1MQkWcU8VgcqLNrJ8EU8=
+go.opentelemetry.io/collector/featuregate v1.30.0/go.mod h1:Y/KsHbvREENKvvN9RlpiWk/IGBK+CATBYzIIpU7nccc=
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
 go.opentelemetry.io/contrib/otelconf v0.15.0 h1:BLNiIUsrNcqhSKpsa6CnhE6LdrpY1A8X0szMVsu99eo=
@@ -612,8 +612,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
-golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
+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-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
diff --git a/internal/components/extensions/helpers.go b/internal/components/extensions/helpers.go
index 3b8ff264..9d7cbd59 100644
--- a/internal/components/extensions/helpers.go
+++ b/internal/components/extensions/helpers.go
@@ -23,8 +23,7 @@ var registry = map[string]components.Parser{
 			return components.ParseSingleEndpointSilent(logger, name, defaultPort, &config.SingleEndpointConfig)
 		}).
 		MustBuild(),
-	"jaeger_query": components.NewSinglePortParserBuilder("jaeger_query", 16686).
-		WithTargetPort(16686).
+	"jaeger_query": NewJaegerQueryExtensionParserBuilder().
 		MustBuild(),
 }
 
diff --git a/internal/components/extensions/jaeger_query_extension.go b/internal/components/extensions/jaeger_query_extension.go
new file mode 100644
index 00000000..60735a9c
--- /dev/null
+++ b/internal/components/extensions/jaeger_query_extension.go
@@ -0,0 +1,88 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package extensions
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/go-logr/logr"
+	"github.com/mitchellh/mapstructure"
+	corev1 "k8s.io/api/core/v1"
+
+	"github.com/open-telemetry/opentelemetry-operator/internal/components"
+	"github.com/open-telemetry/opentelemetry-operator/internal/naming"
+)
+
+const (
+	name = "jaeger_query"
+	port = 16686
+)
+
+var (
+	_ components.Parser = &components.GenericParser[*JaegerQueryExtensionConfig]{}
+)
+
+type JaegerQueryExtensionConfig struct {
+	HTTP jaegerHTTPAddress `mapstructure:"http,omitempty" yaml:"http,omitempty"`
+}
+
+type jaegerHTTPAddress struct {
+	Endpoint string `mapstructure:"endpoint,omitempty" yaml:"endpoint,omitempty"`
+}
+
+func (g *JaegerQueryExtensionConfig) GetPortNumOrDefault(logger logr.Logger, p int32) int32 {
+	num, err := g.GetPortNum()
+	if err != nil {
+		logger.V(3).Info("no port set, using default: %d", p)
+		return p
+	}
+	return num
+}
+
+// GetPortNum attempts to get the port for the given config. If it cannot, the UnsetPort and the given missingPortError
+// are returned.
+func (g *JaegerQueryExtensionConfig) GetPortNum() (int32, error) {
+	if len(g.HTTP.Endpoint) > 0 {
+		return components.PortFromEndpoint(g.HTTP.Endpoint)
+	}
+	return components.UnsetPort, components.PortNotFoundErr
+}
+
+func ParseJaegerQueryExtensionConfig(logger logr.Logger, name string, defaultPort *corev1.ServicePort, cfg *JaegerQueryExtensionConfig) ([]corev1.ServicePort, error) {
+	if cfg == nil {
+		return nil, nil
+	}
+	if _, err := cfg.GetPortNum(); err != nil && defaultPort.Port == components.UnsetPort {
+		logger.WithValues("receiver", defaultPort.Name).Error(err, "couldn't parse the endpoint's port and no default port set")
+		return []corev1.ServicePort{}, err
+	}
+	port := cfg.GetPortNumOrDefault(logger, defaultPort.Port)
+	svcPort := defaultPort
+	svcPort.Name = naming.PortName(name, port)
+	return []corev1.ServicePort{components.ConstructServicePort(svcPort, port)}, nil
+}
+
+func NewJaegerQueryExtensionParserBuilder() components.Builder[*JaegerQueryExtensionConfig] {
+	return components.NewBuilder[*JaegerQueryExtensionConfig]().WithPort(port).WithName(name).WithPortParser(ParseJaegerQueryExtensionConfig).WithDefaultsApplier(endpointDefaulter).WithDefaultRecAddress(components.DefaultRecAddress).WithTargetPort(port)
+}
+
+func endpointDefaulter(_ logr.Logger, defaultRecAddr string, port int32, config *JaegerQueryExtensionConfig) (map[string]interface{}, error) {
+	if config == nil {
+		config = &JaegerQueryExtensionConfig{}
+	}
+
+	if config.HTTP.Endpoint == "" {
+		config.HTTP.Endpoint = fmt.Sprintf("%s:%d", defaultRecAddr, port)
+	} else {
+		v := strings.Split(config.HTTP.Endpoint, ":")
+		if len(v) < 2 || v[0] == "" {
+			config.HTTP.Endpoint = fmt.Sprintf("%s:%s", defaultRecAddr, v[len(v)-1])
+		}
+	}
+
+	res := make(map[string]interface{})
+	err := mapstructure.Decode(config, &res)
+	return res, err
+}
diff --git a/internal/status/collector/handle.go b/internal/status/collector/handle.go
index 2fa8478c..589e5619 100644
--- a/internal/status/collector/handle.go
+++ b/internal/status/collector/handle.go
@@ -8,19 +8,15 @@ import (
 	"fmt"
 
 	"github.com/go-logr/logr"
+	corev1 "k8s.io/api/core/v1"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	"github.com/open-telemetry/opentelemetry-operator/apis/v1beta1"
 	"github.com/open-telemetry/opentelemetry-operator/internal/manifests"
-	"github.com/open-telemetry/opentelemetry-operator/internal/version"
-	collectorupgrade "github.com/open-telemetry/opentelemetry-operator/pkg/collector/upgrade"
 )
 
 const (
-	eventTypeNormal  = "Normal"
-	eventTypeWarning = "Warning"
-
 	reasonError         = "Error"
 	reasonStatusFailure = "StatusFailure"
 	reasonInfo          = "Info"
@@ -30,32 +26,21 @@ const (
 func HandleReconcileStatus(ctx context.Context, log logr.Logger, params manifests.Params, otelcol v1beta1.OpenTelemetryCollector, err error) (ctrl.Result, error) {
 	log.V(2).Info("updating collector status")
 	if err != nil {
-		params.Recorder.Event(&otelcol, eventTypeWarning, reasonError, err.Error())
+		params.Recorder.Event(&otelcol, corev1.EventTypeWarning, reasonError, err.Error())
 		return ctrl.Result{}, err
 	}
-	changed := otelcol.DeepCopy()
 
-	up := &collectorupgrade.VersionUpgrade{
-		Log:      params.Log,
-		Version:  version.Get(),
-		Client:   params.Client,
-		Recorder: params.Recorder,
-	}
-	upgraded, upgradeErr := up.ManagedInstance(ctx, *changed)
-	if upgradeErr != nil {
-		// don't fail to allow setting the status
-		log.V(2).Error(upgradeErr, "failed to upgrade the OpenTelemetry CR")
-	}
-	changed = &upgraded
+	changed := otelcol.DeepCopy()
 	statusErr := updateCollectorStatus(ctx, params.Client, changed)
+
 	if statusErr != nil {
-		params.Recorder.Event(changed, eventTypeWarning, reasonStatusFailure, statusErr.Error())
+		params.Recorder.Event(changed, corev1.EventTypeWarning, reasonStatusFailure, statusErr.Error())
 		return ctrl.Result{}, statusErr
 	}
 	statusPatch := client.MergeFrom(&otelcol)
 	if err := params.Client.Status().Patch(ctx, changed, statusPatch); err != nil {
 		return ctrl.Result{}, fmt.Errorf("failed to apply status changes to the OpenTelemetry CR: %w", err)
 	}
-	params.Recorder.Event(changed, eventTypeNormal, reasonInfo, "applied status changes")
+	params.Recorder.Event(changed, corev1.EventTypeNormal, reasonInfo, "applied status changes")
 	return ctrl.Result{}, nil
 }
diff --git a/main.go b/main.go
index d9754464..bf8f5bf2 100644
--- a/main.go
+++ b/main.go
@@ -52,7 +52,6 @@ import (
 	"github.com/open-telemetry/opentelemetry-operator/internal/rbac"
 	"github.com/open-telemetry/opentelemetry-operator/internal/version"
 	"github.com/open-telemetry/opentelemetry-operator/internal/webhook/podmutation"
-	collectorupgrade "github.com/open-telemetry/opentelemetry-operator/pkg/collector/upgrade"
 	"github.com/open-telemetry/opentelemetry-operator/pkg/constants"
 	"github.com/open-telemetry/opentelemetry-operator/pkg/featuregate"
 	"github.com/open-telemetry/opentelemetry-operator/pkg/instrumentation"
@@ -367,7 +366,7 @@ func main() {
 		}
 	}
 
-	err = addDependencies(ctx, mgr, cfg, v)
+	err = addDependencies(ctx, mgr, cfg)
 	if err != nil {
 		setupLog.Error(err, "failed to add/run bootstrap dependencies to the controller manager")
 		os.Exit(1)
@@ -380,6 +379,7 @@ func main() {
 		Config:   cfg,
 		Recorder: mgr.GetEventRecorderFor("opentelemetry-operator"),
 		Reviewer: reviewer,
+		Version:  v,
 	})
 
 	if err = collectorReconciler.SetupWithManager(mgr); err != nil {
@@ -509,23 +509,9 @@ func main() {
 	}
 }
 
-func addDependencies(_ context.Context, mgr ctrl.Manager, cfg config.Config, v version.Version) error {
+func addDependencies(_ context.Context, mgr ctrl.Manager, cfg config.Config) error {
 	// adds the upgrade mechanism to be executed once the manager is ready
 	err := mgr.Add(manager.RunnableFunc(func(c context.Context) error {
-		up := &collectorupgrade.VersionUpgrade{
-			Log:      ctrl.Log.WithName("collector-upgrade"),
-			Version:  v,
-			Client:   mgr.GetClient(),
-			Recorder: mgr.GetEventRecorderFor("opentelemetry-operator"),
-		}
-		return up.ManagedInstances(c)
-	}))
-	if err != nil {
-		return fmt.Errorf("failed to upgrade OpenTelemetryCollector instances: %w", err)
-	}
-
-	// adds the upgrade mechanism to be executed once the manager is ready
-	err = mgr.Add(manager.RunnableFunc(func(c context.Context) error {
 		u := instrumentationupgrade.NewInstrumentationUpgrade(
 			mgr.GetClient(),
 			ctrl.Log.WithName("instrumentation-upgrade"),
diff --git a/pkg/collector/upgrade/upgrade.go b/pkg/collector/upgrade/upgrade.go
index 6bd7085d..8d286e02 100644
--- a/pkg/collector/upgrade/upgrade.go
+++ b/pkg/collector/upgrade/upgrade.go
@@ -6,11 +6,11 @@ package upgrade
 
 import (
 	"context"
-	"fmt"
 	"reflect"
 
 	semver "github.com/Masterminds/semver/v3"
 	"github.com/go-logr/logr"
+	corev1 "k8s.io/api/core/v1"
 	"k8s.io/client-go/tools/record"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
@@ -39,55 +39,46 @@ func (u VersionUpgrade) semVer() *semver.Version {
 	}
 }
 
-// ManagedInstances finds all the otelcol instances for the current operator and upgrades them, if necessary.
-func (u VersionUpgrade) ManagedInstances(ctx context.Context) error {
-	u.Log.Info("looking for managed instances to upgrade")
-	list := &v1beta1.OpenTelemetryCollectorList{}
-	if err := u.Client.List(ctx, list); err != nil {
-		return fmt.Errorf("failed to list: %w", err)
-	}
-
-	for i := range list.Items {
-		original := list.Items[i]
-		itemLogger := u.Log.WithValues("name", original.Name, "namespace", original.Namespace)
+// NeedsUpgrade checks if this CR needs to be upgraded.
+func (u VersionUpgrade) NeedsUpgrade(instance v1beta1.OpenTelemetryCollector) bool {
+	// CRs with an empty version are ignored, as they're already up-to-date and
+	// the version will be set when the status field is refreshed.
+	return instance.Status.Version != "" &&
+		instance.Status.Version != u.Version.OpenTelemetryCollector &&
+		instance.Spec.ManagementState != v1beta1.ManagementStateUnmanaged &&
+		instance.Spec.UpgradeStrategy != v1beta1.UpgradeStrategyNone
+}
 
-		if original.Spec.ManagementState == v1beta1.ManagementStateUnmanaged {
-			itemLogger.Info("skipping upgrade because instance is not managed")
-			continue
-		}
+// Upgrade performs an upgrade of an OpenTelemetryCollector CR in the cluster.
+func (u VersionUpgrade) Upgrade(ctx context.Context, original v1beta1.OpenTelemetryCollector) error {
+	if !u.NeedsUpgrade(original) {
+		return nil
+	}
 
-		if original.Spec.UpgradeStrategy == v1beta1.UpgradeStrategyNone {
-			itemLogger.Info("skipping instance upgrade due to UpgradeStrategy")
-			continue
-		}
-		upgraded, err := u.ManagedInstance(ctx, original)
-		if err != nil {
-			const msg = "automated update not possible. Configuration must be corrected manually and CR instance must be re-created."
-			itemLogger.Info(msg)
-			u.Recorder.Event(&original, "Error", "Upgrade", msg)
-			continue
+	itemLogger := u.Log.WithValues("name", original.Name, "namespace", original.Namespace)
+	upgraded, err := u.ManagedInstance(ctx, original)
+	if err != nil {
+		const msg = "automated update not possible. Configuration must be corrected manually and CR instance must be re-created."
+		itemLogger.Info(msg)
+		u.Recorder.Event(&original, corev1.EventTypeWarning, "Upgrade", msg)
+		return err
+	}
+	if !reflect.DeepEqual(upgraded, original) {
+		// the resource update overrides the status, so, keep it so that we can reset it later
+		st := upgraded.Status
+		patch := client.MergeFrom(&original)
+		if err := u.Client.Patch(ctx, &upgraded, patch); err != nil {
+			itemLogger.Error(err, "failed to apply changes to instance")
+			return err
 		}
-		if !reflect.DeepEqual(upgraded, list.Items[i]) {
-			// the resource update overrides the status, so, keep it so that we can reset it later
-			st := upgraded.Status
-			patch := client.MergeFrom(&original)
-			if err := u.Client.Patch(ctx, &upgraded, patch); err != nil {
-				itemLogger.Error(err, "failed to apply changes to instance")
-				continue
-			}
 
-			// the status object requires its own update
-			upgraded.Status = st
-			if err := u.Client.Status().Patch(ctx, &upgraded, patch); err != nil {
-				itemLogger.Error(err, "failed to apply changes to instance's status object")
-				continue
-			}
-			itemLogger.Info("instance upgraded", "version", upgraded.Status.Version)
+		// the status object requires its own update
+		upgraded.Status = st
+		if err := u.Client.Status().Patch(ctx, &upgraded, patch); err != nil {
+			itemLogger.Error(err, "failed to apply changes to instance's status object")
+			return err
 		}
-	}
-
-	if len(list.Items) == 0 {
-		u.Log.Info("no instances to upgrade")
+		itemLogger.Info("instance upgraded", "version", upgraded.Status.Version)
 	}
 
 	return nil
@@ -140,7 +131,7 @@ func (u VersionUpgrade) ManagedInstance(_ context.Context, otelcol v1beta1.OpenT
 
 				upgradedV1alpha1, err := available.upgrade(u, otelcolV1alpha1)
 				if err != nil {
-					u.Log.Error(err, "failed to upgrade managed otelcol instances", "name", updated.Name, "namespace", updated.Namespace)
+					u.Log.Error(err, "failed to upgrade managed otelcol instance", "name", updated.Name, "namespace", updated.Namespace)
 					return updated, err
 				}
 				upgradedV1alpha1.Status.Version = available.String()
@@ -153,7 +144,7 @@ func (u VersionUpgrade) ManagedInstance(_ context.Context, otelcol v1beta1.OpenT
 
 				upgraded, err := available.upgradeV1beta1(u, &updated) //available.upgrade(params., &updated)
 				if err != nil {
-					u.Log.Error(err, "failed to upgrade managed otelcol instances", "name", updated.Name, "namespace", updated.Namespace)
+					u.Log.Error(err, "failed to upgrade managed otelcol instance", "name", updated.Name, "namespace", updated.Namespace)
 					return updated, err
 				}
 
diff --git a/versions.txt b/versions.txt
index ee8d6b0f..7d76319a 100644
--- a/versions.txt
+++ b/versions.txt
@@ -2,16 +2,16 @@
 # by default with the OpenTelemetry Operator. This would usually be the latest
 # stable OpenTelemetry version. When you update this file, make sure to update the
 # the docs as well.
-opentelemetry-collector=0.122.1
+opentelemetry-collector=0.123.1
 
 # Represents the current release of the OpenTelemetry Operator.
-operator=0.122.0
+operator=0.123.0
 
 # Represents the current release of the Target Allocator.
-targetallocator=0.122.0
+targetallocator=0.123.0
 
 # Represents the current release of the Operator OpAMP Bridge.
-operator-opamp-bridge=0.122.0
+operator-opamp-bridge=0.123.0
 
 # Represents the current release of Java instrumentation.
 # Should match autoinstrumentation/java/version.txt
@@ -19,7 +19,7 @@ autoinstrumentation-java=1.33.6
 
 # Represents the current release of NodeJS instrumentation.
 # Should match value in autoinstrumentation/nodejs/package.json
-autoinstrumentation-nodejs=0.56.0
+autoinstrumentation-nodejs=0.57.1
 
 # Represents the current release of Python instrumentation.
 # Should match value in autoinstrumentation/python/requirements.txt



Test patch:
diff --git a/apis/v1beta1/config_test.go b/apis/v1beta1/config_test.go
index 573e6208..549e0141 100644
--- a/apis/v1beta1/config_test.go
+++ b/apis/v1beta1/config_test.go
@@ -1097,3 +1097,51 @@ func TestConfig_GetReadinessProbe(t *testing.T) {
 		})
 	}
 }
+
+func TestTelemetryLogsPreservedWithMetrics(t *testing.T) {
+	// Test case where logs configuration exists and metrics is added
+	cfg := &Config{
+		Service: Service{
+			Telemetry: &AnyConfig{
+				Object: map[string]interface{}{
+					"logs": map[string]interface{}{
+						"level": "debug",
+					},
+				},
+			},
+		},
+	}
+
+	expected := &Config{
+		Service: Service{
+			Telemetry: &AnyConfig{
+				Object: map[string]interface{}{
+					"logs": map[string]interface{}{
+						"level": "debug",
+					},
+					"metrics": map[string]interface{}{
+						"readers": []interface{}{
+							map[string]interface{}{
+								"pull": map[string]interface{}{
+									"exporter": map[string]interface{}{
+										"prometheus": map[string]interface{}{
+											"host": "0.0.0.0",
+											"port": int32(8888),
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	err := cfg.Service.ApplyDefaults(logr.Discard())
+	require.NoError(t, err)
+
+	telemetry := cfg.Service.GetTelemetry()
+	require.NotNil(t, telemetry)
+	require.Equal(t, expected, cfg)
+}
diff --git a/cmd/operator-opamp-bridge/agent/agent_test.go b/cmd/operator-opamp-bridge/internal/agent/agent_test.go
similarity index 82%
rename from cmd/operator-opamp-bridge/agent/agent_test.go
rename to cmd/operator-opamp-bridge/internal/agent/agent_test.go
index 4a366953..94b6209e 100644
--- a/cmd/operator-opamp-bridge/agent/agent_test.go
+++ b/cmd/operator-opamp-bridge/internal/agent/agent_test.go
@@ -8,6 +8,7 @@ import (
 	"fmt"
 	"os"
 	"sort"
+	"sync"
 	"testing"
 	"time"
 
@@ -28,8 +29,9 @@ import (
 
 	"github.com/open-telemetry/opentelemetry-operator/apis/v1alpha1"
 	"github.com/open-telemetry/opentelemetry-operator/apis/v1beta1"
-	"github.com/open-telemetry/opentelemetry-operator/cmd/operator-opamp-bridge/config"
-	"github.com/open-telemetry/opentelemetry-operator/cmd/operator-opamp-bridge/operator"
+	"github.com/open-telemetry/opentelemetry-operator/cmd/operator-opamp-bridge/internal/config"
+	"github.com/open-telemetry/opentelemetry-operator/cmd/operator-opamp-bridge/internal/operator"
+	"github.com/open-telemetry/opentelemetry-operator/cmd/operator-opamp-bridge/internal/proxy"
 )
 
 const (
@@ -129,9 +131,48 @@ func getConfigHash(key, file string) string {
 }
 
 var _ client.OpAMPClient = &mockOpampClient{}
+var _ proxy.Server = newMockProxy(nil, nil)
+
+func newMockProxy(configs map[uuid.UUID]*protobufs.EffectiveConfig, healths map[uuid.UUID]*protobufs.ComponentHealth) *mockProxy {
+	return &mockProxy{
+		configs:     configs,
+		healths:     healths,
+		updatesChan: make(chan struct{}, 1),
+	}
+}
+
+type mockProxy struct {
+	mu          sync.Mutex
+	configs     map[uuid.UUID]*protobufs.EffectiveConfig
+	healths     map[uuid.UUID]*protobufs.ComponentHealth
+	updatesChan chan struct{}
+}
+
+func (m *mockProxy) sendUpdate() {
+	m.updatesChan <- struct{}{}
+}
+
+// HasUpdates implements proxy.Server.
+func (m *mockProxy) HasUpdates() <-chan struct{} {
+	return m.updatesChan
+}
+
+func (m *mockProxy) GetConfigurations() map[uuid.UUID]*protobufs.EffectiveConfig {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	return m.configs
+}
+
+func (m *mockProxy) GetHealth() map[uuid.UUID]*protobufs.ComponentHealth {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	return m.healths
+}
 
 type mockOpampClient struct {
+	mu                  sync.Mutex
 	lastStatus          *protobufs.RemoteConfigStatus
+	lastHealth          *protobufs.ComponentHealth
 	lastEffectiveConfig *protobufs.EffectiveConfig
 	settings            types.StartSettings
 }
@@ -149,6 +190,8 @@ func (m *mockOpampClient) RequestConnectionSettings(_ *protobufs.ConnectionSetti
 }
 
 func (m *mockOpampClient) Start(_ context.Context, settings types.StartSettings) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
 	m.settings = settings
 	return nil
 }
@@ -165,11 +208,16 @@ func (m *mockOpampClient) AgentDescription() *protobufs.AgentDescription {
 	return nil
 }
 
-func (m *mockOpampClient) SetHealth(_ *protobufs.ComponentHealth) error {
+func (m *mockOpampClient) SetHealth(ch *protobufs.ComponentHealth) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	m.lastHealth = ch
 	return nil
 }
 
 func (m *mockOpampClient) UpdateEffectiveConfig(ctx context.Context) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
 	effectiveConfig, err := m.settings.Callbacks.GetEffectiveConfig(ctx)
 	if err != nil {
 		return err
@@ -179,6 +227,8 @@ func (m *mockOpampClient) UpdateEffectiveConfig(ctx context.Context) error {
 }
 
 func (m *mockOpampClient) SetRemoteConfigStatus(status *protobufs.RemoteConfigStatus) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
 	m.lastStatus = status
 	return nil
 }
@@ -204,6 +254,7 @@ func getFakeApplier(t *testing.T, conf *config.Config, lists ...runtimeClient.Ob
 
 func TestAgent_getHealth(t *testing.T) {
 	fakeClock := testingclock.NewFakeClock(time.Now())
+	mockInstanceId := uuid.New()
 	startTime, err := timeToUnixNanoUnsigned(fakeClock.Now())
 	require.NoError(t, err)
 	type fields struct {
@@ -213,6 +264,7 @@ func TestAgent_getHealth(t *testing.T) {
 		ctx context.Context
 		// List of mappings from namespace/name to a config file, tests are run in order of list
 		configs []map[string]string
+		healths map[uuid.UUID]*protobufs.ComponentHealth
 		podList *v1.PodList
 	}
 	tests := []struct {
@@ -231,6 +283,7 @@ func TestAgent_getHealth(t *testing.T) {
 				ctx:     context.Background(),
 				configs: nil,
 				podList: mockPodList,
+				healths: map[uuid.UUID]*protobufs.ComponentHealth{},
 			},
 			want: []*protobufs.ComponentHealth{
 				{
@@ -256,6 +309,13 @@ func TestAgent_getHealth(t *testing.T) {
 					},
 				},
 				podList: mockPodList,
+				healths: map[uuid.UUID]*protobufs.ComponentHealth{
+					mockInstanceId: {
+						Healthy:            true,
+						StartTimeUnixNano:  startTime,
+						StatusTimeUnixNano: startTime,
+					},
+				},
 			},
 			want: []*protobufs.ComponentHealth{
 				{
@@ -263,6 +323,11 @@ func TestAgent_getHealth(t *testing.T) {
 					StartTimeUnixNano:  startTime,
 					StatusTimeUnixNano: startTime,
 					ComponentHealthMap: map[string]*protobufs.ComponentHealth{
+						mockInstanceId.String(): {
+							Healthy:            true,
+							StartTimeUnixNano:  startTime,
+							StatusTimeUnixNano: startTime,
+						},
 						"testnamespace/collector": {
 							Healthy:            true,
 							StartTimeUnixNano:  collectorStartTime,
@@ -289,6 +354,7 @@ func TestAgent_getHealth(t *testing.T) {
 					},
 				},
 				podList: mockPodList,
+				healths: map[uuid.UUID]*protobufs.ComponentHealth{},
 			},
 			want: []*protobufs.ComponentHealth{
 				{
@@ -329,6 +395,7 @@ func TestAgent_getHealth(t *testing.T) {
 					},
 				},
 				podList: mockPodList,
+				healths: map[uuid.UUID]*protobufs.ComponentHealth{},
 			},
 			want: []*protobufs.ComponentHealth{
 				{
@@ -368,6 +435,7 @@ func TestAgent_getHealth(t *testing.T) {
 					},
 				},
 				podList: mockPodListUnhealthy,
+				healths: map[uuid.UUID]*protobufs.ComponentHealth{},
 			},
 			want: []*protobufs.ComponentHealth{
 				{
@@ -402,7 +470,8 @@ func TestAgent_getHealth(t *testing.T) {
 			loadErr := config.LoadFromFile(conf, tt.fields.configFile)
 			require.NoError(t, loadErr, "should be able to load config")
 			applier := getFakeApplier(t, conf, tt.args.podList)
-			agent := NewAgent(l, applier, conf, mockClient)
+			mp := newMockProxy(nil, tt.args.healths)
+			agent := NewAgent(l, applier, conf, mockClient, mp)
 			agent.clock = fakeClock
 			err := agent.Start()
 			defer agent.Shutdown()
@@ -432,6 +501,7 @@ func TestAgent_getHealth(t *testing.T) {
 }
 
 func TestAgent_onMessage(t *testing.T) {
+	mockInstanceId := uuid.New()
 	type fields struct {
 		configFile string
 	}
@@ -441,6 +511,8 @@ func TestAgent_onMessage(t *testing.T) {
 		configFile map[string]string
 		// Mapping from namespace/name to a config in testdata (for testing updates)
 		nextConfigFile map[string]string
+		// Mapping from instance id to an effective config
+		configs map[uuid.UUID]*protobufs.EffectiveConfig
 	}
 	type want struct {
 		// Mapping from namespace/name to a list of expected contents
@@ -501,6 +573,77 @@ func TestAgent_onMessage(t *testing.T) {
 				},
 			},
 		},
+		{
+			name: "base case with proxy",
+			fields: fields{
+				configFile: agentTestFileName,
+			},
+			args: args{
+				ctx: context.Background(),
+				configFile: map[string]string{
+					testCollectorKey: collectorBasicFile,
+				},
+				configs: map[uuid.UUID]*protobufs.EffectiveConfig{
+					mockInstanceId: {
+						ConfigMap: &protobufs.AgentConfigMap{
+							ConfigMap: map[string]*protobufs.AgentConfigFile{
+								"": {
+									Body: []byte(`
+										receivers:
+									      otlp:
+									        protocols:
+									          grpc:
+									          http:
+									    processors:
+									      memory_limiter:
+									        check_interval: 1s
+									        limit_percentage: 75
+									        spike_limit_percentage: 15
+									      batch:
+									        send_batch_size: 10000
+									        timeout: 10s
+
+									    exporters:
+									      debug:
+
+									    service:
+									      pipelines:
+									        traces:
+									          receivers: [otlp]
+									          exporters: [debug]
+										`),
+									ContentType: "yaml",
+								},
+							},
+						},
+					},
+				},
+			},
+			want: want{
+				contents: map[string][]string{
+					testCollectorKey: {
+						"kind: OpenTelemetryCollector",
+						"name: " + testCollectorName,
+						"namespace: " + testNamespace,
+						"send_batch_size: 10000",
+						"receivers:",
+						"- otlp",
+						"status:",
+					},
+					proxyPrefix + mockInstanceId.String(): {
+						"receivers:",
+						"otlp:",
+						"service:",
+						"pipelines:",
+						"traces:",
+					},
+				},
+				status: &protobufs.RemoteConfigStatus{
+					LastRemoteConfigHash: []byte(basicYamlConfigHash),
+					Status:               protobufs.RemoteConfigStatuses_RemoteConfigStatuses_APPLIED,
+				},
+			},
+		},
 		{
 			name: "base case http",
 			fields: fields{
@@ -802,13 +945,14 @@ func TestAgent_onMessage(t *testing.T) {
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			mockClient := &mockOpampClient{}
+			mp := newMockProxy(tt.args.configs, nil)
 
 			conf := config.NewConfig(logr.Discard())
 			loadErr := config.LoadFromFile(conf, tt.fields.configFile)
 			require.NoError(t, loadErr, "should be able to load config")
 
 			applier := getFakeApplier(t, conf)
-			agent := NewAgent(l, applier, conf, mockClient)
+			agent := NewAgent(l, applier, conf, mockClient, mp)
 			err := agent.Start()
 			defer agent.Shutdown()
 			require.NoError(t, err, "should be able to start agent")
@@ -823,6 +967,7 @@ func TestAgent_onMessage(t *testing.T) {
 				assert.Equal(t, effectiveConfig, mockClient.lastEffectiveConfig, "client's config should be updated")
 			}
 			assert.NotNilf(t, effectiveConfig.ConfigMap.GetConfigMap(), "configmap should have data")
+			assert.Len(t, effectiveConfig.ConfigMap.ConfigMap, len(tt.want.contents))
 			for colNameNamespace, expectedContents := range tt.want.contents {
 				configFileMap := effectiveConfig.ConfigMap.GetConfigMap()
 				require.Contains(t, configFileMap, colNameNamespace)
@@ -869,7 +1014,8 @@ func Test_CanUpdateIdentity(t *testing.T) {
 	loadErr := config.LoadFromFile(conf, agentTestFileName)
 	require.NoError(t, loadErr, "should be able to load config")
 	applier := getFakeApplier(t, conf)
-	agent := NewAgent(l, applier, conf, mockClient)
+	mp := newMockProxy(nil, nil)
+	agent := NewAgent(l, applier, conf, mockClient, mp)
 	err = agent.Start()
 	defer agent.Shutdown()
 	require.NoError(t, err, "should be able to start agent")
@@ -890,6 +1036,61 @@ func Test_CanUpdateIdentity(t *testing.T) {
 	assert.Equal(t, newId, parsedUUID)
 }
 
+func TestAgent_ListensForUpdates(t *testing.T) {
+	mockClient := &mockOpampClient{}
+	mockProxy := newMockProxy(nil, nil)
+	conf := config.NewConfig(logr.Discard())
+	applier := getFakeApplier(t, conf)
+
+	agent := NewAgent(logr.Discard(), applier, conf, mockClient, mockProxy)
+	err := agent.Start()
+	require.NoError(t, err, "should be able to start agent")
+	defer agent.Shutdown()
+	mockClient.mu.Lock()
+	assert.Len(t, mockClient.lastHealth.ComponentHealthMap, 0)
+	assert.Nil(t, mockClient.lastEffectiveConfig, 0)
+	mockClient.mu.Unlock()
+	mockInstanceId := uuid.New()
+	mockProxy.mu.Lock()
+	mockProxy.healths = map[uuid.UUID]*protobufs.ComponentHealth{
+		mockInstanceId: {
+			Healthy:            true,
+			StartTimeUnixNano:  0,
+			StatusTimeUnixNano: 0,
+		},
+	}
+	mockProxy.configs = map[uuid.UUID]*protobufs.EffectiveConfig{
+		mockInstanceId: {
+			ConfigMap: &protobufs.AgentConfigMap{
+				ConfigMap: map[string]*protobufs.AgentConfigFile{
+					"": {
+						Body:        []byte("hello"),
+						ContentType: "text",
+					},
+				},
+			},
+		},
+	}
+	mockProxy.mu.Unlock()
+
+	mockClient.mu.Lock()
+	assert.Len(t, mockClient.lastHealth.ComponentHealthMap, 0)
+	assert.Nil(t, mockClient.lastEffectiveConfig, 0)
+	mockClient.mu.Unlock()
+
+	// Simulate an update from the proxy
+	go func() {
+		mockProxy.sendUpdate()
+	}()
+
+	assert.Eventually(t, func() bool {
+		mockClient.mu.Lock()
+		defer mockClient.mu.Unlock()
+		return len(mockClient.lastHealth.ComponentHealthMap) == 1 &&
+			len(mockClient.lastEffectiveConfig.ConfigMap.ConfigMap) == 1
+	}, 3*time.Second, 1*time.Second)
+}
+
 func getMessageDataFromConfigFile(filemap map[string]string) (*types.MessageData, error) {
 	toReturn := &types.MessageData{}
 	if filemap == nil {
diff --git a/cmd/operator-opamp-bridge/agent/kube_resource_key_test.go b/cmd/operator-opamp-bridge/internal/agent/kube_resource_key_test.go
similarity index 100%
rename from cmd/operator-opamp-bridge/agent/kube_resource_key_test.go
rename to cmd/operator-opamp-bridge/internal/agent/kube_resource_key_test.go
diff --git a/cmd/operator-opamp-bridge/agent/testdata/agent.yaml b/cmd/operator-opamp-bridge/internal/agent/testdata/agent.yaml
similarity index 100%
rename from cmd/operator-opamp-bridge/agent/testdata/agent.yaml
rename to cmd/operator-opamp-bridge/internal/agent/testdata/agent.yaml
diff --git a/cmd/operator-opamp-bridge/agent/testdata/agentbasiccomponentsallowed.yaml b/cmd/operator-opamp-bridge/internal/agent/testdata/agentbasiccomponentsallowed.yaml
similarity index 100%
rename from cmd/operator-opamp-bridge/agent/testdata/agentbasiccomponentsallowed.yaml
rename to cmd/operator-opamp-bridge/internal/agent/testdata/agentbasiccomponentsallowed.yaml
diff --git a/cmd/operator-opamp-bridge/agent/testdata/agentbatchnotallowed.yaml b/cmd/operator-opamp-bridge/internal/agent/testdata/agentbatchnotallowed.yaml
similarity index 100%
rename from cmd/operator-opamp-bridge/agent/testdata/agentbatchnotallowed.yaml
rename to cmd/operator-opamp-bridge/internal/agent/testdata/agentbatchnotallowed.yaml
diff --git a/cmd/operator-opamp-bridge/agent/testdata/agenthttpbasic.yaml b/cmd/operator-opamp-bridge/internal/agent/testdata/agenthttpbasic.yaml
similarity index 100%
rename from cmd/operator-opamp-bridge/agent/testdata/agenthttpbasic.yaml
rename to cmd/operator-opamp-bridge/internal/agent/testdata/agenthttpbasic.yaml
diff --git a/cmd/operator-opamp-bridge/agent/testdata/agentnoprocessorsallowed.yaml b/cmd/operator-opamp-bridge/internal/agent/testdata/agentnoprocessorsallowed.yaml
similarity index 100%
rename from cmd/operator-opamp-bridge/agent/testdata/agentnoprocessorsallowed.yaml
rename to cmd/operator-opamp-bridge/internal/agent/testdata/agentnoprocessorsallowed.yaml
diff --git a/cmd/operator-opamp-bridge/agent/testdata/basic.yaml b/cmd/operator-opamp-bridge/internal/agent/testdata/basic.yaml
similarity index 100%
rename from cmd/operator-opamp-bridge/agent/testdata/basic.yaml
rename to cmd/operator-opamp-bridge/internal/agent/testdata/basic.yaml
diff --git a/cmd/operator-opamp-bridge/agent/testdata/invalid.yaml b/cmd/operator-opamp-bridge/internal/agent/testdata/invalid.yaml
similarity index 100%
rename from cmd/operator-opamp-bridge/agent/testdata/invalid.yaml
rename to cmd/operator-opamp-bridge/internal/agent/testdata/invalid.yaml
diff --git a/cmd/operator-opamp-bridge/agent/testdata/updated.yaml b/cmd/operator-opamp-bridge/internal/agent/testdata/updated.yaml
similarity index 100%
rename from cmd/operator-opamp-bridge/agent/testdata/updated.yaml
rename to cmd/operator-opamp-bridge/internal/agent/testdata/updated.yaml
diff --git a/cmd/operator-opamp-bridge/config/config_test.go b/cmd/operator-opamp-bridge/internal/config/config_test.go
similarity index 100%
rename from cmd/operator-opamp-bridge/config/config_test.go
rename to cmd/operator-opamp-bridge/internal/config/config_test.go
diff --git a/cmd/operator-opamp-bridge/config/flags_test.go b/cmd/operator-opamp-bridge/internal/config/flags_test.go
similarity index 100%
rename from cmd/operator-opamp-bridge/config/flags_test.go
rename to cmd/operator-opamp-bridge/internal/config/flags_test.go
diff --git a/cmd/operator-opamp-bridge/config/testdata/agent.yaml b/cmd/operator-opamp-bridge/internal/config/testdata/agent.yaml
similarity index 100%
rename from cmd/operator-opamp-bridge/config/testdata/agent.yaml
rename to cmd/operator-opamp-bridge/internal/config/testdata/agent.yaml
diff --git a/cmd/operator-opamp-bridge/config/testdata/agentbadconf.yaml b/cmd/operator-opamp-bridge/internal/config/testdata/agentbadconf.yaml
similarity index 100%
rename from cmd/operator-opamp-bridge/config/testdata/agentbadconf.yaml
rename to cmd/operator-opamp-bridge/internal/config/testdata/agentbadconf.yaml
diff --git a/cmd/operator-opamp-bridge/config/testdata/agentbasiccomponentsallowed.yaml b/cmd/operator-opamp-bridge/internal/config/testdata/agentbasiccomponentsallowed.yaml
similarity index 100%
rename from cmd/operator-opamp-bridge/config/testdata/agentbasiccomponentsallowed.yaml
rename to cmd/operator-opamp-bridge/internal/config/testdata/agentbasiccomponentsallowed.yaml
diff --git a/cmd/operator-opamp-bridge/config/testdata/agenthttpbasic.yaml b/cmd/operator-opamp-bridge/internal/config/testdata/agenthttpbasic.yaml
similarity index 100%
rename from cmd/operator-opamp-bridge/config/testdata/agenthttpbasic.yaml
rename to cmd/operator-opamp-bridge/internal/config/testdata/agenthttpbasic.yaml
diff --git a/cmd/operator-opamp-bridge/config/testdata/agentwithdescription.yaml b/cmd/operator-opamp-bridge/internal/config/testdata/agentwithdescription.yaml
similarity index 100%
rename from cmd/operator-opamp-bridge/config/testdata/agentwithdescription.yaml
rename to cmd/operator-opamp-bridge/internal/config/testdata/agentwithdescription.yaml
diff --git a/cmd/operator-opamp-bridge/config/testdata/agentwithheaders.yaml b/cmd/operator-opamp-bridge/internal/config/testdata/agentwithheaders.yaml
similarity index 100%
rename from cmd/operator-opamp-bridge/config/testdata/agentwithheaders.yaml
rename to cmd/operator-opamp-bridge/internal/config/testdata/agentwithheaders.yaml
diff --git a/cmd/operator-opamp-bridge/operator/client_test.go b/cmd/operator-opamp-bridge/internal/operator/client_test.go
similarity index 100%
rename from cmd/operator-opamp-bridge/operator/client_test.go
rename to cmd/operator-opamp-bridge/internal/operator/client_test.go
diff --git a/cmd/operator-opamp-bridge/operator/testdata/collector-v1alpha1.yaml b/cmd/operator-opamp-bridge/internal/operator/testdata/collector-v1alpha1.yaml
similarity index 100%
rename from cmd/operator-opamp-bridge/operator/testdata/collector-v1alpha1.yaml
rename to cmd/operator-opamp-bridge/internal/operator/testdata/collector-v1alpha1.yaml
diff --git a/cmd/operator-opamp-bridge/operator/testdata/collector.yaml b/cmd/operator-opamp-bridge/internal/operator/testdata/collector.yaml
similarity index 100%
rename from cmd/operator-opamp-bridge/operator/testdata/collector.yaml
rename to cmd/operator-opamp-bridge/internal/operator/testdata/collector.yaml
diff --git a/cmd/operator-opamp-bridge/operator/testdata/invalid-collector.yaml b/cmd/operator-opamp-bridge/internal/operator/testdata/invalid-collector.yaml
similarity index 100%
rename from cmd/operator-opamp-bridge/operator/testdata/invalid-collector.yaml
rename to cmd/operator-opamp-bridge/internal/operator/testdata/invalid-collector.yaml
diff --git a/cmd/operator-opamp-bridge/operator/testdata/reporting-collector.yaml b/cmd/operator-opamp-bridge/internal/operator/testdata/reporting-collector.yaml
similarity index 100%
rename from cmd/operator-opamp-bridge/operator/testdata/reporting-collector.yaml
rename to cmd/operator-opamp-bridge/internal/operator/testdata/reporting-collector.yaml
diff --git a/cmd/operator-opamp-bridge/operator/testdata/unmanaged-collector.yaml b/cmd/operator-opamp-bridge/internal/operator/testdata/unmanaged-collector.yaml
similarity index 100%
rename from cmd/operator-opamp-bridge/operator/testdata/unmanaged-collector.yaml
rename to cmd/operator-opamp-bridge/internal/operator/testdata/unmanaged-collector.yaml
diff --git a/cmd/operator-opamp-bridge/operator/testdata/updated-collector.yaml b/cmd/operator-opamp-bridge/internal/operator/testdata/updated-collector.yaml
similarity index 100%
rename from cmd/operator-opamp-bridge/operator/testdata/updated-collector.yaml
rename to cmd/operator-opamp-bridge/internal/operator/testdata/updated-collector.yaml
diff --git a/cmd/operator-opamp-bridge/internal/proxy/agent_test.go b/cmd/operator-opamp-bridge/internal/proxy/agent_test.go
new file mode 100644
index 00000000..fbb6ad04
--- /dev/null
+++ b/cmd/operator-opamp-bridge/internal/proxy/agent_test.go
@@ -0,0 +1,95 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package proxy
+
+import (
+	"testing"
+
+	"github.com/go-logr/logr"
+	"github.com/google/uuid"
+	"github.com/open-telemetry/opamp-go/protobufs"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestNewAgent(t *testing.T) {
+	logger := logr.Discard()
+	instanceId := uuid.New()
+	conn := &mockConnection{}
+
+	agent := NewAgent(logger, instanceId, conn)
+	require.NotNil(t, agent, "agent should not be nil")
+	assert.Equal(t, instanceId, agent.InstanceId, "instance ID should match")
+	assert.Equal(t, conn, agent.conn, "connection should match")
+}
+
+func TestAgent_hasCapability(t *testing.T) {
+	logger := logr.Discard()
+	instanceId := uuid.New()
+	conn := &mockConnection{}
+	agent := NewAgent(logger, instanceId, conn)
+
+	agent.Status = &protobufs.AgentToServer{
+		Capabilities: uint64(protobufs.AgentCapabilities_AgentCapabilities_ReportsEffectiveConfig),
+	}
+
+	assert.True(t, agent.hasCapability(protobufs.AgentCapabilities_AgentCapabilities_ReportsEffectiveConfig), "should have capability")
+	assert.False(t, agent.hasCapability(protobufs.AgentCapabilities_AgentCapabilities_ReportsHealth), "should not have capability")
+}
+
+func TestAgent_UpdateStatus(t *testing.T) {
+	logger := logr.Discard()
+	instanceId := uuid.New()
+	conn := &mockConnection{}
+	agent := NewAgent(logger, instanceId, conn)
+
+	newStatus := &protobufs.AgentToServer{
+		SequenceNum: 1,
+		AgentDescription: &protobufs.AgentDescription{
+			IdentifyingAttributes: []*protobufs.KeyValue{
+				{Key: "service.name", Value: &protobufs.AnyValue{Value: &protobufs.AnyValue_StringValue{StringValue: "test-service"}}},
+			},
+		},
+		//nolint:gosec
+		Capabilities: uint64(protobufs.AgentCapabilities_AgentCapabilities_ReportsEffectiveConfig),
+	}
+
+	response := &protobufs.ServerToAgent{}
+	agent.UpdateStatus(newStatus, response)
+
+	assert.Equal(t, newStatus, agent.Status, "status should be updated")
+	assert.True(t, response.Flags&uint64(protobufs.ServerToAgentFlags_ServerToAgentFlags_ReportFullState) != 0, "should request full state")
+}
+
+func TestAgent_GetHealth(t *testing.T) {
+	logger := logr.Discard()
+	instanceId := uuid.New()
+	conn := &mockConnection{}
+	agent := NewAgent(logger, instanceId, conn)
+
+	health := &protobufs.ComponentHealth{
+		Healthy: true,
+	}
+	agent.health = health
+
+	assert.Equal(t, health, agent.GetHealth(), "health should match")
+}
+
+func TestAgent_GetConfiguration(t *testing.T) {
+	logger := logr.Discard()
+	instanceId := uuid.New()
+	conn := &mockConnection{}
+	agent := NewAgent(logger, instanceId, conn)
+
+	config := &protobufs.EffectiveConfig{
+		ConfigMap: &protobufs.AgentConfigMap{
+			ConfigMap: map[string]*protobufs.AgentConfigFile{
+				"config.yaml": {Body: []byte("receivers:\n  otlp:\n")},
+			},
+		},
+	}
+	agent.effectiveConfig = config
+
+	assert.Equal(t, config, agent.GetConfiguration(), "configuration should match")
+}
diff --git a/cmd/operator-opamp-bridge/internal/proxy/server_test.go b/cmd/operator-opamp-bridge/internal/proxy/server_test.go
new file mode 100644
index 00000000..fc458891
--- /dev/null
+++ b/cmd/operator-opamp-bridge/internal/proxy/server_test.go
@@ -0,0 +1,145 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package proxy
+
+import (
+	"context"
+	"net"
+	"testing"
+
+	"github.com/go-logr/logr"
+	"github.com/google/uuid"
+	"github.com/open-telemetry/opamp-go/protobufs"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestOpAMPProxy_StartStop(t *testing.T) {
+	logger := logr.Discard()
+	endpoint := "localhost:4321"
+	proxyServer := NewOpAMPProxy(logger, endpoint)
+
+	err := proxyServer.Start()
+	require.NoError(t, err, "should be able to start the server")
+
+	err = proxyServer.Stop(context.Background())
+	require.NoError(t, err, "should be able to stop the server")
+}
+
+func TestOpAMPProxy_OnMessage(t *testing.T) {
+	logger := logr.Discard()
+	endpoint := "localhost:4321"
+	proxyServer := NewOpAMPProxy(logger, endpoint)
+
+	instanceId := uuid.New()
+	conn := &mockConnection{}
+	msg := &protobufs.AgentToServer{
+		InstanceUid: instanceId[:],
+	}
+
+	response := proxyServer.onMessage(context.Background(), conn, msg)
+	require.NotNil(t, response, "response should not be nil")
+}
+
+func TestOpAMPProxy_OnDisconnect(t *testing.T) {
+	logger := logr.Discard()
+	endpoint := "localhost:4321"
+	proxyServer := NewOpAMPProxy(logger, endpoint)
+
+	instanceId := uuid.New()
+	conn := &mockConnection{}
+	proxyServer.agentsById[instanceId] = NewAgent(logger, instanceId, conn)
+	proxyServer.connections[conn] = map[uuid.UUID]bool{instanceId: true}
+
+	proxyServer.onDisconnect(conn)
+
+	assert.Empty(t, proxyServer.agentsById, "agentsById should be empty")
+	assert.Empty(t, proxyServer.connections, "connections should be empty")
+}
+
+func TestOpAMPProxy_GetConfigurations(t *testing.T) {
+	logger := logr.Discard()
+	endpoint := "localhost:4321"
+	proxyServer := NewOpAMPProxy(logger, endpoint)
+
+	instanceId := uuid.New()
+	conn := &mockConnection{}
+	agent := NewAgent(logger, instanceId, conn)
+	proxyServer.agentsById[instanceId] = agent
+
+	configs := proxyServer.GetConfigurations()
+	require.NotNil(t, configs, "configs should not be nil")
+	assert.Contains(t, configs, instanceId, "configs should contain the instance ID")
+}
+
+func TestOpAMPProxy_GetHealth(t *testing.T) {
+	logger := logr.Discard()
+	endpoint := "localhost:4321"
+	proxyServer := NewOpAMPProxy(logger, endpoint)
+
+	instanceId := uuid.New()
+	conn := &mockConnection{}
+	agent := NewAgent(logger, instanceId, conn)
+	proxyServer.agentsById[instanceId] = agent
+
+	healths := proxyServer.GetHealth()
+	require.NotNil(t, healths, "healths should not be nil")
+	assert.Contains(t, healths, instanceId, "healths should contain the instance ID")
+}
+
+func TestGetInstanceId(t *testing.T) {
+	tests := []struct {
+		name        string
+		instanceUid []byte
+		wantErr     bool
+	}{
+		{
+			name:        "valid ULID",
+			instanceUid: []byte("01F8MECHZX3TBDSZ7XRADM79XE"),
+			wantErr:     false,
+		},
+		{
+			name: "valid UUID",
+			instanceUid: func() []byte {
+				bytes, _ := uuid.New().MarshalBinary()
+				return bytes
+			}(),
+			wantErr: false,
+		},
+		{
+			name:        "invalid length",
+			instanceUid: []byte("invalid"),
+			wantErr:     true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			_, err := getInstanceId(&protobufs.AgentToServer{InstanceUid: tt.instanceUid})
+			if (err != nil) != tt.wantErr {
+				t.Errorf("getInstanceId() error = %v, wantErr %v", err, tt.wantErr)
+			}
+		})
+	}
+}
+
+type mockConnection struct{}
+
+// Connection implements types.Connection.
+func (m *mockConnection) Connection() net.Conn {
+	panic("unimplemented")
+}
+
+// Disconnect implements types.Connection.
+func (m *mockConnection) Disconnect() error {
+	panic("unimplemented")
+}
+
+func (m *mockConnection) Send(ctx context.Context, msg *protobufs.ServerToAgent) error {
+	return nil
+}
+
+func (m *mockConnection) Close() error {
+	return nil
+}
diff --git a/controllers/reconcile_test.go b/controllers/reconcile_test.go
index 92356e1d..39167131 100644
--- a/controllers/reconcile_test.go
+++ b/controllers/reconcile_test.go
@@ -45,6 +45,8 @@ import (
 	"github.com/open-telemetry/opentelemetry-operator/internal/config"
 	"github.com/open-telemetry/opentelemetry-operator/internal/manifests"
 	"github.com/open-telemetry/opentelemetry-operator/internal/naming"
+	"github.com/open-telemetry/opentelemetry-operator/internal/version"
+	"github.com/open-telemetry/opentelemetry-operator/pkg/collector/upgrade"
 	"github.com/open-telemetry/opentelemetry-operator/pkg/featuregate"
 )
 
@@ -1298,6 +1300,181 @@ service:
 	require.Error(t, clientErr)
 }
 
+func TestUpgrade(t *testing.T) {
+	testCtx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	reconciler := createTestReconcilerWithVersion(
+		t, testCtx,
+		config.New(
+			config.WithCollectorImage("default-collector"),
+			config.WithTargetAllocatorImage("default-ta-allocator"),
+		),
+		version.Version{OpenTelemetryCollector: upgrade.Latest.String()},
+	)
+
+	for _, tt := range []struct {
+		name          string
+		input         v1beta1.OpenTelemetryCollector
+		expectRequeue bool
+		expected      check[v1beta1.OpenTelemetryCollector]
+	}{
+		{
+			name: "needs-upgrade",
+			input: v1beta1.OpenTelemetryCollector{
+				Spec: v1beta1.OpenTelemetryCollectorSpec{
+					OpenTelemetryCommonFields: v1beta1.OpenTelemetryCommonFields{
+						Args: map[string]string{
+							// this feature gate will be removed in the 0.110.0 upgrade step
+							"feature-gates": "-component.UseLocalHostAsDefaultHost",
+						},
+					},
+					Config: v1beta1.Config{
+						Receivers: v1beta1.AnyConfig{
+							Object: map[string]any{
+								"nop": map[string]any{},
+							},
+						},
+						Exporters: v1beta1.AnyConfig{
+							Object: map[string]any{
+								"nop": map[string]any{},
+							},
+						},
+						Service: v1beta1.Service{
+							Pipelines: map[string]*v1beta1.Pipeline{
+								"traces": {
+									Exporters: []string{"nop"},
+									Receivers: []string{"nop"},
+								},
+							},
+						},
+					},
+				},
+				Status: v1beta1.OpenTelemetryCollectorStatus{
+					Version: "0.109.0",
+				},
+			},
+			expectRequeue: true,
+			expected: func(t *testing.T, otelcol v1beta1.OpenTelemetryCollector) {
+				assert.Equal(t, upgrade.Latest.String(), otelcol.Status.Version)
+				// The '-component.UseLocalHostAsDefaultHost' feature gate was removed in the 0.110.0 upgrade step
+				assert.Equal(t, "", otelcol.Spec.OpenTelemetryCommonFields.Args["feature-gates"])
+			},
+		},
+		{
+			name: "up-to-date",
+			input: v1beta1.OpenTelemetryCollector{
+				Spec: v1beta1.OpenTelemetryCollectorSpec{
+					Config: v1beta1.Config{
+						Receivers: v1beta1.AnyConfig{
+							Object: map[string]any{
+								"nop": map[string]any{},
+							},
+						},
+						Exporters: v1beta1.AnyConfig{
+							Object: map[string]any{
+								"nop": map[string]any{},
+							},
+						},
+						Service: v1beta1.Service{
+							Pipelines: map[string]*v1beta1.Pipeline{
+								"traces": {
+									Exporters: []string{"nop"},
+									Receivers: []string{"nop"},
+								},
+							},
+						},
+					},
+				},
+				Status: v1beta1.OpenTelemetryCollectorStatus{
+					Version: upgrade.Latest.String(),
+				},
+			},
+			expectRequeue: false,
+			expected: func(t *testing.T, otelcol v1beta1.OpenTelemetryCollector) {
+				assert.Equal(t, upgrade.Latest.String(), otelcol.Status.Version)
+			},
+		},
+		{
+			name: "empty-version",
+			input: v1beta1.OpenTelemetryCollector{
+				Spec: v1beta1.OpenTelemetryCollectorSpec{
+					Config: v1beta1.Config{
+						Receivers: v1beta1.AnyConfig{
+							Object: map[string]any{
+								"nop": map[string]any{},
+							},
+						},
+						Exporters: v1beta1.AnyConfig{
+							Object: map[string]any{
+								"nop": map[string]any{},
+							},
+						},
+						Service: v1beta1.Service{
+							Pipelines: map[string]*v1beta1.Pipeline{
+								"traces": {
+									Exporters: []string{"nop"},
+									Receivers: []string{"nop"},
+								},
+							},
+						},
+					},
+				},
+				Status: v1beta1.OpenTelemetryCollectorStatus{
+					Version: "",
+				},
+			},
+			expectRequeue: false,
+			expected: func(t *testing.T, otelcol v1beta1.OpenTelemetryCollector) {
+				// updateCollectorStatus() sets version.OpenTelemetryCollector(), which is 0.0.0 in unit tests
+				assert.Equal(t, "0.0.0", otelcol.Status.Version)
+			},
+		},
+	} {
+		t.Run(tt.name, func(t *testing.T) {
+			otelcol := tt.input.DeepCopy()
+			otelcol.Namespace = "default"
+			otelcol.Name = tt.name
+			nsn := types.NamespacedName{Name: otelcol.Name, Namespace: otelcol.Namespace}
+
+			// Create otelcol CR
+			err := k8sClient.Create(testCtx, otelcol)
+			require.NoError(t, err)
+
+			// The status subresource must be updated separately
+			otelcol.Status = tt.input.Status
+			err = k8sClient.Status().Update(testCtx, otelcol)
+			require.NoError(t, err)
+
+			// Update cache.
+			// Otherwise, adding the finalizer in opentelemetrycollector_controller intermittently fails with:
+			// Operation cannot be fulfilled on opentelemetrycollectors.opentelemetry.io "...": the object has been modified; please apply your changes to the latest version and try again
+			_ = k8sClient.Get(ctx, nsn, otelcol)
+
+			// First reconcile
+			req := k8sreconcile.Request{NamespacedName: nsn}
+			reconcile, err := reconciler.Reconcile(testCtx, req)
+			require.NoError(t, err)
+			require.Equal(t, tt.expectRequeue, reconcile.Requeue)
+
+			// Second reconcile (if upgrade was run)
+			if reconcile.Requeue {
+				time.Sleep(reconcile.RequeueAfter)
+
+				reconcile, err = reconciler.Reconcile(testCtx, req)
+				require.NoError(t, err)
+				require.False(t, reconcile.Requeue)
+			}
+
+			// Compare upgraded CR with expected
+			upgraded := v1beta1.OpenTelemetryCollector{}
+			err = k8sClient.Get(ctx, nsn, &upgraded)
+			require.NoError(t, err)
+			tt.expected(t, upgraded)
+		})
+	}
+}
+
 func namespacedObjectName(name string, namespace string) types.NamespacedName {
 	return types.NamespacedName{
 		Namespace: namespace,
@@ -1353,7 +1530,7 @@ func IsOwnedBy(obj metav1.Object, owner *v1beta1.OpenTelemetryCollector) bool {
 	return isOwner
 }
 
-func createTestReconciler(t *testing.T, ctx context.Context, cfg config.Config) *controllers.OpenTelemetryCollectorReconciler {
+func createTestReconcilerWithVersion(t *testing.T, ctx context.Context, cfg config.Config, v version.Version) *controllers.OpenTelemetryCollectorReconciler {
 	t.Helper()
 	// we need to set up caches for our reconciler
 	runtimeCluster, err := runtimecluster.New(restCfg, func(options *runtimecluster.Options) {
@@ -1372,6 +1549,7 @@ func createTestReconciler(t *testing.T, ctx context.Context, cfg config.Config)
 		Scheme:   testScheme,
 		Recorder: record.NewFakeRecorder(20),
 		Config:   cfg,
+		Version:  v,
 	})
 	err = reconciler.SetupCaches(runtimeCluster)
 	require.NoError(t, err)
@@ -1380,6 +1558,11 @@ func createTestReconciler(t *testing.T, ctx context.Context, cfg config.Config)
 	return reconciler
 }
 
+func createTestReconciler(t *testing.T, ctx context.Context, cfg config.Config) *controllers.OpenTelemetryCollectorReconciler {
+	t.Helper()
+	return createTestReconcilerWithVersion(t, ctx, cfg, version.Get())
+}
+
 func sanitizeResourceName(name string) string {
 	sanitized := strings.ToLower(name)
 	re := regexp.MustCompile("[^a-z0-9-]")
diff --git a/internal/components/extensions/jaeger_query_extension_test.go b/internal/components/extensions/jaeger_query_extension_test.go
new file mode 100644
index 00000000..e0c9e7e1
--- /dev/null
+++ b/internal/components/extensions/jaeger_query_extension_test.go
@@ -0,0 +1,64 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package extensions
+
+import (
+	"testing"
+
+	"github.com/go-logr/logr"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/util/intstr"
+)
+
+func TestJaegerQueryExtensionParser(t *testing.T) {
+	jaegerBuilder := NewJaegerQueryExtensionParserBuilder()
+	genericBuilder, err := jaegerBuilder.Build()
+	require.NoError(t, err)
+
+	assert.Equal(t, "jaeger_query", genericBuilder.ParserType())
+	assert.Equal(t, "__jaeger_query", genericBuilder.ParserName())
+
+	defaultCfg, err := genericBuilder.GetDefaultConfig(logr.Discard(), nil)
+	require.NoError(t, err)
+
+	ports, err := genericBuilder.Ports(logr.Discard(), "jaeger_query", defaultCfg)
+	require.NoError(t, err)
+	assert.Equal(t, []corev1.ServicePort{{
+		Name:       "jaeger-query",
+		Port:       16686,
+		TargetPort: intstr.FromInt32(16686),
+	}}, ports)
+}
+
+func TestJaegerQueryExtensionParser_config(t *testing.T) {
+	jaegerBuilder := NewJaegerQueryExtensionParserBuilder()
+	genericBuilder, err := jaegerBuilder.Build()
+	require.NoError(t, err)
+
+	tests := []struct {
+		name   string
+		config interface{}
+		want   interface{}
+	}{
+		{
+			name:   "valid config",
+			config: map[string]interface{}{"http": map[string]interface{}{"endpoint": "127.0.0.0:16686"}},
+			want:   map[string]interface{}{"http": map[string]interface{}{"endpoint": "127.0.0.0:16686"}},
+		},
+		{
+			name: "missing config",
+			want: map[string]interface{}{"http": map[string]interface{}{"endpoint": "0.0.0.0:16686"}},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			cfg, errCfg := genericBuilder.GetDefaultConfig(logr.Discard(), test.config)
+			assert.Equal(t, test.want, cfg)
+			require.NoError(t, errCfg)
+		})
+	}
+}
diff --git a/pkg/collector/upgrade/upgrade_test.go b/pkg/collector/upgrade/upgrade_test.go
index ef4e3e00..622fb653 100644
--- a/pkg/collector/upgrade/upgrade_test.go
+++ b/pkg/collector/upgrade/upgrade_test.go
@@ -22,6 +22,72 @@ import (
 
 var logger = logf.Log.WithName("unit-tests")
 
+func TestNeedsUpgrade(t *testing.T) {
+	up := &upgrade.VersionUpgrade{
+		Version: version.Version{OpenTelemetryCollector: "0.10.0"},
+	}
+
+	for _, tt := range []struct {
+		desc      string
+		collector v1beta1.OpenTelemetryCollector
+		expected  bool
+	}{
+		{
+			desc: "needs upgrade",
+			collector: v1beta1.OpenTelemetryCollector{
+				Status: v1beta1.OpenTelemetryCollectorStatus{
+					Version: "0.1.0",
+				},
+			},
+			expected: true,
+		},
+		{
+			desc: "already up-to-date",
+			collector: v1beta1.OpenTelemetryCollector{
+				Status: v1beta1.OpenTelemetryCollectorStatus{
+					Version: "0.10.0",
+				},
+			},
+			expected: false,
+		},
+		{
+			desc:      "empty version, already up-to-date",
+			collector: v1beta1.OpenTelemetryCollector{},
+			expected:  false,
+		},
+		{
+			desc: "needs upgrade, but is ManagementState = Unmanaged",
+			collector: v1beta1.OpenTelemetryCollector{
+				Spec: v1beta1.OpenTelemetryCollectorSpec{
+					OpenTelemetryCommonFields: v1beta1.OpenTelemetryCommonFields{
+						ManagementState: v1beta1.ManagementStateUnmanaged,
+					},
+				},
+				Status: v1beta1.OpenTelemetryCollectorStatus{
+					Version: "0.1.0",
+				},
+			},
+			expected: false,
+		},
+		{
+			desc: "needs upgrade, but UpgradeStrategy = None",
+			collector: v1beta1.OpenTelemetryCollector{
+				Spec: v1beta1.OpenTelemetryCollectorSpec{
+					UpgradeStrategy: v1beta1.UpgradeStrategyNone,
+				},
+				Status: v1beta1.OpenTelemetryCollectorStatus{
+					Version: "0.1.0",
+				},
+			},
+			expected: false,
+		},
+	} {
+		t.Run(tt.desc, func(t *testing.T) {
+			assert.Equal(t, tt.expected, up.NeedsUpgrade(tt.collector))
+		})
+	}
+}
+
 func TestShouldUpgradeAllToLatestBasedOnUpgradeStrategy(t *testing.T) {
 	const beginV = "0.0.1" // this is the first version we have an upgrade function
 
@@ -47,7 +113,7 @@ func TestShouldUpgradeAllToLatestBasedOnUpgradeStrategy(t *testing.T) {
 			require.NoError(t, err)
 
 			// sanity check
-			persisted := &v1alpha1.OpenTelemetryCollector{}
+			persisted := &v1beta1.OpenTelemetryCollector{}
 			err = k8sClient.Get(context.Background(), nsn, persisted)
 			require.NoError(t, err)
 			require.Equal(t, beginV, persisted.Status.Version)
@@ -59,7 +125,7 @@ func TestShouldUpgradeAllToLatestBasedOnUpgradeStrategy(t *testing.T) {
 			}
 
 			// test
-			err = up.ManagedInstances(context.Background())
+			err = up.Upgrade(context.Background(), *persisted)
 			assert.NoError(t, err)
 
 			// verify
@@ -139,7 +205,7 @@ func TestEnvVarUpdates(t *testing.T) {
 	}
 
 	// test
-	err = up.ManagedInstances(context.Background())
+	err = up.Upgrade(context.Background(), *persisted)
 	assert.NoError(t, err)
 
 	// verify
diff --git a/tests/e2e-instrumentation/instrumentation-java-multicontainer/00-install-collector.yaml b/tests/e2e-instrumentation/instrumentation-java-multicontainer/00-install-collector.yaml
index 5db3bc0a..35568d28 100644
--- a/tests/e2e-instrumentation/instrumentation-java-multicontainer/00-install-collector.yaml
+++ b/tests/e2e-instrumentation/instrumentation-java-multicontainer/00-install-collector.yaml
@@ -1,22 +1,27 @@
-apiVersion: opentelemetry.io/v1alpha1
+apiVersion: opentelemetry.io/v1beta1
 kind: OpenTelemetryCollector
 metadata:
   name: sidecar
 spec:
   mode: sidecar
-  config: |
+  config:
     receivers:
       otlp:
         protocols:
-          grpc:
-          http:
-    processors:
+          grpc: {}
+          http: {}
+
+    processors: {}
 
     exporters:
       debug:
+        verbosity: detailed
 
     service:
       pipelines:
         traces:
           receivers: [otlp]
           exporters: [debug]
+        metrics:
+          receivers: [otlp]
+          exporters: [debug]
diff --git a/tests/e2e-instrumentation/instrumentation-java-multicontainer/00-install-instrumentation.yaml b/tests/e2e-instrumentation/instrumentation-java-multicontainer/00-install-instrumentation.yaml
index f77a3f83..497b7813 100644
--- a/tests/e2e-instrumentation/instrumentation-java-multicontainer/00-install-instrumentation.yaml
+++ b/tests/e2e-instrumentation/instrumentation-java-multicontainer/00-install-instrumentation.yaml
@@ -9,11 +9,9 @@ spec:
     - name: OTEL_EXPORTER_OTLP_ENDPOINT
       value: http://localhost:4317
     - name: OTEL_EXPORTER_OTLP_TIMEOUT
-      value: "20"
+      value: "20000"
     - name: OTEL_TRACES_SAMPLER
-      value: parentbased_traceidratio
-    - name: OTEL_TRACES_SAMPLER_ARG
-      value: "0.85"
+      value: always_on
     - name: SPLUNK_TRACE_RESPONSE_HEADER_ENABLED
       value: "true"
   exporter:
diff --git a/tests/e2e-instrumentation/instrumentation-java-multicontainer/01-assert.yaml b/tests/e2e-instrumentation/instrumentation-java-multicontainer/01-assert.yaml
index bed310bf..257d61c1 100644
--- a/tests/e2e-instrumentation/instrumentation-java-multicontainer/01-assert.yaml
+++ b/tests/e2e-instrumentation/instrumentation-java-multicontainer/01-assert.yaml
@@ -31,11 +31,9 @@ spec:
     - name: OTEL_EXPORTER_OTLP_ENDPOINT
       value: http://localhost:4317
     - name: OTEL_EXPORTER_OTLP_TIMEOUT
-      value: "20"
+      value: "20000"
     - name: OTEL_TRACES_SAMPLER
-      value: parentbased_traceidratio
-    - name: OTEL_TRACES_SAMPLER_ARG
-      value: "0.85"
+      value: always_on
     - name: SPLUNK_TRACE_RESPONSE_HEADER_ENABLED
       value: "true"
     - name: OTEL_SERVICE_NAME
@@ -81,11 +79,9 @@ spec:
     - name: OTEL_EXPORTER_OTLP_ENDPOINT
       value: http://localhost:4317
     - name: OTEL_EXPORTER_OTLP_TIMEOUT
-      value: "20"
+      value: "20000"
     - name: OTEL_TRACES_SAMPLER
-      value: parentbased_traceidratio
-    - name: OTEL_TRACES_SAMPLER_ARG
-      value: "0.85"
+      value: always_on
     - name: SPLUNK_TRACE_RESPONSE_HEADER_ENABLED
       value: "true"
     - name: OTEL_SERVICE_NAME
@@ -112,6 +108,9 @@ spec:
   - args:
     - --config=env:OTEL_CONFIG
     name: otc-container
+    volumeMounts:
+    - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
+      readOnly: true
   initContainers:
   - name: opentelemetry-auto-instrumentation-java
 status:
diff --git a/tests/e2e-instrumentation/instrumentation-java-multicontainer/01-install-app.yaml b/tests/e2e-instrumentation/instrumentation-java-multicontainer/01-install-app.yaml
index d099624d..691f83e3 100644
--- a/tests/e2e-instrumentation/instrumentation-java-multicontainer/01-install-app.yaml
+++ b/tests/e2e-instrumentation/instrumentation-java-multicontainer/01-install-app.yaml
@@ -23,5 +23,18 @@ spec:
       containers:
       - name: myapp
         image: ghcr.io/open-telemetry/opentelemetry-operator/e2e-test-app-java:main
+        securityContext:
+          allowPrivilegeEscalation: false
+          capabilities:
+            drop: ["ALL"]
+        readinessProbe:
+          httpGet:
+            path: /
+            port: 8080
+            scheme: HTTP
+          initialDelaySeconds: 5
+          periodSeconds: 15
+          timeoutSeconds: 2
+          failureThreshold: 3
       - name: myrabbit
         image: rabbitmq:3
diff --git a/tests/e2e-instrumentation/instrumentation-java-multicontainer/02-assert.yaml b/tests/e2e-instrumentation/instrumentation-java-multicontainer/02-assert.yaml
index 6fcb1861..c642a148 100644
--- a/tests/e2e-instrumentation/instrumentation-java-multicontainer/02-assert.yaml
+++ b/tests/e2e-instrumentation/instrumentation-java-multicontainer/02-assert.yaml
@@ -1,14 +1,3 @@
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: my-java-multi
-status:
-  availableReplicas: 1
-  readyReplicas: 1
-  replicas: 1
-  updatedReplicas: 1
-
----
 apiVersion: v1
 kind: Pod
 metadata:
@@ -42,11 +31,9 @@ spec:
     - name: OTEL_EXPORTER_OTLP_ENDPOINT
       value: http://localhost:4317
     - name: OTEL_EXPORTER_OTLP_TIMEOUT
-      value: "20"
+      value: "20000"
     - name: OTEL_TRACES_SAMPLER
-      value: parentbased_traceidratio
-    - name: OTEL_TRACES_SAMPLER_ARG
-      value: "0.85"
+      value: always_on
     - name: SPLUNK_TRACE_RESPONSE_HEADER_ENABLED
       value: "true"
     - name: OTEL_SERVICE_NAME
@@ -65,6 +52,9 @@ spec:
       value: jaeger,b3
     - name: OTEL_RESOURCE_ATTRIBUTES
     name: myapp
+    ports:
+    - containerPort: 8080
+      protocol: TCP
     volumeMounts:
     - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
       readOnly: true
@@ -78,6 +68,9 @@ spec:
   - args:
     - --config=env:OTEL_CONFIG
     name: otc-container
+    volumeMounts:
+    - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
+      readOnly: true
   initContainers:
   - name: opentelemetry-auto-instrumentation-java
 status:
diff --git a/tests/e2e-instrumentation/instrumentation-java-multicontainer/02-install-app.yaml b/tests/e2e-instrumentation/instrumentation-java-multicontainer/02-install-app.yaml
index 7cac5656..00964848 100644
--- a/tests/e2e-instrumentation/instrumentation-java-multicontainer/02-install-app.yaml
+++ b/tests/e2e-instrumentation/instrumentation-java-multicontainer/02-install-app.yaml
@@ -22,11 +22,22 @@ spec:
         fsGroup: 3000
       containers:
       - name: myapp
+        ports:
+        - containerPort: 8080
         image: ghcr.io/open-telemetry/opentelemetry-operator/e2e-test-app-java:main
         securityContext:
           allowPrivilegeEscalation: false
           capabilities:
             drop: ["ALL"]
+        readinessProbe:
+          httpGet:
+            path: /
+            port: 8080
+            scheme: HTTP
+          initialDelaySeconds: 5
+          periodSeconds: 15
+          timeoutSeconds: 2
+          failureThreshold: 3
       - name: myrabbit
         image: rabbitmq:3
         securityContext:
diff --git a/tests/e2e-instrumentation/instrumentation-java-multicontainer/chainsaw-test.yaml b/tests/e2e-instrumentation/instrumentation-java-multicontainer/chainsaw-test.yaml
index bb72bab4..87a73277 100755
--- a/tests/e2e-instrumentation/instrumentation-java-multicontainer/chainsaw-test.yaml
+++ b/tests/e2e-instrumentation/instrumentation-java-multicontainer/chainsaw-test.yaml
@@ -40,12 +40,203 @@ spec:
     catch:
       - podLogs:
           selector: app=my-java-multi
+  - name: Make a request to the app
+    try:
+      - command:
+          env:
+          - name: appnamespace
+            value: ($namespace)
+          entrypoint: kubectl
+          args:
+            - get
+            - pod
+            - -n
+            - $appnamespace
+            - -l
+            - app=my-java-multi
+            - -o
+            - jsonpath={.items[0].metadata.name}
+          outputs:
+          - name: podName
+            value: ($stdout)
+      - proxy:
+          apiVersion: v1
+          kind: Pod
+          name: ($podName)
+          namespace: ($namespace)
+          port: "8080"
+  - name: Wait for telemetry data to be collected
+    try:
+    - script:
+        env:
+          - name: LABEL_SELECTOR
+            value: "app=my-java-multi"
+          - name: CONTAINER_NAME
+            value: "otc-container" 
+          - name: RETRY_TIMEOUT
+            value: "120"
+          - name: RETRY_SLEEP
+            value: "5"
+          - name: SEARCH_STRINGS_ENV
+            value: |
+              AggregationTemporality: Cumulative
+              Name: process.runtime.jvm.memory.usage
+              Description: Measure of memory used
+        timeout: 2m
+        content: ../../test-e2e-apps/scripts/check_pod_logs.sh
+  - name: Check the instrumented app has sent the telemetry data successfully
+    try:
+      - command:
+          entrypoint: kubectl
+          args:
+            - get
+            - pod
+            - -n
+            - ${NAMESPACE}
+            - -l
+            - app=my-java-multi
+            - -o
+            - jsonpath={.items[0].metadata.name}
+          outputs:
+          - name: podName
+            value: ($stdout)
+      - script:
+          env:
+          - name: podName
+            value: ($podName)
+          content: |
+            #!/bin/bash
+            # set -ex
+            kubectl get --raw /api/v1/namespaces/$NAMESPACE/pods/${podName}:8888/proxy/metrics
+          outputs:
+          - name: metrics
+            value: (x_metrics_decode($stdout))
+          check:
+            ($error == null): true
+      - assert:
+          resource:
+            ($metrics[?as_string(metric."__name__") == 'otelcol_exporter_sent_spans_total'].value | [0] > `1`): true
+            ($metrics[?as_string(metric."__name__") == 'otelcol_receiver_accepted_spans_total'].value | [0] > `1`): true
+            ($metrics[?as_string(metric."__name__") == 'otelcol_exporter_sent_metric_points_total'].value | [0] > `1`): true
+            ($metrics[?as_string(metric."__name__") == 'otelcol_receiver_accepted_metric_points_total'].value | [0] > `1`): true
+    catch:
+      - podLogs:
+          selector: app=my-java-multi
+          container: otc-container
   - name: step-03
     try:
+    - command:
+        env:
+        - name: appnamespace
+          value: ($namespace)
+        entrypoint: kubectl
+        args:
+          - get
+          - pod
+          - -n
+          - $appnamespace
+          - -l
+          - app=my-java-multi
+          - -o
+          - jsonpath={.items[0].metadata.name}
+        outputs:
+        - name: podName
+          value: ($stdout)
     - apply:
         file: 02-install-app.yaml
     - assert:
         file: 02-assert.yaml
+    - wait:
+        apiVersion: v1
+        kind: Pod
+        name: ($podName)
+        namespace: ($namespace)
+        timeout: 1m
+        for:
+          deletion: {}
+    catch:
+      - podLogs:
+          selector: app=my-java-multi
+  - name: Make a request to the app
+    try:
+      - command:
+          env:
+          - name: appnamespace
+            value: ($namespace)
+          entrypoint: kubectl
+          args:
+            - get
+            - pod
+            - -n
+            - $appnamespace
+            - -l
+            - app=my-java-multi
+            - -o
+            - jsonpath={.items[0].metadata.name}
+          outputs:
+          - name: podName
+            value: ($stdout)
+      - proxy:
+          apiVersion: v1
+          kind: Pod
+          name: ($podName)
+          namespace: ($namespace)
+          port: "8080"
+  - name: Wait for telemetry data to be collected
+    try:
+    - script:
+        env:
+          - name: LABEL_SELECTOR
+            value: "app=my-java-multi"
+          - name: CONTAINER_NAME
+            value: "otc-container" 
+          - name: RETRY_TIMEOUT
+            value: "120"
+          - name: RETRY_SLEEP
+            value: "5"
+          - name: SEARCH_STRINGS_ENV
+            value: |
+              AggregationTemporality: Cumulative
+              Name: process.runtime.jvm.memory.usage
+              Description: Measure of memory used
+        timeout: 2m
+        content: ../../test-e2e-apps/scripts/check_pod_logs.sh
+  - name: Check the instrumented app has sent the telemetry data successfully
+    try:
+      - command:
+          entrypoint: kubectl
+          args:
+            - get
+            - pod
+            - -n
+            - ${NAMESPACE}
+            - -l
+            - app=my-java-multi
+            - -o
+            - jsonpath={.items[0].metadata.name}
+          outputs:
+          - name: podName
+            value: ($stdout)
+      - script:
+          env:
+          - name: podName
+            value: ($podName)
+          content: |
+            #!/bin/bash
+            # set -ex
+            kubectl get --raw /api/v1/namespaces/$NAMESPACE/pods/${podName}:8888/proxy/metrics
+          outputs:
+          - name: metrics
+            value: (x_metrics_decode($stdout))
+          check:
+            ($error == null): true
+      - assert:
+          resource:
+            ($metrics[?as_string(metric."__name__") == 'otelcol_exporter_sent_spans_total'].value | [0] > `1`): true
+            ($metrics[?as_string(metric."__name__") == 'otelcol_receiver_accepted_spans_total'].value | [0] > `1`): true
+            ($metrics[?as_string(metric."__name__") == 'otelcol_exporter_sent_metric_points_total'].value | [0] > `1`): true
+            ($metrics[?as_string(metric."__name__") == 'otelcol_receiver_accepted_metric_points_total'].value | [0] > `1`): true
     catch:
       - podLogs:
           selector: app=my-java-multi
+          container: otc-container
\ No newline at end of file
diff --git a/tests/e2e-instrumentation/instrumentation-java-other-ns/02-install-collector.yaml b/tests/e2e-instrumentation/instrumentation-java-other-ns/02-install-collector.yaml
index 34a26ebb..e32ff2b4 100644
--- a/tests/e2e-instrumentation/instrumentation-java-other-ns/02-install-collector.yaml
+++ b/tests/e2e-instrumentation/instrumentation-java-other-ns/02-install-collector.yaml
@@ -1,22 +1,27 @@
-apiVersion: opentelemetry.io/v1alpha1
+apiVersion: opentelemetry.io/v1beta1
 kind: OpenTelemetryCollector
 metadata:
   name: sidecar
 spec:
-  config: |
+  config:
     receivers:
       otlp:
         protocols:
-          grpc:
-          http:
-    processors:
+          grpc: {}
+          http: {}
+
+    processors: {}
 
     exporters:
       debug:
+        verbosity: detailed
 
     service:
       pipelines:
         traces:
           receivers: [otlp]
           exporters: [debug]
+        metrics:
+          receivers: [otlp]
+          exporters: [debug]
   mode: sidecar
diff --git a/tests/e2e-instrumentation/instrumentation-java-other-ns/02-install-instrumentation.yaml b/tests/e2e-instrumentation/instrumentation-java-other-ns/02-install-instrumentation.yaml
index 1cc0dfd2..99f93a49 100644
--- a/tests/e2e-instrumentation/instrumentation-java-other-ns/02-install-instrumentation.yaml
+++ b/tests/e2e-instrumentation/instrumentation-java-other-ns/02-install-instrumentation.yaml
@@ -16,11 +16,9 @@ spec:
     - name: OTEL_EXPORTER_OTLP_ENDPOINT
       value: http://localhost:4317
     - name: OTEL_EXPORTER_OTLP_TIMEOUT
-      value: "20"
+      value: "20000"
     - name: OTEL_TRACES_SAMPLER
-      value: parentbased_traceidratio
-    - name: OTEL_TRACES_SAMPLER_ARG
-      value: "0.85"
+      value: always_on
     - name: SPLUNK_TRACE_RESPONSE_HEADER_ENABLED
       value: "true"
   exporter:
diff --git a/tests/e2e-instrumentation/instrumentation-java-other-ns/03-assert.yaml b/tests/e2e-instrumentation/instrumentation-java-other-ns/03-assert.yaml
index 888e3a92..2100208c 100644
--- a/tests/e2e-instrumentation/instrumentation-java-other-ns/03-assert.yaml
+++ b/tests/e2e-instrumentation/instrumentation-java-other-ns/03-assert.yaml
@@ -30,11 +30,9 @@ spec:
     - name: OTEL_EXPORTER_OTLP_ENDPOINT
       value: http://localhost:4317
     - name: OTEL_EXPORTER_OTLP_TIMEOUT
-      value: "20"
+      value: "20000"
     - name: OTEL_TRACES_SAMPLER
-      value: parentbased_traceidratio
-    - name: OTEL_TRACES_SAMPLER_ARG
-      value: "0.85"
+      value: always_on
     - name: SPLUNK_TRACE_RESPONSE_HEADER_ENABLED
       value: "true"
     - name: OTEL_SERVICE_NAME
@@ -53,6 +51,9 @@ spec:
       value: jaeger,b3
     - name: OTEL_RESOURCE_ATTRIBUTES
     name: myapp
+    ports:
+    - containerPort: 8080
+      protocol: TCP
     volumeMounts:
     - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
       readOnly: true
@@ -61,6 +62,9 @@ spec:
   - args:
     - --config=env:OTEL_CONFIG
     name: otc-container
+    volumeMounts:
+    - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
+      readOnly: true
   initContainers:
   - name: opentelemetry-auto-instrumentation-java
 status:
diff --git a/tests/e2e-instrumentation/instrumentation-java-other-ns/03-install-app.yaml b/tests/e2e-instrumentation/instrumentation-java-other-ns/03-install-app.yaml
index 861a09bf..14e49f7a 100644
--- a/tests/e2e-instrumentation/instrumentation-java-other-ns/03-install-app.yaml
+++ b/tests/e2e-instrumentation/instrumentation-java-other-ns/03-install-app.yaml
@@ -21,8 +21,19 @@ spec:
         fsGroup: 3000
       containers:
       - name: myapp
+        ports:
+        - containerPort: 8080
         image: ghcr.io/open-telemetry/opentelemetry-operator/e2e-test-app-java:main
         securityContext:
           allowPrivilegeEscalation: false
           capabilities:
             drop: ["ALL"]
+        readinessProbe:
+          httpGet:
+            path: /
+            port: 8080
+            scheme: HTTP
+          initialDelaySeconds: 5
+          periodSeconds: 15
+          timeoutSeconds: 2
+          failureThreshold: 3
diff --git a/tests/e2e-instrumentation/instrumentation-java-other-ns/chainsaw-test.yaml b/tests/e2e-instrumentation/instrumentation-java-other-ns/chainsaw-test.yaml
index ab24f89c..28441540 100755
--- a/tests/e2e-instrumentation/instrumentation-java-other-ns/chainsaw-test.yaml
+++ b/tests/e2e-instrumentation/instrumentation-java-other-ns/chainsaw-test.yaml
@@ -2,7 +2,6 @@
 apiVersion: chainsaw.kyverno.io/v1alpha1
 kind: Test
 metadata:
-  creationTimestamp: null
   name: instrumentation-java-other-ns
 spec:
   steps:
@@ -45,10 +44,86 @@ spec:
     catch:
       - podLogs:
           selector: app=my-java-other-ns
-  - name: step-04
+  - name: Make a request to the app
     try:
-    - delete:
-        ref:
+      - command:
+          env:
+          - name: appnamespace
+            value: ($namespace)
+          entrypoint: kubectl
+          args:
+            - get
+            - pod
+            - -n
+            - $appnamespace
+            - -l
+            - app=my-java-other-ns
+            - -o
+            - jsonpath={.items[0].metadata.name}
+          outputs:
+          - name: podName
+            value: ($stdout)
+      - proxy:
           apiVersion: v1
-          kind: Namespace
-          name: my-other-ns
+          kind: Pod
+          name: ($podName)
+          namespace: ($namespace)
+          port: "8080"
+  - name: Wait for telemetry data to be collected
+    try:
+    - script:
+        env:
+          - name: LABEL_SELECTOR
+            value: "app=my-java-other-ns"
+          - name: CONTAINER_NAME
+            value: "otc-container" 
+          - name: RETRY_TIMEOUT
+            value: "120"
+          - name: RETRY_SLEEP
+            value: "5"
+          - name: SEARCH_STRINGS_ENV
+            value: |
+              AggregationTemporality: Cumulative
+              Name: process.runtime.jvm.memory.usage
+              Description: Measure of memory used
+        timeout: 2m
+        content: ../../test-e2e-apps/scripts/check_pod_logs.sh
+  - name: Check the instrumented app has sent the telemetry data successfully
+    try:
+      - command:
+          entrypoint: kubectl
+          args:
+            - get
+            - pod
+            - -n
+            - ${NAMESPACE}
+            - -l
+            - app=my-java-other-ns
+            - -o
+            - jsonpath={.items[0].metadata.name}
+          outputs:
+          - name: podName
+            value: ($stdout)
+      - script:
+          env:
+          - name: podName
+            value: ($podName)
+          content: |
+            #!/bin/bash
+            # set -ex
+            kubectl get --raw /api/v1/namespaces/$NAMESPACE/pods/${podName}:8888/proxy/metrics
+          outputs:
+          - name: metrics
+            value: (x_metrics_decode($stdout))
+          check:
+            ($error == null): true
+      - assert:
+          resource:
+            ($metrics[?as_string(metric."__name__") == 'otelcol_exporter_sent_spans_total'].value | [0] > `1`): true
+            ($metrics[?as_string(metric."__name__") == 'otelcol_receiver_accepted_spans_total'].value | [0] > `1`): true
+            ($metrics[?as_string(metric."__name__") == 'otelcol_exporter_sent_metric_points_total'].value | [0] > `1`): true
+            ($metrics[?as_string(metric."__name__") == 'otelcol_receiver_accepted_metric_points_total'].value | [0] > `1`): true
+    catch:
+      - podLogs:
+          selector: app=my-java-other-ns
+          container: otc-container
\ No newline at end of file
diff --git a/tests/e2e-instrumentation/instrumentation-java-tls/00-install-collector.yaml b/tests/e2e-instrumentation/instrumentation-java-tls/00-install-collector.yaml
index 536c3924..4fe17d08 100644
--- a/tests/e2e-instrumentation/instrumentation-java-tls/00-install-collector.yaml
+++ b/tests/e2e-instrumentation/instrumentation-java-tls/00-install-collector.yaml
@@ -31,13 +31,19 @@ spec:
               cert_file: /certs/tls.crt
               key_file: /certs/tls.key
               client_ca_file: /certs-ca/ca.crt
-    processors:
+
+    processors: {}
 
     exporters:
-      debug: {}
+      debug:
+        verbosity: detailed
 
     service:
       pipelines:
         traces:
           receivers: [otlp]
           exporters: [debug]
+        metrics:
+          receivers: [otlp]
+          exporters: [debug]
+ 
\ No newline at end of file
diff --git a/tests/e2e-instrumentation/instrumentation-java-tls/01-assert.yaml b/tests/e2e-instrumentation/instrumentation-java-tls/01-assert.yaml
index aab95db1..59c3711b 100644
--- a/tests/e2e-instrumentation/instrumentation-java-tls/01-assert.yaml
+++ b/tests/e2e-instrumentation/instrumentation-java-tls/01-assert.yaml
@@ -46,6 +46,9 @@ spec:
       value: "1"
     - name: OTEL_RESOURCE_ATTRIBUTES
     name: myapp
+    ports:
+    - containerPort: 8080
+      protocol: TCP
     volumeMounts:
       - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
         readOnly: true
diff --git a/tests/e2e-instrumentation/instrumentation-java-tls/01-install-app.yaml b/tests/e2e-instrumentation/instrumentation-java-tls/01-install-app.yaml
index 0d16826e..294a1626 100644
--- a/tests/e2e-instrumentation/instrumentation-java-tls/01-install-app.yaml
+++ b/tests/e2e-instrumentation/instrumentation-java-tls/01-install-app.yaml
@@ -20,8 +20,19 @@ spec:
         fsGroup: 3000
       containers:
       - name: myapp
+        ports:
+        - containerPort: 8080
         image: ghcr.io/open-telemetry/opentelemetry-operator/e2e-test-app-java:main
         securityContext:
           allowPrivilegeEscalation: false
           capabilities:
             drop: ["ALL"]
+        readinessProbe:
+          httpGet:
+            path: /
+            port: 8080
+            scheme: HTTP
+          initialDelaySeconds: 5
+          periodSeconds: 15
+          timeoutSeconds: 2
+          failureThreshold: 3
diff --git a/tests/e2e-instrumentation/instrumentation-java-tls/chainsaw-test.yaml b/tests/e2e-instrumentation/instrumentation-java-tls/chainsaw-test.yaml
index f552743f..275c8234 100755
--- a/tests/e2e-instrumentation/instrumentation-java-tls/chainsaw-test.yaml
+++ b/tests/e2e-instrumentation/instrumentation-java-tls/chainsaw-test.yaml
@@ -2,7 +2,6 @@
 apiVersion: chainsaw.kyverno.io/v1alpha1
 kind: Test
 metadata:
-  creationTimestamp: null
   name: instrumentation-java-tls
 spec:
   steps:
@@ -43,4 +42,84 @@ spec:
         file: 01-assert.yaml
     catch:
       - podLogs:
-          selector: app=my-java
\ No newline at end of file
+          selector: app=my-java
+  - name: Make a request to the app
+    try:
+      - command:
+          entrypoint: kubectl
+          args:
+            - get
+            - pod
+            - -n
+            - ${NAMESPACE}
+            - -l
+            - app=my-java
+            - -o
+            - jsonpath={.items[0].metadata.name}
+          outputs:
+          - name: podName
+            value: ($stdout)
+      - proxy:
+          apiVersion: v1
+          kind: Pod
+          name: ($podName)
+          namespace: ${NAMESPACE}
+          port: "8080"
+  - name: Wait for telemetry data to be collected
+    try:
+    - script:
+        env:
+          - name: LABEL_SELECTOR
+            value: "app.kubernetes.io/name=simplest-collector"
+          - name: CONTAINER_NAME
+            value: "otc-container" 
+          - name: RETRY_TIMEOUT
+            value: "120"
+          - name: RETRY_SLEEP
+            value: "5"
+          - name: SEARCH_STRINGS_ENV
+            value: |
+              AggregationTemporality: Cumulative
+              Name: process.runtime.jvm.memory.usage
+              Description: Measure of memory used
+        timeout: 2m
+        content: ../../test-e2e-apps/scripts/check_pod_logs.sh
+  - name: Check the instrumented app has sent the telemetry data successfully
+    try:
+      - command:
+          entrypoint: kubectl
+          args:
+            - get
+            - pod
+            - -n
+            - ${NAMESPACE}
+            - -l
+            - app.kubernetes.io/name=simplest-collector
+            - -o
+            - jsonpath={.items[0].metadata.name}
+          outputs:
+          - name: podName
+            value: ($stdout)
+      - script:
+          env:
+          - name: podName
+            value: ($podName)
+          content: |
+            #!/bin/bash
+            # set -ex
+            kubectl get --raw /api/v1/namespaces/$NAMESPACE/pods/${podName}:8888/proxy/metrics
+          outputs:
+          - name: metrics
+            value: (x_metrics_decode($stdout))
+          check:
+            ($error == null): true
+      - assert:
+          resource:
+            ($metrics[?as_string(metric."__name__") == 'otelcol_exporter_sent_spans_total'].value | [0] > `1`): true
+            ($metrics[?as_string(metric."__name__") == 'otelcol_receiver_accepted_spans_total'].value | [0] > `1`): true
+            ($metrics[?as_string(metric."__name__") == 'otelcol_exporter_sent_metric_points_total'].value | [0] > `1`): true
+            ($metrics[?as_string(metric."__name__") == 'otelcol_receiver_accepted_metric_points_total'].value | [0] > `1`): true
+    catch:
+      - podLogs:
+          selector: app.kubernetes.io/name=simplest-collector
+          container: otc-container
diff --git a/tests/e2e-instrumentation/instrumentation-java/00-install-collector.yaml b/tests/e2e-instrumentation/instrumentation-java/00-install-collector.yaml
index 34a26ebb..67a1b09d 100644
--- a/tests/e2e-instrumentation/instrumentation-java/00-install-collector.yaml
+++ b/tests/e2e-instrumentation/instrumentation-java/00-install-collector.yaml
@@ -1,22 +1,27 @@
-apiVersion: opentelemetry.io/v1alpha1
+apiVersion: opentelemetry.io/v1beta1
 kind: OpenTelemetryCollector
 metadata:
   name: sidecar
 spec:
-  config: |
+  config:
     receivers:
       otlp:
         protocols:
-          grpc:
-          http:
-    processors:
+          grpc: {}
+          http: {}
+    
+    processors: {}
 
     exporters:
       debug:
+        verbosity: detailed
 
     service:
       pipelines:
         traces:
           receivers: [otlp]
           exporters: [debug]
+        metrics:
+          receivers: [otlp]
+          exporters: [debug]
   mode: sidecar
diff --git a/tests/e2e-instrumentation/instrumentation-java/00-install-instrumentation.yaml b/tests/e2e-instrumentation/instrumentation-java/00-install-instrumentation.yaml
index f77a3f83..e06f19ce 100644
--- a/tests/e2e-instrumentation/instrumentation-java/00-install-instrumentation.yaml
+++ b/tests/e2e-instrumentation/instrumentation-java/00-install-instrumentation.yaml
@@ -9,11 +9,9 @@ spec:
     - name: OTEL_EXPORTER_OTLP_ENDPOINT
       value: http://localhost:4317
     - name: OTEL_EXPORTER_OTLP_TIMEOUT
-      value: "20"
+      value: "20000"
     - name: OTEL_TRACES_SAMPLER
-      value: parentbased_traceidratio
-    - name: OTEL_TRACES_SAMPLER_ARG
-      value: "0.85"
+      value: always_on
     - name: SPLUNK_TRACE_RESPONSE_HEADER_ENABLED
       value: "true"
   exporter:
@@ -21,9 +19,6 @@ spec:
   propagators:
     - jaeger
     - b3
-  sampler:
-    type: parentbased_traceidratio
-    argument: "0.25"
   java:
     env:
     - name: OTEL_JAVAAGENT_DEBUG
diff --git a/tests/e2e-instrumentation/instrumentation-java/01-assert.yaml b/tests/e2e-instrumentation/instrumentation-java/01-assert.yaml
index cd2123e3..f1d1e583 100644
--- a/tests/e2e-instrumentation/instrumentation-java/01-assert.yaml
+++ b/tests/e2e-instrumentation/instrumentation-java/01-assert.yaml
@@ -30,11 +30,9 @@ spec:
     - name: OTEL_EXPORTER_OTLP_ENDPOINT
       value: http://localhost:4317
     - name: OTEL_EXPORTER_OTLP_TIMEOUT
-      value: "20"
+      value: "20000"
     - name: OTEL_TRACES_SAMPLER
-      value: parentbased_traceidratio
-    - name: OTEL_TRACES_SAMPLER_ARG
-      value: "0.85"
+      value: always_on
     - name: SPLUNK_TRACE_RESPONSE_HEADER_ENABLED
       value: "true"
     - name: OTEL_SERVICE_NAME
@@ -53,6 +51,9 @@ spec:
       value: jaeger,b3
     - name: OTEL_RESOURCE_ATTRIBUTES
     name: myapp
+    ports:
+    - containerPort: 8080
+      protocol: TCP
     volumeMounts:
     - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
       readOnly: true
@@ -61,6 +62,9 @@ spec:
   - args:
     - --config=env:OTEL_CONFIG
     name: otc-container
+    volumeMounts:
+    - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
+      readOnly: true
   initContainers:
   - name: opentelemetry-auto-instrumentation-java
 status:
diff --git a/tests/e2e-instrumentation/instrumentation-java/01-install-app.yaml b/tests/e2e-instrumentation/instrumentation-java/01-install-app.yaml
index 4655644b..db1fef06 100644
--- a/tests/e2e-instrumentation/instrumentation-java/01-install-app.yaml
+++ b/tests/e2e-instrumentation/instrumentation-java/01-install-app.yaml
@@ -21,8 +21,19 @@ spec:
         fsGroup: 3000
       containers:
       - name: myapp
+        ports:
+        - containerPort: 8080
         image: ghcr.io/open-telemetry/opentelemetry-operator/e2e-test-app-java:main
         securityContext:
           allowPrivilegeEscalation: false
           capabilities:
             drop: ["ALL"]
+        readinessProbe:
+          httpGet:
+            path: /
+            port: 8080
+            scheme: HTTP
+          initialDelaySeconds: 5
+          periodSeconds: 15
+          timeoutSeconds: 2
+          failureThreshold: 3
diff --git a/tests/e2e-instrumentation/instrumentation-java/chainsaw-test.yaml b/tests/e2e-instrumentation/instrumentation-java/chainsaw-test.yaml
index bffdefbc..b05b3e29 100755
--- a/tests/e2e-instrumentation/instrumentation-java/chainsaw-test.yaml
+++ b/tests/e2e-instrumentation/instrumentation-java/chainsaw-test.yaml
@@ -2,7 +2,6 @@
 apiVersion: chainsaw.kyverno.io/v1alpha1
 kind: Test
 metadata:
-  creationTimestamp: null
   name: instrumentation-java
 spec:
   steps:
@@ -37,4 +36,84 @@ spec:
         file: 01-assert.yaml
     catch:
       - podLogs:
-          selector: app=my-java
\ No newline at end of file
+          selector: app=my-java
+  - name: Make a request to the app
+    try:
+      - command:
+          entrypoint: kubectl
+          args:
+            - get
+            - pod
+            - -n
+            - ${NAMESPACE}
+            - -l
+            - app=my-java
+            - -o
+            - jsonpath={.items[0].metadata.name}
+          outputs:
+          - name: podName
+            value: ($stdout)
+      - proxy:
+          apiVersion: v1
+          kind: Pod
+          name: ($podName)
+          namespace: ${NAMESPACE}
+          port: "8080"
+  - name: Wait for telemetry data
+    try:
+    - script:
+        env:
+          - name: LABEL_SELECTOR
+            value: "app=my-java"
+          - name: CONTAINER_NAME
+            value: "otc-container" 
+          - name: RETRY_TIMEOUT
+            value: "120"
+          - name: RETRY_SLEEP
+            value: "5"
+          - name: SEARCH_STRINGS_ENV
+            value: |
+              AggregationTemporality: Cumulative
+              Name: process.runtime.jvm.memory.usage
+              Description: Measure of memory used
+        timeout: 2m
+        content: ../../test-e2e-apps/scripts/check_pod_logs.sh
+  - name: Check the instrumented app has sent the telemetry data successfully
+    try:
+      - command:
+          entrypoint: kubectl
+          args:
+            - get
+            - pod
+            - -n
+            - ${NAMESPACE}
+            - -l
+            - app=my-java
+            - -o
+            - jsonpath={.items[0].metadata.name}
+          outputs:
+          - name: podName
+            value: ($stdout)
+      - script:
+          env:
+          - name: podName
+            value: ($podName)
+          content: |
+            #!/bin/bash
+            # set -ex
+            kubectl get --raw /api/v1/namespaces/$NAMESPACE/pods/${podName}:8888/proxy/metrics
+          outputs:
+          - name: metrics
+            value: (x_metrics_decode($stdout))
+          check:
+            ($error == null): true
+      - assert:
+          resource:
+            ($metrics[?as_string(metric."__name__") == 'otelcol_exporter_sent_spans_total'].value | [0] > `1`): true
+            ($metrics[?as_string(metric."__name__") == 'otelcol_receiver_accepted_spans_total'].value | [0] > `1`): true
+            ($metrics[?as_string(metric."__name__") == 'otelcol_exporter_sent_metric_points_total'].value | [0] > `1`): true
+            ($metrics[?as_string(metric."__name__") == 'otelcol_receiver_accepted_metric_points_total'].value | [0] > `1`): true
+    catch:
+      - podLogs:
+          selector: app=my-java
+          container: otc-container
\ No newline at end of file
diff --git a/tests/e2e/extension/00-assert-jaeger-extension.yaml b/tests/e2e/extension/00-assert-jaeger-extension.yaml
index c62406a1..d6904d28 100644
--- a/tests/e2e/extension/00-assert-jaeger-extension.yaml
+++ b/tests/e2e/extension/00-assert-jaeger-extension.yaml
@@ -1,29 +1,26 @@
-apiVersion: v1
-items:
-- apiVersion: apps/v1
-  kind: Deployment
-  metadata:
-    name: jaeger-inmemory-collector
-  spec:
-    template:
-      spec:
-        containers:
-        - ports:
-          - containerPort: 16686
-            name: jaeger-query
-            protocol: TCP
-          - containerPort: 8888
-            name: metrics
-            protocol: TCP
-          - containerPort: 4317
-            name: otlp-grpc
-            protocol: TCP
-          - containerPort: 4318
-            name: otlp-http
-            protocol: TCP
-kind: List
+apiVersion: apps/v1
+kind: Deployment
 metadata:
-  resourceVersion: ""
+  name: jaeger-inmemory-collector
+spec:
+  template:
+    spec:
+      containers:
+      - ports:
+        - containerPort: 16686
+          name: jaeger-query
+          protocol: TCP
+        - containerPort: 8888
+          name: metrics
+          protocol: TCP
+        - containerPort: 4317
+          name: otlp-grpc
+          protocol: TCP
+        - containerPort: 4318
+          name: otlp-http
+          protocol: TCP
+status:
+  readyReplicas: 1
 ---
 apiVersion: v1
 kind: Service
diff --git a/tests/test-e2e-apps/bridge-server/go.mod b/tests/test-e2e-apps/bridge-server/go.mod
index a9bbf309..842d9990 100644
--- a/tests/test-e2e-apps/bridge-server/go.mod
+++ b/tests/test-e2e-apps/bridge-server/go.mod
@@ -18,5 +18,5 @@ require (
 	go.opentelemetry.io/otel v1.27.0 // indirect
 	go.opentelemetry.io/otel/metric v1.27.0 // indirect
 	go.opentelemetry.io/otel/trace v1.27.0 // indirect
-	golang.org/x/net v0.36.0 // indirect
+	golang.org/x/net v0.38.0 // indirect
 )
diff --git a/tests/test-e2e-apps/bridge-server/go.sum b/tests/test-e2e-apps/bridge-server/go.sum
index d46a10cb..f3815109 100644
--- a/tests/test-e2e-apps/bridge-server/go.sum
+++ b/tests/test-e2e-apps/bridge-server/go.sum
@@ -30,8 +30,8 @@ go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0
 go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak=
 go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw=
 go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4=
-golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
-golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
+golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
+golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
 google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
 google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
diff --git a/tests/test-e2e-apps/scripts/check_pod_logs.sh b/tests/test-e2e-apps/scripts/check_pod_logs.sh
new file mode 100755
index 00000000..44f1afab
--- /dev/null
+++ b/tests/test-e2e-apps/scripts/check_pod_logs.sh
@@ -0,0 +1,154 @@
+#!/bin/bash
+# This script repeatedly checks pod logs from a specific container until all specified strings are found
+# simultaneously or a timeout occurs.
+
+# --- Configuration ---
+LABEL_SELECTOR=${LABEL_SELECTOR} # Read from env
+CONTAINER_NAME=${CONTAINER_NAME} # Read from env
+RETRY_TIMEOUT=${RETRY_TIMEOUT:-120} # Read from env, default 120
+RETRY_SLEEP=${RETRY_SLEEP:-5}    # Read from env, default 5
+SEARCH_STRINGS_INPUT=${SEARCH_STRINGS_ENV} # Read the delimited string from env
+
+# --- Input Validation ---
+if [[ -z "$LABEL_SELECTOR" ]]; then
+  echo "ERROR: LABEL_SELECTOR environment variable is not set."
+  exit 1
+fi
+if [[ -z "$CONTAINER_NAME" ]]; then
+  echo "ERROR: CONTAINER_NAME environment variable is not set."
+  exit 1
+fi
+if [[ -z "$SEARCH_STRINGS_INPUT" ]]; then
+  echo "ERROR: SEARCH_STRINGS_ENV environment variable is not set or empty."
+  exit 1
+fi
+
+# --- Process Search Strings ---
+declare -a SEARCH_STRINGS # Declare the array
+
+# Read the multiline env var into the array, IFS sets the delimiter to newline
+# Handles strings potentially containing spaces correctly.
+# The <(...) construct avoids a subshell, making the array available after the loop.
+# Using printf and \0 ensures robustness even if strings contain special characters.
+IFS=$'\n' read -r -d '' -a SEARCH_STRINGS < <(printf '%s\0' "$SEARCH_STRINGS_INPUT")
+
+if [[ ${#SEARCH_STRINGS[@]} -eq 0 ]]; then
+  echo "ERROR: Failed to parse any search strings from SEARCH_STRINGS_ENV."
+  exit 1
+fi
+# --- End Configuration Processing ---
+
+
+# --- Script Execution ---
+echo "Namespace: $NAMESPACE" # NAMESPACE is implicitly set by Chainsaw
+echo "Label Selector: $LABEL_SELECTOR"
+echo "Container Name: $CONTAINER_NAME"
+echo "Timeout: ${RETRY_TIMEOUT}s, Sleep: ${RETRY_SLEEP}s"
+echo "Searching for ${#SEARCH_STRINGS[@]} strings:"
+printf "  - '%s'\n" "${SEARCH_STRINGS[@]}" # Print strings for verification, quoting them
+
+
+# --- Get Initial Pod Name ---
+echo "Finding target pod..."
+# Ensure kubectl uses the namespace chainsaw provides. Use --request-timeout for robustness.
+PODS_JSONPATH_OUTPUT=$(kubectl get pods -n "$NAMESPACE" -l "$LABEL_SELECTOR" --request-timeout=10s -o jsonpath='{.items[0].metadata.name}')
+KUBECTL_GET_EXIT_CODE=$?
+if [ $KUBECTL_GET_EXIT_CODE -ne 0 ]; then
+    echo "ERROR: Failed to run initial kubectl get pods (Exit Code: $KUBECTL_GET_EXIT_CODE). Is kubectl configured correctly for namespace '$NAMESPACE'?"
+    exit 1
+fi
+# Handle case where no pod is found gracefully
+if [[ -z "$PODS_JSONPATH_OUTPUT" ]]; then
+    echo "ERROR: No pods found with label '$LABEL_SELECTOR' in namespace '$NAMESPACE'"
+    exit 1
+fi
+# Assuming only one pod matches, if multiple could match, logic needs adjustment
+POD=$PODS_JSONPATH_OUTPUT
+echo "Target Pod: $POD"
+# --- End Get Initial Pod Name ---
+
+# --- Main Retry Loop ---
+START_TIME=$(date +%s)
+LAST_MISSING_COUNT=${#SEARCH_STRINGS[@]} # Track missing count to reduce log noise
+
+while true; do
+    CURRENT_TIME=$(date +%s)
+    ELAPSED_TIME=$(( CURRENT_TIME - START_TIME ))
+
+    # 1. Check for Timeout
+    if [ "$ELAPSED_TIME" -ge "$RETRY_TIMEOUT" ]; then
+        echo "-----------------------------------------------------"
+        echo "ERROR: Timeout ($RETRY_TIMEOUT seconds) reached. Not all required strings were found in container '$CONTAINER_NAME' of pod '$POD'."
+        # Attempt to print last known missing strings, might be empty if loop never found anything
+        if [[ ${#MISSING_STRINGS_THIS_ATTEMPT[@]} -gt 0 ]]; then
+            echo "Last known missing strings:"
+            printf "  - '%s'\n" "${MISSING_STRINGS_THIS_ATTEMPT[@]}"
+        fi
+        # Display last few lines of logs for debugging
+        echo "Last 20 lines of logs from $POD/$CONTAINER_NAME:"
+        kubectl logs "$POD" -n "$NAMESPACE" -c "$CONTAINER_NAME" --tail=20 || echo "  (failed to retrieve final logs)"
+        echo "-----------------------------------------------------"
+        exit 1
+    fi
+
+    # echo "-----------------------------------------------------" # Reduced verbosity
+    echo "Attempting log check on $POD/$CONTAINER_NAME (Elapsed: ${ELAPSED_TIME}s / ${RETRY_TIMEOUT}s)"
+
+    # 2. Get Logs for this attempt from the SPECIFIC container
+    # Using --tail=-1 to get all logs. Consider limiting if logs are huge and only recent ones matter.
+    LOGS=$(kubectl logs "$POD" -n "$NAMESPACE" -c "$CONTAINER_NAME" --tail=-1 --request-timeout=10s)
+    KUBECTL_LOGS_EXIT_CODE=$?
+
+    if [ $KUBECTL_LOGS_EXIT_CODE -ne 0 ]; then
+         echo "Warning: Failed to get logs for container '$CONTAINER_NAME' in pod '$POD' (Exit code: $KUBECTL_LOGS_EXIT_CODE). Retrying after sleep..."
+         sleep "$RETRY_SLEEP"
+         continue # Go to next loop iteration
+    fi
+
+    # It's possible logs are valid but empty initially, so don't exit, just report.
+    # if [ -z "$LOGS" ]; then
+    #      echo "Warning: Logs for container '$CONTAINER_NAME' in pod '$POD' were empty on this attempt. Retrying after sleep..."
+    #      sleep "$RETRY_SLEEP"
+    #      continue # Go to next loop iteration
+    # fi
+
+    # 3. Check for all strings in the current logs
+    ALL_FOUND_THIS_ATTEMPT=true
+    MISSING_STRINGS_THIS_ATTEMPT=() # Reset missing strings for this attempt
+    for STRING in "${SEARCH_STRINGS[@]}"; do
+        # Use grep -F (fixed string) and -q (quiet) for efficiency
+        if ! echo "$LOGS" | grep -Fq -- "$STRING"; then
+            ALL_FOUND_THIS_ATTEMPT=false
+            MISSING_STRINGS_THIS_ATTEMPT+=("$STRING")
+            # Optimization: uncomment break if you only need to know *if* something is missing,
+            # not the full list of missing items each time.
+            # break
+        fi
+    done
+
+    # 4. Evaluate outcome of this attempt
+    if [ "$ALL_FOUND_THIS_ATTEMPT" = true ]; then
+        echo "-----------------------------------------------------"
+        echo "Success: All ${#SEARCH_STRINGS[@]} strings found simultaneously in container '$CONTAINER_NAME' of pod '$POD'."
+        echo "-----------------------------------------------------"
+        exit 0 # Successful exit!
+    else
+        # Report missing strings only if the list changed since last time or on the first attempt
+        CURRENT_MISSING_COUNT=${#MISSING_STRINGS_THIS_ATTEMPT[@]}
+        # Use -ne for numeric comparison
+        if [ "$CURRENT_MISSING_COUNT" -ne "$LAST_MISSING_COUNT" ] || [ "$ELAPSED_TIME" -lt "$RETRY_SLEEP" ]; then # Log on first effective attempt too
+             echo "Attempt Failed: Missing ${CURRENT_MISSING_COUNT} string(s):"
+             printf "  - '%s'\n" "${MISSING_STRINGS_THIS_ATTEMPT[@]}" # Print missing strings, quoted
+             LAST_MISSING_COUNT=$CURRENT_MISSING_COUNT
+        else
+             # Avoid repeating the full list if the same strings are missing
+             echo "Attempt Failed: Still missing ${CURRENT_MISSING_COUNT} string(s). Waiting..."
+        fi
+        echo "Retrying in $RETRY_SLEEP seconds..."
+        sleep "$RETRY_SLEEP"
+    fi
+done
+# --- End Main Retry Loop ---
+
+# Fallback exit status (should not be reached normally)
+exit 1
\ No newline at end of file