├── .dockerignore ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── standard-issue-template.md ├── dependabot.yml └── workflows │ ├── build-publish-az-pipeline-poc.yml │ ├── build-publish-poc-1es.yml │ ├── build-publish.yml │ ├── ci-pipeline.yaml │ ├── codeql-analysis.yml │ ├── dependency-review.yml │ ├── patch.yml │ └── scorecards.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .vscode └── launch.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SECURITY.md ├── builder ├── Dockerfile.linux └── Dockerfile.windows ├── cmd └── aks-periscope │ └── aks-periscope.go ├── codecov.yml ├── deployment ├── base │ ├── cluster-role-binding.yaml │ ├── cluster-role.yaml │ ├── crd.yaml │ ├── daemon-set.yaml │ ├── kustomization.yaml │ ├── namespace.yaml │ └── service-account.yaml ├── components │ └── win-hpc │ │ ├── CollectDiagnostics.ps1 │ │ ├── daemon-set.yaml │ │ └── kustomization.yaml ├── embed.go └── overlays │ ├── dev │ ├── README.md │ └── kustomization.yaml │ ├── dynamic-image │ ├── README.md │ └── kustomization.template.yaml │ └── external │ ├── README.md │ └── kustomization.yaml ├── docs ├── appendix.md ├── goalsvsnongoals.md ├── testing-resources │ ├── test-run-0-start.svg │ ├── test-run-1-build.svg │ ├── test-run-2-create-cluster.svg │ ├── test-run-3-pull.svg │ ├── test-run-4-load.svg │ └── test-run-5-deploy.svg ├── testing.md └── windows-vs-linux.md ├── go.mod ├── go.sum ├── pkg ├── collector │ ├── dns_collector.go │ ├── dns_collector_test.go │ ├── helm_collector.go │ ├── helm_collector_test.go │ ├── iptables_collector.go │ ├── iptables_collector_test.go │ ├── kubeletcmd_collector.go │ ├── kubeletcmd_collector_test.go │ ├── kubeobjects_collector.go │ ├── kubeobjects_collector_test.go │ ├── networkoutbound_collector.go │ ├── networkoutbound_collector_test.go │ ├── nodelogs_collector.go │ ├── nodelogs_collector_test.go │ ├── osm_collector.go │ ├── osm_collector_test.go │ ├── pdb_collector.go │ ├── pdb_collector_test.go │ ├── pods_containerlogs_collector.go │ ├── pods_containerlogs_collector_test.go │ ├── shared_test.go │ ├── smi_collector.go │ ├── smi_collector_test.go │ ├── systemlogs_collector.go │ ├── systemlogs_collector_test.go │ ├── systemperf_collector.go │ ├── systemperf_collector_test.go │ ├── windowslogs_collector.go │ └── windowslogs_collector_test.go ├── diagnoser │ ├── networkconfig_diagnoser.go │ └── networkoutbound_diagnoser.go ├── exporter │ ├── azureblob_exporter.go │ └── zip.go ├── interfaces │ ├── collector.go │ ├── dataProducer.go │ ├── dataValue.go │ ├── diagnoser.go │ ├── exporter.go │ └── fileSystemAccessor.go ├── test │ ├── clusterFixture.go │ ├── clusterResourceManagement.go │ ├── dockerImageManagement.go │ ├── fakeFileSystem.go │ ├── resources │ │ ├── Dockerfile │ │ └── tools-resources │ │ │ ├── kind-config │ │ │ └── config.yaml │ │ │ ├── kube-objects │ │ │ └── test-resources.yaml │ │ │ ├── metrics-server │ │ │ └── components.yaml │ │ │ ├── osm-apps │ │ │ ├── bookbuyer.yaml │ │ │ ├── bookstore-v2.yaml │ │ │ ├── bookstore.yaml │ │ │ ├── bookthief.yaml │ │ │ ├── bookwarehouse.yaml │ │ │ ├── mysql.yaml │ │ │ ├── traffic-access.yaml │ │ │ └── traffic-split.yaml │ │ │ ├── osm-config │ │ │ └── override.yaml │ │ │ └── testchart │ │ │ ├── .helmignore │ │ │ ├── Chart.yaml │ │ │ ├── templates │ │ │ ├── NOTES.txt │ │ │ ├── _helpers.tpl │ │ │ ├── deployment.yaml │ │ │ ├── hpa.yaml │ │ │ ├── ingress.yaml │ │ │ ├── service.yaml │ │ │ ├── serviceaccount.yaml │ │ │ └── tests │ │ │ │ └── test-connection.yaml │ │ │ └── values.yaml │ ├── toolCommandRunner.go │ ├── toolsImageBuilder.go │ └── toolsImageCommands.go └── utils │ ├── fileContentWatcher.go │ ├── fileContentWatcher_test.go │ ├── filePathDataValue.go │ ├── fileSystem.go │ ├── fileSystem_test.go │ ├── helper.go │ ├── knownFilePaths.go │ ├── kubeCommandRunner.go │ ├── osIdentifier.go │ ├── runtimeInfo.go │ └── stringDataValue.go └── tools └── printdiagnostic.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | /docs 2 | /tests 3 | /builder 4 | /deployment 5 | *.md 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | **Describe the bug** 2 | A clear and concise description of what the bug is. 3 | 4 | **To Reproduce** 5 | Steps to reproduce the behavior: (for example) 6 | 1. Go to '...' 7 | 2. Click on or Deploy on '....' 8 | 3. Enter details and run '....' 9 | 4. See error 10 | 11 | **Expected behavior** 12 | A clear and concise description of what you expected to happen. 13 | 14 | **Screenshots** 15 | If applicable, add screenshots to help explain your problem. 16 | 17 | **Desktop (please complete the following information):** 18 | - OS: [e.g. iOS] 19 | - Version [e.g. 22] 20 | 21 | **Additional context** 22 | Add any other context about the problem here. 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/standard-issue-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Standard issue template 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: (for example) 15 | 1. Go to '...' 16 | 2. Click on or Deploy on '....' 17 | 3. Enter details and run '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Version [e.g. 22] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | groups: 8 | k8s.io: 9 | patterns: 10 | - "k8s.io/*" 11 | 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "weekly" 16 | 17 | - package-ecosystem: docker 18 | directory: /pkg/test/resources 19 | schedule: 20 | interval: weekly 21 | -------------------------------------------------------------------------------- /.github/workflows/build-publish-az-pipeline-poc.yml: -------------------------------------------------------------------------------- 1 | ################################################################################# 2 | # OneBranch Pipelines # 3 | # This pipeline was created by EasyStart from a sample located at: # 4 | # https://aka.ms/obpipelines/easystart/samples # 5 | # Documentation: https://aka.ms/obpipelines # 6 | # Yaml Schema: https://aka.ms/obpipelines/yaml/schema # 7 | # Retail Tasks: https://aka.ms/obpipelines/tasks # 8 | # Support: https://aka.ms/onebranchsup # 9 | ################################################################################# 10 | 11 | trigger: none 12 | 13 | variables: 14 | CDP_DEFINITION_BUILD_COUNT: $[counter('', 0)] # needed for onebranch.pipeline.version task https://aka.ms/obpipelines/versioning 15 | LinuxContainerImage: 'mcr.microsoft.com/onebranch/cbl-mariner/build:2.0' # Docker image which is used to build the project https://aka.ms/obpipelines/containers 16 | DEBIAN_FRONTEND: noninteractive 17 | 18 | resources: 19 | repositories: 20 | - repository: templates 21 | type: git 22 | name: OneBranch.Pipelines/GovernedTemplates 23 | ref: refs/heads/main 24 | 25 | extends: 26 | template: v2/OneBranch.NonOfficial.CrossPlat.yml@templates # https://aka.ms/obpipelines/templates 27 | parameters: 28 | globalSdl: # https://aka.ms/obpipelines/sdl 29 | # tsa: 30 | # enabled: true # SDL results of non-official builds aren't uploaded to TSA by default. 31 | # credscan: 32 | # suppressionsFile: $(Build.SourcesDirectory)\.config\CredScanSuppressions.json 33 | policheck: 34 | break: true # always break the build on policheck issues. You can disable it by setting to 'false' 35 | # suppression: 36 | # suppressionFile: $(Build.SourcesDirectory)\.gdn\global.gdnsuppress 37 | 38 | stages: 39 | - stage: linux_stage 40 | jobs: 41 | - job: linux_job 42 | pool: 43 | type: linux 44 | 45 | variables: # More settings at https://aka.ms/obpipelines/yaml/jobs 46 | ob_outputDirectory: '$(Build.SourcesDirectory)/out' # this directory is uploaded to pipeline artifacts, reddog and cloudvault. More info at https://aka.ms/obpipelines/artifacts 47 | 48 | steps: # These steps will be run in unrestricted container's network 49 | - task: onebranch.pipeline.version@1 50 | displayName: 'Setup BuildNumber' 51 | inputs: 52 | system: 'RevisionCounter' 53 | major: '1' 54 | minor: '0' 55 | exclude_commit: true 56 | - task: onebranch.pipeline.containercontrol@1 57 | displayName: "Login to acr" 58 | inputs: 59 | command: login 60 | acr_name: pocaksperitest 61 | endpoint: pocaksperitest 62 | -------------------------------------------------------------------------------- /.github/workflows/build-publish-poc-1es.yml: -------------------------------------------------------------------------------- 1 | name: 1ES POC 2 | on: [workflow_dispatch] 3 | 4 | permissions: 5 | id-token: write 6 | contents: read 7 | 8 | jobs: 9 | common: 10 | runs-on: 11 | labels: ["self-hosted", "1ES.Pool=1es-aks-periscope-pool-msit-poc"] 12 | defaults: 13 | run: 14 | shell: pwsh 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Get Changelog Entry 18 | id: changelog_reader 19 | uses: mindsers/changelog-reader-action@v2 20 | with: 21 | validation_depth: 10 22 | path: ./CHANGELOG.md 23 | - name: Get Version Info 24 | id: read_metadata 25 | run: | 26 | echo "Version: ${{ steps.changelog_reader.outputs.version }}" 27 | echo "Changes: ${{ steps.changelog_reader.outputs.changes }}" 28 | $tagbase = "${{ vars.AZURE_REGISTRY_SERVER }}/periscope-test:${{ steps.changelog_reader.outputs.version }}" 29 | echo "tagbase=$tagbase" >> $env:GITHUB_OUTPUT 30 | outputs: 31 | tagbase: ${{ steps.read_metadata.outputs.tagbase }} 32 | version: ${{ steps.changelog_reader.outputs.version }} 33 | changes: ${{ steps.changelog_reader.outputs.changes }} 34 | publish: 35 | runs-on: 36 | labels: ["self-hosted", "1ES.Pool=1es-aks-periscope-pool-msit-poc"] 37 | needs: common 38 | defaults: 39 | run: 40 | shell: pwsh 41 | steps: 42 | - uses: actions/checkout@v4 43 | # - name: 'Az CLI login' 44 | - name: "Login to ACR" 45 | run: | 46 | az login --identity 47 | - name: 'Publish to ACR' 48 | id: publish 49 | run: | 50 | $tag = "${{ needs.common.outputs.tagbase }}-mariner2.0" 51 | echo "tag-ubuntu-latest=$tag" >> $env:GITHUB_OUTPUT 52 | docker build -f ./builder/Dockerfile.linux --build-arg BASE_IMAGE=mcr.microsoft.com/cbl-mariner/distroless/base:2.0 -t $tag . 53 | az acr login -n ${{ vars.AZURE_REGISTRY_SERVER }} 54 | docker push $tag 55 | outputs: 56 | linux: ${{ steps.publish.outputs.tag-ubuntu-latest }} 57 | -------------------------------------------------------------------------------- /.github/workflows/ci-pipeline.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ master ] 7 | pull_request: 8 | branches: [ master ] 9 | 10 | env: 11 | GO_VERSION: '1.19.4' 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | lint: 18 | permissions: 19 | contents: read # for actions/checkout to fetch code 20 | pull-requests: read # for golangci/golangci-lint-action to fetch pull requests 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Harden Runner 24 | uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 25 | with: 26 | egress-policy: audit 27 | 28 | - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 29 | - name: Set up Go 30 | uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 31 | with: 32 | go-version: ${{ env.GO_VERSION }} 33 | - name: Linter 34 | uses: golangci/golangci-lint-action@3cfe3a4abbb849e10058ce4af15d205b6da42804 # v4.0.0 35 | with: 36 | version: latest 37 | # the default timeout is 1 minute - this is too short and results in frequent timeout errors so we increase it here 38 | args: --timeout 3m0s 39 | build: 40 | runs-on: ubuntu-latest 41 | steps: 42 | - name: Harden Runner 43 | uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 44 | with: 45 | egress-policy: audit 46 | 47 | - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 48 | - name: Set up Go 49 | uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 50 | with: 51 | go-version: ${{ env.GO_VERSION }} 52 | - name: Download modules 53 | run: go mod download 54 | - name: Build project 55 | run: go build ./cmd/aks-periscope 56 | tests: 57 | runs-on: ubuntu-latest 58 | steps: 59 | - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 60 | with: 61 | fetch-depth: 2 62 | - name: Set up Go 63 | uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 64 | with: 65 | go-version: ${{ env.GO_VERSION }} 66 | - name: Go tests 67 | run: go test -v -race -coverprofile=coverage.out -covermode=atomic ./... 68 | - name: Upload coverage to Codecov 69 | run: bash <(curl -s https://codecov.io/bash) -C $(Build.SourceVersion) 70 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "master", master* ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "master" ] 20 | schedule: 21 | - cron: '37 13 * * 5' 22 | 23 | permissions: 24 | contents: read 25 | 26 | jobs: 27 | analyze: 28 | name: Analyze 29 | runs-on: ubuntu-latest 30 | permissions: 31 | actions: read 32 | contents: read 33 | security-events: write 34 | 35 | strategy: 36 | fail-fast: false 37 | matrix: 38 | language: [ 'go' ] 39 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 40 | # Use only 'java' to analyze code written in Java, Kotlin or both 41 | # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both 42 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 43 | 44 | steps: 45 | - name: Harden Runner 46 | uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 47 | with: 48 | egress-policy: audit 49 | 50 | - name: Checkout repository 51 | uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 52 | 53 | # Initializes the CodeQL tools for scanning. 54 | - name: Initialize CodeQL 55 | uses: github/codeql-action/init@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v3.24.6 56 | with: 57 | languages: ${{ matrix.language }} 58 | # If you wish to specify custom queries, you can do so here or in a config file. 59 | # By default, queries listed here will override any specified in a config file. 60 | # Prefix the list here with "+" to use these queries and those in the config file. 61 | 62 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 63 | # queries: security-extended,security-and-quality 64 | 65 | 66 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 67 | # If this step fails, then you should remove it and run the build manually (see below) 68 | - name: Autobuild 69 | uses: github/codeql-action/autobuild@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v3.24.6 70 | 71 | # ℹ️ Command-line programs to run using the OS shell. 72 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 73 | 74 | # If the Autobuild fails above, remove it and uncomment the following three lines. 75 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 76 | 77 | # - run: | 78 | # echo "Run, Build Application using script" 79 | # ./location_of_script_within_repo/buildscript.sh 80 | 81 | - name: Perform CodeQL Analysis 82 | uses: github/codeql-action/analyze@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v3.24.6 83 | with: 84 | category: "/language:${{matrix.language}}" 85 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | # Dependency Review Action 2 | # 3 | # This Action will scan dependency manifest files that change as part of a Pull Request, 4 | # surfacing known-vulnerable versions of the packages declared or updated in the PR. 5 | # Once installed, if the workflow run is marked as required, 6 | # PRs introducing known-vulnerable packages will be blocked from merging. 7 | # 8 | # Source repository: https://github.com/actions/dependency-review-action 9 | name: 'Dependency Review' 10 | on: [pull_request] 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | dependency-review: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Harden Runner 20 | uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 21 | with: 22 | egress-policy: audit 23 | 24 | - name: 'Checkout Repository' 25 | uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 26 | - name: 'Dependency Review' 27 | uses: actions/dependency-review-action@9129d7d40b8c12c1ed0f60400d00c92d437adcce # v4.1.3 28 | -------------------------------------------------------------------------------- /.github/workflows/scorecards.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. They are provided 2 | # by a third-party and are governed by separate terms of service, privacy 3 | # policy, and support documentation. 4 | 5 | name: Scorecard supply-chain security 6 | on: 7 | # For Branch-Protection check. Only the default branch is supported. See 8 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 9 | branch_protection_rule: 10 | # To guarantee Maintained check is occasionally updated. See 11 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 12 | schedule: 13 | - cron: '20 7 * * 2' 14 | push: 15 | branches: ["master"] 16 | 17 | # Declare default permissions as read only. 18 | permissions: read-all 19 | 20 | jobs: 21 | analysis: 22 | name: Scorecard analysis 23 | runs-on: ubuntu-latest 24 | permissions: 25 | # Needed to upload the results to code-scanning dashboard. 26 | security-events: write 27 | # Needed to publish results and get a badge (see publish_results below). 28 | id-token: write 29 | contents: read 30 | actions: read 31 | 32 | steps: 33 | - name: Harden Runner 34 | uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 35 | with: 36 | egress-policy: audit 37 | 38 | - name: "Checkout code" 39 | uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 40 | with: 41 | persist-credentials: false 42 | 43 | - name: "Run analysis" 44 | uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 45 | with: 46 | results_file: results.sarif 47 | results_format: sarif 48 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 49 | # - you want to enable the Branch-Protection check on a *public* repository, or 50 | # - you are installing Scorecards on a *private* repository 51 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. 52 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 53 | 54 | # Public repositories: 55 | # - Publish results to OpenSSF REST API for easy access by consumers 56 | # - Allows the repository to include the Scorecard badge. 57 | # - See https://github.com/ossf/scorecard-action#publishing-results. 58 | # For private repositories: 59 | # - `publish_results` will always be set to `false`, regardless 60 | # of the value entered here. 61 | publish_results: true 62 | 63 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 64 | # format to the repository Actions tab. 65 | - name: "Upload artifact" 66 | uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 67 | with: 68 | name: SARIF file 69 | path: results.sarif 70 | retention-days: 5 71 | 72 | # Upload the results to GitHub's code scanning dashboard. 73 | - name: "Upload to code-scanning" 74 | uses: github/codeql-action/upload-sarif@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v3.24.6 75 | with: 76 | sarif_file: results.sarif 77 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/gitleaks/gitleaks 3 | rev: v8.16.3 4 | hooks: 5 | - id: gitleaks 6 | - repo: https://github.com/golangci/golangci-lint 7 | rev: v1.52.2 8 | hooks: 9 | - id: golangci-lint 10 | - repo: https://github.com/jumanjihouse/pre-commit-hooks 11 | rev: 3.0.0 12 | hooks: 13 | - id: shellcheck 14 | - repo: https://github.com/pre-commit/pre-commit-hooks 15 | rev: v4.4.0 16 | hooks: 17 | - id: end-of-file-fixer 18 | - id: trailing-whitespace 19 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch Tests", 9 | "type": "go", 10 | "request": "launch", 11 | "program": "${fileDirname}", 12 | "mode": "test", 13 | "env": {}, 14 | "args": ["-test.v"], 15 | "showLog": true 16 | }, 17 | { 18 | "name": "Launch Tests with race check", 19 | "type": "go", 20 | "request": "launch", 21 | "program": "${fileDirname}", 22 | "mode": "test", 23 | "env": {}, 24 | "buildFlags": "-race", 25 | "args": ["-test.v"], 26 | "showLog": true 27 | }, 28 | ] 29 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [0.0.13] 4 | 5 | * Remove unnecessary permissions in documentation for SAS generation by @peterbom in https://github.com/Azure/aks-periscope/pull/220 6 | * Add nsenter to Linux Periscope image by @peterbom in https://github.com/Azure/aks-periscope/pull/221 7 | * Update resource requests and limits for all Periscope pods by @peterbom in https://github.com/Azure/aks-periscope/pull/222 8 | * Handle individual errors for entries in zip file by @peterbom in https://github.com/Azure/aks-periscope/pull/226 9 | 10 | Thanks to @Tatsinnit 11 | 12 | ## [0.0.12] 13 | 14 | * Fix/remove confusing diagram. by @Tatsinnit in https://github.com/Azure/aks-periscope/pull/200 15 | * Update deployment notes for image registries to include ACR as well as GHCR by @peterbom in https://github.com/Azure/aks-periscope/pull/202 16 | * Update codeql to latest versions and use. by @Tatsinnit in https://github.com/Azure/aks-periscope/pull/206 17 | * Feature/upgrade go1.19 by @bravebeaver in https://github.com/Azure/aks-periscope/pull/210 18 | * Allow building and using images for Windows Server 2022 as well as 2019 by @peterbom in https://github.com/Azure/aks-periscope/pull/212 19 | 20 | Thanks to @bravebeaver and @Tatsinnit 21 | 22 | ## [0.0.11] 23 | 24 | * Allow multiple 'runs' for Periscope by @peterbom in https://github.com/Azure/aks-periscope/pull/196 25 | 26 | Thanks to @Tatsinnit 27 | 28 | ## [0.0.10] 29 | 30 | * Add test coverage for OSM and SMI collectors by @peterbom in https://github.com/Azure/aks-periscope/pull/173 31 | * Use client-go for OSM collector by @peterbom in https://github.com/Azure/aks-periscope/pull/178 32 | * Use client-go for SMI collector by @peterbom in https://github.com/Azure/aks-periscope/pull/179 33 | * Remove dependency on kubectl binary by @peterbom in https://github.com/Azure/aks-periscope/pull/181 34 | * Add required permissions for OSM and SMI collectors by @peterbom in https://github.com/Azure/aks-periscope/pull/182 35 | * update containerd package. by @Tatsinnit in https://github.com/Azure/aks-periscope/pull/185 36 | * Update read-me. by @Tatsinnit in https://github.com/Azure/aks-periscope/pull/186 37 | * Allow specified resource types and names for kube-objects collector by @peterbom in https://github.com/Azure/aks-periscope/pull/188 38 | * Add note to readme about kubectl version by @peterbom in https://github.com/Azure/aks-periscope/pull/190 39 | * Add Windows log collection collector by @peterbom in https://github.com/Azure/aks-periscope/pull/191 40 | 41 | Thanks to @Tatsinnit, @SanyaKochhar, @johnsonshi, @AbelHu 42 | 43 | ## [0.0.9] 44 | 45 | * Return error from createContainerURL if storage settings are not configured by @peterbom in https://github.com/Azure/aks-periscope/pull/156 46 | * Remove old redundant deployment file. by @Tatsinnit in https://github.com/Azure/aks-periscope/pull/162 47 | * Fix Node Logs Collector to use separate keys for each log file by @peterbom in https://github.com/Azure/aks-periscope/pull/166 48 | * Support Kustomize for development and consuming tools by @peterbom in https://github.com/Azure/aks-periscope/pull/164 49 | * Allow Periscope to run on Windows nodes by @peterbom in https://github.com/Azure/aks-periscope/pull/167 50 | * Make it easier to run and debug tests locally by @peterbom in https://github.com/Azure/aks-periscope/pull/170 51 | * Document the automated testing approach introduced earlier by @peterbom in https://github.com/Azure/aks-periscope/pull/172 52 | * Add notes for differences in Windows behaviour by @peterbom in https://github.com/Azure/aks-periscope/pull/174 53 | * Adding Microsoft SECURITY.MD by @microsoft-github-policy-service in https://github.com/Azure/aks-periscope/pull/175 54 | 55 | Thanks to @peterbom, @rzhang628 56 | 57 | ## [0.0.8] 58 | 59 | * A few minor edits to README.md by @davefellows in #147 60 | * Add pod disrupution budget information collector. by @Tatsinnit in #135 61 | * Behaviour fix, Upload API fix. by @Tatsinnit in #138 62 | * Use client-go and remove unnecessary kubectl. by @Tatsinnit in #136 63 | * update v1beta1 apiextension to v1. by @Tatsinnit in #139 64 | * Improve CI and add iptables and kubeletcmd test structure. by @Tatsinnit in #140 65 | * Enable mechanism for container sas key to be passed. by @Tatsinnit in #143 66 | * add systemlogs test. by @Tatsinnit in #149 67 | * Temporary disabling non-compliant collectors from test cov. by @Tatsinnit in #144 68 | 69 | 70 | Thanks to @sophsoph321, @peterbom, @davefellows, @rzhang628, @bcho, @SanyaKochhar, @johnsonshi for interactions, reviews and various enagements. -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /builder/Dockerfile.linux: -------------------------------------------------------------------------------- 1 | # Base image depends on target platform. 2 | # See: https://mcr.microsoft.com/en-us/product/cbl-mariner/distroless/base/about 3 | ARG BASE_IMAGE=mcr.microsoft.com/cbl-mariner/distroless/base:2.0 4 | 5 | # Builder 6 | # golang builder image is multi-platform 7 | FROM golang:1.19.5 AS builder 8 | 9 | ENV GO111MODULE=on CGO_ENABLED=0 10 | 11 | WORKDIR /build 12 | 13 | COPY go.mod . 14 | COPY go.sum . 15 | RUN go mod download 16 | 17 | COPY . . 18 | 19 | RUN go build ./cmd/aks-periscope 20 | 21 | # Add dependencies for building nsenter 22 | RUN apt-get update && \ 23 | apt-get install -y autoconf autopoint bison gettext libtool 24 | 25 | # Create a statically-compiled nsenter binary (see: https://github.com/alexei-led/nsenter/blob/master/Dockerfile) 26 | # nsenter versions: https://www.kernel.org/pub/linux/utils/util-linux/ 27 | ADD https://github.com/util-linux/util-linux/archive/v2.38.tar.gz . 28 | RUN tar -xf v2.38.tar.gz && mv util-linux-2.38 util-linux 29 | WORKDIR /build/util-linux 30 | RUN ./autogen.sh && ./configure 31 | RUN make LDFLAGS="--static" nsenter 32 | 33 | # Runner 34 | FROM $BASE_IMAGE 35 | 36 | COPY --from=builder /build/aks-periscope / 37 | COPY --from=builder /build/util-linux/nsenter /usr/bin/ 38 | 39 | ENTRYPOINT ["/aks-periscope"] 40 | -------------------------------------------------------------------------------- /builder/Dockerfile.windows: -------------------------------------------------------------------------------- 1 | # Base image depends on target platform. 2 | # See: https://mcr.microsoft.com/en-us/product/windows/nanoserver/about 3 | ARG BASE_IMAGE=mcr.microsoft.com/windows/nanoserver:ltsc2019 4 | 5 | # Builder 6 | # golang builder image is multi-platform 7 | FROM golang:1.19.5 AS builder 8 | 9 | ENV GO111MODULE=on CGO_ENABLED=0 10 | 11 | WORKDIR /build 12 | 13 | COPY go.mod . 14 | COPY go.sum . 15 | RUN go mod download 16 | 17 | COPY . . 18 | 19 | RUN go build ./cmd/aks-periscope 20 | 21 | # Runner 22 | FROM $BASE_IMAGE 23 | 24 | COPY --from=builder /build/aks-periscope.exe / 25 | 26 | ENTRYPOINT ["/aks-periscope.exe"] 27 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "pkg/utils" 3 | -------------------------------------------------------------------------------- /deployment/base/cluster-role-binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: aks-periscope-role-binding 5 | subjects: 6 | - kind: ServiceAccount 7 | name: aks-periscope-service-account 8 | roleRef: 9 | kind: ClusterRole 10 | name: aks-periscope-role 11 | apiGroup: rbac.authorization.k8s.io 12 | --- 13 | apiVersion: rbac.authorization.k8s.io/v1 14 | kind: ClusterRoleBinding 15 | metadata: 16 | name: aks-periscope-role-binding-view 17 | subjects: 18 | - kind: ServiceAccount 19 | name: aks-periscope-service-account 20 | roleRef: 21 | kind: ClusterRole 22 | name: view 23 | apiGroup: rbac.authorization.k8s.io 24 | -------------------------------------------------------------------------------- /deployment/base/cluster-role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: aks-periscope-role 5 | rules: 6 | - apiGroups: ["","metrics.k8s.io"] 7 | resources: ["pods", "nodes"] 8 | verbs: ["get", "watch", "list"] 9 | - apiGroups: [""] 10 | resources: ["secrets"] 11 | verbs: ["list"] 12 | - apiGroups: [""] 13 | resources: ["pods/portforward"] 14 | verbs: ["create"] 15 | - apiGroups: ["aks-periscope.azure.github.com"] 16 | resources: ["diagnostics"] 17 | verbs: ["get", "watch", "list", "create", "patch"] 18 | - apiGroups: ["apiextensions.k8s.io"] 19 | resources: ["customresourcedefinitions"] 20 | verbs: ["get", "list", "watch"] 21 | - apiGroups: ["access.smi-spec.io", "specs.smi-spec.io", "split.smi-spec.io"] 22 | resources: ["*"] 23 | verbs: ["get", "list", "watch"] 24 | - apiGroups: ["config.openservicemesh.io"] 25 | resources: ["meshconfigs"] 26 | verbs: ["get", "list"] 27 | - apiGroups: ["admissionregistration.k8s.io"] 28 | resources: ["mutatingwebhookconfigurations", "validatingwebhookconfigurations"] 29 | verbs: ["get", "list"] 30 | -------------------------------------------------------------------------------- /deployment/base/crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: diagnostics.aks-periscope.azure.github.com 5 | spec: 6 | group: aks-periscope.azure.github.com 7 | versions: 8 | - name: v1 9 | served: true 10 | storage: true 11 | schema: 12 | openAPIV3Schema: 13 | type: object 14 | properties: 15 | spec: 16 | type: object 17 | properties: 18 | dns: 19 | type: string 20 | networkoutbound: 21 | type: string 22 | networkconfig: 23 | type: string 24 | scope: Namespaced 25 | names: 26 | plural: diagnostics 27 | singular: diagnostic 28 | kind: Diagnostic 29 | shortNames: 30 | - apd -------------------------------------------------------------------------------- /deployment/base/daemon-set.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: DaemonSet 3 | metadata: 4 | name: aks-periscope 5 | labels: 6 | app: aks-periscope 7 | spec: 8 | selector: 9 | matchLabels: 10 | app: aks-periscope 11 | template: 12 | metadata: 13 | labels: 14 | app: aks-periscope 15 | spec: 16 | serviceAccountName: aks-periscope-service-account 17 | hostPID: true 18 | nodeSelector: 19 | kubernetes.io/os: linux 20 | containers: 21 | - name: aks-periscope 22 | image: periscope-linux 23 | securityContext: 24 | privileged: true 25 | imagePullPolicy: Always 26 | env: 27 | - name: HOST_NODE_NAME 28 | valueFrom: 29 | fieldRef: 30 | fieldPath: spec.nodeName 31 | volumeMounts: 32 | - name: diag-config-volume 33 | mountPath: /config 34 | - name: storage-secret-volume 35 | mountPath: /secret 36 | - name: varlog 37 | mountPath: /var/log 38 | - name: resolvlog 39 | mountPath: /run/systemd/resolve 40 | - name: etcvmlog 41 | mountPath: /etchostlogs 42 | resources: 43 | requests: 44 | memory: "40Mi" 45 | cpu: "1m" 46 | limits: 47 | memory: "500Mi" 48 | cpu: "1000m" 49 | volumes: 50 | - name: diag-config-volume 51 | configMap: 52 | name: diagnostic-config 53 | - name: storage-secret-volume 54 | secret: 55 | secretName: azureblob-secret 56 | - name: varlog 57 | hostPath: 58 | path: /var/log 59 | - name: resolvlog 60 | hostPath: 61 | path: /run/systemd/resolve 62 | - name: etcvmlog 63 | hostPath: 64 | path: /etc 65 | --- 66 | apiVersion: apps/v1 67 | kind: DaemonSet 68 | metadata: 69 | name: aks-periscope-win 70 | labels: 71 | app: aks-periscope 72 | spec: 73 | selector: 74 | matchLabels: 75 | app: aks-periscope 76 | template: 77 | metadata: 78 | labels: 79 | app: aks-periscope 80 | spec: 81 | serviceAccountName: aks-periscope-service-account 82 | hostPID: true 83 | nodeSelector: 84 | kubernetes.io/os: windows 85 | containers: 86 | - name: aks-periscope 87 | image: periscope-windows 88 | imagePullPolicy: Always 89 | env: 90 | - name: HOST_NODE_NAME 91 | valueFrom: 92 | fieldRef: 93 | fieldPath: spec.nodeName 94 | volumeMounts: 95 | - name: diag-config-volume 96 | mountPath: /config 97 | - name: storage-secret-volume 98 | mountPath: /secret 99 | - name: k 100 | mountPath: /k 101 | - name: azuredata 102 | mountPath: /AzureData 103 | resources: 104 | requests: 105 | memory: "100Mi" 106 | cpu: "100m" 107 | limits: 108 | memory: "1Gi" 109 | cpu: "1000m" 110 | volumes: 111 | - name: diag-config-volume 112 | configMap: 113 | name: diagnostic-config 114 | - name: storage-secret-volume 115 | secret: 116 | secretName: azureblob-secret 117 | - name: k 118 | hostPath: 119 | path: /k 120 | - name: azuredata 121 | hostPath: 122 | path: /AzureData 123 | -------------------------------------------------------------------------------- /deployment/base/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | namespace: aks-periscope 5 | 6 | resources: 7 | - namespace.yaml 8 | - cluster-role.yaml 9 | - cluster-role-binding.yaml 10 | - crd.yaml 11 | - daemon-set.yaml 12 | - service-account.yaml 13 | 14 | configMapGenerator: 15 | - name: diagnostic-config 16 | literals: 17 | - DIAGNOSTIC_RUN_ID= 18 | - DIAGNOSTIC_CONTAINERLOGS_LIST=kube-system 19 | - DIAGNOSTIC_KUBEOBJECTS_LIST=kube-system/pod kube-system/service kube-system/deployment 20 | - DIAGNOSTIC_NODELOGS_LIST_LINUX="/var/log/azure/cluster-provision.log /var/log/cloud-init.log" 21 | - DIAGNOSTIC_NODELOGS_LIST_WINDOWS="C:\AzureData\CustomDataSetupScript.log" 22 | 23 | secretGenerator: 24 | - name: azureblob-secret 25 | literals: 26 | - AZURE_BLOB_ACCOUNT_NAME= 27 | - AZURE_BLOB_SAS_KEY= 28 | - AZURE_BLOB_CONTAINER_NAME= 29 | 30 | generatorOptions: 31 | disableNameSuffixHash: true 32 | -------------------------------------------------------------------------------- /deployment/base/namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: aks-periscope 5 | -------------------------------------------------------------------------------- /deployment/base/service-account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: aks-periscope-service-account 5 | -------------------------------------------------------------------------------- /deployment/components/win-hpc/CollectDiagnostics.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = "Stop" 2 | 3 | $runIdPath = Join-Path $env:CONTAINER_SANDBOX_MOUNT_POINT "config\run_id" 4 | $outputFolder = "\k\periscope-diagnostic-output" 5 | $logsPath = "${outputFolder}\logs" 6 | 7 | # Ensure the output directory exists 8 | New-Item -ItemType Directory $outputFolder -Force 9 | 10 | # For tracking contents of run_id file (and run diagnostics collection script when it changes) 11 | $previousRunId = "" 12 | 13 | while ($true) { 14 | $runId = Get-Content $runIdPath 15 | if ($runId -and $runId -ne $previousRunId) { 16 | Write-Host "Collecting diagnostics for ${runId}" 17 | 18 | # The FileInfo containing the zip file is the last output from the script 19 | $outputs = (& "C:\k\Debug\collect-windows-logs.ps1") 20 | $logsZipFileInfo = $outputs[$outputs.Length - 1] 21 | 22 | # Replace any existing log files with the unzipped content 23 | Remove-Item -Path "${outputFolder}\*" -Force -Recurse 24 | Expand-Archive -Path $logsZipFileInfo.FullName -Force -DestinationPath $logsPath 25 | 26 | # Create an empty file to notify any watchers that log collection is completed for this run, 27 | # and update previous-run tracker to avoid repeated re-runs. 28 | New-Item "${outputFolder}\${runId}" 29 | $previousRunId = $runId 30 | } 31 | 32 | Start-Sleep -Seconds 10 33 | } 34 | -------------------------------------------------------------------------------- /deployment/components/win-hpc/daemon-set.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: DaemonSet 3 | metadata: 4 | name: diagnostics-collection-win2019 5 | labels: 6 | app: diagnostics-collection 7 | spec: 8 | selector: 9 | matchLabels: 10 | app: diagnostics-collection 11 | template: 12 | metadata: 13 | labels: 14 | app: diagnostics-collection 15 | spec: 16 | hostPID: true 17 | nodeSelector: 18 | kubernetes.io/os: windows 19 | kubernetes.azure.com/os-sku: Windows2019 20 | hostNetwork: true 21 | securityContext: 22 | windowsOptions: 23 | hostProcess: true 24 | runAsUserName: "NT AUTHORITY\\SYSTEM" 25 | containers: 26 | - name: diagnostics-collection 27 | image: mcr.microsoft.com/windows/nanoserver:ltsc2019 28 | imagePullPolicy: Always 29 | command: 30 | - powershell 31 | args: 32 | - scripts\CollectDiagnostics.ps1 33 | volumeMounts: 34 | - name: diag-config-volume 35 | mountPath: config 36 | - name: script-config-volume 37 | mountPath: scripts 38 | resources: 39 | requests: 40 | memory: "1000Mi" 41 | cpu: "100m" 42 | limits: 43 | memory: "2000Mi" 44 | cpu: "1000m" 45 | volumes: 46 | - name: diag-config-volume 47 | configMap: 48 | name: diagnostic-config 49 | items: 50 | - key: DIAGNOSTIC_RUN_ID 51 | path: run_id 52 | - name: script-config-volume 53 | configMap: 54 | name: script-config 55 | --- 56 | apiVersion: apps/v1 57 | kind: DaemonSet 58 | metadata: 59 | name: diagnostics-collection-win2022 60 | labels: 61 | app: diagnostics-collection 62 | spec: 63 | selector: 64 | matchLabels: 65 | app: diagnostics-collection 66 | template: 67 | metadata: 68 | labels: 69 | app: diagnostics-collection 70 | spec: 71 | hostPID: true 72 | nodeSelector: 73 | kubernetes.io/os: windows 74 | kubernetes.azure.com/os-sku: Windows2022 75 | hostNetwork: true 76 | securityContext: 77 | windowsOptions: 78 | hostProcess: true 79 | runAsUserName: "NT AUTHORITY\\SYSTEM" 80 | containers: 81 | - name: diagnostics-collection 82 | image: mcr.microsoft.com/windows/nanoserver:ltsc2022 83 | imagePullPolicy: Always 84 | command: 85 | - powershell 86 | args: 87 | - scripts\CollectDiagnostics.ps1 88 | volumeMounts: 89 | - name: diag-config-volume 90 | mountPath: config 91 | - name: script-config-volume 92 | mountPath: scripts 93 | resources: 94 | requests: 95 | memory: "1000Mi" 96 | cpu: "100m" 97 | limits: 98 | memory: "2000Mi" 99 | cpu: "1000m" 100 | volumes: 101 | - name: diag-config-volume 102 | configMap: 103 | name: diagnostic-config 104 | items: 105 | - key: DIAGNOSTIC_RUN_ID 106 | path: run_id 107 | - name: script-config-volume 108 | configMap: 109 | name: script-config -------------------------------------------------------------------------------- /deployment/components/win-hpc/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1alpha1 2 | kind: Component 3 | 4 | namespace: aks-periscope 5 | 6 | resources: 7 | - daemon-set.yaml 8 | 9 | configMapGenerator: 10 | - name: diagnostic-config 11 | behavior: merge 12 | literals: 13 | - FEATURE_WINHPC=1 14 | - name: script-config 15 | behavior: create 16 | files: 17 | - CollectDiagnostics.ps1 18 | 19 | generatorOptions: 20 | disableNameSuffixHash: true 21 | -------------------------------------------------------------------------------- /deployment/embed.go: -------------------------------------------------------------------------------- 1 | package deployment 2 | 3 | import ( 4 | "embed" 5 | ) 6 | 7 | // This is here to expose the deployment files to code for automated testing. 8 | //go:embed base/* 9 | var Resources embed.FS 10 | -------------------------------------------------------------------------------- /deployment/overlays/dev/README.md: -------------------------------------------------------------------------------- 1 | # Dev Overlay 2 | 3 | This can be used for running a locally-built Periscope image in a `Kind` cluster. The environment files are `gitignore`d to avoid committing any credentials or user-specific configuration to source control. 4 | 5 | Because `Kind` runs on Linux only, the Linux `DaemonSet` will refer to the locally-built image, whereas the Windows `DaemonSet` will refer to the latest published production Windows MCR image (to test Windows changes, use the ['dynamic image'](../dynamic-image/README.md) overlay). 6 | 7 | It will deploy to its own namespace, `aks-periscope-dev` to avoid conflicts with any existing Periscope deployment. 8 | 9 | ## Building a Local Image 10 | 11 | First, build the image and make sure it's loaded in `Kind`. If it's not, the pod will fail trying to pull the image (because it's local). 12 | 13 | ```sh 14 | docker build -f ./builder/Dockerfile.linux -t periscope-local . 15 | # Include a --name argument here if not using the default kind cluster. 16 | kind load docker-image periscope-local 17 | ``` 18 | 19 | ## Setting up Configuration Data 20 | 21 | To run correctly, Periscope requires some storage account configuration that is different for each user. It also has some optional 'diagnostic' configuration (node log locations, etc.). 22 | 23 | We need to make sure this doesn't get into source control, so it is stored in `gitignore`d `.env` files. 24 | 25 | ```sh 26 | # Create a SAS 27 | sub_id=... 28 | stg_account=... 29 | blob_container=... 30 | sas_expiry=`date -u -d "30 minutes" '+%Y-%m-%dT%H:%MZ'` 31 | sas=$(az storage account generate-sas \ 32 | --account-name $stg_account \ 33 | --subscription $sub_id \ 34 | --permissions rlacw \ 35 | --services b \ 36 | --resource-types sco \ 37 | --expiry $sas_expiry \ 38 | -o tsv) 39 | # Set up configuration data for Kustomize 40 | # (for further customization, the variables in the .env.config file can be configured to override the defaults) 41 | touch ./deployment/overlays/dev/.env.config 42 | cat < ./deployment/overlays/dev/.env.secret 43 | AZURE_BLOB_ACCOUNT_NAME=${stg_account} 44 | AZURE_BLOB_SAS_KEY=?${sas} 45 | AZURE_BLOB_CONTAINER_NAME=${blob_container} 46 | EOF 47 | ``` 48 | 49 | ## Deploying to Kind 50 | 51 | Once the `.env` files are in place, `Kustomize` has all the information it needs to generate the `yaml` resource specification for Periscope. 52 | 53 | ```sh 54 | # Ensure kubectl has the right cluster context 55 | export KUBECONFIG=... 56 | # Deploy 57 | kubectl apply -k ./deployment/overlays/dev 58 | ``` 59 | -------------------------------------------------------------------------------- /deployment/overlays/dev/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | namespace: aks-periscope-dev 5 | 6 | bases: 7 | - ../../base 8 | 9 | patches: 10 | - target: 11 | group: apps 12 | kind: DaemonSet 13 | name: aks-periscope 14 | version: v1 15 | patch: |- 16 | - op: replace 17 | path: '/spec/template/spec/containers/0/imagePullPolicy' 18 | value: Never 19 | 20 | images: 21 | - name: periscope-linux 22 | newName: periscope-local 23 | newTag: latest 24 | - name: periscope-windows 25 | newName: mcr.microsoft.com/aks/periscope 26 | newTag: latest 27 | 28 | secretGenerator: 29 | - name: azureblob-secret 30 | behavior: replace 31 | envs: 32 | - .env.secret 33 | 34 | configMapGenerator: 35 | - name: diagnostic-config 36 | behavior: merge 37 | envs: 38 | - .env.config 39 | -------------------------------------------------------------------------------- /deployment/overlays/dynamic-image/kustomization.template.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | namespace: aks-periscope 5 | 6 | bases: 7 | - ../../base 8 | 9 | images: 10 | - name: periscope-linux 11 | newName: ${IMAGE_NAME} 12 | newTag: "${IMAGE_TAG}" 13 | - name: periscope-windows 14 | newName: ${IMAGE_NAME} 15 | newTag: "${IMAGE_TAG}" 16 | 17 | secretGenerator: 18 | - name: azureblob-secret 19 | behavior: replace 20 | envs: 21 | - .env.secret 22 | - name: acr-secret 23 | behavior: create 24 | files: 25 | - .dockerconfigjson=acr.dockerconfigjson 26 | 27 | configMapGenerator: 28 | - name: diagnostic-config 29 | behavior: merge 30 | envs: 31 | - .env.config 32 | 33 | patches: 34 | - target: 35 | kind: DaemonSet 36 | patch: |- 37 | - op: add 38 | path: /spec/template/spec/imagePullSecrets 39 | value: [{ name: acr-secret }] -------------------------------------------------------------------------------- /deployment/overlays/external/README.md: -------------------------------------------------------------------------------- 1 | # External Overlay (Deprecated) 2 | 3 | This overlay produces the Periscope resource specification for the production images in MCR. The output of this can be consumed by external tools, like VS Code and AZ CLI. 4 | 5 | **NOTE**: The preferred approach for consuming tools is to use `Kustomize` directly. See [main notes](../../../README.md#dependent-consuming-tools-and-working-contract). 6 | 7 | The storage account data is not known at this time. The consuming tools are responsible for substituting all configuration data into the output, so this ensures we produce well-known placeholders for the various settings. 8 | 9 | ```sh 10 | # Important: set the desired MCR version tag 11 | export IMAGE_TAG=... 12 | export SAS_KEY_PLACEHOLDER="# " 13 | export ACCOUNT_NAME_PLACEHOLDER="# " 14 | export CONTAINER_NAME_PLACEHOLDER="# " 15 | # In the kustomize output, the placeholder will be base-64 encoded. 16 | # Work out what it will be, so we can replace it with our desired placeholder. 17 | sas_key_env_var_b64=$(echo -n '${SAS_KEY_PLACEHOLDER}' | base64) 18 | kubectl kustomize ./deployment/overlays/external | sed -e "s/$sas_key_env_var_b64/$SAS_KEY_PLACEHOLDER/g" | envsubst 19 | ``` -------------------------------------------------------------------------------- /deployment/overlays/external/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | namespace: aks-periscope 5 | 6 | bases: 7 | - ../../base 8 | 9 | images: 10 | - name: periscope-linux 11 | newName: mcr.microsoft.com/aks/periscope 12 | newTag: "${IMAGE_TAG}" 13 | - name: periscope-windows 14 | newName: mcr.microsoft.com/aks/periscope 15 | newTag: "${IMAGE_TAG}" 16 | 17 | secretGenerator: 18 | - name: azureblob-secret 19 | behavior: replace 20 | literals: 21 | - AZURE_BLOB_SAS_KEY=${SAS_KEY_PLACEHOLDER} 22 | 23 | # Consuming applications perform substitutions for account/container in unencoded text. 24 | # For compatibility, use a ConfigMap. 25 | configMapGenerator: 26 | - name: storage-config 27 | literals: 28 | - AZURE_BLOB_ACCOUNT_NAME=${ACCOUNT_NAME_PLACEHOLDER} 29 | - AZURE_BLOB_CONTAINER_NAME=${CONTAINER_NAME_PLACEHOLDER} 30 | 31 | patches: 32 | - target: 33 | group: apps 34 | kind: DaemonSet 35 | name: aks-periscope 36 | version: v1 37 | patch: |- 38 | - op: add 39 | path: '/spec/template/spec/containers/0/envFrom/-' 40 | value: 41 | configMapRef: 42 | name: storage-config 43 | - target: 44 | group: apps 45 | kind: DaemonSet 46 | name: aks-periscope-win 47 | version: v1 48 | patch: |- 49 | - op: add 50 | path: '/spec/template/spec/containers/0/envFrom/-' 51 | value: 52 | configMapRef: 53 | name: storage-config 54 | 55 | generatorOptions: 56 | disableNameSuffixHash: true -------------------------------------------------------------------------------- /docs/appendix.md: -------------------------------------------------------------------------------- 1 | # Appendix 2 | 3 | Alternatively, AKS Periscope can be deployed directly with `kubectl`. The steps are: 4 | 5 | 1. Download the deployment file: 6 | ``` 7 | deployment/aks-periscope.yaml 8 | ``` 9 | 10 | By default, the collected logs, metrics and node level diagnostic information will be exported to Azure Blob Service. An Azure Blob Service account and a Shared Access Signature (SAS) token need to be provisioned in advance. These values should be based64 encoded and be set in the `AZURE_BLOB_ACCOUNT_NAME` and `AZURE_BLOB_SAS_KEY` in above aks-periscope.yaml. 11 | 12 | * `AZURE_BLOB_ACCOUNT_NAME` holds a base64 encoded storage account ID (e.g. "mystorageaccountname"). 13 | * `AZURE_BLOB_SAS_KEY` holds a base64 encoded **Account level** SAS token granting the following permissions: ss=b srt=sco sp=rlacw (description of params here: https://docs.microsoft.com/en-us/rest/api/storageservices/create-account-sas#specifying-account-sas-parameters). 14 | * this should be just the **query string** component of the SAS key, e.g. "?sv=2019-12-12&ss=btqf&...." not the full uri. 15 | * Azure Storage Explorer or the Azure Portal can be used to generate the SAS. 16 | 17 | Base64 encoding can be performed on linux via: 18 | echo -n "string-to-encode" | base64 19 | 20 | Additionally, to collect container logs and describe Kubenetes objects (pods and services) in namespaces beyond the default `kube-system`, user can configure the `containerlogs-config` and `kubeobjects-config` in above aks-periscope.yaml. 21 | 22 | 2. If Periscope has been previously deployed to the cluster, it will need to be manually removed first or the "kubectl apply" command below will succeed, but Periscope will silently fail to run: 23 | ``` 24 | kubectl delete -f aks-periscope.yaml 25 | ``` 26 | 27 | 3. Deploy the daemon set using kubectl: 28 | ``` 29 | kubectl apply -f aks-periscope.yaml 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/goalsvsnongoals.md: -------------------------------------------------------------------------------- 1 | # Goal vs Non-Goals 2 | 3 | ## Goals: 4 | 5 | The goal of `AKS Periscope` is to allow `AKS` customers to run initial diagnostics and collect and export the logs (like into an Azure Blob storage account) to help them analyse and identify potential problems. AKS cluster issues are many times are caused by wrong configuration of their environment, such as networking or permission issues. This tool will allow `AKS` customers to run initial diagnostics and collect logs and custom analyses that help them identify the underlying problems. 6 | 7 | The logs does this tool collects are documented here - https://github.com/Azure/aks-periscope#overview 8 | 9 | ## Non-Goals: 10 | 11 | This tool is written with `AKS` in mind and does not cover any broader k8s distros or services. In the broader OSS tool system, there are many OSS tools which provide better support for other scenarios, but this tool is `AKS` specific. 12 | -------------------------------------------------------------------------------- /docs/windows-vs-linux.md: -------------------------------------------------------------------------------- 1 | # Windows vs. Linux 2 | 3 | AKS Periscope runs on both Windows and Linux nodes, but the information it collects differs between each OS. This is a summary of those differences. 4 | 5 | ## Collectors not enabled on Windows 6 | 7 | The following collectors are currently unavailable on Windows: 8 | 9 | - DNS: This relies on `resolv.conf`, which is unavailable in Windows. 10 | - IPTables: The `iptables` command is not available on Windows. 11 | - Kubelet: This shows the arguments used to invoke the kubelet process. Windows containers do not support shared process namespaces, and so we cannot see processes on the host node. 12 | - SystemLogs: This uses `journalctl` to retrieve system logs, which is not available on Windows. 13 | 14 | ## Node Logs differences 15 | 16 | Since Windows and Linux nodes have a completely different file structure, the files collected by the `NodeLogsCollector` differ between OS. These are configurable, but by default Periscope will collect: 17 | 18 | **Linux** 19 | - /var/log/azure/cluster-provision.log 20 | - /var/log/cloud-init.log 21 | 22 | **Windows** 23 | - C:\AzureData\CustomDataSetupScript.log 24 | -------------------------------------------------------------------------------- /pkg/collector/dns_collector.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/Azure/aks-periscope/pkg/interfaces" 8 | "github.com/Azure/aks-periscope/pkg/utils" 9 | ) 10 | 11 | // DNSCollector defines a DNS Collector struct 12 | type DNSCollector struct { 13 | HostConf string 14 | ContainerConf string 15 | osIdentifier utils.OSIdentifier 16 | filePaths *utils.KnownFilePaths 17 | fileSystem interfaces.FileSystemAccessor 18 | } 19 | 20 | // NewDNSCollector is a constructor 21 | func NewDNSCollector(osIdentifier utils.OSIdentifier, filePaths *utils.KnownFilePaths, fileSystem interfaces.FileSystemAccessor) *DNSCollector { 22 | return &DNSCollector{ 23 | HostConf: "", 24 | ContainerConf: "", 25 | osIdentifier: osIdentifier, 26 | filePaths: filePaths, 27 | fileSystem: fileSystem, 28 | } 29 | } 30 | 31 | func (collector *DNSCollector) GetName() string { 32 | return "dns" 33 | } 34 | 35 | func (collector *DNSCollector) CheckSupported() error { 36 | // NOTE: This *might* be achievable in Windows using APIs that query the registry, see: 37 | // https://kubernetes.io/docs/setup/production-environment/windows/intro-windows-in-kubernetes/#networking 38 | // But for now it's restricted to Linux containers only, in which we can read `resolv.conf`. 39 | if collector.osIdentifier != utils.Linux { 40 | return fmt.Errorf("unsupported OS: %s", collector.osIdentifier) 41 | } 42 | 43 | return nil 44 | } 45 | 46 | // Collect implements the interface method 47 | func (collector *DNSCollector) Collect() error { 48 | collector.HostConf = collector.getConfFileContent(collector.filePaths.ResolvConfHost) 49 | collector.ContainerConf = collector.getConfFileContent(collector.filePaths.ResolvConfContainer) 50 | 51 | return nil 52 | } 53 | 54 | func (collector *DNSCollector) getConfFileContent(filePath string) string { 55 | content, err := utils.GetContent(func() (io.ReadCloser, error) { return collector.fileSystem.GetFileReader(filePath) }) 56 | if err != nil { 57 | return err.Error() 58 | } 59 | 60 | return content 61 | } 62 | 63 | func (collector *DNSCollector) GetData() map[string]interfaces.DataValue { 64 | return map[string]interfaces.DataValue{ 65 | "virtualmachine": utils.NewStringDataValue(collector.HostConf), 66 | "kubernetes": utils.NewStringDataValue(collector.ContainerConf), 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /pkg/collector/dns_collector_test.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/Azure/aks-periscope/pkg/test" 7 | "github.com/Azure/aks-periscope/pkg/utils" 8 | ) 9 | 10 | func TestDNSCollectorGetName(t *testing.T) { 11 | const expectedName = "dns" 12 | 13 | c := NewDNSCollector("", nil, nil) 14 | actualName := c.GetName() 15 | if actualName != expectedName { 16 | t.Errorf("unexpected name: expected %s, found %s", expectedName, actualName) 17 | } 18 | } 19 | 20 | func TestDNSCollectorCheckSupported(t *testing.T) { 21 | tests := []struct { 22 | osIdentifier utils.OSIdentifier 23 | wantErr bool 24 | }{ 25 | { 26 | osIdentifier: utils.Windows, 27 | wantErr: true, 28 | }, 29 | { 30 | osIdentifier: utils.Linux, 31 | wantErr: false, 32 | }, 33 | } 34 | 35 | for _, tt := range tests { 36 | c := NewDNSCollector(tt.osIdentifier, nil, nil) 37 | err := c.CheckSupported() 38 | if (err != nil) != tt.wantErr { 39 | t.Errorf("CheckSupported() error = %v, wantErr %v", err, tt.wantErr) 40 | } 41 | } 42 | } 43 | 44 | func TestDNSCollectorCollect(t *testing.T) { 45 | const expectedHostConfContent = "hostconf" 46 | const expectedContainerConfContent = "containerconf" 47 | 48 | tests := []struct { 49 | name string 50 | files map[string]string 51 | wantErr bool 52 | wantData map[string]string 53 | }{ 54 | { 55 | name: "missing host conf", 56 | files: map[string]string{ 57 | "/etc/resolv.conf": expectedContainerConfContent, 58 | }, 59 | wantErr: true, 60 | wantData: nil, 61 | }, 62 | { 63 | name: "missing container conf", 64 | files: map[string]string{ 65 | "/host/etc/resolv.conf": expectedHostConfContent, 66 | }, 67 | wantErr: true, 68 | wantData: nil, 69 | }, 70 | { 71 | name: "existing files", 72 | files: map[string]string{ 73 | "/host/etc/resolv.conf": expectedHostConfContent, 74 | "/etc/resolv.conf": expectedContainerConfContent, 75 | }, 76 | wantErr: false, 77 | wantData: map[string]string{ 78 | "virtualmachine": expectedHostConfContent, 79 | "kubernetes": expectedContainerConfContent, 80 | }, 81 | }, 82 | } 83 | 84 | filePaths := &utils.KnownFilePaths{ 85 | ResolvConfHost: "/host/etc/resolv.conf", 86 | ResolvConfContainer: "/etc/resolv.conf", 87 | } 88 | 89 | for _, tt := range tests { 90 | t.Run(tt.name, func(t *testing.T) { 91 | fs := test.NewFakeFileSystem(tt.files) 92 | 93 | c := NewDNSCollector(utils.Linux, filePaths, fs) 94 | err := c.Collect() 95 | 96 | if err != nil { 97 | if !tt.wantErr { 98 | t.Errorf("Collect() error = %v, wantErr %v", err, tt.wantErr) 99 | } 100 | } else { 101 | dataItems := c.GetData() 102 | for key, expectedValue := range tt.wantData { 103 | result, ok := dataItems[key] 104 | if !ok { 105 | t.Errorf("missing key %s", key) 106 | } 107 | 108 | testDataValue(t, result, func(actualValue string) { 109 | if actualValue != expectedValue { 110 | t.Errorf("unexpected value for key %s.\nExpected '%s'\nFound '%s'", key, expectedValue, actualValue) 111 | } 112 | }) 113 | } 114 | } 115 | }) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /pkg/collector/helm_collector.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "strings" 8 | "time" 9 | 10 | "k8s.io/apimachinery/pkg/api/meta" 11 | "k8s.io/client-go/discovery" 12 | "k8s.io/client-go/tools/clientcmd" 13 | 14 | "github.com/Azure/aks-periscope/pkg/interfaces" 15 | "github.com/Azure/aks-periscope/pkg/utils" 16 | "helm.sh/helm/v3/pkg/action" 17 | "helm.sh/helm/v3/pkg/release" 18 | restclient "k8s.io/client-go/rest" 19 | ) 20 | 21 | type HelmRelease struct { 22 | Name string `json:"name"` 23 | Namespace string `json:"namespace"` 24 | Status release.Status `json:"status"` 25 | ChartName string `json:"chart"` 26 | History []HelmReleaseHistory `json:"history"` 27 | } 28 | 29 | type HelmReleaseHistory struct { 30 | Date time.Time `json:"lastDeployment"` 31 | Message string `json:"description"` 32 | Status release.Status `json:"status"` 33 | Revision int `json:"revision"` 34 | AppVersion string `json:"appVersion"` 35 | } 36 | 37 | // HelmCollector defines a Helm Collector struct 38 | type HelmCollector struct { 39 | data map[string]string 40 | kubeconfig *restclient.Config 41 | runtimeInfo *utils.RuntimeInfo 42 | } 43 | 44 | // NewHelmCollector is a constructor 45 | func NewHelmCollector(config *restclient.Config, runtimeInfo *utils.RuntimeInfo) *HelmCollector { 46 | return &HelmCollector{ 47 | data: make(map[string]string), 48 | kubeconfig: config, 49 | runtimeInfo: runtimeInfo, 50 | } 51 | } 52 | 53 | func (collector *HelmCollector) GetName() string { 54 | return "helm" 55 | } 56 | 57 | func (collector *HelmCollector) CheckSupported() error { 58 | if !utils.Contains(collector.runtimeInfo.CollectorList, "connectedCluster") { 59 | return fmt.Errorf("not included because 'connectedCluster' not in COLLECTOR_LIST variable. Included values: %s", strings.Join(collector.runtimeInfo.CollectorList, " ")) 60 | } 61 | 62 | return nil 63 | } 64 | 65 | // Implement the RESTClientGetter interface for helm client initialization. 66 | // This allows us to provide our restclient.Config directly, without having 67 | // to copy individual fields. 68 | func (collector *HelmCollector) ToRESTConfig() (*restclient.Config, error) { 69 | return collector.kubeconfig, nil 70 | } 71 | func (collector *HelmCollector) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) { 72 | return nil, nil 73 | } 74 | func (collector *HelmCollector) ToRESTMapper() (meta.RESTMapper, error) { 75 | return nil, nil 76 | } 77 | func (collector *HelmCollector) ToRawKubeConfigLoader() clientcmd.ClientConfig { 78 | return nil 79 | } 80 | 81 | // Collect implements the interface method 82 | func (collector *HelmCollector) Collect() error { 83 | actionConfig := new(action.Configuration) 84 | 85 | if err := actionConfig.Init(collector, "", "", log.Printf); err != nil { 86 | return fmt.Errorf("init action configuration: %w", err) 87 | } 88 | 89 | releases, err := action.NewList(actionConfig).Run() 90 | if err != nil { 91 | return fmt.Errorf("list helm releases: %w", err) 92 | } 93 | 94 | result := make([]HelmRelease, 0) 95 | 96 | for _, release := range releases { 97 | release.Chart.AppVersion() 98 | r := HelmRelease{ 99 | Name: release.Name, 100 | Namespace: release.Namespace, 101 | Status: release.Info.Status, 102 | ChartName: release.Chart.Name(), 103 | } 104 | 105 | histories, err := action.NewHistory(actionConfig).Run(release.Name) 106 | 107 | if err != nil { 108 | log.Printf("Get release %s history failed: %v", release.Name, err) 109 | } else { 110 | r.History = make([]HelmReleaseHistory, 0) 111 | for _, history := range histories { 112 | h := HelmReleaseHistory{ 113 | Date: history.Info.LastDeployed.Time, 114 | Message: history.Info.Description, 115 | Status: history.Info.Status, 116 | Revision: history.Version, 117 | AppVersion: history.Chart.AppVersion(), 118 | } 119 | r.History = append(r.History, h) 120 | } 121 | } 122 | 123 | result = append(result, r) 124 | } 125 | 126 | b, err := json.Marshal(result) 127 | 128 | if err != nil { 129 | return fmt.Errorf("marshall helm releases to json: %w", err) 130 | } 131 | 132 | collector.data["helm_list"] = string(b) 133 | 134 | return nil 135 | } 136 | 137 | func (collector *HelmCollector) GetData() map[string]interfaces.DataValue { 138 | return utils.ToDataValueMap(collector.data) 139 | } 140 | -------------------------------------------------------------------------------- /pkg/collector/helm_collector_test.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/Azure/aks-periscope/pkg/test" 9 | "github.com/Azure/aks-periscope/pkg/utils" 10 | "k8s.io/client-go/rest" 11 | ) 12 | 13 | const ( 14 | namespacePrefix = "helmtest" 15 | releaseName = "helmtest-release" 16 | ) 17 | 18 | func setupHelmTest(t *testing.T) *rest.Config { 19 | fixture, _ := test.GetClusterFixture() 20 | 21 | namespace, err := fixture.CreateTestNamespace(namespacePrefix) 22 | if err != nil { 23 | t.Fatalf("Error creating test namespace %s: %v", namespace, err) 24 | } 25 | 26 | installChartCommand := fmt.Sprintf("helm install %s /resources/testchart --namespace %s", releaseName, namespace) 27 | _, err = fixture.CommandRunner.Run(installChartCommand, fixture.AdminAccess.GetKubeConfigBinding()) 28 | if err != nil { 29 | t.Fatalf("Error installing helm release %s into %s namespace: %v", releaseName, namespace, err) 30 | } 31 | 32 | return fixture.PeriscopeAccess.ClientConfig 33 | } 34 | 35 | func TestHelmCollectorGetName(t *testing.T) { 36 | const expectedName = "helm" 37 | 38 | c := NewHelmCollector(nil, nil) 39 | actualName := c.GetName() 40 | if actualName != expectedName { 41 | t.Errorf("unexpected name: expected %s, found %s", expectedName, actualName) 42 | } 43 | } 44 | 45 | func TestHelmCollectorCheckSupported(t *testing.T) { 46 | tests := []struct { 47 | name string 48 | collectorList []string 49 | wantErr bool 50 | }{ 51 | { 52 | name: "'connectedCluster' in COLLECTOR_LIST", 53 | collectorList: []string{"connectedCluster"}, 54 | wantErr: false, 55 | }, 56 | { 57 | name: "'connectedCluster' not in COLLECTOR_LIST", 58 | collectorList: []string{}, 59 | wantErr: true, 60 | }, 61 | } 62 | 63 | for _, tt := range tests { 64 | runtimeInfo := &utils.RuntimeInfo{ 65 | CollectorList: tt.collectorList, 66 | } 67 | c := NewHelmCollector(nil, runtimeInfo) 68 | err := c.CheckSupported() 69 | if (err != nil) != tt.wantErr { 70 | t.Errorf("%s error = %v, wantErr %v", tt.name, err, tt.wantErr) 71 | } 72 | } 73 | } 74 | 75 | func TestHelmCollectorCollect(t *testing.T) { 76 | clientConfig := setupHelmTest(t) 77 | 78 | tests := []struct { 79 | name string 80 | want int 81 | wantErr bool 82 | }{ 83 | { 84 | name: "get release history", 85 | want: 1, 86 | wantErr: false, 87 | }, 88 | } 89 | 90 | runtimeInfo := &utils.RuntimeInfo{ 91 | CollectorList: []string{"connectedCluster"}, 92 | } 93 | 94 | c := NewHelmCollector(clientConfig, runtimeInfo) 95 | 96 | for _, tt := range tests { 97 | t.Run(tt.name, func(t *testing.T) { 98 | err := c.Collect() 99 | if (err != nil) != tt.wantErr { 100 | t.Errorf("Collect() error = %v, wantErr %v", err, tt.wantErr) 101 | } 102 | 103 | result := c.GetData()["helm_list"] 104 | testDataValue(t, result, func(raw string) { 105 | var releases []HelmRelease 106 | 107 | if err := json.Unmarshal([]byte(raw), &releases); err != nil { 108 | t.Errorf("unmarshal GetData(): %v", err) 109 | } 110 | 111 | if len(releases) < tt.want { 112 | t.Errorf("len(GetData()) = %v, want %v", len(releases), tt.want) 113 | } 114 | }) 115 | }) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /pkg/collector/iptables_collector.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/Azure/aks-periscope/pkg/interfaces" 8 | "github.com/Azure/aks-periscope/pkg/utils" 9 | ) 10 | 11 | // IPTablesCollector defines a IPTables Collector struct 12 | type IPTablesCollector struct { 13 | data map[string]string 14 | osIdentifier utils.OSIdentifier 15 | runtimeInfo *utils.RuntimeInfo 16 | } 17 | 18 | // NewIPTablesCollector is a constructor 19 | func NewIPTablesCollector(osIdentifier utils.OSIdentifier, runtimeInfo *utils.RuntimeInfo) *IPTablesCollector { 20 | return &IPTablesCollector{ 21 | data: make(map[string]string), 22 | osIdentifier: osIdentifier, 23 | runtimeInfo: runtimeInfo, 24 | } 25 | } 26 | 27 | func (collector *IPTablesCollector) GetName() string { 28 | return "iptables" 29 | } 30 | 31 | func (collector *IPTablesCollector) CheckSupported() error { 32 | // There's no obvious alternative to `iptables` on Windows. 33 | if collector.osIdentifier != utils.Linux { 34 | return fmt.Errorf("unsupported OS: %s", collector.osIdentifier) 35 | } 36 | 37 | if utils.Contains(collector.runtimeInfo.CollectorList, "connectedCluster") { 38 | return fmt.Errorf("not included because 'connectedCluster' is in COLLECTOR_LIST variable. Included values: %s", strings.Join(collector.runtimeInfo.CollectorList, " ")) 39 | } 40 | 41 | return nil 42 | } 43 | 44 | // Collect implements the interface method 45 | func (collector *IPTablesCollector) Collect() error { 46 | output, err := utils.RunCommandOnHost("iptables", "-t", "nat", "-L") 47 | if err != nil { 48 | return err 49 | } 50 | 51 | collector.data["iptables"] = output 52 | 53 | return nil 54 | } 55 | 56 | func (collector *IPTablesCollector) GetData() map[string]interfaces.DataValue { 57 | return utils.ToDataValueMap(collector.data) 58 | } 59 | -------------------------------------------------------------------------------- /pkg/collector/iptables_collector_test.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/Azure/aks-periscope/pkg/utils" 7 | ) 8 | 9 | func TestIPTablesCollectorGetName(t *testing.T) { 10 | const expectedName = "iptables" 11 | 12 | c := NewIPTablesCollector("", nil) 13 | actualName := c.GetName() 14 | if actualName != expectedName { 15 | t.Errorf("unexpected name: expected %s, found %s", expectedName, actualName) 16 | } 17 | } 18 | 19 | func TestIPTablesCollectorCheckSupported(t *testing.T) { 20 | tests := []struct { 21 | name string 22 | osIdentifier utils.OSIdentifier 23 | collectorList []string 24 | wantErr bool 25 | }{ 26 | { 27 | name: "windows", 28 | osIdentifier: utils.Windows, 29 | collectorList: []string{"connectedCluster"}, 30 | wantErr: true, 31 | }, 32 | { 33 | name: "'connectedCluster' in COLLECTOR_LIST", 34 | osIdentifier: utils.Linux, 35 | collectorList: []string{"connectedCluster"}, 36 | wantErr: true, 37 | }, 38 | { 39 | name: "'connectedCluster' not in COLLECTOR_LIST", 40 | osIdentifier: utils.Linux, 41 | collectorList: []string{}, 42 | wantErr: false, 43 | }, 44 | } 45 | 46 | for _, tt := range tests { 47 | runtimeInfo := &utils.RuntimeInfo{ 48 | CollectorList: tt.collectorList, 49 | } 50 | c := NewIPTablesCollector(tt.osIdentifier, runtimeInfo) 51 | err := c.CheckSupported() 52 | if (err != nil) != tt.wantErr { 53 | t.Errorf("%s error = %v, wantErr %v", tt.name, err, tt.wantErr) 54 | } 55 | } 56 | } 57 | 58 | func TestIPTablesCollectorCollect(t *testing.T) { 59 | tests := []struct { 60 | name string 61 | want int 62 | wantErr bool 63 | }{ 64 | { 65 | name: "get iptables logs", 66 | want: 1, 67 | wantErr: true, 68 | }, 69 | } 70 | 71 | runtimeInfo := &utils.RuntimeInfo{ 72 | CollectorList: []string{}, 73 | } 74 | c := NewIPTablesCollector(utils.Linux, runtimeInfo) 75 | 76 | for _, tt := range tests { 77 | t.Run(tt.name, func(t *testing.T) { 78 | err := c.Collect() 79 | if (err != nil) == tt.wantErr { 80 | t.Logf("Collect() error = %v, wantErr %v", err, tt.wantErr) 81 | } 82 | }) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /pkg/collector/kubeletcmd_collector.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/Azure/aks-periscope/pkg/interfaces" 8 | "github.com/Azure/aks-periscope/pkg/utils" 9 | ) 10 | 11 | // KubeletCmdCollector defines a KubeletCmd Collector struct 12 | type KubeletCmdCollector struct { 13 | KubeletCommand string 14 | osIdentifier utils.OSIdentifier 15 | runtimeInfo *utils.RuntimeInfo 16 | } 17 | 18 | // NewKubeletCmdCollector is a constructor 19 | func NewKubeletCmdCollector(osIdentifier utils.OSIdentifier, runtimeInfo *utils.RuntimeInfo) *KubeletCmdCollector { 20 | return &KubeletCmdCollector{ 21 | KubeletCommand: "", 22 | osIdentifier: osIdentifier, 23 | runtimeInfo: runtimeInfo, 24 | } 25 | } 26 | 27 | func (collector *KubeletCmdCollector) GetName() string { 28 | return "kubeletcmd" 29 | } 30 | 31 | func (collector *KubeletCmdCollector) CheckSupported() error { 32 | // This looks to be impossible on Windows, since Windows containers don't support shared process namespaces, 33 | // and hence processes on the host are completely isolated from the container. See: 34 | // https://docs.microsoft.com/en-us/virtualization/windowscontainers/manage-containers/hyperv-container#piercing-the-isolation-boundary 35 | if collector.osIdentifier != utils.Linux { 36 | return fmt.Errorf("unsupported OS: %s", collector.osIdentifier) 37 | } 38 | 39 | if utils.Contains(collector.runtimeInfo.CollectorList, "connectedCluster") { 40 | return fmt.Errorf("not included because 'connectedCluster' is in COLLECTOR_LIST variable. Included values: %s", strings.Join(collector.runtimeInfo.CollectorList, " ")) 41 | } 42 | 43 | return nil 44 | } 45 | 46 | // Collect implements the interface method 47 | func (collector *KubeletCmdCollector) Collect() error { 48 | output, err := utils.RunCommandOnHost("ps", "-o", "cmd=", "-C", "kubelet") 49 | if err != nil { 50 | return err 51 | } 52 | 53 | collector.KubeletCommand = output 54 | 55 | return nil 56 | } 57 | 58 | func (collector *KubeletCmdCollector) GetData() map[string]interfaces.DataValue { 59 | return map[string]interfaces.DataValue{ 60 | "kubeletcmd": utils.NewStringDataValue(collector.KubeletCommand), 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /pkg/collector/kubeletcmd_collector_test.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/Azure/aks-periscope/pkg/utils" 7 | ) 8 | 9 | func TestKubeletCmdCollectorGetName(t *testing.T) { 10 | const expectedName = "kubeletcmd" 11 | 12 | c := NewKubeletCmdCollector("", nil) 13 | actualName := c.GetName() 14 | if actualName != expectedName { 15 | t.Errorf("unexpected name: expected %s, found %s", expectedName, actualName) 16 | } 17 | } 18 | 19 | func TestKubeletCmdCollectorCheckSupported(t *testing.T) { 20 | tests := []struct { 21 | name string 22 | osIdentifier utils.OSIdentifier 23 | collectorList []string 24 | wantErr bool 25 | }{ 26 | { 27 | name: "windows", 28 | osIdentifier: utils.Windows, 29 | collectorList: []string{"connectedCluster"}, 30 | wantErr: true, 31 | }, 32 | { 33 | name: "'connectedCluster' in COLLECTOR_LIST", 34 | osIdentifier: utils.Linux, 35 | collectorList: []string{"connectedCluster"}, 36 | wantErr: true, 37 | }, 38 | { 39 | name: "'connectedCluster' not in COLLECTOR_LIST", 40 | osIdentifier: utils.Linux, 41 | collectorList: []string{}, 42 | wantErr: false, 43 | }, 44 | } 45 | 46 | for _, tt := range tests { 47 | runtimeInfo := &utils.RuntimeInfo{ 48 | CollectorList: tt.collectorList, 49 | } 50 | c := NewKubeletCmdCollector(tt.osIdentifier, runtimeInfo) 51 | err := c.CheckSupported() 52 | if (err != nil) != tt.wantErr { 53 | t.Errorf("%s error = %v, wantErr %v", tt.name, err, tt.wantErr) 54 | } 55 | } 56 | } 57 | 58 | func TestKubeletCmdCollectorCollect(t *testing.T) { 59 | tests := []struct { 60 | name string 61 | want int 62 | wantErr bool 63 | }{ 64 | { 65 | name: "get kubeletcmd logs", 66 | want: 1, 67 | wantErr: true, 68 | }, 69 | } 70 | 71 | runtimeInfo := &utils.RuntimeInfo{ 72 | CollectorList: []string{}, 73 | } 74 | c := NewKubeletCmdCollector(utils.Linux, runtimeInfo) 75 | 76 | for _, tt := range tests { 77 | t.Run(tt.name, func(t *testing.T) { 78 | err := c.Collect() 79 | if (err != nil) == tt.wantErr { 80 | t.Logf("Collect() error = %v, wantErr %v", err, tt.wantErr) 81 | } 82 | }) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /pkg/collector/kubeobjects_collector.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strings" 7 | 8 | "github.com/Azure/aks-periscope/pkg/interfaces" 9 | "github.com/Azure/aks-periscope/pkg/utils" 10 | "k8s.io/apimachinery/pkg/api/meta" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | "k8s.io/apimachinery/pkg/runtime/schema" 13 | "k8s.io/client-go/discovery" 14 | memory "k8s.io/client-go/discovery/cached" 15 | restclient "k8s.io/client-go/rest" 16 | "k8s.io/client-go/restmapper" 17 | "k8s.io/kubectl/pkg/describe" 18 | ) 19 | 20 | // KubeObjectsCollector defines a KubeObjects Collector struct 21 | type KubeObjectsCollector struct { 22 | data map[string]string 23 | kubeconfig *restclient.Config 24 | commandRunner *utils.KubeCommandRunner 25 | runtimeInfo *utils.RuntimeInfo 26 | } 27 | 28 | // NewKubeObjectsCollector is a constructor 29 | func NewKubeObjectsCollector(config *restclient.Config, runtimeInfo *utils.RuntimeInfo) *KubeObjectsCollector { 30 | return &KubeObjectsCollector{ 31 | data: make(map[string]string), 32 | kubeconfig: config, 33 | commandRunner: utils.NewKubeCommandRunner(config), 34 | runtimeInfo: runtimeInfo, 35 | } 36 | } 37 | 38 | func (collector *KubeObjectsCollector) GetName() string { 39 | return "kubeobjects" 40 | } 41 | 42 | func (collector *KubeObjectsCollector) CheckSupported() error { 43 | return nil 44 | } 45 | 46 | // Collect implements the interface method 47 | func (collector *KubeObjectsCollector) Collect() error { 48 | // Create a discovery client for querying resource metadata 49 | discoveryClient, err := discovery.NewDiscoveryClientForConfig(collector.kubeconfig) 50 | if err != nil { 51 | return fmt.Errorf("error creating discovery client: %w", err) 52 | } 53 | 54 | // Create a RESTMapper to handle the mapping between GroupKind and GroupVersionResource 55 | mapper := restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(discoveryClient)) 56 | 57 | for _, kubernetesObject := range collector.runtimeInfo.KubernetesObjects { 58 | kubernetesObjectParts := strings.Split(kubernetesObject, "/") 59 | if len(kubernetesObjectParts) < 2 { 60 | log.Printf("Invalid kube-objects value: %s", kubernetesObject) 61 | continue 62 | } 63 | 64 | namespace := kubernetesObjectParts[0] 65 | groupResource := schema.ParseGroupResource(kubernetesObjectParts[1]) 66 | 67 | groupVersionKind, err := mapper.KindFor(groupResource.WithVersion("")) 68 | if err != nil { 69 | log.Printf("Unable to determine Kind for resource %s: %v", groupResource.String(), err) 70 | continue 71 | } 72 | 73 | describer, ok := describe.DescriberFor(groupVersionKind.GroupKind(), collector.kubeconfig) 74 | if !ok { 75 | log.Printf("Unable to create Describer for Kind %s", groupVersionKind.String()) 76 | continue 77 | } 78 | 79 | // Get the resources within the namespace to describe 80 | var resourceNames []string 81 | if len(kubernetesObjectParts) > 2 { 82 | resourceNames = []string{kubernetesObjectParts[2]} 83 | } else { 84 | resourceNames, err = collector.getResourcesInNamespace(mapper, &groupResource, namespace) 85 | if err != nil { 86 | log.Printf("Unable to get %s resources in %s: %v", groupResource.String(), namespace, err) 87 | continue 88 | } 89 | } 90 | 91 | for _, resourceName := range resourceNames { 92 | output, err := describer.Describe(namespace, resourceName, describe.DescriberSettings{ShowEvents: true}) 93 | if err != nil { 94 | log.Printf("Error describing %s %s in namespace %s: %v", groupVersionKind.String(), resourceName, namespace, err) 95 | continue 96 | } 97 | 98 | key := fmt.Sprintf("%s_%s_%s", namespace, groupResource.String(), resourceName) 99 | collector.data[key] = output 100 | } 101 | } 102 | 103 | return nil 104 | } 105 | 106 | func (collector *KubeObjectsCollector) getResourcesInNamespace(mapper meta.RESTMapper, groupResource *schema.GroupResource, namespace string) ([]string, error) { 107 | groupVersionResource, err := mapper.ResourceFor(groupResource.WithVersion("")) 108 | if err != nil { 109 | return []string{}, fmt.Errorf("error determining Version for resource %s: %v", groupResource.String(), err) 110 | } 111 | 112 | resources, err := collector.commandRunner.GetUnstructuredList(&groupVersionResource, namespace, &metav1.ListOptions{}) 113 | if err != nil { 114 | return []string{}, fmt.Errorf("error listing %s: %v", groupVersionResource.String(), err) 115 | } 116 | 117 | resourceNames := make([]string, len(resources.Items)) 118 | for i, resource := range resources.Items { 119 | resourceNames[i] = resource.GetName() 120 | } 121 | 122 | return resourceNames, nil 123 | } 124 | 125 | func (collector *KubeObjectsCollector) GetData() map[string]interfaces.DataValue { 126 | return utils.ToDataValueMap(collector.data) 127 | } 128 | -------------------------------------------------------------------------------- /pkg/collector/networkoutbound_collector.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net" 7 | "time" 8 | 9 | "github.com/Azure/aks-periscope/pkg/interfaces" 10 | "github.com/Azure/aks-periscope/pkg/utils" 11 | ) 12 | 13 | type networkOutboundType struct { 14 | Type string `json:"Type"` 15 | URL string `json:"URL"` 16 | } 17 | 18 | // NetworkOutboundDatum defines a NetworkOutbound Datum 19 | type NetworkOutboundDatum struct { 20 | TimeStamp time.Time `json:"TimeStamp"` 21 | networkOutboundType 22 | Status string `json:"Status"` 23 | } 24 | 25 | // NetworkOutboundCollector defines a NetworkOutbound Collector struct 26 | type NetworkOutboundCollector struct { 27 | data map[string]string 28 | } 29 | 30 | // NewNetworkOutboundCollector is a constructor 31 | func NewNetworkOutboundCollector() *NetworkOutboundCollector { 32 | return &NetworkOutboundCollector{ 33 | data: make(map[string]string), 34 | } 35 | } 36 | 37 | func (collector *NetworkOutboundCollector) GetName() string { 38 | return "networkoutbound" 39 | } 40 | 41 | func (collector *NetworkOutboundCollector) CheckSupported() error { 42 | return nil 43 | } 44 | 45 | // Collect implements the interface method 46 | func (collector *NetworkOutboundCollector) Collect() error { 47 | outboundTypes := []networkOutboundType{} 48 | outboundTypes = append(outboundTypes, 49 | networkOutboundType{ 50 | Type: "Internet", 51 | URL: "google.com:80", 52 | }, 53 | ) 54 | outboundTypes = append(outboundTypes, 55 | networkOutboundType{ 56 | Type: "AKS API Server", 57 | URL: "kubernetes.default.svc.cluster.local:443", 58 | }, 59 | ) 60 | outboundTypes = append(outboundTypes, 61 | networkOutboundType{ 62 | Type: "Azure Container Registry", 63 | URL: "azurecr.io:80", 64 | }, 65 | ) 66 | outboundTypes = append(outboundTypes, 67 | networkOutboundType{ 68 | Type: "Microsoft Container Registry", 69 | URL: "mcr.microsoft.com:80", 70 | }, 71 | ) 72 | 73 | for _, outboundType := range outboundTypes { 74 | timeout := time.Duration(5 * time.Second) 75 | _, err := net.DialTimeout("tcp", outboundType.URL, timeout) 76 | 77 | status := "Connected" 78 | if err != nil { 79 | status = "Error: " + err.Error() 80 | } 81 | 82 | data := &NetworkOutboundDatum{ 83 | TimeStamp: time.Now().Truncate(1 * time.Second), 84 | networkOutboundType: outboundType, 85 | Status: status, 86 | } 87 | 88 | dataBytes, err := json.Marshal(data) 89 | if err != nil { 90 | return fmt.Errorf("marshal data: %w", err) 91 | } 92 | 93 | collector.data[outboundType.Type] = string(dataBytes) 94 | } 95 | 96 | return nil 97 | } 98 | 99 | func (collector *NetworkOutboundCollector) GetData() map[string]interfaces.DataValue { 100 | return utils.ToDataValueMap(collector.data) 101 | } 102 | -------------------------------------------------------------------------------- /pkg/collector/networkoutbound_collector_test.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestNetworkOutboundCollectorGetName(t *testing.T) { 8 | const expectedName = "networkoutbound" 9 | 10 | c := NewNetworkOutboundCollector() 11 | actualName := c.GetName() 12 | if actualName != expectedName { 13 | t.Errorf("unexpected name: expected %s, found %s", expectedName, actualName) 14 | } 15 | } 16 | 17 | func TestNetworkOutboundCollectorCheckSupported(t *testing.T) { 18 | c := NewNetworkOutboundCollector() 19 | err := c.CheckSupported() 20 | if err != nil { 21 | t.Errorf("error checking supported: %v", err) 22 | } 23 | } 24 | 25 | func TestNetworkOutboundCollectorCollect(t *testing.T) { 26 | tests := []struct { 27 | name string 28 | want int 29 | wantErr bool 30 | }{ 31 | { 32 | name: "get networkbound logs", 33 | want: 1, 34 | wantErr: false, 35 | }, 36 | } 37 | 38 | c := NewNetworkOutboundCollector() 39 | 40 | for _, tt := range tests { 41 | t.Run(tt.name, func(t *testing.T) { 42 | err := c.Collect() 43 | 44 | if (err != nil) != tt.wantErr { 45 | t.Errorf("Collect() error = %v, wantErr %v", err, tt.wantErr) 46 | } 47 | raw := c.GetData() 48 | 49 | if len(raw) < tt.want { 50 | t.Errorf("len(GetData()) = %v, want %v", len(raw), tt.want) 51 | } 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /pkg/collector/nodelogs_collector.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/Azure/aks-periscope/pkg/interfaces" 8 | "github.com/Azure/aks-periscope/pkg/utils" 9 | ) 10 | 11 | // NodeLogsCollector defines a NodeLogs Collector struct 12 | type NodeLogsCollector struct { 13 | data map[string]interfaces.DataValue 14 | runtimeInfo *utils.RuntimeInfo 15 | fileSystem interfaces.FileSystemAccessor 16 | } 17 | 18 | // NewNodeLogsCollector is a constructor 19 | func NewNodeLogsCollector(runtimeInfo *utils.RuntimeInfo, fileSystem interfaces.FileSystemAccessor) *NodeLogsCollector { 20 | return &NodeLogsCollector{ 21 | data: make(map[string]interfaces.DataValue), 22 | runtimeInfo: runtimeInfo, 23 | fileSystem: fileSystem, 24 | } 25 | } 26 | 27 | func (collector *NodeLogsCollector) GetName() string { 28 | return "nodelogs" 29 | } 30 | 31 | func (collector *NodeLogsCollector) CheckSupported() error { 32 | if utils.Contains(collector.runtimeInfo.CollectorList, "connectedCluster") { 33 | return fmt.Errorf("not included because 'connectedCluster' is in COLLECTOR_LIST variable. Included values: %s", strings.Join(collector.runtimeInfo.CollectorList, " ")) 34 | } 35 | 36 | // Although the files read by this collector may be different between Windows and Linux, 37 | // they are defined in a ConfigMap which is expected to be populated correctly for the OS. 38 | return nil 39 | } 40 | 41 | // Collect implements the interface method 42 | func (collector *NodeLogsCollector) Collect() error { 43 | for _, nodeLog := range collector.runtimeInfo.NodeLogs { 44 | normalizedNodeLog := strings.Replace(nodeLog, "/", "_", -1) 45 | if normalizedNodeLog[0] == '_' { 46 | normalizedNodeLog = normalizedNodeLog[1:] 47 | } 48 | 49 | size, err := collector.fileSystem.GetFileSize(nodeLog) 50 | if err != nil { 51 | return fmt.Errorf("error getting file size for %s: %w", nodeLog, err) 52 | } 53 | 54 | collector.data[normalizedNodeLog] = utils.NewFilePathDataValue(collector.fileSystem, nodeLog, size) 55 | } 56 | 57 | return nil 58 | } 59 | 60 | func (collector *NodeLogsCollector) GetData() map[string]interfaces.DataValue { 61 | return collector.data 62 | } 63 | -------------------------------------------------------------------------------- /pkg/collector/nodelogs_collector_test.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/Azure/aks-periscope/pkg/test" 7 | "github.com/Azure/aks-periscope/pkg/utils" 8 | ) 9 | 10 | func TestNodeLogsCollectorGetName(t *testing.T) { 11 | const expectedName = "nodelogs" 12 | 13 | c := NewNodeLogsCollector(nil, nil) 14 | actualName := c.GetName() 15 | if actualName != expectedName { 16 | t.Errorf("unexpected name: expected %s, found %s", expectedName, actualName) 17 | } 18 | } 19 | 20 | func TestNodeLogsCollectorCheckSupported(t *testing.T) { 21 | tests := []struct { 22 | name string 23 | collectorList []string 24 | wantErr bool 25 | }{ 26 | { 27 | name: "'connectedCluster' in COLLECTOR_LIST", 28 | collectorList: []string{"connectedCluster"}, 29 | wantErr: true, 30 | }, 31 | { 32 | name: "'connectedCluster' not in COLLECTOR_LIST", 33 | collectorList: []string{}, 34 | wantErr: false, 35 | }, 36 | } 37 | 38 | for _, tt := range tests { 39 | runtimeInfo := &utils.RuntimeInfo{ 40 | CollectorList: tt.collectorList, 41 | } 42 | c := NewNodeLogsCollector(runtimeInfo, nil) 43 | err := c.CheckSupported() 44 | if (err != nil) != tt.wantErr { 45 | t.Errorf("%s error = %v, wantErr %v", tt.name, err, tt.wantErr) 46 | } 47 | } 48 | } 49 | 50 | func TestNodeLogsCollectorCollect(t *testing.T) { 51 | const ( 52 | file1Name = "/var/log/test1.log" 53 | file1ExpectedKey = "var_log_test1.log" 54 | file1Content = "Test 1 Content" 55 | 56 | file2Name = "/var/log/test2.log" 57 | file2ExpectedKey = "var_log_test2.log" 58 | file2Content = "Test 2 Content" 59 | ) 60 | 61 | testLogFiles := map[string]string{ 62 | file1Name: file1Content, 63 | file2Name: file2Content, 64 | } 65 | 66 | tests := []struct { 67 | name string 68 | filePaths []string 69 | wantData map[string]string 70 | wantErr bool 71 | }{ 72 | { 73 | name: "missing first log file", 74 | filePaths: []string{"/var/log/missing.log", file2Name}, 75 | wantData: nil, 76 | wantErr: true, 77 | }, 78 | { 79 | name: "missing second log file", 80 | filePaths: []string{file1Name, "/var/log/missing.log"}, 81 | wantData: nil, 82 | wantErr: true, 83 | }, 84 | { 85 | name: "all log files exist", 86 | filePaths: []string{file1Name, file2Name}, 87 | wantData: map[string]string{ 88 | file1ExpectedKey: file1Content, 89 | file2ExpectedKey: file2Content, 90 | }, 91 | wantErr: false, 92 | }, 93 | } 94 | 95 | fs := test.NewFakeFileSystem(testLogFiles) 96 | 97 | for _, tt := range tests { 98 | t.Run(tt.name, func(t *testing.T) { 99 | runtimeInfo := &utils.RuntimeInfo{ 100 | NodeLogs: []string{file1Name, file2Name}, 101 | CollectorList: []string{}, 102 | } 103 | c := NewNodeLogsCollector(runtimeInfo, fs) 104 | err := c.Collect() 105 | 106 | if err != nil { 107 | if !tt.wantErr { 108 | t.Errorf("Collect() error = %v, wantErr %v", err, tt.wantErr) 109 | } 110 | } else { 111 | dataItems := c.GetData() 112 | for key, expectedValue := range tt.wantData { 113 | result, ok := dataItems[key] 114 | if !ok { 115 | t.Errorf("missing key %s", key) 116 | } 117 | 118 | testDataValue(t, result, func(actualValue string) { 119 | if actualValue != expectedValue { 120 | t.Errorf("unexpected value for key %s.\nExpected '%s'\nFound '%s'", key, expectedValue, actualValue) 121 | } 122 | }) 123 | } 124 | } 125 | }) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /pkg/collector/pdb_collector.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/Azure/aks-periscope/pkg/interfaces" 10 | "github.com/Azure/aks-periscope/pkg/utils" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | "k8s.io/client-go/kubernetes" 13 | restclient "k8s.io/client-go/rest" 14 | ) 15 | 16 | type PDBInfo struct { 17 | Name string `json:"name"` 18 | MinAvailable string `json:"minavailable"` 19 | MaxUnavailable string `json:"maxunavailable"` 20 | DisruptionsAllowed int32 `json:"disruptionsallowed"` 21 | } 22 | 23 | // PDBCollector defines a Pod disruption Budget Collector struct 24 | type PDBCollector struct { 25 | data map[string]string 26 | kubeconfig *restclient.Config 27 | runtimeInfo *utils.RuntimeInfo 28 | } 29 | 30 | // NewPDBCollector is a constructor 31 | func NewPDBCollector(config *restclient.Config, runtimeInfo *utils.RuntimeInfo) *PDBCollector { 32 | return &PDBCollector{ 33 | data: make(map[string]string), 34 | kubeconfig: config, 35 | runtimeInfo: runtimeInfo, 36 | } 37 | } 38 | 39 | func (collector *PDBCollector) GetName() string { 40 | return "poddisruptionbudget" 41 | } 42 | 43 | func (collector *PDBCollector) CheckSupported() error { 44 | if utils.Contains(collector.runtimeInfo.CollectorList, "connectedCluster") { 45 | return fmt.Errorf("not included because 'connectedCluster' is in COLLECTOR_LIST variable. Included values: %s", strings.Join(collector.runtimeInfo.CollectorList, " ")) 46 | } 47 | 48 | return nil 49 | } 50 | 51 | // Collect implements the interface method 52 | func (collector *PDBCollector) Collect() error { 53 | // Creates the clientset 54 | clientset, err := kubernetes.NewForConfig(collector.kubeconfig) 55 | if err != nil { 56 | return fmt.Errorf("getting access to K8S failed: %w", err) 57 | } 58 | 59 | ctxBackground := context.Background() 60 | 61 | namespacesList, err := clientset.CoreV1().Namespaces().List(ctxBackground, metav1.ListOptions{}) 62 | if err != nil { 63 | return fmt.Errorf("unable to list namespaces in the cluster: %w", err) 64 | } 65 | 66 | for _, namespace := range namespacesList.Items { 67 | podDistInterface, err := clientset.PolicyV1().PodDisruptionBudgets(namespace.Name).List(ctxBackground, metav1.ListOptions{}) 68 | if err != nil { 69 | return fmt.Errorf("listing PDB error: %w", err) 70 | } 71 | 72 | pdbresult := make([]PDBInfo, 0) 73 | for _, i := range podDistInterface.Items { 74 | pdbinfo := PDBInfo{ 75 | Name: i.Name, 76 | MinAvailable: i.Spec.MinAvailable.String(), 77 | MaxUnavailable: i.Spec.MaxUnavailable.String(), 78 | DisruptionsAllowed: i.Status.DisruptionsAllowed, 79 | } 80 | pdbresult = append(pdbresult, pdbinfo) 81 | } 82 | 83 | data, err := json.Marshal(pdbresult) 84 | 85 | if err != nil { 86 | return fmt.Errorf("marshall PDB to json: %w", err) 87 | } 88 | collector.data["pdb-"+namespace.Name] = string(data) 89 | } 90 | 91 | return nil 92 | } 93 | 94 | func (collector *PDBCollector) GetData() map[string]interfaces.DataValue { 95 | return utils.ToDataValueMap(collector.data) 96 | } 97 | -------------------------------------------------------------------------------- /pkg/collector/pdb_collector_test.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/Azure/aks-periscope/pkg/test" 7 | "github.com/Azure/aks-periscope/pkg/utils" 8 | ) 9 | 10 | func TestPDBCollectorGetName(t *testing.T) { 11 | const expectedName = "poddisruptionbudget" 12 | 13 | c := NewPDBCollector(nil, nil) 14 | actualName := c.GetName() 15 | if actualName != expectedName { 16 | t.Errorf("unexpected name: expected %s, found %s", expectedName, actualName) 17 | } 18 | } 19 | 20 | func TestPDBCollectorCheckSupported(t *testing.T) { 21 | tests := []struct { 22 | name string 23 | collectorList []string 24 | wantErr bool 25 | }{ 26 | { 27 | name: "'connectedCluster' in COLLECTOR_LIST", 28 | collectorList: []string{"connectedCluster"}, 29 | wantErr: true, 30 | }, 31 | { 32 | name: "'connectedCluster' not in COLLECTOR_LIST", 33 | collectorList: []string{}, 34 | wantErr: false, 35 | }, 36 | } 37 | 38 | for _, tt := range tests { 39 | runtimeInfo := &utils.RuntimeInfo{ 40 | CollectorList: tt.collectorList, 41 | } 42 | c := NewPDBCollector(nil, runtimeInfo) 43 | err := c.CheckSupported() 44 | if (err != nil) != tt.wantErr { 45 | t.Errorf("%s error = %v, wantErr %v", tt.name, err, tt.wantErr) 46 | } 47 | } 48 | } 49 | 50 | func TestPDBCollectorCollect(t *testing.T) { 51 | tests := []struct { 52 | name string 53 | want int 54 | wantErr bool 55 | }{ 56 | { 57 | name: "get pdb information for logs", 58 | want: 1, 59 | wantErr: false, 60 | }, 61 | } 62 | 63 | fixture, _ := test.GetClusterFixture() 64 | 65 | runtimeInfo := &utils.RuntimeInfo{ 66 | CollectorList: []string{}, 67 | } 68 | 69 | c := NewPDBCollector(fixture.PeriscopeAccess.ClientConfig, runtimeInfo) 70 | 71 | for _, tt := range tests { 72 | t.Run(tt.name, func(t *testing.T) { 73 | err := c.Collect() 74 | 75 | if (err != nil) != tt.wantErr { 76 | t.Errorf("Collect() error = %v, wantErr %v", err, tt.wantErr) 77 | } 78 | raw := c.GetData() 79 | 80 | if len(raw) < tt.want { 81 | t.Errorf("len(GetData()) = %v, want %v", len(raw), tt.want) 82 | } 83 | }) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /pkg/collector/pods_containerlogs_collector.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "strings" 10 | "time" 11 | 12 | "github.com/Azure/aks-periscope/pkg/interfaces" 13 | "github.com/Azure/aks-periscope/pkg/utils" 14 | v1 "k8s.io/api/core/v1" 15 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 16 | "k8s.io/client-go/kubernetes" 17 | restclient "k8s.io/client-go/rest" 18 | ) 19 | 20 | // PodsContainerLogsCollector defines a Pods Container Logs Collector struct 21 | type PodsContainerLogsCollector struct { 22 | data map[string]string 23 | kubeconfig *restclient.Config 24 | runtimeInfo *utils.RuntimeInfo 25 | } 26 | 27 | type PodsContainerStruct struct { 28 | Name string `json:"name"` 29 | Ready string `json:"ready"` 30 | Status string `json:"status"` 31 | Restart int32 `json:"restart"` 32 | Age time.Duration `json:"age"` 33 | ContainerName string `json:"containerName"` 34 | ContainerLog string `json:"containerLog"` 35 | } 36 | 37 | // NewPodsContainerLogs is a constructor 38 | func NewPodsContainerLogsCollector(config *restclient.Config, runtimeInfo *utils.RuntimeInfo) *PodsContainerLogsCollector { 39 | return &PodsContainerLogsCollector{ 40 | data: make(map[string]string), 41 | kubeconfig: config, 42 | runtimeInfo: runtimeInfo, 43 | } 44 | } 45 | 46 | func (collector *PodsContainerLogsCollector) GetName() string { 47 | return "podscontainerlogs" 48 | } 49 | 50 | func (collector *PodsContainerLogsCollector) CheckSupported() error { 51 | if !utils.Contains(collector.runtimeInfo.CollectorList, "connectedCluster") { 52 | return fmt.Errorf("not included because 'connectedCluster' not in COLLECTOR_LIST variable. Included values: %s", strings.Join(collector.runtimeInfo.CollectorList, " ")) 53 | } 54 | 55 | return nil 56 | } 57 | 58 | // Collect implements the interface method 59 | func (collector *PodsContainerLogsCollector) Collect() error { 60 | // Creates the clientset 61 | clientset, err := kubernetes.NewForConfig(collector.kubeconfig) 62 | if err != nil { 63 | return fmt.Errorf("getting access to K8S failed: %w", err) 64 | } 65 | 66 | for _, namespace := range collector.runtimeInfo.ContainerLogsNamespaces { 67 | // List the pods in the given namespace 68 | podList, err := clientset.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{}) 69 | 70 | if err != nil { 71 | return fmt.Errorf("getting pods failed: %w", err) 72 | } 73 | 74 | // List all the pods similar to kubectl get pods -n 75 | for _, pod := range podList.Items { 76 | // Calculate the age of the pod 77 | podCreationTime := pod.GetCreationTimestamp() 78 | age := time.Since(podCreationTime.Time).Round(time.Second) 79 | 80 | // Get the status of each of the pods 81 | podStatus := pod.Status 82 | 83 | var containerRestarts int32 84 | var containerReady int 85 | 86 | // If a pod has multiple containers, get the status from all 87 | for container := range pod.Spec.Containers { 88 | containerRestarts += podStatus.ContainerStatuses[container].RestartCount 89 | 90 | if podStatus.ContainerStatuses[container].Ready { 91 | containerReady++ 92 | } 93 | } 94 | for _, containerItem := range pod.Spec.Containers { 95 | containerName := containerItem.Name 96 | // Get pods container logs 97 | containerLogs, err := getPodContainerLogs(namespace, pod.Name, containerName, clientset) 98 | 99 | if err != nil { 100 | return fmt.Errorf("getting container logs failed: %w", err) 101 | } 102 | 103 | podsContainerData := &PodsContainerStruct{ 104 | Name: pod.Name, 105 | Ready: fmt.Sprintf("%v/%v", containerReady, len(pod.Spec.Containers)), 106 | Status: string(podStatus.Phase), 107 | Restart: containerRestarts, 108 | Age: age, 109 | ContainerName: containerName, 110 | ContainerLog: containerLogs, 111 | } 112 | 113 | data, err := json.Marshal(podsContainerData) 114 | if err != nil { 115 | return fmt.Errorf("marshalling podsContainerData: %w", err) 116 | } 117 | 118 | // Append this to data to be printed in a table 119 | collector.data[pod.Name+"-"+containerName] = string(data) 120 | } 121 | } 122 | } 123 | 124 | return nil 125 | } 126 | 127 | func (collector *PodsContainerLogsCollector) GetData() map[string]interfaces.DataValue { 128 | return utils.ToDataValueMap(collector.data) 129 | } 130 | 131 | func getPodContainerLogs( 132 | namespace string, 133 | podName string, 134 | containerName string, 135 | clientset *kubernetes.Clientset) (string, error) { 136 | 137 | count := int64(100) 138 | podLogOptions := v1.PodLogOptions{ 139 | Container: containerName, 140 | TailLines: &count, 141 | } 142 | 143 | podLogRequest := clientset.CoreV1(). 144 | Pods(namespace). 145 | GetLogs(podName, &podLogOptions) 146 | stream, err := podLogRequest.Stream(context.Background()) 147 | 148 | if err != nil { 149 | return "", fmt.Errorf("getting pod logs request failed: %w", err) 150 | } 151 | defer stream.Close() 152 | returnData := "" 153 | 154 | buf := new(bytes.Buffer) 155 | _, err = io.Copy(buf, stream) 156 | 157 | if err != nil { 158 | return "", fmt.Errorf("pod logs stream read failure: %w", err) 159 | } 160 | 161 | returnData = buf.String() 162 | 163 | return returnData, err 164 | } 165 | -------------------------------------------------------------------------------- /pkg/collector/pods_containerlogs_collector_test.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/Azure/aks-periscope/pkg/test" 7 | "github.com/Azure/aks-periscope/pkg/utils" 8 | ) 9 | 10 | func TestPodsContainerLogsCollectorGetName(t *testing.T) { 11 | const expectedName = "podscontainerlogs" 12 | 13 | c := NewPodsContainerLogsCollector(nil, nil) 14 | actualName := c.GetName() 15 | if actualName != expectedName { 16 | t.Errorf("unexpected name: expected %s, found %s", expectedName, actualName) 17 | } 18 | } 19 | 20 | func TestPodsContainerLogsCollectorCheckSupported(t *testing.T) { 21 | tests := []struct { 22 | name string 23 | collectorList []string 24 | wantErr bool 25 | }{ 26 | { 27 | name: "'connectedCluster' in COLLECTOR_LIST", 28 | collectorList: []string{"connectedCluster"}, 29 | wantErr: false, 30 | }, 31 | { 32 | name: "'connectedCluster' not in COLLECTOR_LIST", 33 | collectorList: []string{}, 34 | wantErr: true, 35 | }, 36 | } 37 | 38 | for _, tt := range tests { 39 | runtimeInfo := &utils.RuntimeInfo{ 40 | CollectorList: tt.collectorList, 41 | } 42 | c := NewPodsContainerLogsCollector(nil, runtimeInfo) 43 | err := c.CheckSupported() 44 | if (err != nil) != tt.wantErr { 45 | t.Errorf("%s error = %v, wantErr %v", tt.name, err, tt.wantErr) 46 | } 47 | } 48 | } 49 | 50 | func TestPodsContainerLogsCollectorCollect(t *testing.T) { 51 | tests := []struct { 52 | name string 53 | want int 54 | wantErr bool 55 | }{ 56 | { 57 | name: "get pods container logs", 58 | want: 1, 59 | wantErr: false, 60 | }, 61 | } 62 | 63 | fixture, _ := test.GetClusterFixture() 64 | 65 | runtimeInfo := &utils.RuntimeInfo{ 66 | ContainerLogsNamespaces: []string{"kube-system"}, 67 | } 68 | c := NewPodsContainerLogsCollector(fixture.PeriscopeAccess.ClientConfig, runtimeInfo) 69 | 70 | for _, tt := range tests { 71 | t.Run(tt.name, func(t *testing.T) { 72 | err := c.Collect() 73 | 74 | if (err != nil) != tt.wantErr { 75 | t.Errorf("Collect() error = %v, wantErr %v", err, tt.wantErr) 76 | } 77 | raw := c.GetData() 78 | 79 | if len(raw) < tt.want { 80 | t.Errorf("len(GetData()) = %v, want %v", len(raw), tt.want) 81 | } 82 | }) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /pkg/collector/shared_test.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "os" 7 | "regexp" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/Azure/aks-periscope/pkg/interfaces" 12 | "github.com/Azure/aks-periscope/pkg/test" 13 | "github.com/Azure/aks-periscope/pkg/utils" 14 | ) 15 | 16 | // TestMain coordinates the execution of all tests in the package. This is required because they all share 17 | // common initialization and cleanup code. 18 | func TestMain(m *testing.M) { 19 | fixture, err := test.GetClusterFixture() 20 | if err != nil { 21 | // Initialization failed, so clean up and exit before even running tests. 22 | fixture.Cleanup() 23 | log.Fatalf("Error initializing tests: %v", err) 24 | } 25 | code := runTests(m, fixture) 26 | os.Exit(code) 27 | } 28 | 29 | func runTests(m *testing.M, fixture *test.ClusterFixture) int { 30 | // Always clean up after running all the tests. This is not strictly necessary, 31 | // but helps ensure a clean test cluster for subsequent local test runs. 32 | defer fixture.Cleanup() 33 | 34 | // Run all the tests in the package. 35 | code := m.Run() 36 | if code != 0 { 37 | // Output some informtation that may help diagnose test failures. 38 | fixture.PrintDiagnostics() 39 | } 40 | 41 | // Check our tests haven't resulted in any unexpected Docker image usage 42 | err := fixture.CheckDockerImages() 43 | if err != nil { 44 | // Fail the test run (even if the actual tests passed) to avoid merging code that 45 | // pulls images during tests. 46 | log.Printf("Failing due to unexpected Docker image usage (see test.dockerImageManager): %v", err) 47 | code = 1 48 | } 49 | 50 | return code 51 | } 52 | 53 | func testDataValue(t *testing.T, dataValue interfaces.DataValue, test func(string)) { 54 | value, err := utils.GetContent(func() (io.ReadCloser, error) { return dataValue.GetReader() }) 55 | if err != nil { 56 | t.Errorf("error reading value: %v", err) 57 | } 58 | test(value) 59 | } 60 | 61 | func compareCollectorData(t *testing.T, expectedData map[string]*regexp.Regexp, actualData map[string]interfaces.DataValue) { 62 | missingDataKeys := []string{} 63 | for key, regexp := range expectedData { 64 | dataValue, ok := actualData[key] 65 | if ok { 66 | testDataValue(t, dataValue, func(value string) { 67 | if !regexp.MatchString(value) { 68 | t.Errorf("unexpected value for %s\n\texpected: %s\n\tfound: %s", key, regexp.String(), value) 69 | } 70 | }) 71 | } else { 72 | missingDataKeys = append(missingDataKeys, key) 73 | } 74 | } 75 | if len(missingDataKeys) > 0 { 76 | t.Errorf("missing keys in actual data:\n%s", strings.Join(missingDataKeys, "\n")) 77 | } 78 | 79 | unexpectedDataKeys := []string{} 80 | for key := range actualData { 81 | if _, ok := expectedData[key]; !ok { 82 | unexpectedDataKeys = append(unexpectedDataKeys, key) 83 | } 84 | } 85 | if len(unexpectedDataKeys) > 0 { 86 | t.Errorf("unexpected keys in actual data:\n%s", strings.Join(unexpectedDataKeys, "\n")) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /pkg/collector/smi_collector.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/Azure/aks-periscope/pkg/interfaces" 8 | "github.com/Azure/aks-periscope/pkg/utils" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 11 | "k8s.io/apimachinery/pkg/runtime/schema" 12 | "k8s.io/client-go/rest" 13 | ) 14 | 15 | // SmiCollector defines an Smi Collector struct 16 | type SmiCollector struct { 17 | data map[string]string 18 | kubeconfig *rest.Config 19 | commandRunner *utils.KubeCommandRunner 20 | runtimeInfo *utils.RuntimeInfo 21 | } 22 | 23 | // NewSmiCollector is a constructor 24 | func NewSmiCollector(config *rest.Config, runtimeInfo *utils.RuntimeInfo) *SmiCollector { 25 | return &SmiCollector{ 26 | data: make(map[string]string), 27 | kubeconfig: config, 28 | commandRunner: utils.NewKubeCommandRunner(config), 29 | runtimeInfo: runtimeInfo, 30 | } 31 | } 32 | 33 | func (collector *SmiCollector) GetName() string { 34 | return "smi" 35 | } 36 | 37 | func (collector *SmiCollector) CheckSupported() error { 38 | if !utils.Contains(collector.runtimeInfo.CollectorList, "OSM") && !utils.Contains(collector.runtimeInfo.CollectorList, "SMI") { 39 | return fmt.Errorf("not included because neither 'OSM' or 'SMI' are in COLLECTOR_LIST variable. Included values: %s", strings.Join(collector.runtimeInfo.CollectorList, " ")) 40 | } 41 | 42 | return nil 43 | } 44 | 45 | func (collector *SmiCollector) GetData() map[string]interfaces.DataValue { 46 | return utils.ToDataValueMap(collector.data) 47 | } 48 | 49 | // Collect implements the interface method 50 | func (collector *SmiCollector) Collect() error { 51 | smiCrds, err := collector.getAllSmiCrds() 52 | if err != nil { 53 | return fmt.Errorf("error getting SMI CRDs: %w", err) 54 | } 55 | 56 | // Store the CRD definitions as collector data 57 | for _, smiCrd := range smiCrds { 58 | trimmedName := strings.TrimSuffix(smiCrd.GetName(), ".io") 59 | yaml, err := collector.commandRunner.PrintAsYaml(&smiCrd) 60 | if err != nil { 61 | return fmt.Errorf("error printing CRD %s as YAML: %w", trimmedName, err) 62 | } 63 | key := fmt.Sprintf("smi/crd_%s", trimmedName) 64 | collector.data[key] = yaml 65 | } 66 | 67 | // Get the GroupVersionResource identifiers for all the resources for these CRDs 68 | gvrs := []schema.GroupVersionResource{} 69 | for _, smiCrd := range smiCrds { 70 | gvr, err := collector.commandRunner.GetGVRFromCRD(&smiCrd) 71 | if err != nil { 72 | return fmt.Errorf("error getting GVR from CRD %s: %+v", smiCrd.GetName(), err) 73 | } 74 | gvrs = append(gvrs, *gvr) 75 | } 76 | 77 | // Get the resources in all the namespaces for all possible versions of all the CRDs. 78 | smiResources, err := collector.getSmiCustomResourcesFromAllNamespaces(gvrs) 79 | if err != nil { 80 | return fmt.Errorf("error getting custom SMI resources for all namespaces: %w", err) 81 | } 82 | 83 | // Store the resource definitions as collector data 84 | for _, resource := range smiResources { 85 | crdName := resource.GroupResource().String() // e.g. "traffictargets.access.smi-spec.io" 86 | key := fmt.Sprintf("smi/namespace_%s/%s_%s_custom_resource", resource.namespace, crdName, resource.name) 87 | collector.data[key] = resource.yaml 88 | } 89 | 90 | return nil 91 | } 92 | 93 | type smiResource struct { 94 | namespace string 95 | schema.GroupVersionResource 96 | name string 97 | yaml string 98 | } 99 | 100 | func (collector *SmiCollector) getAllSmiCrds() ([]unstructured.Unstructured, error) { 101 | // Get all the CRDs in the cluster (we'll filter them according to a pattern, so can't retrieve them by name). 102 | crds, err := collector.commandRunner.GetCRDUnstructuredList() 103 | if err != nil { 104 | return nil, fmt.Errorf("error listing CRDs in cluster") 105 | } 106 | 107 | results := []unstructured.Unstructured{} 108 | for _, crd := range crds.Items { 109 | if strings.Contains(crd.GetName(), "smi-spec.io") { 110 | results = append(results, crd) 111 | } 112 | } 113 | 114 | return results, nil 115 | } 116 | 117 | func (collector *SmiCollector) getSmiCustomResourcesFromAllNamespaces(gvrs []schema.GroupVersionResource) ([]smiResource, error) { 118 | result := []smiResource{} 119 | for _, gvr := range gvrs { 120 | // Find resources in all namespaces 121 | resources, err := collector.commandRunner.GetUnstructuredList(&gvr, "", &metav1.ListOptions{}) 122 | if err != nil { 123 | return nil, fmt.Errorf("error listing %s resources: %w", gvr.String(), err) 124 | } 125 | for _, resource := range resources.Items { 126 | yaml, err := collector.commandRunner.PrintAsYaml(&resource) 127 | if err != nil { 128 | return nil, fmt.Errorf("error getting yaml for %s: %w", resource.GetName(), err) 129 | } 130 | result = append(result, smiResource{ 131 | namespace: resource.GetNamespace(), 132 | GroupVersionResource: gvr, 133 | name: resource.GetName(), 134 | yaml: yaml, 135 | }) 136 | } 137 | } 138 | 139 | return result, nil 140 | } 141 | -------------------------------------------------------------------------------- /pkg/collector/systemlogs_collector.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/Azure/aks-periscope/pkg/interfaces" 8 | "github.com/Azure/aks-periscope/pkg/utils" 9 | ) 10 | 11 | // SystemLogsCollector defines a SystemLogs Collector struct 12 | type SystemLogsCollector struct { 13 | data map[string]string 14 | osIdentifier utils.OSIdentifier 15 | runtimeInfo *utils.RuntimeInfo 16 | } 17 | 18 | // NewSystemLogsCollector is a constructor 19 | func NewSystemLogsCollector(osIdentifier utils.OSIdentifier, runtimeInfo *utils.RuntimeInfo) *SystemLogsCollector { 20 | return &SystemLogsCollector{ 21 | data: make(map[string]string), 22 | osIdentifier: osIdentifier, 23 | runtimeInfo: runtimeInfo, 24 | } 25 | } 26 | 27 | func (collector *SystemLogsCollector) GetName() string { 28 | return "systemlogs" 29 | } 30 | 31 | func (collector *SystemLogsCollector) CheckSupported() error { 32 | // This uses `journalctl` to retrieve system logs, which is not available on Windows. 33 | // It may be possible in future to identify useful Windows log files and configure this to 34 | // output those. 35 | if collector.osIdentifier != utils.Linux { 36 | return fmt.Errorf("unsupported OS: %s", collector.osIdentifier) 37 | } 38 | 39 | if utils.Contains(collector.runtimeInfo.CollectorList, "connectedCluster") { 40 | return fmt.Errorf("not included because 'connectedCluster' is in COLLECTOR_LIST variable. Included values: %s", strings.Join(collector.runtimeInfo.CollectorList, " ")) 41 | } 42 | 43 | return nil 44 | } 45 | 46 | // Collect implements the interface method 47 | func (collector *SystemLogsCollector) Collect() error { 48 | systemServices := []string{"docker", "kubelet"} 49 | 50 | for _, systemService := range systemServices { 51 | output, err := utils.RunCommandOnHost("journalctl", "-u", systemService) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | collector.data[systemService] = output 57 | } 58 | 59 | return nil 60 | } 61 | 62 | func (collector *SystemLogsCollector) GetData() map[string]interfaces.DataValue { 63 | return utils.ToDataValueMap(collector.data) 64 | } 65 | -------------------------------------------------------------------------------- /pkg/collector/systemlogs_collector_test.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/Azure/aks-periscope/pkg/utils" 7 | ) 8 | 9 | func TestSystemLogsCollectorGetName(t *testing.T) { 10 | const expectedName = "systemlogs" 11 | 12 | c := NewSystemLogsCollector("", nil) 13 | actualName := c.GetName() 14 | if actualName != expectedName { 15 | t.Errorf("unexpected name: expected %s, found %s", expectedName, actualName) 16 | } 17 | } 18 | 19 | func TestSystemLogsCollectorCheckSupported(t *testing.T) { 20 | tests := []struct { 21 | name string 22 | osIdentifier utils.OSIdentifier 23 | collectorList []string 24 | wantErr bool 25 | }{ 26 | { 27 | name: "windows", 28 | osIdentifier: utils.Windows, 29 | collectorList: []string{"connectedCluster"}, 30 | wantErr: true, 31 | }, 32 | { 33 | name: "'connectedCluster' in COLLECTOR_LIST", 34 | osIdentifier: utils.Linux, 35 | collectorList: []string{"connectedCluster"}, 36 | wantErr: true, 37 | }, 38 | { 39 | name: "'connectedCluster' not in COLLECTOR_LIST", 40 | osIdentifier: utils.Linux, 41 | collectorList: []string{}, 42 | wantErr: false, 43 | }, 44 | } 45 | 46 | for _, tt := range tests { 47 | runtimeInfo := &utils.RuntimeInfo{ 48 | CollectorList: tt.collectorList, 49 | } 50 | c := NewSystemLogsCollector(tt.osIdentifier, runtimeInfo) 51 | err := c.CheckSupported() 52 | if (err != nil) != tt.wantErr { 53 | t.Errorf("%s error = %v, wantErr %v", tt.name, err, tt.wantErr) 54 | } 55 | } 56 | } 57 | 58 | func TestSystemLogsCollectorCollect(t *testing.T) { 59 | tests := []struct { 60 | name string 61 | want int 62 | wantErr bool 63 | }{ 64 | { 65 | name: "get system logs", 66 | want: 1, 67 | wantErr: true, 68 | }, 69 | } 70 | 71 | runtimeInfo := &utils.RuntimeInfo{ 72 | CollectorList: []string{}, 73 | } 74 | 75 | c := NewSystemLogsCollector(utils.Linux, runtimeInfo) 76 | 77 | for _, tt := range tests { 78 | t.Run(tt.name, func(t *testing.T) { 79 | err := c.Collect() 80 | if (err != nil) != tt.wantErr { 81 | t.Errorf("Collect() error = %v, wantErr %v", err, tt.wantErr) 82 | } 83 | }) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /pkg/collector/systemperf_collector.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/Azure/aks-periscope/pkg/interfaces" 10 | "github.com/Azure/aks-periscope/pkg/utils" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | restclient "k8s.io/client-go/rest" 13 | metrics "k8s.io/metrics/pkg/client/clientset/versioned" 14 | ) 15 | 16 | // SystemPerfCollector defines a SystemPerf Collector struct 17 | type SystemPerfCollector struct { 18 | data map[string]string 19 | kubeconfig *restclient.Config 20 | runtimeInfo *utils.RuntimeInfo 21 | } 22 | 23 | type NodeMetrics struct { 24 | NodeName string `json:"name"` 25 | CPUUsage int64 `json:"cpuUsage"` 26 | MemoryUsage int64 `json:"memoryUsage"` 27 | } 28 | 29 | type PodMetrics struct { 30 | ContainerName string `json:"name"` 31 | CPUUsage int64 `json:"cpuUsage"` 32 | MemoryUsage int64 `json:"memoryUsage"` 33 | } 34 | 35 | // NewSystemPerfCollector is a constructor 36 | func NewSystemPerfCollector(config *restclient.Config, runtimeInfo *utils.RuntimeInfo) *SystemPerfCollector { 37 | return &SystemPerfCollector{ 38 | data: make(map[string]string), 39 | kubeconfig: config, 40 | runtimeInfo: runtimeInfo, 41 | } 42 | } 43 | 44 | func (collector *SystemPerfCollector) GetName() string { 45 | return "systemperf" 46 | } 47 | 48 | func (collector *SystemPerfCollector) CheckSupported() error { 49 | if utils.Contains(collector.runtimeInfo.CollectorList, "connectedCluster") { 50 | return fmt.Errorf("not included because 'connectedCluster' is in COLLECTOR_LIST variable. Included values: %s", strings.Join(collector.runtimeInfo.CollectorList, " ")) 51 | } 52 | 53 | return nil 54 | } 55 | 56 | // Collect implements the interface method 57 | func (collector *SystemPerfCollector) Collect() error { 58 | metric, err := metrics.NewForConfig(collector.kubeconfig) 59 | if err != nil { 60 | return fmt.Errorf("metrics for config error: %w", err) 61 | } 62 | 63 | nodeMetrics, err := metric.MetricsV1beta1().NodeMetricses().List(context.TODO(), metav1.ListOptions{}) 64 | if err != nil { 65 | return fmt.Errorf("node metrics error: %w", err) 66 | } 67 | 68 | noderesult := make([]NodeMetrics, 0) 69 | 70 | for _, nodeMetric := range nodeMetrics.Items { 71 | cpuQuantity := nodeMetric.Usage.Cpu().MilliValue() 72 | memQuantity, ok := nodeMetric.Usage.Memory().AsInt64() 73 | if !ok { 74 | return err 75 | } 76 | 77 | nm := NodeMetrics{ 78 | NodeName: nodeMetric.Name, 79 | CPUUsage: cpuQuantity, 80 | MemoryUsage: memQuantity, 81 | } 82 | 83 | noderesult = append(noderesult, nm) 84 | } 85 | jsonNodeResult, err := json.Marshal(noderesult) 86 | if err != nil { 87 | return fmt.Errorf("marshall node metrics to json: %w", err) 88 | } 89 | 90 | collector.data["nodes"] = string(jsonNodeResult) 91 | 92 | podMetrics, err := metric.MetricsV1beta1().PodMetricses(metav1.NamespaceAll).List(context.TODO(), metav1.ListOptions{}) 93 | if err != nil { 94 | return fmt.Errorf("pod metrics failure: %w", err) 95 | } 96 | 97 | podresult := make([]PodMetrics, 0) 98 | 99 | for _, podMetric := range podMetrics.Items { 100 | podContainers := podMetric.Containers 101 | for _, container := range podContainers { 102 | cpuQuantity := container.Usage.Cpu().MilliValue() 103 | memQuantity, ok := container.Usage.Memory().AsInt64() 104 | if !ok { 105 | return fmt.Errorf("usage memory failure: %w", err) 106 | } 107 | 108 | pm := PodMetrics{ 109 | ContainerName: container.Name, 110 | CPUUsage: cpuQuantity, 111 | MemoryUsage: memQuantity, 112 | } 113 | 114 | podresult = append(podresult, pm) 115 | } 116 | } 117 | jsonPodResult, err := json.Marshal(podresult) 118 | if err != nil { 119 | return fmt.Errorf("marshall pod metrics to json: %w", err) 120 | } 121 | 122 | collector.data["pods"] = string(jsonPodResult) 123 | 124 | return nil 125 | } 126 | 127 | func (collector *SystemPerfCollector) GetData() map[string]interfaces.DataValue { 128 | return utils.ToDataValueMap(collector.data) 129 | } 130 | -------------------------------------------------------------------------------- /pkg/collector/systemperf_collector_test.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/Azure/aks-periscope/pkg/test" 8 | "github.com/Azure/aks-periscope/pkg/utils" 9 | ) 10 | 11 | func TestSystemPerfCollectorGetName(t *testing.T) { 12 | const expectedName = "systemperf" 13 | 14 | c := NewSystemPerfCollector(nil, nil) 15 | actualName := c.GetName() 16 | if actualName != expectedName { 17 | t.Errorf("unexpected name: expected %s, found %s", expectedName, actualName) 18 | } 19 | } 20 | 21 | func TestSystemPerfCollectorCheckSupported(t *testing.T) { 22 | tests := []struct { 23 | name string 24 | collectorList []string 25 | wantErr bool 26 | }{ 27 | { 28 | name: "'connectedCluster' in COLLECTOR_LIST", 29 | collectorList: []string{"connectedCluster"}, 30 | wantErr: true, 31 | }, 32 | { 33 | name: "'connectedCluster' not in COLLECTOR_LIST", 34 | collectorList: []string{}, 35 | wantErr: false, 36 | }, 37 | } 38 | 39 | for _, tt := range tests { 40 | runtimeInfo := &utils.RuntimeInfo{ 41 | CollectorList: tt.collectorList, 42 | } 43 | c := NewSystemPerfCollector(nil, runtimeInfo) 44 | err := c.CheckSupported() 45 | if (err != nil) != tt.wantErr { 46 | t.Errorf("%s error = %v, wantErr %v", tt.name, err, tt.wantErr) 47 | } 48 | } 49 | } 50 | 51 | func TestSystemPerfCollectorCollect(t *testing.T) { 52 | tests := []struct { 53 | name string 54 | want int 55 | wantErr bool 56 | }{ 57 | { 58 | name: "get node logs", 59 | want: 1, 60 | wantErr: false, 61 | }, 62 | } 63 | 64 | fixture, _ := test.GetClusterFixture() 65 | 66 | runtimeInfo := &utils.RuntimeInfo{ 67 | CollectorList: []string{}, 68 | } 69 | 70 | c := NewSystemPerfCollector(fixture.PeriscopeAccess.ClientConfig, runtimeInfo) 71 | 72 | for _, tt := range tests { 73 | t.Run(tt.name, func(t *testing.T) { 74 | err := c.Collect() 75 | if (err != nil) != tt.wantErr { 76 | t.Errorf("Collect() error = %v, wantErr %v", err, tt.wantErr) 77 | } 78 | 79 | result := c.GetData()["nodes"] 80 | testDataValue(t, result, func(raw string) { 81 | var nodeMetrices []NodeMetrics 82 | 83 | if err := json.Unmarshal([]byte(raw), &nodeMetrices); err != nil { 84 | t.Errorf("unmarshal GetData(): %v", err) 85 | } 86 | 87 | if len(nodeMetrices) < tt.want { 88 | t.Errorf("len(GetData()) = %v, want %v", len(nodeMetrices), tt.want) 89 | } 90 | }) 91 | }) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /pkg/collector/windowslogs_collector.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "path" 8 | "strings" 9 | "time" 10 | 11 | "github.com/Azure/aks-periscope/pkg/interfaces" 12 | "github.com/Azure/aks-periscope/pkg/utils" 13 | "k8s.io/apimachinery/pkg/util/wait" 14 | ) 15 | 16 | const windowsLogsCollectorPrefix = "collect-windows-logs/" 17 | 18 | type WindowsLogsCollector struct { 19 | data map[string]interfaces.DataValue 20 | osIdentifier utils.OSIdentifier 21 | runtimeInfo *utils.RuntimeInfo 22 | filePaths *utils.KnownFilePaths 23 | fileSystem interfaces.FileSystemAccessor 24 | pollInterval time.Duration 25 | timeout time.Duration 26 | } 27 | 28 | func NewWindowsLogsCollector(osIdentifier utils.OSIdentifier, runtimeInfo *utils.RuntimeInfo, filePaths *utils.KnownFilePaths, fileSystem interfaces.FileSystemAccessor, pollInterval, timeout time.Duration) *WindowsLogsCollector { 29 | return &WindowsLogsCollector{ 30 | data: make(map[string]interfaces.DataValue), 31 | osIdentifier: osIdentifier, 32 | runtimeInfo: runtimeInfo, 33 | filePaths: filePaths, 34 | fileSystem: fileSystem, 35 | pollInterval: pollInterval, 36 | timeout: timeout, 37 | } 38 | } 39 | 40 | func (collector *WindowsLogsCollector) GetName() string { 41 | return "windowslogs" 42 | } 43 | 44 | func (collector *WindowsLogsCollector) CheckSupported() error { 45 | // This is specifically for Windows. 46 | if collector.osIdentifier != utils.Windows { 47 | return fmt.Errorf("unsupported OS: %s", collector.osIdentifier) 48 | } 49 | 50 | // Even for Windows, this is only supported on kubernetes v1.23 or higher. It is up to consumers 51 | // to deploy the resources needed to support this. To ensure consumers have explicitly specified 52 | // this to run, we check for a well-known runtime variable. 53 | if !collector.runtimeInfo.HasFeature(utils.WindowsHpc) { 54 | return fmt.Errorf("feature not set: %s", utils.WindowsHpc) 55 | } 56 | 57 | // This relies on us having a known 'run ID'. 58 | if len(collector.runtimeInfo.RunId) == 0 { 59 | return errors.New("diagnostic run ID not set") 60 | } 61 | 62 | return nil 63 | } 64 | 65 | // Collect implements the interface method 66 | func (collector *WindowsLogsCollector) Collect() error { 67 | // Exporting the logs is done by a separate process, which will place an empty file in a known 68 | // location to indicate completion. The name of that file is the current 'run ID'. 69 | completionNotificationPath := path.Join(collector.filePaths.WindowsLogsOutput, collector.runtimeInfo.RunId) 70 | 71 | // Poll to check existence of this file. 72 | err := wait.PollUntilContextTimeout(context.Background(), collector.pollInterval, collector.timeout, false, 73 | func(context.Context) (bool, error) { 74 | return collector.fileSystem.FileExists(completionNotificationPath) 75 | }) 76 | 77 | if err != nil { 78 | return fmt.Errorf("error waiting for windows log collection: %w", err) 79 | } 80 | 81 | // We should now expect to find a 'logs' directory containing all the logs for this run. 82 | logsDirectory := path.Join(collector.filePaths.WindowsLogsOutput, "logs") 83 | logFilePaths, err := collector.fileSystem.ListFiles(logsDirectory) 84 | if err != nil { 85 | return fmt.Errorf("error listing files in %s: %w", logsDirectory, err) 86 | } 87 | 88 | for _, logFilePath := range logFilePaths { 89 | size, err := collector.fileSystem.GetFileSize(logFilePath) 90 | if err != nil { 91 | return fmt.Errorf("error getting file size %s: %w", logFilePath, err) 92 | } 93 | 94 | relativePath := windowsLogsCollectorPrefix + strings.TrimPrefix(logFilePath, logsDirectory+"/") 95 | collector.data[relativePath] = utils.NewFilePathDataValue(collector.fileSystem, logFilePath, size) 96 | } 97 | 98 | return nil 99 | } 100 | 101 | func (collector *WindowsLogsCollector) GetData() map[string]interfaces.DataValue { 102 | return collector.data 103 | } 104 | -------------------------------------------------------------------------------- /pkg/collector/windowslogs_collector_test.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/Azure/aks-periscope/pkg/test" 9 | "github.com/Azure/aks-periscope/pkg/utils" 10 | ) 11 | 12 | func TestWindowsLogsCollectorGetName(t *testing.T) { 13 | const expectedName = "windowslogs" 14 | 15 | c := NewWindowsLogsCollector("", nil, nil, nil, 0, 0) 16 | actualName := c.GetName() 17 | if actualName != expectedName { 18 | t.Errorf("unexpected name: expected %s, found %s", expectedName, actualName) 19 | } 20 | } 21 | 22 | func TestWindowsLogsCollectorCheckSupported(t *testing.T) { 23 | tests := []struct { 24 | name string 25 | runId string 26 | features []utils.Feature 27 | osIdentifier utils.OSIdentifier 28 | wantErr bool 29 | }{ 30 | { 31 | name: "Run ID not set", 32 | runId: "", 33 | features: []utils.Feature{utils.WindowsHpc}, 34 | osIdentifier: utils.Windows, 35 | wantErr: true, 36 | }, 37 | { 38 | name: "Feature not set", 39 | runId: "this_run", 40 | features: []utils.Feature{}, 41 | osIdentifier: utils.Windows, 42 | wantErr: true, 43 | }, 44 | { 45 | name: "Linux", 46 | runId: "this_run", 47 | features: []utils.Feature{utils.WindowsHpc}, 48 | osIdentifier: utils.Linux, 49 | wantErr: true, 50 | }, 51 | { 52 | name: "Supported", 53 | runId: "this_run", 54 | features: []utils.Feature{utils.WindowsHpc}, 55 | osIdentifier: utils.Windows, 56 | wantErr: false, 57 | }, 58 | } 59 | 60 | for _, tt := range tests { 61 | t.Run(tt.name, func(t *testing.T) { 62 | runtimeInfo := &utils.RuntimeInfo{ 63 | RunId: tt.runId, 64 | Features: map[utils.Feature]bool{}, 65 | } 66 | for _, feature := range tt.features { 67 | runtimeInfo.Features[feature] = true 68 | } 69 | 70 | c := NewWindowsLogsCollector(tt.osIdentifier, runtimeInfo, nil, nil, 0, 0) 71 | err := c.CheckSupported() 72 | if (err != nil) != tt.wantErr { 73 | t.Errorf("CheckSupported() error = %v, wantErr %v", err, tt.wantErr) 74 | } 75 | }) 76 | } 77 | } 78 | 79 | func TestWindowsLogsCollectorCollect(t *testing.T) { 80 | const expectedLogOutput = "log file content" 81 | const runId = "test_run" 82 | notificationPath := fmt.Sprintf("/output/%s", runId) 83 | 84 | tests := []struct { 85 | name string 86 | exportedFiles map[string]string 87 | errorPaths []string 88 | wantErr bool 89 | wantData map[string]string 90 | }{ 91 | { 92 | name: "timeout elapses - no completion notification", 93 | exportedFiles: map[string]string{ 94 | "/output/logs/test.log": expectedLogOutput, 95 | }, 96 | errorPaths: []string{}, 97 | wantErr: true, 98 | wantData: nil, 99 | }, 100 | { 101 | name: "missing logs directory", 102 | exportedFiles: map[string]string{ 103 | notificationPath: "", 104 | "/output/not-in-logs.log": expectedLogOutput, 105 | }, 106 | errorPaths: []string{}, 107 | wantErr: true, 108 | wantData: nil, 109 | }, 110 | { 111 | name: "list files error", 112 | exportedFiles: map[string]string{ 113 | notificationPath: "", 114 | "/output/logs/test.log": expectedLogOutput, 115 | }, 116 | errorPaths: []string{"/output/logs"}, 117 | wantErr: true, 118 | wantData: nil, 119 | }, 120 | { 121 | name: "read log files error", 122 | exportedFiles: map[string]string{ 123 | notificationPath: "", 124 | "/output/logs/test.log": expectedLogOutput, 125 | }, 126 | errorPaths: []string{"/output/logs/test.log"}, 127 | wantErr: true, 128 | wantData: nil, 129 | }, 130 | { 131 | name: "successful log collection", 132 | exportedFiles: map[string]string{ 133 | notificationPath: "", 134 | "/output/logs/test.log": expectedLogOutput, 135 | }, 136 | errorPaths: []string{}, 137 | wantErr: false, 138 | wantData: map[string]string{ 139 | "collect-windows-logs/test.log": expectedLogOutput, 140 | }, 141 | }, 142 | } 143 | 144 | runtimeInfo := &utils.RuntimeInfo{ 145 | RunId: runId, 146 | Features: map[utils.Feature]bool{utils.WindowsHpc: true}, 147 | } 148 | 149 | filePaths := &utils.KnownFilePaths{WindowsLogsOutput: "/output"} 150 | 151 | for _, tt := range tests { 152 | t.Run(tt.name, func(t *testing.T) { 153 | fs := test.NewFakeFileSystem(map[string]string{}) 154 | 155 | c := NewWindowsLogsCollector(utils.Windows, runtimeInfo, filePaths, fs, time.Microsecond, time.Second) 156 | 157 | for path, content := range tt.exportedFiles { 158 | fs.AddOrUpdateFile(path, content) 159 | } 160 | 161 | for _, path := range tt.errorPaths { 162 | fs.SetFileAccessError(path, fmt.Errorf("expected error accessing %s", path)) 163 | } 164 | 165 | err := c.Collect() 166 | 167 | if err != nil { 168 | if !tt.wantErr { 169 | t.Errorf("Collect() error = %v, wantErr %v", err, tt.wantErr) 170 | } 171 | } else { 172 | dataItems := c.GetData() 173 | for key, expectedValue := range tt.wantData { 174 | result, ok := dataItems[key] 175 | if !ok { 176 | t.Errorf("missing key %s", key) 177 | continue 178 | } 179 | 180 | testDataValue(t, result, func(actualValue string) { 181 | if actualValue != expectedValue { 182 | t.Errorf("unexpected value for key %s.\nExpected '%s'\nFound '%s'", key, expectedValue, actualValue) 183 | } 184 | }) 185 | } 186 | } 187 | }) 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /pkg/diagnoser/networkconfig_diagnoser.go: -------------------------------------------------------------------------------- 1 | package diagnoser 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/Azure/aks-periscope/pkg/collector" 10 | "github.com/Azure/aks-periscope/pkg/interfaces" 11 | "github.com/Azure/aks-periscope/pkg/utils" 12 | ) 13 | 14 | type networkConfigDiagnosticDatum struct { 15 | HostName string `json:"HostName"` 16 | NetworkPlugin string `json:"NetworkPlugin"` 17 | VirtualMachineDNS []string `json:"VirtualMachineDNS"` 18 | KubernetesDNS []string `json:"KubernetesDNS"` 19 | MaxPodsPerNode int `json:"MaxPodsPerNode"` 20 | } 21 | 22 | // NetworkConfigDiagnoser defines a NetworkConfig Diagnoser struct 23 | type NetworkConfigDiagnoser struct { 24 | runtimeInfo *utils.RuntimeInfo 25 | dnsCollector *collector.DNSCollector 26 | kubeletCmdCollector *collector.KubeletCmdCollector 27 | data map[string]string 28 | } 29 | 30 | // NewNetworkConfigDiagnoser is a constructor 31 | func NewNetworkConfigDiagnoser(runtimeInfo *utils.RuntimeInfo, dnsCollector *collector.DNSCollector, kubeletCmdCollector *collector.KubeletCmdCollector) *NetworkConfigDiagnoser { 32 | return &NetworkConfigDiagnoser{ 33 | runtimeInfo: runtimeInfo, 34 | dnsCollector: dnsCollector, 35 | kubeletCmdCollector: kubeletCmdCollector, 36 | data: make(map[string]string), 37 | } 38 | } 39 | 40 | func (collector *NetworkConfigDiagnoser) GetName() string { 41 | return "networkconfig" 42 | } 43 | 44 | // Diagnose implements the interface method 45 | func (diagnoser *NetworkConfigDiagnoser) Diagnose() error { 46 | networkConfigDiagnosticData := networkConfigDiagnosticDatum{HostName: diagnoser.runtimeInfo.HostNodeName} 47 | 48 | networkConfigDiagnosticData.VirtualMachineDNS = diagnoser.getDns(diagnoser.dnsCollector.HostConf) 49 | networkConfigDiagnosticData.KubernetesDNS = diagnoser.getDns(diagnoser.dnsCollector.ContainerConf) 50 | 51 | parts := strings.Split(diagnoser.kubeletCmdCollector.KubeletCommand, " ") 52 | for _, part := range parts { 53 | if strings.HasPrefix(part, "--network-plugin=") { 54 | networkPlugin := part[17:] 55 | if networkPlugin == "cni" { 56 | networkPlugin = "azurecni" 57 | } 58 | 59 | networkConfigDiagnosticData.NetworkPlugin = networkPlugin 60 | } 61 | 62 | if strings.HasPrefix(part, "--max-pods=") { 63 | maxPodsPerNodeString := part[11:] 64 | maxPodsPerNode, _ := strconv.Atoi(maxPodsPerNodeString) 65 | networkConfigDiagnosticData.MaxPodsPerNode = maxPodsPerNode 66 | } 67 | } 68 | 69 | dataBytes, err := json.Marshal(networkConfigDiagnosticData) 70 | if err != nil { 71 | return fmt.Errorf("marshal data from NetworkConfig Diagnoser: %w", err) 72 | } 73 | 74 | diagnoser.data["networkconfig"] = string(dataBytes) 75 | 76 | return nil 77 | } 78 | 79 | func (diagnoser *NetworkConfigDiagnoser) getDns(confFileContent string) []string { 80 | var dns []string 81 | words := strings.Split(confFileContent, " ") 82 | for i := range words { 83 | if words[i] == "nameserver" { 84 | dns = append(dns, strings.TrimSuffix(words[i+1], "\n")) 85 | } 86 | } 87 | 88 | return dns 89 | } 90 | 91 | func (collector *NetworkConfigDiagnoser) GetData() map[string]interfaces.DataValue { 92 | return utils.ToDataValueMap(collector.data) 93 | } 94 | -------------------------------------------------------------------------------- /pkg/diagnoser/networkoutbound_diagnoser.go: -------------------------------------------------------------------------------- 1 | package diagnoser 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "log" 8 | "strings" 9 | "time" 10 | 11 | "github.com/Azure/aks-periscope/pkg/collector" 12 | "github.com/Azure/aks-periscope/pkg/interfaces" 13 | "github.com/Azure/aks-periscope/pkg/utils" 14 | ) 15 | 16 | type networkOutboundDiagnosticDatum struct { 17 | HostName string `json:"HostName"` 18 | Type string `json:"Type"` 19 | Start time.Time `json:"Start"` 20 | End time.Time `json:"End"` 21 | Status string `json:"Status"` 22 | } 23 | 24 | // NetworkOutboundDiagnoser defines a NetworkOutbound Diagnoser struct 25 | type NetworkOutboundDiagnoser struct { 26 | runtimeInfo *utils.RuntimeInfo 27 | networkOutboundCollector *collector.NetworkOutboundCollector 28 | data map[string]string 29 | } 30 | 31 | // NewNetworkOutboundDiagnoser is a constructor 32 | func NewNetworkOutboundDiagnoser(runtimeInfo *utils.RuntimeInfo, networkOutboundCollector *collector.NetworkOutboundCollector) *NetworkOutboundDiagnoser { 33 | return &NetworkOutboundDiagnoser{ 34 | runtimeInfo: runtimeInfo, 35 | networkOutboundCollector: networkOutboundCollector, 36 | data: make(map[string]string), 37 | } 38 | } 39 | 40 | func (collector *NetworkOutboundDiagnoser) GetName() string { 41 | return "networkoutbound" 42 | } 43 | 44 | // Diagnose implements the interface method 45 | func (diagnoser *NetworkOutboundDiagnoser) Diagnose() error { 46 | outboundDiagnosticData := []networkOutboundDiagnosticDatum{} 47 | 48 | for _, value := range diagnoser.networkOutboundCollector.GetData() { 49 | dataPoint := networkOutboundDiagnosticDatum{HostName: diagnoser.runtimeInfo.HostNodeName} 50 | 51 | // TODO: We could read this directly from the collector object, rather than deserializing it from the output. 52 | // However, this diagnoser no longer does what it was originally intended to do, and as it is it doesn't 53 | // really provide any value. 54 | // The NetworkOutboundCollector used to append to a file that could potentially contain multiple status values 55 | // over time, and this diagnoser would aggregate this data into timestamps for each status change. But now 56 | // its output is effectively identical to that of the collector. 57 | data, err := utils.GetContent(func() (io.ReadCloser, error) { return value.GetReader() }) 58 | 59 | if err != nil { 60 | log.Printf("Retrieving data failed: %v", err) 61 | continue 62 | } 63 | 64 | lines := strings.Split(data, "\n") 65 | for _, line := range lines { 66 | var outboundDatum collector.NetworkOutboundDatum 67 | err := json.Unmarshal([]byte(line), &outboundDatum) 68 | if err != nil { 69 | log.Printf("Unmarshal failed: %v", err) 70 | continue 71 | } 72 | 73 | if dataPoint.Start.IsZero() { 74 | setDataPoint(&outboundDatum, &dataPoint) 75 | } else { 76 | if outboundDatum.Status != dataPoint.Status { 77 | outboundDiagnosticData = append(outboundDiagnosticData, dataPoint) 78 | setDataPoint(&outboundDatum, &dataPoint) 79 | } else { 80 | if int(outboundDatum.TimeStamp.Sub(dataPoint.End).Seconds()) > 5 { 81 | outboundDiagnosticData = append(outboundDiagnosticData, dataPoint) 82 | setDataPoint(&outboundDatum, &dataPoint) 83 | } else { 84 | dataPoint.End = outboundDatum.TimeStamp 85 | } 86 | } 87 | } 88 | } 89 | 90 | if !dataPoint.Start.IsZero() { 91 | outboundDiagnosticData = append(outboundDiagnosticData, dataPoint) 92 | } 93 | } 94 | 95 | dataBytes, err := json.Marshal(outboundDiagnosticData) 96 | if err != nil { 97 | return fmt.Errorf("marshal data from NetworkOutbound Diagnoser: %w", err) 98 | } 99 | 100 | diagnoser.data["networkoutbound"] = string(dataBytes) 101 | 102 | return nil 103 | } 104 | 105 | func (collector *NetworkOutboundDiagnoser) GetData() map[string]interfaces.DataValue { 106 | return utils.ToDataValueMap(collector.data) 107 | } 108 | 109 | func setDataPoint(outboundDatum *collector.NetworkOutboundDatum, dataPoint *networkOutboundDiagnosticDatum) { 110 | dataPoint.Type = outboundDatum.Type 111 | dataPoint.Start = outboundDatum.TimeStamp 112 | dataPoint.End = outboundDatum.TimeStamp 113 | dataPoint.Status = outboundDatum.Status 114 | } 115 | -------------------------------------------------------------------------------- /pkg/exporter/azureblob_exporter.go: -------------------------------------------------------------------------------- 1 | package exporter 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "log" 9 | "net/url" 10 | 11 | "github.com/Azure/aks-periscope/pkg/interfaces" 12 | "github.com/Azure/aks-periscope/pkg/utils" 13 | "github.com/Azure/azure-storage-blob-go/azblob" 14 | ) 15 | 16 | // AzureBlobExporter defines an Azure Blob Exporter 17 | type AzureBlobExporter struct { 18 | runtimeInfo *utils.RuntimeInfo 19 | knownFilePaths *utils.KnownFilePaths 20 | containerName string 21 | } 22 | 23 | type StorageKeyType string 24 | 25 | const ( 26 | Container StorageKeyType = "Container" 27 | ) 28 | 29 | var storageKeyTypes = map[string]StorageKeyType{ 30 | "Container": Container, 31 | } 32 | 33 | func NewAzureBlobExporter(runtimeInfo *utils.RuntimeInfo, knownFilePaths *utils.KnownFilePaths, containerName string) *AzureBlobExporter { 34 | return &AzureBlobExporter{ 35 | runtimeInfo: runtimeInfo, 36 | knownFilePaths: knownFilePaths, 37 | containerName: containerName, 38 | } 39 | } 40 | 41 | func createContainerURL(runtimeInfo *utils.RuntimeInfo, knownFilePaths *utils.KnownFilePaths) (azblob.ContainerURL, error) { 42 | if runtimeInfo.StorageAccountName == "" || runtimeInfo.StorageSasKey == "" || runtimeInfo.StorageContainerName == "" { 43 | log.Print("Storage Account information were not provided. Export to Azure Storage Account will be skipped.") 44 | return azblob.ContainerURL{}, errors.New("Storage not configured.") 45 | } 46 | 47 | ctx := context.Background() 48 | 49 | pipeline := azblob.NewPipeline(azblob.NewAnonymousCredential(), azblob.PipelineOptions{}) 50 | 51 | ses := utils.GetStorageEndpointSuffix(knownFilePaths) 52 | url, err := url.Parse(fmt.Sprintf("https://%s.blob.%s/%s%s", runtimeInfo.StorageAccountName, ses, runtimeInfo.StorageContainerName, runtimeInfo.StorageSasKey)) 53 | if err != nil { 54 | return azblob.ContainerURL{}, fmt.Errorf("build blob container url: %w", err) 55 | } 56 | 57 | containerURL := azblob.NewContainerURL(*url, pipeline) 58 | 59 | if _, ok := storageKeyTypes[runtimeInfo.StorageSasKeyType]; ok { 60 | return containerURL, nil 61 | } 62 | 63 | _, err = containerURL.Create(ctx, azblob.Metadata{}, azblob.PublicAccessNone) 64 | if err != nil { 65 | storageError, ok := err.(azblob.StorageError) 66 | if ok { 67 | switch storageError.ServiceCode() { 68 | case azblob.ServiceCodeContainerAlreadyExists: 69 | default: 70 | return azblob.ContainerURL{}, fmt.Errorf("create container with storage error: %w", err) 71 | } 72 | } else { 73 | return azblob.ContainerURL{}, fmt.Errorf("create container: %w", err) 74 | } 75 | } 76 | 77 | return containerURL, nil 78 | } 79 | 80 | // Export implements the interface method 81 | func (exporter *AzureBlobExporter) Export(producer interfaces.DataProducer) error { 82 | containerURL, err := createContainerURL(exporter.runtimeInfo, exporter.knownFilePaths) 83 | if err != nil { 84 | return err 85 | } 86 | 87 | for key, value := range producer.GetData() { 88 | blobURL := containerURL.NewBlockBlobURL(fmt.Sprintf("%s/%s/%s", exporter.containerName, exporter.runtimeInfo.HostNodeName, key)) 89 | 90 | log.Printf("\tAppend blob file: %s (of size %d bytes)", key, value.GetLength()) 91 | 92 | err = func() error { 93 | valueReadCloser, err := value.GetReader() 94 | if err != nil { 95 | return err 96 | } 97 | 98 | defer valueReadCloser.Close() 99 | 100 | _, err = azblob.UploadStreamToBlockBlob(context.Background(), valueReadCloser, blobURL, azblob.UploadStreamToBlockBlobOptions{}) 101 | return err 102 | }() 103 | 104 | if err != nil { 105 | return fmt.Errorf("append file %s to blob: %w", key, err) 106 | } 107 | } 108 | 109 | return nil 110 | } 111 | 112 | func (exporter *AzureBlobExporter) ExportReader(name string, reader io.ReadSeeker) error { 113 | containerURL, err := createContainerURL(exporter.runtimeInfo, exporter.knownFilePaths) 114 | if err != nil { 115 | return err 116 | } 117 | 118 | blobUrl := containerURL.NewBlockBlobURL(fmt.Sprintf("%s/%s/%s", exporter.containerName, exporter.runtimeInfo.HostNodeName, name)) 119 | log.Printf("Uploading the file with blob name: %s\n", name) 120 | _, err = azblob.UploadStreamToBlockBlob(context.Background(), reader, blobUrl, azblob.UploadStreamToBlockBlobOptions{}) 121 | 122 | return err 123 | } 124 | -------------------------------------------------------------------------------- /pkg/exporter/zip.go: -------------------------------------------------------------------------------- 1 | package exporter 2 | 3 | import ( 4 | "archive/zip" 5 | "bytes" 6 | "io" 7 | "log" 8 | 9 | "github.com/Azure/aks-periscope/pkg/interfaces" 10 | ) 11 | 12 | func Zip(data []interfaces.DataProducer) (*bytes.Buffer, error) { 13 | buffer := new(bytes.Buffer) 14 | z := zip.NewWriter(buffer) 15 | defer z.Close() 16 | 17 | for _, prd := range data { 18 | for name, value := range prd.GetData() { 19 | key := prd.GetName() + "/" + name 20 | dataf, err := z.Create(key) 21 | if err != nil { 22 | // If there's an error creating one value, log the error and continue. 23 | // We don't this to prevent all the other logs from being exported. 24 | log.Printf("Error creating zip entry %q: %v", key, err) 25 | continue 26 | } 27 | 28 | err = func() error { 29 | valueReader, err := value.GetReader() 30 | if err != nil { 31 | return err 32 | } 33 | 34 | defer valueReader.Close() 35 | 36 | _, err = io.Copy(dataf, valueReader) 37 | return err 38 | }() 39 | 40 | if err != nil { 41 | // If there's an error writing one value, log the error and continue. 42 | // This will leave the entry in the zip empty but allow export of other entries. 43 | log.Printf("Error writing zip entry %q: %v", key, err) 44 | continue 45 | } 46 | } 47 | } 48 | 49 | z.Flush() 50 | 51 | return buffer, nil 52 | } 53 | -------------------------------------------------------------------------------- /pkg/interfaces/collector.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | // Collector defines interface for a collector 4 | type Collector interface { 5 | GetName() string 6 | 7 | CheckSupported() error 8 | 9 | Collect() error 10 | 11 | GetData() map[string]DataValue 12 | } 13 | -------------------------------------------------------------------------------- /pkg/interfaces/dataProducer.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | // DataProducer defines an object producing data 4 | type DataProducer interface { 5 | GetData() map[string]DataValue 6 | 7 | GetName() string 8 | } 9 | -------------------------------------------------------------------------------- /pkg/interfaces/dataValue.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | import "io" 4 | 5 | type DataValue interface { 6 | GetLength() int64 7 | 8 | GetReader() (io.ReadCloser, error) 9 | } 10 | -------------------------------------------------------------------------------- /pkg/interfaces/diagnoser.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | // Diagnoser defines interface for a diagnoser 4 | type Diagnoser interface { 5 | GetName() string 6 | 7 | Diagnose() error 8 | 9 | GetData() map[string]DataValue 10 | } 11 | -------------------------------------------------------------------------------- /pkg/interfaces/exporter.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | // Exporter defines interface for an exporter 4 | type Exporter interface { 5 | Export(DataProducer) error 6 | } 7 | -------------------------------------------------------------------------------- /pkg/interfaces/fileSystemAccessor.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | import "io" 4 | 5 | type FileSystemAccessor interface { 6 | GetFileReader(filePath string) (io.ReadCloser, error) 7 | FileExists(filePath string) (bool, error) 8 | GetFileSize(filePath string) (int64, error) 9 | ListFiles(directoryPath string) ([]string, error) 10 | } 11 | -------------------------------------------------------------------------------- /pkg/test/fakeFileSystem.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strings" 7 | "sync" 8 | ) 9 | 10 | // FakeFileSystem can be used to test code that uses the FileSystemAccessor interface to 11 | // access the file system. 12 | type FakeFileSystem struct { 13 | lookup map[string]string 14 | errorFiles map[string]error 15 | lock sync.RWMutex 16 | } 17 | 18 | // NewFakeFileSystem creates a FileSystemAccessor based on a map where the keys represent 19 | // file paths and the values represent file content. 20 | func NewFakeFileSystem(lookup map[string]string) *FakeFileSystem { 21 | return &FakeFileSystem{ 22 | lookup: lookup, 23 | errorFiles: map[string]error{}, 24 | lock: sync.RWMutex{}, 25 | } 26 | } 27 | 28 | // GetFileReader implements the FileSystemAccessor interface 29 | func (ffs *FakeFileSystem) GetFileReader(path string) (io.ReadCloser, error) { 30 | ffs.lock.RLock() 31 | defer ffs.lock.RUnlock() 32 | 33 | content, ok := ffs.lookup[path] 34 | if !ok { 35 | return nil, fmt.Errorf("file not found: %s", path) 36 | } 37 | if err := ffs.getError(path); err != nil { 38 | return nil, err 39 | } 40 | return io.NopCloser(strings.NewReader(content)), nil 41 | } 42 | 43 | // FileExists implements the FileSystemAccessor interface 44 | func (ffs *FakeFileSystem) FileExists(path string) (bool, error) { 45 | ffs.lock.RLock() 46 | defer ffs.lock.RUnlock() 47 | 48 | if err := ffs.getError(path); err != nil { 49 | return false, err 50 | } 51 | _, ok := ffs.lookup[path] 52 | return ok, nil 53 | } 54 | 55 | // GetFileSize implements the FileSystemAccessor interface 56 | func (ffs *FakeFileSystem) GetFileSize(path string) (int64, error) { 57 | ffs.lock.RLock() 58 | defer ffs.lock.RUnlock() 59 | 60 | if err := ffs.getError(path); err != nil { 61 | return 0, err 62 | } 63 | content, ok := ffs.lookup[path] 64 | if !ok { 65 | return 0, fmt.Errorf("file not found: %s", path) 66 | } 67 | 68 | return int64(len(content)), nil 69 | } 70 | 71 | // ListFiles implements the FileSystemAccessor interface 72 | func (ffs *FakeFileSystem) ListFiles(directoryPath string) ([]string, error) { 73 | ffs.lock.RLock() 74 | defer ffs.lock.RUnlock() 75 | 76 | files := []string{} 77 | if err := ffs.getError(directoryPath); err != nil { 78 | return files, err 79 | } 80 | for path := range ffs.lookup { 81 | if strings.HasPrefix(path, directoryPath+"/") { 82 | files = append(files, path) 83 | } 84 | } 85 | return files, nil 86 | } 87 | 88 | func (ffs *FakeFileSystem) SetFileAccessError(path string, err error) { 89 | ffs.lock.Lock() 90 | defer ffs.lock.Unlock() 91 | 92 | ffs.errorFiles[path] = err 93 | } 94 | 95 | func (ffs *FakeFileSystem) AddOrUpdateFile(path, content string) { 96 | ffs.lock.Lock() 97 | defer ffs.lock.Unlock() 98 | 99 | ffs.lookup[path] = content 100 | } 101 | 102 | func (ffs *FakeFileSystem) DeleteFile(path string) { 103 | ffs.lock.Lock() 104 | defer ffs.lock.Unlock() 105 | 106 | delete(ffs.lookup, path) 107 | } 108 | 109 | func (ffs *FakeFileSystem) getError(path string) error { 110 | if err, ok := ffs.errorFiles[path]; ok { 111 | return err 112 | } 113 | return nil 114 | } 115 | -------------------------------------------------------------------------------- /pkg/test/resources/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker:20.10.16-alpine3.15@sha256:c24538b2a7a081efc81185772bd8066d33cbf1f3e1a8249657395bdad4d5f844 2 | 3 | ARG OSM_VERSION 4 | 5 | # Add binaries/archives 6 | RUN apk add gettext && \ 7 | wget -O /usr/local/bin/kind https://kind.sigs.k8s.io/dl/v0.12.0/kind-linux-amd64 && \ 8 | wget -O /helm.tar.gz https://get.helm.sh/helm-v3.8.2-linux-amd64.tar.gz && \ 9 | wget -O /usr/local/bin/kubectl https://dl.k8s.io/release/v1.23.5/bin/linux/amd64/kubectl && \ 10 | wget -O /osm.tar.gz https://github.com/openservicemesh/osm/releases/download/v$OSM_VERSION/osm-v$OSM_VERSION-linux-amd64.tar.gz 11 | 12 | # Set file modes and extract 13 | RUN chmod 755 /usr/local/bin/kind && \ 14 | chmod 755 /usr/local/bin/kubectl && \ 15 | tar -zxvf /helm.tar.gz && mv /linux-amd64/helm /usr/local/bin/helm && \ 16 | tar -zxvf /osm.tar.gz && mv /linux-amd64/osm /usr/local/bin/osm 17 | 18 | # Copy resources 19 | ADD tools-resources /resources 20 | ADD deployment /deployment 21 | -------------------------------------------------------------------------------- /pkg/test/resources/tools-resources/kind-config/config.yaml: -------------------------------------------------------------------------------- 1 | kind: Cluster 2 | apiVersion: kind.x-k8s.io/v1alpha4 3 | nodes: 4 | - role: control-plane 5 | - role: worker 6 | - role: worker 7 | -------------------------------------------------------------------------------- /pkg/test/resources/tools-resources/kube-objects/test-resources.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: test-configmap-1 5 | data: 6 | test_value_1: "a" 7 | test_value_2: "b" 8 | --- 9 | apiVersion: v1 10 | kind: ConfigMap 11 | metadata: 12 | name: test-configmap-2 13 | data: 14 | test_value_1: "c" 15 | test_value_2: "d" 16 | --- 17 | apiVersion: v1 18 | kind: ConfigMap 19 | metadata: 20 | name: test-configmap-3 21 | data: 22 | test_value_1: "e" 23 | test_value_2: "f" 24 | -------------------------------------------------------------------------------- /pkg/test/resources/tools-resources/metrics-server/components.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | labels: 5 | k8s-app: metrics-server 6 | name: metrics-server 7 | namespace: kube-system 8 | --- 9 | apiVersion: rbac.authorization.k8s.io/v1 10 | kind: ClusterRole 11 | metadata: 12 | labels: 13 | k8s-app: metrics-server 14 | rbac.authorization.k8s.io/aggregate-to-admin: "true" 15 | rbac.authorization.k8s.io/aggregate-to-edit: "true" 16 | rbac.authorization.k8s.io/aggregate-to-view: "true" 17 | name: system:aggregated-metrics-reader 18 | rules: 19 | - apiGroups: 20 | - metrics.k8s.io 21 | resources: 22 | - pods 23 | - nodes 24 | verbs: 25 | - get 26 | - list 27 | - watch 28 | --- 29 | apiVersion: rbac.authorization.k8s.io/v1 30 | kind: ClusterRole 31 | metadata: 32 | labels: 33 | k8s-app: metrics-server 34 | name: system:metrics-server 35 | rules: 36 | - apiGroups: 37 | - "" 38 | resources: 39 | - nodes/metrics 40 | verbs: 41 | - get 42 | - apiGroups: 43 | - "" 44 | resources: 45 | - pods 46 | - nodes 47 | verbs: 48 | - get 49 | - list 50 | - watch 51 | --- 52 | apiVersion: rbac.authorization.k8s.io/v1 53 | kind: RoleBinding 54 | metadata: 55 | labels: 56 | k8s-app: metrics-server 57 | name: metrics-server-auth-reader 58 | namespace: kube-system 59 | roleRef: 60 | apiGroup: rbac.authorization.k8s.io 61 | kind: Role 62 | name: extension-apiserver-authentication-reader 63 | subjects: 64 | - kind: ServiceAccount 65 | name: metrics-server 66 | namespace: kube-system 67 | --- 68 | apiVersion: rbac.authorization.k8s.io/v1 69 | kind: ClusterRoleBinding 70 | metadata: 71 | labels: 72 | k8s-app: metrics-server 73 | name: metrics-server:system:auth-delegator 74 | roleRef: 75 | apiGroup: rbac.authorization.k8s.io 76 | kind: ClusterRole 77 | name: system:auth-delegator 78 | subjects: 79 | - kind: ServiceAccount 80 | name: metrics-server 81 | namespace: kube-system 82 | --- 83 | apiVersion: rbac.authorization.k8s.io/v1 84 | kind: ClusterRoleBinding 85 | metadata: 86 | labels: 87 | k8s-app: metrics-server 88 | name: system:metrics-server 89 | roleRef: 90 | apiGroup: rbac.authorization.k8s.io 91 | kind: ClusterRole 92 | name: system:metrics-server 93 | subjects: 94 | - kind: ServiceAccount 95 | name: metrics-server 96 | namespace: kube-system 97 | --- 98 | apiVersion: v1 99 | kind: Service 100 | metadata: 101 | labels: 102 | k8s-app: metrics-server 103 | name: metrics-server 104 | namespace: kube-system 105 | spec: 106 | ports: 107 | - name: https 108 | port: 443 109 | protocol: TCP 110 | targetPort: https 111 | selector: 112 | k8s-app: metrics-server 113 | --- 114 | apiVersion: apps/v1 115 | kind: Deployment 116 | metadata: 117 | labels: 118 | k8s-app: metrics-server 119 | name: metrics-server 120 | namespace: kube-system 121 | spec: 122 | selector: 123 | matchLabels: 124 | k8s-app: metrics-server 125 | strategy: 126 | rollingUpdate: 127 | maxUnavailable: 0 128 | template: 129 | metadata: 130 | labels: 131 | k8s-app: metrics-server 132 | spec: 133 | containers: 134 | - args: 135 | - --cert-dir=/tmp 136 | - --secure-port=4443 137 | - --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname 138 | - --kubelet-use-node-status-port 139 | - --metric-resolution=15s 140 | # To allow running in Kind: https://github.com/kubernetes-sigs/metrics-server/issues/812#issuecomment-907586608 141 | - --kubelet-insecure-tls 142 | image: k8s.gcr.io/metrics-server/metrics-server:v0.6.1 143 | imagePullPolicy: IfNotPresent 144 | livenessProbe: 145 | failureThreshold: 3 146 | httpGet: 147 | path: /livez 148 | port: https 149 | scheme: HTTPS 150 | periodSeconds: 10 151 | name: metrics-server 152 | ports: 153 | - containerPort: 4443 154 | name: https 155 | protocol: TCP 156 | readinessProbe: 157 | failureThreshold: 3 158 | httpGet: 159 | path: /readyz 160 | port: https 161 | scheme: HTTPS 162 | initialDelaySeconds: 20 163 | periodSeconds: 10 164 | resources: 165 | requests: 166 | cpu: 100m 167 | memory: 200Mi 168 | securityContext: 169 | allowPrivilegeEscalation: false 170 | readOnlyRootFilesystem: true 171 | runAsNonRoot: true 172 | runAsUser: 1000 173 | volumeMounts: 174 | - mountPath: /tmp 175 | name: tmp-dir 176 | nodeSelector: 177 | kubernetes.io/os: linux 178 | priorityClassName: system-cluster-critical 179 | serviceAccountName: metrics-server 180 | volumes: 181 | - emptyDir: {} 182 | name: tmp-dir 183 | --- 184 | apiVersion: apiregistration.k8s.io/v1 185 | kind: APIService 186 | metadata: 187 | labels: 188 | k8s-app: metrics-server 189 | name: v1beta1.metrics.k8s.io 190 | spec: 191 | group: metrics.k8s.io 192 | groupPriorityMinimum: 100 193 | insecureSkipTLSVerify: true 194 | service: 195 | name: metrics-server 196 | namespace: kube-system 197 | version: v1beta1 198 | versionPriority: 100 199 | -------------------------------------------------------------------------------- /pkg/test/resources/tools-resources/osm-apps/bookbuyer.yaml: -------------------------------------------------------------------------------- 1 | # Create bookbuyer Service Account 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: bookbuyer 6 | namespace: ${BOOKBUYER_NS} 7 | 8 | --- 9 | 10 | # Create bookbuyer Deployment 11 | apiVersion: apps/v1 12 | kind: Deployment 13 | metadata: 14 | name: bookbuyer 15 | namespace: ${BOOKBUYER_NS} 16 | spec: 17 | replicas: 1 18 | selector: 19 | matchLabels: 20 | app: bookbuyer 21 | version: v1 22 | template: 23 | metadata: 24 | labels: 25 | app: bookbuyer 26 | version: v1 27 | spec: 28 | serviceAccountName: bookbuyer 29 | nodeSelector: 30 | kubernetes.io/arch: amd64 31 | kubernetes.io/os: linux 32 | containers: 33 | - name: bookbuyer 34 | image: openservicemesh/bookbuyer:v${OSM_VERSION} 35 | imagePullPolicy: Never 36 | command: ["/bookbuyer"] 37 | env: 38 | - name: "BOOKSTORE_NAMESPACE" 39 | value: ${BOOKSTORE_NS} 40 | - name: "BOOKSTORE_SVC" 41 | value: bookstore 42 | resources: 43 | limits: 44 | cpu: "0.1" 45 | memory: "10Mi" 46 | -------------------------------------------------------------------------------- /pkg/test/resources/tools-resources/osm-apps/bookstore-v2.yaml: -------------------------------------------------------------------------------- 1 | # Create bookstore-v2 Service 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: bookstore-v2 6 | namespace: ${BOOKSTORE_NS} 7 | labels: 8 | app: bookstore-v2 9 | spec: 10 | ports: 11 | - port: 14001 12 | name: bookstore-port 13 | selector: 14 | app: bookstore-v2 15 | 16 | --- 17 | 18 | # Create bookstore-v2 Service Account 19 | apiVersion: v1 20 | kind: ServiceAccount 21 | metadata: 22 | name: bookstore-v2 23 | namespace: ${BOOKSTORE_NS} 24 | 25 | --- 26 | 27 | # Create bookstore-v2 Deployment 28 | apiVersion: apps/v1 29 | kind: Deployment 30 | metadata: 31 | name: bookstore-v2 32 | namespace: ${BOOKSTORE_NS} 33 | spec: 34 | replicas: 1 35 | selector: 36 | matchLabels: 37 | app: bookstore-v2 38 | template: 39 | metadata: 40 | labels: 41 | app: bookstore-v2 42 | spec: 43 | serviceAccountName: bookstore-v2 44 | nodeSelector: 45 | kubernetes.io/arch: amd64 46 | kubernetes.io/os: linux 47 | containers: 48 | - name: bookstore 49 | image: openservicemesh/bookstore:v${OSM_VERSION} 50 | imagePullPolicy: Never 51 | ports: 52 | - containerPort: 14001 53 | name: web 54 | command: ["/bookstore"] 55 | args: ["--port", "14001"] 56 | env: 57 | - name: BOOKWAREHOUSE_NAMESPACE 58 | value: ${BOOKWAREHOUSE_NS} 59 | - name: IDENTITY 60 | value: bookstore-v2 61 | resources: 62 | limits: 63 | cpu: "0.1" 64 | memory: "10Mi" 65 | -------------------------------------------------------------------------------- /pkg/test/resources/tools-resources/osm-apps/bookstore.yaml: -------------------------------------------------------------------------------- 1 | # Create bookstore Service 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: bookstore 6 | namespace: ${BOOKSTORE_NS} 7 | labels: 8 | app: bookstore 9 | spec: 10 | ports: 11 | - port: 14001 12 | name: bookstore-port 13 | selector: 14 | app: bookstore 15 | 16 | --- 17 | 18 | # Create bookstore Service Account 19 | apiVersion: v1 20 | kind: ServiceAccount 21 | metadata: 22 | name: bookstore 23 | namespace: ${BOOKSTORE_NS} 24 | 25 | --- 26 | 27 | # Create bookstore Deployment 28 | apiVersion: apps/v1 29 | kind: Deployment 30 | metadata: 31 | name: bookstore 32 | namespace: ${BOOKSTORE_NS} 33 | spec: 34 | replicas: 1 35 | selector: 36 | matchLabels: 37 | app: bookstore 38 | template: 39 | metadata: 40 | labels: 41 | app: bookstore 42 | spec: 43 | serviceAccountName: bookstore 44 | nodeSelector: 45 | kubernetes.io/arch: amd64 46 | kubernetes.io/os: linux 47 | containers: 48 | - name: bookstore 49 | image: openservicemesh/bookstore:v${OSM_VERSION} 50 | imagePullPolicy: Never 51 | ports: 52 | - containerPort: 14001 53 | name: web 54 | command: ["/bookstore"] 55 | args: ["--port", "14001"] 56 | env: 57 | - name: BOOKWAREHOUSE_NAMESPACE 58 | value: ${BOOKWAREHOUSE_NS} 59 | - name: IDENTITY 60 | value: bookstore-v1 61 | resources: 62 | limits: 63 | cpu: "0.1" 64 | memory: "10Mi" 65 | -------------------------------------------------------------------------------- /pkg/test/resources/tools-resources/osm-apps/bookthief.yaml: -------------------------------------------------------------------------------- 1 | # Create bookthief ServiceAccount 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: bookthief 6 | namespace: ${BOOKTHIEF_NS} 7 | 8 | --- 9 | 10 | # Create bookthief Deployment 11 | apiVersion: apps/v1 12 | kind: Deployment 13 | metadata: 14 | name: bookthief 15 | namespace: ${BOOKTHIEF_NS} 16 | spec: 17 | replicas: 1 18 | selector: 19 | matchLabels: 20 | app: bookthief 21 | template: 22 | metadata: 23 | labels: 24 | app: bookthief 25 | version: v1 26 | spec: 27 | serviceAccountName: bookthief 28 | nodeSelector: 29 | kubernetes.io/arch: amd64 30 | kubernetes.io/os: linux 31 | containers: 32 | - name: bookthief 33 | image: openservicemesh/bookthief:v${OSM_VERSION} 34 | imagePullPolicy: Never 35 | command: ["/bookthief"] 36 | env: 37 | - name: "BOOKSTORE_NAMESPACE" 38 | value: ${BOOKSTORE_NS} 39 | - name: "BOOKSTORE_SVC" 40 | value: bookstore 41 | - name: "BOOKTHIEF_EXPECTED_RESPONSE_CODE" 42 | value: "503" 43 | resources: 44 | limits: 45 | cpu: "0.1" 46 | memory: "10Mi" 47 | -------------------------------------------------------------------------------- /pkg/test/resources/tools-resources/osm-apps/bookwarehouse.yaml: -------------------------------------------------------------------------------- 1 | # Create bookwarehouse Service Account 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: bookwarehouse 6 | namespace: ${BOOKWAREHOUSE_NS} 7 | 8 | --- 9 | 10 | # Create bookwarehouse Service 11 | apiVersion: v1 12 | kind: Service 13 | metadata: 14 | name: bookwarehouse 15 | namespace: ${BOOKWAREHOUSE_NS} 16 | labels: 17 | app: bookwarehouse 18 | spec: 19 | ports: 20 | - port: 14001 21 | name: bookwarehouse-port 22 | selector: 23 | app: bookwarehouse 24 | 25 | --- 26 | 27 | # Create bookwarehouse Deployment 28 | apiVersion: apps/v1 29 | kind: Deployment 30 | metadata: 31 | name: bookwarehouse 32 | namespace: ${BOOKWAREHOUSE_NS} 33 | spec: 34 | replicas: 1 35 | selector: 36 | matchLabels: 37 | app: bookwarehouse 38 | template: 39 | metadata: 40 | labels: 41 | app: bookwarehouse 42 | version: v1 43 | spec: 44 | serviceAccountName: bookwarehouse 45 | nodeSelector: 46 | kubernetes.io/arch: amd64 47 | kubernetes.io/os: linux 48 | containers: 49 | - name: bookwarehouse 50 | image: openservicemesh/bookwarehouse:v${OSM_VERSION} 51 | imagePullPolicy: Never 52 | command: ["/bookwarehouse"] 53 | resources: 54 | limits: 55 | cpu: "0.1" 56 | memory: "10Mi" 57 | -------------------------------------------------------------------------------- /pkg/test/resources/tools-resources/osm-apps/mysql.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: mysql 5 | namespace: ${BOOKWAREHOUSE_NS} 6 | --- 7 | apiVersion: v1 8 | kind: Service 9 | metadata: 10 | name: mysql 11 | namespace: ${BOOKWAREHOUSE_NS} 12 | spec: 13 | ports: 14 | - port: 3306 15 | targetPort: 3306 16 | name: client 17 | appProtocol: tcp 18 | selector: 19 | app: mysql 20 | clusterIP: None 21 | --- 22 | apiVersion: apps/v1 23 | kind: StatefulSet 24 | metadata: 25 | name: mysql 26 | namespace: ${BOOKWAREHOUSE_NS} 27 | spec: 28 | serviceName: mysql 29 | replicas: 1 30 | selector: 31 | matchLabels: 32 | app: mysql 33 | template: 34 | metadata: 35 | labels: 36 | app: mysql 37 | spec: 38 | serviceAccountName: mysql 39 | nodeSelector: 40 | kubernetes.io/os: linux 41 | containers: 42 | - image: mysql:5.6 43 | name: mysql 44 | env: 45 | - name: MYSQL_ROOT_PASSWORD 46 | value: mypassword 47 | - name: MYSQL_DATABASE 48 | value: booksdemo 49 | ports: 50 | - containerPort: 3306 51 | name: mysql 52 | volumeMounts: 53 | - mountPath: /mysql-data 54 | name: data 55 | readinessProbe: 56 | tcpSocket: 57 | port: 3306 58 | initialDelaySeconds: 15 59 | periodSeconds: 10 60 | resources: 61 | limits: 62 | cpu: "0.1" 63 | memory: "512Mi" 64 | volumes: 65 | - name: data 66 | emptyDir: {} 67 | volumeClaimTemplates: 68 | - metadata: 69 | name: data 70 | spec: 71 | accessModes: [ "ReadWriteOnce" ] 72 | resources: 73 | requests: 74 | storage: 250M 75 | -------------------------------------------------------------------------------- /pkg/test/resources/tools-resources/osm-apps/traffic-access.yaml: -------------------------------------------------------------------------------- 1 | kind: TrafficTarget 2 | apiVersion: access.smi-spec.io/v1alpha3 3 | metadata: 4 | name: bookstore 5 | namespace: ${BOOKSTORE_NS} 6 | spec: 7 | destination: 8 | kind: ServiceAccount 9 | name: bookstore 10 | namespace: ${BOOKSTORE_NS} 11 | rules: 12 | - kind: HTTPRouteGroup 13 | name: bookstore-service-routes 14 | matches: 15 | - buy-a-book 16 | - books-bought 17 | sources: 18 | - kind: ServiceAccount 19 | name: bookbuyer 20 | namespace: ${BOOKBUYER_NS} 21 | --- 22 | kind: TrafficTarget 23 | apiVersion: access.smi-spec.io/v1alpha3 24 | metadata: 25 | name: bookstore-v2 26 | namespace: ${BOOKSTORE_NS} 27 | spec: 28 | destination: 29 | kind: ServiceAccount 30 | name: bookstore-v2 31 | namespace: ${BOOKSTORE_NS} 32 | rules: 33 | - kind: HTTPRouteGroup 34 | name: bookstore-service-routes 35 | matches: 36 | - buy-a-book 37 | - books-bought 38 | sources: 39 | - kind: ServiceAccount 40 | name: bookbuyer 41 | namespace: ${BOOKBUYER_NS} 42 | --- 43 | apiVersion: specs.smi-spec.io/v1alpha4 44 | kind: HTTPRouteGroup 45 | metadata: 46 | name: bookstore-service-routes 47 | namespace: ${BOOKSTORE_NS} 48 | spec: 49 | matches: 50 | - name: books-bought 51 | pathRegex: /books-bought 52 | methods: 53 | - GET 54 | headers: 55 | - "user-agent": ".*-http-client/*.*" 56 | - "client-app": "bookbuyer" 57 | - name: buy-a-book 58 | pathRegex: ".*a-book.*new" 59 | methods: 60 | - GET 61 | --- 62 | kind: TrafficTarget 63 | apiVersion: access.smi-spec.io/v1alpha3 64 | metadata: 65 | name: bookstore-access-bookwarehouse 66 | namespace: ${BOOKWAREHOUSE_NS} 67 | spec: 68 | destination: 69 | kind: ServiceAccount 70 | name: bookwarehouse 71 | namespace: ${BOOKWAREHOUSE_NS} 72 | rules: 73 | - kind: HTTPRouteGroup 74 | name: bookwarehouse-service-routes 75 | matches: 76 | - restock-books 77 | sources: 78 | - kind: ServiceAccount 79 | name: bookstore 80 | namespace: ${BOOKSTORE_NS} 81 | --- 82 | apiVersion: specs.smi-spec.io/v1alpha4 83 | kind: HTTPRouteGroup 84 | metadata: 85 | name: bookwarehouse-service-routes 86 | namespace: ${BOOKWAREHOUSE_NS} 87 | spec: 88 | matches: 89 | - name: restock-books 90 | methods: 91 | - POST 92 | --- 93 | kind: TrafficTarget 94 | apiVersion: access.smi-spec.io/v1alpha3 95 | metadata: 96 | name: mysql 97 | namespace: ${BOOKWAREHOUSE_NS} 98 | spec: 99 | destination: 100 | kind: ServiceAccount 101 | name: mysql 102 | namespace: ${BOOKWAREHOUSE_NS} 103 | rules: 104 | - kind: TCPRoute 105 | name: mysql 106 | sources: 107 | - kind: ServiceAccount 108 | name: bookwarehouse 109 | namespace: ${BOOKWAREHOUSE_NS} 110 | --- 111 | apiVersion: specs.smi-spec.io/v1alpha4 112 | kind: TCPRoute 113 | metadata: 114 | name: mysql 115 | namespace: ${BOOKWAREHOUSE_NS} 116 | spec: 117 | matches: 118 | ports: 119 | - 3306 120 | -------------------------------------------------------------------------------- /pkg/test/resources/tools-resources/osm-apps/traffic-split.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: split.smi-spec.io/v1alpha2 2 | kind: TrafficSplit 3 | metadata: 4 | name: bookstore-split 5 | namespace: ${BOOKSTORE_NS} 6 | spec: 7 | # The root service is a Kubernetes Service FQDN. Because a Kubernetes Service FQDN can be a short form as well, 8 | # any of the following options are allowed and accepted values for the Service: 9 | # - bookstore 10 | # - bookstore.bookstore 11 | # - bookstore.bookstore.svc 12 | # - bookstore.bookstore.svc.cluster 13 | # - bookstore.bookstore.svc.cluster.local 14 | service: bookstore # 15 | backends: 16 | - service: bookstore 17 | weight: 70 18 | - service: bookstore-v2 19 | weight: 30 20 | -------------------------------------------------------------------------------- /pkg/test/resources/tools-resources/osm-config/override.yaml: -------------------------------------------------------------------------------- 1 | # Overrides used to specify known images to be loaded into the cluster, as well as resource limits. 2 | # Release tags for different versions of OSM are here: https://github.com/openservicemesh/osm/tags 3 | # To see the default values for a specific version tag, view: 4 | # https://github.com/openservicemesh/osm/blob/{VERSION_TAG}/charts/osm/values.yaml 5 | osm: 6 | image: 7 | # -- Container image pull policy for control plane containers 8 | # This is overridden here because we want tests to fail if any containers try to use an unexpected image. 9 | pullPolicy: Never 10 | 11 | # -- Envoy sidecar image for Linux workloads 12 | sidecarImage: docker.io/envoyproxy/envoy-alpine:v1.21.2 13 | curlImage: docker.io/curlimages/curl:7.83.0 14 | 15 | # -- OSM controller parameters 16 | osmController: 17 | # -- OSM controller's container resource parameters. See https://docs.openservicemesh.io/docs/guides/ha_scale/scale/ for more details. 18 | resource: 19 | limits: 20 | cpu: "0.5" 21 | memory: "256M" 22 | 23 | # -- Log level for the Envoy proxy sidecar. Non developers should generally never set this value. In production environments the LogLevel should be set to `error` 24 | envoyLogLevel: info 25 | 26 | # -- OSM's sidecar injector parameters 27 | injector: 28 | # -- Sidecar injector's container resource parameters 29 | resource: 30 | limits: 31 | cpu: "0.3" 32 | memory: "64M" 33 | 34 | # -- OSM bootstrap parameters 35 | osmBootstrap: 36 | # -- OSM bootstrap's container resource parameters 37 | resource: 38 | limits: 39 | cpu: "0.3" 40 | memory: "128M" 41 | -------------------------------------------------------------------------------- /pkg/test/resources/tools-resources/testchart/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /pkg/test/resources/tools-resources/testchart/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: testchart 3 | description: A Helm chart for Kubernetes 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.1.0 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: "1.16.0" 25 | -------------------------------------------------------------------------------- /pkg/test/resources/tools-resources/testchart/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 1. Get the application URL by running these commands: 2 | {{- if .Values.ingress.enabled }} 3 | {{- range $host := .Values.ingress.hosts }} 4 | {{- range .paths }} 5 | http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} 6 | {{- end }} 7 | {{- end }} 8 | {{- else if contains "NodePort" .Values.service.type }} 9 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "testchart.fullname" . }}) 10 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 11 | echo http://$NODE_IP:$NODE_PORT 12 | {{- else if contains "LoadBalancer" .Values.service.type }} 13 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 14 | You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "testchart.fullname" . }}' 15 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "testchart.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") 16 | echo http://$SERVICE_IP:{{ .Values.service.port }} 17 | {{- else if contains "ClusterIP" .Values.service.type }} 18 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "testchart.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") 19 | export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") 20 | echo "Visit http://127.0.0.1:8080 to use your application" 21 | kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT 22 | {{- end }} 23 | -------------------------------------------------------------------------------- /pkg/test/resources/tools-resources/testchart/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "testchart.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "testchart.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "testchart.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "testchart.labels" -}} 37 | helm.sh/chart: {{ include "testchart.chart" . }} 38 | {{ include "testchart.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "testchart.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "testchart.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "testchart.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "testchart.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /pkg/test/resources/tools-resources/testchart/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "testchart.fullname" . }} 5 | labels: 6 | {{- include "testchart.labels" . | nindent 4 }} 7 | spec: 8 | {{- if not .Values.autoscaling.enabled }} 9 | replicas: {{ .Values.replicaCount }} 10 | {{- end }} 11 | selector: 12 | matchLabels: 13 | {{- include "testchart.selectorLabels" . | nindent 6 }} 14 | template: 15 | metadata: 16 | {{- with .Values.podAnnotations }} 17 | annotations: 18 | {{- toYaml . | nindent 8 }} 19 | {{- end }} 20 | labels: 21 | {{- include "testchart.selectorLabels" . | nindent 8 }} 22 | spec: 23 | {{- with .Values.imagePullSecrets }} 24 | imagePullSecrets: 25 | {{- toYaml . | nindent 8 }} 26 | {{- end }} 27 | serviceAccountName: {{ include "testchart.serviceAccountName" . }} 28 | securityContext: 29 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 30 | containers: 31 | - name: {{ .Chart.Name }} 32 | securityContext: 33 | {{- toYaml .Values.securityContext | nindent 12 }} 34 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 35 | imagePullPolicy: {{ .Values.image.pullPolicy }} 36 | ports: 37 | - name: http 38 | containerPort: 80 39 | protocol: TCP 40 | livenessProbe: 41 | httpGet: 42 | path: / 43 | port: http 44 | readinessProbe: 45 | httpGet: 46 | path: / 47 | port: http 48 | resources: 49 | {{- toYaml .Values.resources | nindent 12 }} 50 | {{- with .Values.nodeSelector }} 51 | nodeSelector: 52 | {{- toYaml . | nindent 8 }} 53 | {{- end }} 54 | {{- with .Values.affinity }} 55 | affinity: 56 | {{- toYaml . | nindent 8 }} 57 | {{- end }} 58 | {{- with .Values.tolerations }} 59 | tolerations: 60 | {{- toYaml . | nindent 8 }} 61 | {{- end }} 62 | -------------------------------------------------------------------------------- /pkg/test/resources/tools-resources/testchart/templates/hpa.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.autoscaling.enabled }} 2 | apiVersion: autoscaling/v2beta1 3 | kind: HorizontalPodAutoscaler 4 | metadata: 5 | name: {{ include "testchart.fullname" . }} 6 | labels: 7 | {{- include "testchart.labels" . | nindent 4 }} 8 | spec: 9 | scaleTargetRef: 10 | apiVersion: apps/v1 11 | kind: Deployment 12 | name: {{ include "testchart.fullname" . }} 13 | minReplicas: {{ .Values.autoscaling.minReplicas }} 14 | maxReplicas: {{ .Values.autoscaling.maxReplicas }} 15 | metrics: 16 | {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} 17 | - type: Resource 18 | resource: 19 | name: cpu 20 | targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} 21 | {{- end }} 22 | {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} 23 | - type: Resource 24 | resource: 25 | name: memory 26 | targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} 27 | {{- end }} 28 | {{- end }} 29 | -------------------------------------------------------------------------------- /pkg/test/resources/tools-resources/testchart/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | {{- $fullName := include "testchart.fullname" . -}} 3 | {{- $svcPort := .Values.service.port -}} 4 | {{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} 5 | {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} 6 | {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} 7 | {{- end }} 8 | {{- end }} 9 | {{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} 10 | apiVersion: networking.k8s.io/v1 11 | {{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} 12 | apiVersion: networking.k8s.io/v1beta1 13 | {{- else -}} 14 | apiVersion: extensions/v1beta1 15 | {{- end }} 16 | kind: Ingress 17 | metadata: 18 | name: {{ $fullName }} 19 | labels: 20 | {{- include "testchart.labels" . | nindent 4 }} 21 | {{- with .Values.ingress.annotations }} 22 | annotations: 23 | {{- toYaml . | nindent 4 }} 24 | {{- end }} 25 | spec: 26 | {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} 27 | ingressClassName: {{ .Values.ingress.className }} 28 | {{- end }} 29 | {{- if .Values.ingress.tls }} 30 | tls: 31 | {{- range .Values.ingress.tls }} 32 | - hosts: 33 | {{- range .hosts }} 34 | - {{ . | quote }} 35 | {{- end }} 36 | secretName: {{ .secretName }} 37 | {{- end }} 38 | {{- end }} 39 | rules: 40 | {{- range .Values.ingress.hosts }} 41 | - host: {{ .host | quote }} 42 | http: 43 | paths: 44 | {{- range .paths }} 45 | - path: {{ .path }} 46 | {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} 47 | pathType: {{ .pathType }} 48 | {{- end }} 49 | backend: 50 | {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} 51 | service: 52 | name: {{ $fullName }} 53 | port: 54 | number: {{ $svcPort }} 55 | {{- else }} 56 | serviceName: {{ $fullName }} 57 | servicePort: {{ $svcPort }} 58 | {{- end }} 59 | {{- end }} 60 | {{- end }} 61 | {{- end }} 62 | -------------------------------------------------------------------------------- /pkg/test/resources/tools-resources/testchart/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "testchart.fullname" . }} 5 | labels: 6 | {{- include "testchart.labels" . | nindent 4 }} 7 | spec: 8 | type: {{ .Values.service.type }} 9 | ports: 10 | - port: {{ .Values.service.port }} 11 | targetPort: http 12 | protocol: TCP 13 | name: http 14 | selector: 15 | {{- include "testchart.selectorLabels" . | nindent 4 }} 16 | -------------------------------------------------------------------------------- /pkg/test/resources/tools-resources/testchart/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "testchart.serviceAccountName" . }} 6 | labels: 7 | {{- include "testchart.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | {{- end }} 13 | -------------------------------------------------------------------------------- /pkg/test/resources/tools-resources/testchart/templates/tests/test-connection.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: "{{ include "testchart.fullname" . }}-test-connection" 5 | labels: 6 | {{- include "testchart.labels" . | nindent 4 }} 7 | annotations: 8 | "helm.sh/hook": test 9 | spec: 10 | containers: 11 | - name: wget 12 | image: busybox 13 | command: ['wget'] 14 | args: ['{{ include "testchart.fullname" . }}:{{ .Values.service.port }}'] 15 | restartPolicy: Never 16 | -------------------------------------------------------------------------------- /pkg/test/resources/tools-resources/testchart/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for testchart. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | 7 | image: 8 | repository: nginx 9 | pullPolicy: IfNotPresent 10 | # Overrides the image tag whose default is the chart appVersion. 11 | tag: "" 12 | 13 | imagePullSecrets: [] 14 | nameOverride: "" 15 | fullnameOverride: "" 16 | 17 | serviceAccount: 18 | # Specifies whether a service account should be created 19 | create: true 20 | # Annotations to add to the service account 21 | annotations: {} 22 | # The name of the service account to use. 23 | # If not set and create is true, a name is generated using the fullname template 24 | name: "" 25 | 26 | podAnnotations: {} 27 | 28 | podSecurityContext: {} 29 | # fsGroup: 2000 30 | 31 | securityContext: {} 32 | # capabilities: 33 | # drop: 34 | # - ALL 35 | # readOnlyRootFilesystem: true 36 | # runAsNonRoot: true 37 | # runAsUser: 1000 38 | 39 | service: 40 | type: ClusterIP 41 | port: 80 42 | 43 | ingress: 44 | enabled: false 45 | className: "" 46 | annotations: {} 47 | # kubernetes.io/ingress.class: nginx 48 | # kubernetes.io/tls-acme: "true" 49 | hosts: 50 | - host: chart-example.local 51 | paths: 52 | - path: / 53 | pathType: ImplementationSpecific 54 | tls: [] 55 | # - secretName: chart-example-tls 56 | # hosts: 57 | # - chart-example.local 58 | 59 | resources: 60 | # We usually recommend not to specify default resources and to leave this as a conscious 61 | # choice for the user. This also increases chances charts run on environments with little 62 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 63 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 64 | limits: 65 | cpu: 100m 66 | memory: 10Mi 67 | # requests: 68 | # cpu: 100m 69 | # memory: 128Mi 70 | 71 | autoscaling: 72 | enabled: false 73 | minReplicas: 1 74 | maxReplicas: 100 75 | targetCPUUtilizationPercentage: 80 76 | # targetMemoryUtilizationPercentage: 80 77 | 78 | nodeSelector: {} 79 | 80 | tolerations: [] 81 | 82 | affinity: {} 83 | -------------------------------------------------------------------------------- /pkg/test/toolCommandRunner.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "log" 8 | 9 | "github.com/docker/docker/api/types" 10 | "github.com/docker/docker/api/types/container" 11 | "github.com/docker/docker/client" 12 | "github.com/docker/docker/pkg/stdcopy" 13 | ) 14 | 15 | // ToolsCommandRunner provides a means to invoke command-line tools within a Docker container 16 | // made for testing purposes. 17 | type ToolsCommandRunner struct { 18 | client *client.Client 19 | } 20 | 21 | func NewToolsCommandRunner(client *client.Client) *ToolsCommandRunner { 22 | return &ToolsCommandRunner{ 23 | client: client, 24 | } 25 | } 26 | 27 | // Run executest the specified command in the tools container, with the specified volume bindings. 28 | // It returns the stdout of the executed command. 29 | func (creator *ToolsCommandRunner) Run(command string, volumeBinds ...string) (string, error) { 30 | config := &container.Config{ 31 | Image: ToolsImageName, 32 | Cmd: []string{"sh", "-c", command}, 33 | } 34 | 35 | // Always bind the docker socket because we're expecting to use the docker client within the container. 36 | // Host networking is required to connect to the cluster API server, which is exposed on a port on the host. 37 | hostConfig := &container.HostConfig{ 38 | Binds: append(volumeBinds, "/var/run/docker.sock:/var/run/docker.sock"), 39 | NetworkMode: "host", 40 | } 41 | cont, err := creator.client.ContainerCreate( 42 | context.Background(), 43 | config, 44 | hostConfig, 45 | nil, 46 | nil, 47 | ToolsImageName, 48 | ) 49 | 50 | if err != nil { 51 | return "", fmt.Errorf("failed to create container\nCommand: %s\nError: %w", command, err) 52 | } 53 | 54 | // Remove container after running command, whether successful or not. 55 | // There is an auto-remove option when creating the container, but we avoid this because 56 | // it introduces a race condition while we wait for the container. 57 | defer removeContainer(creator.client, cont.ID) 58 | 59 | err = creator.client.ContainerStart(context.Background(), cont.ID, types.ContainerStartOptions{}) 60 | if err != nil { 61 | return "", fmt.Errorf("failed to create container\nCommand: %s\nError: %w", command, err) 62 | } 63 | 64 | waitResultChan, errChan := creator.client.ContainerWait(context.Background(), cont.ID, container.WaitConditionNotRunning) 65 | select { 66 | case err := <-errChan: 67 | return "", fmt.Errorf("failed waiting for command %s: %w", command, err) 68 | case result := <-waitResultChan: 69 | if result.StatusCode != 0 { 70 | stdout, stderr, err := getContainerLogs(creator.client, cont.ID) 71 | if err != nil { 72 | return "", fmt.Errorf("command failed with status %d, but unable to read container logs.\nCommand: %s\nError: %w", result.StatusCode, command, err) 73 | } 74 | return "", fmt.Errorf("command failed with status %d\nCommand: %s\nStdout: %s\nStderr: %s", result.StatusCode, command, stdout, stderr) 75 | } 76 | } 77 | 78 | stdout, _, err := getContainerLogs(creator.client, cont.ID) 79 | if err != nil { 80 | return "", fmt.Errorf("unable to read container logs.\nCommand: %s\nError: %w", command, err) 81 | } 82 | 83 | return stdout, nil 84 | } 85 | 86 | func removeContainer(client *client.Client, containerId string) { 87 | err := client.ContainerRemove(context.Background(), containerId, types.ContainerRemoveOptions{}) 88 | if err != nil { 89 | log.Printf("error removing container ID %s: %v", containerId, err) 90 | } 91 | } 92 | 93 | func getContainerLogs(client *client.Client, containerId string) (string, string, error) { 94 | options := types.ContainerLogsOptions{ 95 | ShowStdout: true, 96 | ShowStderr: true, 97 | } 98 | 99 | body, err := client.ContainerLogs(context.Background(), containerId, options) 100 | if err != nil { 101 | return "", "", fmt.Errorf("error getting logs: %w", err) 102 | } 103 | 104 | defer body.Close() 105 | 106 | var stdOutBuff, stdErrBuff bytes.Buffer 107 | _, err = stdcopy.StdCopy(&stdOutBuff, &stdErrBuff, body) 108 | if err != nil { 109 | return "", "", fmt.Errorf("error reading logs: %w", err) 110 | } 111 | 112 | return stdOutBuff.String(), stdErrBuff.String(), nil 113 | } 114 | -------------------------------------------------------------------------------- /pkg/test/toolsImageBuilder.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "archive/tar" 5 | "bytes" 6 | "context" 7 | "embed" 8 | "fmt" 9 | "io" 10 | "os" 11 | "path" 12 | 13 | "github.com/Azure/aks-periscope/deployment" 14 | "github.com/docker/docker/api/types" 15 | "github.com/docker/docker/client" 16 | ) 17 | 18 | const ToolsImageName = "aks-periscope-test-tools" 19 | 20 | // Include file prefixed with '_' explicitly 21 | //go:embed resources/Dockerfile 22 | //go:embed resources/tools-resources/* 23 | //go:embed resources/tools-resources/testchart/templates/_helpers.tpl 24 | var resources embed.FS 25 | 26 | // ToolsImageBuilder provides a method for building the Docker image that contains all the tools 27 | // involved in initializing a Kind cluster for tests. 28 | type ToolsImageBuilder struct { 29 | client *client.Client 30 | } 31 | 32 | func NewToolsImageBuilder(client *client.Client) *ToolsImageBuilder { 33 | return &ToolsImageBuilder{ 34 | client: client, 35 | } 36 | } 37 | 38 | func (builder *ToolsImageBuilder) Build() error { 39 | ctx := context.Background() 40 | 41 | archiveContent, err := createArchive() 42 | if err != nil { 43 | return fmt.Errorf("error creating resources archive: %w", err) 44 | } 45 | 46 | dockerFileTarReader := bytes.NewReader(archiveContent) 47 | 48 | osmVersionVar := osmVersion // need a variable here, because we can't get a pointer to a const string 49 | buildOptions := types.ImageBuildOptions{ 50 | Context: dockerFileTarReader, 51 | Dockerfile: "Dockerfile", 52 | Remove: true, 53 | Tags: []string{ToolsImageName}, 54 | BuildArgs: map[string]*string{ 55 | "OSM_VERSION": &osmVersionVar, 56 | }, 57 | } 58 | 59 | imageBuildResponse, err := builder.client.ImageBuild(ctx, dockerFileTarReader, buildOptions) 60 | if err != nil { 61 | return fmt.Errorf("error building docker image: %w", err) 62 | } 63 | 64 | defer imageBuildResponse.Body.Close() 65 | 66 | // Read the STDOUT from the build process 67 | _, err = io.Copy(os.Stdout, imageBuildResponse.Body) 68 | if err != nil { 69 | return fmt.Errorf("error copying build output to stdout: %w", err) 70 | } 71 | 72 | return nil 73 | } 74 | 75 | func createArchive() ([]byte, error) { 76 | buffer := new(bytes.Buffer) 77 | tarWriter := tar.NewWriter(buffer) 78 | defer tarWriter.Close() 79 | 80 | // Include resources from the test package 81 | err := addToArchive(tarWriter, resources, "resources", "") 82 | if err != nil { 83 | return nil, fmt.Errorf("error creating archive for resources: %w", err) 84 | } 85 | 86 | // Include resources from the repo deployment folder 87 | err = addToArchive(tarWriter, deployment.Resources, ".", "deployment") 88 | if err != nil { 89 | return nil, fmt.Errorf("error creating archive for deployment resources: %w", err) 90 | } 91 | 92 | return buffer.Bytes(), nil 93 | } 94 | 95 | func addToArchive(tarWriter *tar.Writer, srcFS embed.FS, srcDirPath, destDirPath string) error { 96 | dirEntries, err := srcFS.ReadDir(srcDirPath) 97 | if err != nil { 98 | return fmt.Errorf("error reading directory %s: %w", srcDirPath, err) 99 | } 100 | for _, dirEntry := range dirEntries { 101 | srcItemPath := path.Join(srcDirPath, dirEntry.Name()) 102 | destItemPath := path.Join(destDirPath, dirEntry.Name()) 103 | 104 | srcItemInfo, err := dirEntry.Info() 105 | if err != nil { 106 | return fmt.Errorf("error getting info for %s: %w", srcItemPath, err) 107 | } 108 | 109 | tarHeader, err := tar.FileInfoHeader(srcItemInfo, "") 110 | if err != nil { 111 | return fmt.Errorf("error creating tar header for %s: %w", destItemPath, err) 112 | } 113 | 114 | tarHeader.Name = destItemPath 115 | if dirEntry.IsDir() { 116 | tarHeader.Name += "/" 117 | } 118 | 119 | err = tarWriter.WriteHeader(tarHeader) 120 | if err != nil { 121 | return fmt.Errorf("error writing tar header for %s: %w", destItemPath, err) 122 | } 123 | 124 | if dirEntry.IsDir() { 125 | if err = addToArchive(tarWriter, srcFS, srcItemPath, destItemPath); err != nil { 126 | return err 127 | } 128 | } else { 129 | content, err := srcFS.ReadFile(srcItemPath) 130 | if err != nil { 131 | return fmt.Errorf("error reading file %s: %w", srcItemPath, err) 132 | } 133 | 134 | _, err = tarWriter.Write(content) 135 | if err != nil { 136 | return fmt.Errorf("error writing file file content to archive %s: %w", destItemPath, err) 137 | } 138 | } 139 | } 140 | return nil 141 | } 142 | -------------------------------------------------------------------------------- /pkg/utils/fileContentWatcher.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "io" 5 | "sync" 6 | "time" 7 | 8 | "github.com/Azure/aks-periscope/pkg/interfaces" 9 | ) 10 | 11 | type fileContentItem struct { 12 | content string 13 | err error 14 | lock sync.RWMutex 15 | contentHandlers []chan string 16 | errorHandlers []chan error 17 | } 18 | 19 | // FileContentWatcher allows clients to register to receive notifications via a channel when a file's content changes 20 | // or there is an error reading that file. It uses polling and stores file content in memory, valuing simplicity over 21 | // sophisticated approaches involving cross-platform inotify or hashing mechanisms. With that in mind, it is appropriate 22 | // for watching a small number of small files. 23 | type FileContentWatcher struct { 24 | fileSystem interfaces.FileSystemAccessor 25 | pollInterval time.Duration 26 | ticker *time.Ticker 27 | items map[string]*fileContentItem 28 | } 29 | 30 | // NewFileContentWatcher constructs a FileContentWatcher based on the specified FileSystemAccessor and polling interval. 31 | // This will initially contain no handlers, and will not start polling until the Start method is called. 32 | func NewFileContentWatcher(fileSystem interfaces.FileSystemAccessor, pollInterval time.Duration) *FileContentWatcher { 33 | return &FileContentWatcher{ 34 | fileSystem: fileSystem, 35 | pollInterval: pollInterval, 36 | ticker: nil, 37 | items: map[string]*fileContentItem{}, 38 | } 39 | } 40 | 41 | // AddHandler supplies channels for receiving notifications when the specified file is read or changed, or when there is 42 | // an error reading it. No files will be read or notifications sent until the Start method is called. 43 | func (w *FileContentWatcher) AddHandler(filePath string, contentChan chan string, errChan chan error) { 44 | if item, ok := w.items[filePath]; ok { 45 | w.items[filePath].contentHandlers = append(item.contentHandlers, contentChan) 46 | w.items[filePath].errorHandlers = append(item.errorHandlers, errChan) 47 | } else { 48 | w.items[filePath] = &fileContentItem{ 49 | content: "", 50 | err: nil, 51 | lock: sync.RWMutex{}, 52 | contentHandlers: []chan string{contentChan}, 53 | errorHandlers: []chan error{errChan}, 54 | } 55 | } 56 | } 57 | 58 | func (item *fileContentItem) updateIfChanged(content string, err error) bool { 59 | item.lock.Lock() 60 | defer item.lock.Unlock() 61 | 62 | if err != nil || content != item.content { 63 | item.content = content 64 | item.err = err 65 | return true 66 | } 67 | 68 | return false 69 | } 70 | 71 | func (item *fileContentItem) handleUpdated(filePath string) { 72 | item.lock.RLock() 73 | defer item.lock.RUnlock() 74 | 75 | if item.err != nil { 76 | for _, handler := range item.errorHandlers { 77 | handler <- item.err 78 | } 79 | } else { 80 | for _, handler := range item.contentHandlers { 81 | handler <- item.content 82 | } 83 | } 84 | } 85 | 86 | func (w *FileContentWatcher) checkFilePaths() { 87 | for filePath, item := range w.items { 88 | content, err := GetContent(func() (io.ReadCloser, error) { return w.fileSystem.GetFileReader(filePath) }) 89 | if item.updateIfChanged(content, err) { 90 | item.handleUpdated(filePath) 91 | } 92 | } 93 | } 94 | 95 | // Start tells the FileContentWatcher to periodically read the files for which a handler has been registered, 96 | // starting immediately. 97 | func (w *FileContentWatcher) Start() { 98 | if w.ticker == nil { 99 | w.ticker = time.NewTicker(w.pollInterval) 100 | 101 | go func() { 102 | w.checkFilePaths() 103 | for { 104 | <-w.ticker.C 105 | w.checkFilePaths() 106 | } 107 | }() 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /pkg/utils/filePathDataValue.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/Azure/aks-periscope/pkg/interfaces" 7 | ) 8 | 9 | type FilePathDataValue struct { 10 | fileSystem interfaces.FileSystemAccessor 11 | filePath string 12 | fileSize int64 13 | } 14 | 15 | func NewFilePathDataValue(fileSystem interfaces.FileSystemAccessor, filePath string, fileSize int64) *FilePathDataValue { 16 | return &FilePathDataValue{ 17 | fileSystem: fileSystem, 18 | filePath: filePath, 19 | fileSize: fileSize, 20 | } 21 | } 22 | 23 | func (v *FilePathDataValue) GetLength() int64 { 24 | return v.fileSize 25 | } 26 | 27 | func (v *FilePathDataValue) GetReader() (io.ReadCloser, error) { 28 | return v.fileSystem.GetFileReader(v.filePath) 29 | } 30 | -------------------------------------------------------------------------------- /pkg/utils/fileSystem.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | type FileSystem struct{} 12 | 13 | func NewFileSystem() *FileSystem { 14 | return &FileSystem{} 15 | } 16 | 17 | func (fs *FileSystem) GetFileReader(filePath string) (io.ReadCloser, error) { 18 | return os.Open(filePath) 19 | } 20 | 21 | func (fs *FileSystem) FileExists(filePath string) (bool, error) { 22 | if _, err := os.Stat(filePath); err == nil { 23 | return true, nil 24 | } else if errors.Is(err, os.ErrNotExist) { 25 | return false, nil 26 | } else { 27 | return false, fmt.Errorf("error checking existence of file %s: %w", filePath, err) 28 | } 29 | } 30 | 31 | func (fs *FileSystem) GetFileSize(filePath string) (int64, error) { 32 | info, err := os.Stat(filePath) 33 | if err != nil { 34 | return 0, fmt.Errorf("error getting file info for %s: %w", filePath, err) 35 | } 36 | 37 | return info.Size(), nil 38 | } 39 | 40 | func (fs *FileSystem) ListFiles(directoryPath string) ([]string, error) { 41 | paths := []string{} 42 | pathAdder := func(path string, info os.FileInfo, err error) error { 43 | if err == nil && !info.IsDir() { 44 | // Always use forward-slash-separated paths for consistency 45 | paths = append(paths, filepath.ToSlash(path)) 46 | } 47 | return err 48 | } 49 | 50 | if err := filepath.Walk(directoryPath, pathAdder); err != nil { 51 | return paths, fmt.Errorf("error listing files in %s: %w", directoryPath, err) 52 | } 53 | 54 | return paths, nil 55 | } 56 | -------------------------------------------------------------------------------- /pkg/utils/fileSystem_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "path" 7 | "testing" 8 | 9 | "github.com/google/uuid" 10 | ) 11 | 12 | func setup(t *testing.T) (*os.File, func()) { 13 | file, err := os.CreateTemp("", "") 14 | if err != nil { 15 | t.Fatalf("Failed to create temp file: %v", err) 16 | } 17 | 18 | teardown := func() { 19 | file.Close() 20 | os.Remove(file.Name()) 21 | } 22 | 23 | return file, teardown 24 | } 25 | 26 | func TestGetFileReaderForExistingFile(t *testing.T) { 27 | testFile, teardown := setup(t) 28 | defer teardown() 29 | 30 | const expectedContent = "Test File Content" 31 | 32 | _, err := testFile.Write([]byte(expectedContent)) 33 | if err != nil { 34 | t.Errorf("failed to write to file %s: %s", testFile.Name(), expectedContent) 35 | } 36 | 37 | fs := NewFileSystem() 38 | actualContent, err := GetContent(func() (io.ReadCloser, error) { return fs.GetFileReader(testFile.Name()) }) 39 | if err != nil { 40 | t.Errorf("error reading content from %s", testFile.Name()) 41 | } 42 | 43 | if actualContent != expectedContent { 44 | t.Errorf("unexpected file content.\nExpected '%s'\nFound '%s'", expectedContent, actualContent) 45 | } 46 | } 47 | 48 | func TestGetFileContentForMissingFile(t *testing.T) { 49 | cwd, err := os.Getwd() 50 | if err != nil { 51 | t.Errorf("error getting current directory: %v", err) 52 | } 53 | 54 | missingFilePath := path.Join(cwd, uuid.New().String()) 55 | 56 | fs := NewFileSystem() 57 | _, err = fs.GetFileReader(missingFilePath) 58 | if err == nil { 59 | t.Errorf("no error reading missing file %s", missingFilePath) 60 | } 61 | } 62 | 63 | func TestFileExistsForExistingFile(t *testing.T) { 64 | testFile, teardown := setup(t) 65 | defer teardown() 66 | 67 | fs := NewFileSystem() 68 | exists, err := fs.FileExists(testFile.Name()) 69 | 70 | if err != nil { 71 | t.Errorf("error checking existence of file %s", testFile.Name()) 72 | } 73 | 74 | if !exists { 75 | t.Errorf("file exists but FileExists returned false") 76 | } 77 | } 78 | 79 | func TestFileExistsForMissingFile(t *testing.T) { 80 | cwd, err := os.Getwd() 81 | if err != nil { 82 | t.Errorf("error getting current directory: %v", err) 83 | } 84 | 85 | missingFilePath := path.Join(cwd, uuid.New().String()) 86 | 87 | fs := NewFileSystem() 88 | exists, err := fs.FileExists(missingFilePath) 89 | 90 | if err != nil { 91 | t.Errorf("error checking existence of missing file %s", missingFilePath) 92 | } 93 | 94 | if exists { 95 | t.Errorf("file does not exist but FileExists returned true") 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /pkg/utils/helper.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net/http" 9 | "os" 10 | "os/exec" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | const ( 16 | // PublicAzureStorageEndpointSuffix defines default Storage Endpoint Suffix 17 | PublicAzureStorageEndpointSuffix = "core.windows.net" 18 | // AzureStackCloudName references the value that will be under the key "cloud" in azure.json if the application is running on Azure Stack Cloud 19 | // https://kubernetes-sigs.github.io/cloud-provider-azure/install/configs/#azure-stack-configuration -- See this documentation for the well-known cloud name. 20 | AzureStackCloudName = "AzureStackCloud" 21 | ) 22 | 23 | // Azure defines Azure configuration 24 | type Azure struct { 25 | Cloud string `json:"cloud"` 26 | } 27 | 28 | // AzureStackCloud defines Azure Stack Cloud configuration 29 | type AzureStackCloud struct { 30 | StorageEndpointSuffix string `json:"storageEndpointSuffix"` 31 | } 32 | 33 | type CommandOutputStreams struct { 34 | Stdout string 35 | Stderr string 36 | } 37 | 38 | // IsAzureStackCloud returns true if the application is running on Azure Stack Cloud 39 | func IsAzureStackCloud(filePaths *KnownFilePaths) bool { 40 | azureFile, err := os.ReadFile(filePaths.AzureJson) 41 | if err != nil { 42 | return false 43 | } 44 | var azure Azure 45 | if err = json.Unmarshal([]byte(azureFile), &azure); err != nil { 46 | return false 47 | } 48 | cloud := azure.Cloud 49 | return strings.EqualFold(cloud, AzureStackCloudName) 50 | } 51 | 52 | func CopyFile(source, destination string) error { 53 | sourceFile, err := os.Open(source) 54 | if err != nil { 55 | return fmt.Errorf("unable to open source file %s: %w", source, err) 56 | } 57 | defer sourceFile.Close() 58 | 59 | destFile, err := os.Create(destination) 60 | if err != nil { 61 | return fmt.Errorf("error creating file %s: %w", destination, err) 62 | } 63 | defer destFile.Close() 64 | 65 | _, err = io.Copy(destFile, sourceFile) 66 | if err != nil { 67 | return fmt.Errorf("error copying data to file %s: %w", destination, err) 68 | } 69 | return nil 70 | } 71 | 72 | // GetStorageEndpointSuffix returns the SES url from the JSON file as a string 73 | func GetStorageEndpointSuffix(knownFilePaths *KnownFilePaths) string { 74 | if IsAzureStackCloud(knownFilePaths) { 75 | ascFile, err := os.ReadFile(knownFilePaths.AzureStackCloudJson) 76 | if err != nil { 77 | log.Fatalf("unable to locate %s to extract storage endpoint suffix: %v", knownFilePaths.AzureStackCloudJson, err) 78 | } 79 | var azurestackcloud AzureStackCloud 80 | if err = json.Unmarshal([]byte(ascFile), &azurestackcloud); err != nil { 81 | log.Fatalf("unable to read %s file: %v", knownFilePaths.AzureStackCloudJson, err) 82 | } 83 | return azurestackcloud.StorageEndpointSuffix 84 | } 85 | return PublicAzureStorageEndpointSuffix 86 | } 87 | 88 | // RunCommandOnHost runs a command on host system 89 | func RunCommandOnHost(command string, arg ...string) (string, error) { 90 | args := []string{"--target", "1", "--mount", "--uts", "--ipc", "--net", "--pid"} 91 | args = append(args, "--") 92 | args = append(args, command) 93 | args = append(args, arg...) 94 | 95 | cmd := exec.Command("nsenter", args...) 96 | out, err := cmd.CombinedOutput() 97 | if err != nil { 98 | return "", fmt.Errorf("fail to run command on host: %+v", err) 99 | } 100 | 101 | return string(out), nil 102 | } 103 | 104 | // Tries to issue an HTTP GET request up to maxRetries times 105 | func GetUrlWithRetries(url string, maxRetries int) ([]byte, error) { 106 | retry := 1 107 | for { 108 | resp, err := http.Get(url) 109 | if err != nil { 110 | if retry == maxRetries { 111 | return nil, fmt.Errorf("max retries reached for request HTTP Get %s: %w", url, err) 112 | } 113 | retry++ 114 | time.Sleep(5 * time.Second) 115 | } else { 116 | defer resp.Body.Close() 117 | return io.ReadAll(resp.Body) 118 | } 119 | } 120 | } 121 | 122 | func Contains(flagsList []string, flag string) bool { 123 | for _, f := range flagsList { 124 | if strings.EqualFold(f, flag) { 125 | return true 126 | } 127 | } 128 | return false 129 | } 130 | 131 | func GetContent(readCloserProvider func() (io.ReadCloser, error)) (string, error) { 132 | reader, err := readCloserProvider() 133 | if err != nil { 134 | return "", err 135 | } 136 | 137 | defer reader.Close() 138 | 139 | content, err := io.ReadAll(reader) 140 | if err != nil { 141 | return "", err 142 | } 143 | 144 | return string(content), nil 145 | } 146 | -------------------------------------------------------------------------------- /pkg/utils/knownFilePaths.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | ) 7 | 8 | type KnownFilePaths struct { 9 | AzureJson string 10 | AzureStackCloudJson string 11 | WindowsLogsOutput string 12 | ResolvConfHost string 13 | ResolvConfContainer string 14 | AzureStackCertHost string 15 | AzureStackCertContainer string 16 | NodeLogsList string 17 | Config string 18 | Secret string 19 | } 20 | 21 | type ConfigKey string 22 | type SecretKey string 23 | 24 | const ( 25 | CollectorListKey ConfigKey = "COLLECTOR_LIST" 26 | ContainerLogsListKey ConfigKey = "DIAGNOSTIC_CONTAINERLOGS_LIST" 27 | KubeObjectsListKey ConfigKey = "DIAGNOSTIC_KUBEOBJECTS_LIST" 28 | NodeLogsLinuxKey ConfigKey = "DIAGNOSTIC_NODELOGS_LIST_LINUX" 29 | NodeLogsWindowsKey ConfigKey = "DIAGNOSTIC_NODELOGS_LIST_WINDOWS" 30 | RunIdKey ConfigKey = "DIAGNOSTIC_RUN_ID" 31 | ) 32 | 33 | const ( 34 | AccountNameKey SecretKey = "AZURE_BLOB_ACCOUNT_NAME" 35 | SasTokenKey SecretKey = "AZURE_BLOB_SAS_KEY" 36 | ContainerNameKey SecretKey = "AZURE_BLOB_CONTAINER_NAME" 37 | SasTokenTypeKey SecretKey = "AZURE_STORAGE_SAS_KEY_TYPE" 38 | ) 39 | 40 | // GetKnownFilePaths get known file paths 41 | func GetKnownFilePaths(osIdentifier OSIdentifier) (*KnownFilePaths, error) { 42 | switch osIdentifier { 43 | case Windows: 44 | return &KnownFilePaths{ 45 | AzureJson: "/k/azure.json", 46 | AzureStackCloudJson: "/k/azurestackcloud.json", 47 | WindowsLogsOutput: "/k/periscope-diagnostic-output", 48 | NodeLogsList: "/config/" + string(NodeLogsWindowsKey), 49 | Config: "/config", 50 | Secret: "/secret", 51 | }, nil 52 | case Linux: 53 | // Since Azure Stack Hub does not support multiple node pools, we assume we don't need to worry about this for Windows 54 | // https://docs.microsoft.com/en-us/azure-stack/user/aks-overview?view=azs-2108#supported-platform-features 55 | return &KnownFilePaths{ 56 | AzureJson: "/etc/kubernetes/azure.json", 57 | AzureStackCloudJson: "/etc/kubernetes/azurestackcloud.json", 58 | ResolvConfHost: "/etchostlogs/resolv.conf", 59 | ResolvConfContainer: "/etc/resolv.conf", 60 | AzureStackCertHost: "/etchostlogs/ssl/certs/azsCertificate.pem", 61 | AzureStackCertContainer: "/etc/ssl/certs/azsCertificate.pem", 62 | NodeLogsList: "/config/" + string(NodeLogsLinuxKey), 63 | Config: "/config", 64 | Secret: "/secret", 65 | }, nil 66 | default: 67 | return nil, fmt.Errorf("unexpected OS: %s", osIdentifier) 68 | } 69 | } 70 | 71 | func (p *KnownFilePaths) GetConfigPath(key ConfigKey) string { 72 | return filepath.Join(p.Config, string(key)) 73 | } 74 | 75 | func (p *KnownFilePaths) GetSecretPath(key SecretKey) string { 76 | return filepath.Join(p.Secret, string(key)) 77 | } 78 | 79 | func (p *KnownFilePaths) GetFeaturePath(feature Feature) string { 80 | return filepath.Join(p.Config, fmt.Sprintf("FEATURE_%s", feature)) 81 | } 82 | -------------------------------------------------------------------------------- /pkg/utils/osIdentifier.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "fmt" 4 | 5 | type OSIdentifier string 6 | 7 | const ( 8 | Linux OSIdentifier = "linux" 9 | Windows OSIdentifier = "windows" 10 | ) 11 | 12 | func StringToOSIdentifier(identifier string) (OSIdentifier, error) { 13 | switch identifier { 14 | case string(Linux): 15 | return Linux, nil 16 | case string(Windows): 17 | return Windows, nil 18 | default: 19 | return "", fmt.Errorf("unknown OS identifier '%s'", identifier) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pkg/utils/runtimeInfo.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "os" 8 | "strings" 9 | 10 | "github.com/Azure/aks-periscope/pkg/interfaces" 11 | "github.com/hashicorp/go-multierror" 12 | ) 13 | 14 | type Feature string 15 | 16 | const ( 17 | WindowsHpc Feature = "WINHPC" 18 | ) 19 | 20 | func getKnownFeatures() []Feature { 21 | return []Feature{WindowsHpc} 22 | } 23 | 24 | type RuntimeInfo struct { 25 | RunId string 26 | HostNodeName string 27 | CollectorList []string 28 | KubernetesObjects []string 29 | NodeLogs []string 30 | ContainerLogsNamespaces []string 31 | StorageAccountName string 32 | StorageSasKey string 33 | StorageContainerName string 34 | StorageSasKeyType string 35 | Features map[Feature]bool 36 | } 37 | 38 | // GetRuntimeInfo gets runtime info 39 | func GetRuntimeInfo(fs interfaces.FileSystemAccessor, filePaths *KnownFilePaths) (*RuntimeInfo, error) { 40 | var errs error 41 | 42 | // Config 43 | runId, errs := readFileContent(fs, filePaths.GetConfigPath(RunIdKey), true, errs) 44 | collectorList, errs := readFileContent(fs, filePaths.GetConfigPath(CollectorListKey), false, errs) 45 | kubernetesObjects, errs := readFileContent(fs, filePaths.GetConfigPath(KubeObjectsListKey), false, errs) 46 | nodeLogs, errs := readFileContent(fs, filePaths.NodeLogsList, false, errs) 47 | containerLogsNamespaces, errs := readFileContent(fs, filePaths.GetConfigPath(ContainerLogsListKey), false, errs) 48 | 49 | // Secret 50 | storageAccountName, errs := readFileContent(fs, filePaths.GetSecretPath(AccountNameKey), false, errs) 51 | storageSasKey, errs := readFileContent(fs, filePaths.GetSecretPath(SasTokenKey), false, errs) 52 | storageContainerName, errs := readFileContent(fs, filePaths.GetSecretPath(ContainerNameKey), false, errs) 53 | storageSasKeyType, errs := readFileContent(fs, filePaths.GetSecretPath(SasTokenTypeKey), false, errs) 54 | 55 | // We can't use `os.Hostname` for this, because this gives us the _container_ hostname (i.e. the pod name, by default). 56 | // An earlier approach was to `cat /etc/hostname` but that will not work for Windows containers. 57 | // Instead we expect the host node name to be exposed to the pod in an environment variable, via the 'downward API', see: 58 | // https://kubernetes.io/docs/tasks/inject-data-application/environment-variable-expose-pod-information/#use-pod-fields-as-values-for-environment-variables 59 | hostName := os.Getenv("HOST_NODE_NAME") 60 | if len(hostName) == 0 { 61 | errs = multierror.Append(errs, errors.New("variable HOST_NODE_NAME value not set for container")) 62 | } 63 | 64 | features := map[Feature]bool{} 65 | for _, feature := range getKnownFeatures() { 66 | featureFilePath := filePaths.GetFeaturePath(feature) 67 | var enabled string 68 | enabled, errs = readFileContent(fs, featureFilePath, false, errs) 69 | if len(enabled) > 0 { 70 | features[feature] = true 71 | } 72 | } 73 | 74 | if errs != nil { 75 | return nil, errs 76 | } 77 | 78 | return &RuntimeInfo{ 79 | RunId: runId, 80 | HostNodeName: hostName, 81 | CollectorList: strings.Fields(collectorList), 82 | KubernetesObjects: strings.Fields(kubernetesObjects), 83 | NodeLogs: strings.Fields(nodeLogs), 84 | ContainerLogsNamespaces: strings.Fields(containerLogsNamespaces), 85 | StorageAccountName: storageAccountName, 86 | StorageSasKey: storageSasKey, 87 | StorageContainerName: storageContainerName, 88 | StorageSasKeyType: storageSasKeyType, 89 | Features: features, 90 | }, nil 91 | } 92 | 93 | func readFileContent(fs interfaces.FileSystemAccessor, filePath string, mandatory bool, readErrors error) (string, error) { 94 | exists, err := fs.FileExists(filePath) 95 | if err != nil { 96 | return "", multierror.Append(readErrors, fmt.Errorf("error checking existence of %s: %w", filePath, err)) 97 | } 98 | if !exists { 99 | if mandatory { 100 | return "", multierror.Append(readErrors, fmt.Errorf("mandatory file does not exist: %s", filePath)) 101 | } 102 | 103 | return "", readErrors 104 | } 105 | 106 | value, err := GetContent(func() (io.ReadCloser, error) { return fs.GetFileReader(filePath) }) 107 | if err != nil { 108 | return "", multierror.Append(readErrors, fmt.Errorf("error reading %s: %w", filePath, err)) 109 | } 110 | if mandatory && len(value) == 0 { 111 | return "", multierror.Append(readErrors, fmt.Errorf("mandatory file has no content: %s", filePath)) 112 | } 113 | return value, readErrors 114 | } 115 | 116 | func (runtimeInfo *RuntimeInfo) HasFeature(feature Feature) bool { 117 | _, ok := runtimeInfo.Features[feature] 118 | return ok 119 | } 120 | -------------------------------------------------------------------------------- /pkg/utils/stringDataValue.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "io" 5 | "strings" 6 | 7 | "github.com/Azure/aks-periscope/pkg/interfaces" 8 | ) 9 | 10 | type StringDataValue struct { 11 | value string 12 | } 13 | 14 | func NewStringDataValue(value string) *StringDataValue { 15 | return &StringDataValue{ 16 | value: value, 17 | } 18 | } 19 | 20 | func (v *StringDataValue) GetLength() int64 { 21 | return int64(len(v.value)) 22 | } 23 | 24 | func (v *StringDataValue) GetReader() (io.ReadCloser, error) { 25 | return io.NopCloser(strings.NewReader(v.value)), nil 26 | } 27 | 28 | func ToDataValueMap(data map[string]string) map[string]interfaces.DataValue { 29 | result := make(map[string]interfaces.DataValue, len(data)) 30 | for key, value := range data { 31 | result[key] = NewStringDataValue(value) 32 | } 33 | 34 | return result 35 | } 36 | -------------------------------------------------------------------------------- /tools/printdiagnostic.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo 4 | echo 1. Network Setup 5 | for NODEAPD in $(kubectl -n aks-periscope get apd -o name) 6 | do 7 | kubectl -n aks-periscope get $NODEAPD -o jsonpath="{.spec.networkconfig}" | jq . 8 | done 9 | 10 | echo 11 | echo 2. Network Outbound Check 12 | for NODEAPD in $(kubectl -n aks-periscope get apd -o name) 13 | do 14 | kubectl -n aks-periscope get $NODEAPD -o jsonpath="{.spec.networkoutbound}" | jq . 15 | echo 16 | done --------------------------------------------------------------------------------