├── .dockerignore ├── .github ├── CODEOWNERS ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── post-merge.yml │ └── pre-merge.yml ├── .gitignore ├── .golangci.yml ├── .markdownlint.yml ├── .tool-versions ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── LICENSES └── Apache-2.0.txt ├── Makefile ├── README.md ├── REUSE.toml ├── SECURITY.md ├── VERSION ├── api ├── projectstream.pb.go ├── projectstream.proto └── projectstream_grpc.pb.go ├── cmd └── observability-tenant-controller │ └── observability-tenant-controller.go ├── deployments └── observability-tenant-controller │ ├── Chart.yaml │ ├── files │ └── config │ │ └── config.yaml │ ├── templates │ ├── _checks.tpl │ ├── _helpers.tpl │ ├── configmap.yaml │ ├── deployment.yaml │ ├── network_policy.yaml │ ├── service.yaml │ └── serviceaccount.yaml │ └── values.yaml ├── go.mod ├── go.sum ├── internal ├── alertingmonitor │ ├── alertingmonitor.go │ ├── alertingmonitor_suite_test.go │ └── alertingmonitor_test.go ├── config │ ├── model.go │ ├── reader.go │ ├── reader_test.go │ └── testdata │ │ ├── test_config.yaml │ │ └── test_config_malformed.yaml ├── controller │ └── controller.go ├── jobs │ └── jobs.go ├── loki │ ├── loki.go │ └── loki_test.go ├── mimir │ ├── mimir.go │ └── mimir_test.go ├── projects │ └── projects.go ├── sre │ ├── sre.go │ ├── sre_suite_test.go │ └── sre_test.go ├── util │ ├── util.go │ └── util_test.go └── watcher │ ├── watcher.go │ └── watcher_test.go ├── scripts ├── installTools.sh └── lintJsons.sh └── yamllint-conf.yaml /.dockerignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | build/ -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | # These owners will be the default owners for everything in the repo. Unless a 5 | # later match takes precedence, these owners will be requested for review when 6 | # someone opens a pull request. 7 | 8 | # Everything requires Observability team review by default 9 | * @p-zak @pperycz @tdorauintc @jakubsikorski @mholowni @ltalarcz 10 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | ### Description 7 | 8 | Please include a summary of the changes and the related issue. List any dependencies that are required for this change. 9 | 10 | Fixes # (issue) 11 | 12 | ### Any Newly Introduced Dependencies 13 | 14 | Please describe any newly introduced 3rd party dependencies in this change. List their name, license information and how they are used in the project. 15 | 16 | ### How Has This Been Tested? 17 | 18 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration 19 | 20 | ### Checklist: 21 | 22 | - [ ] I agree to use the APACHE-2.0 license for my code changes 23 | - [ ] I have not introduced any 3rd party dependency changes 24 | - [ ] I have performed a self-review of my code 25 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | --- 5 | version: 2 6 | updates: 7 | - package-ecosystem: "gomod" 8 | directories: 9 | - "/" 10 | schedule: 11 | interval: daily 12 | open-pull-requests-limit: 10 13 | commit-message: 14 | prefix: "[gomod] " 15 | - package-ecosystem: "github-actions" 16 | directory: "/" 17 | schedule: 18 | interval: daily 19 | open-pull-requests-limit: 10 20 | commit-message: 21 | prefix: "[gha] " 22 | - package-ecosystem: "docker" 23 | directories: 24 | - "/" 25 | schedule: 26 | interval: daily 27 | open-pull-requests-limit: 10 28 | commit-message: 29 | prefix: "[docker] " 30 | -------------------------------------------------------------------------------- /.github/workflows/post-merge.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | name: Post-Merge CI Pipeline 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | - release-* 11 | workflow_dispatch: 12 | 13 | permissions: {} 14 | 15 | jobs: 16 | post-merge: 17 | permissions: 18 | # it combines `read-all` with `contents: write` and `security-events: write` - all of them needed by `post-merge.yml` workflow 19 | contents: write 20 | actions: read 21 | attestations: read 22 | checks: read 23 | deployments: read 24 | id-token: write 25 | issues: read 26 | models: read 27 | discussions: read 28 | packages: read 29 | pages: read 30 | pull-requests: read 31 | repository-projects: read 32 | security-events: write 33 | statuses: read 34 | uses: open-edge-platform/orch-ci/.github/workflows/post-merge.yml@37eef2d2a0909dfe8ff26bb0730ab2f13dfbcaf6 # 0.1.25 35 | with: 36 | run_version_check: true 37 | run_version_tag: true 38 | bootstrap_tools: "go,gotools,nodejs,python,golangci-lint2,helm,shellcheck,hadolint,yq,jq,protolint" 39 | run_dep_version_check: false 40 | cache_go: true 41 | run_build: true 42 | # run_lint and run_test - to have full, reusable cache for all PRs 43 | # https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/caching-dependencies-to-speed-up-workflows#restrictions-for-accessing-a-cache 44 | run_lint: true 45 | run_test: true 46 | remove_cache_go: true 47 | run_docker_build: true 48 | run_docker_push: true 49 | run_helm_build: true 50 | run_helm_push: true 51 | run_version_dev: false 52 | secrets: 53 | SYS_ORCH_GITHUB: ${{ secrets.SYS_ORCH_GITHUB }} 54 | COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }} 55 | COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }} 56 | NO_AUTH_ECR_PUSH_USERNAME: ${{ secrets.NO_AUTH_ECR_PUSH_USERNAME }} 57 | NO_AUTH_ECR_PUSH_PASSWD: ${{ secrets.NO_AUTH_ECR_PUSH_PASSWD }} 58 | MSTEAMS_WEBHOOK: ${{ secrets.TEAMS_WEBHOOK }} 59 | -------------------------------------------------------------------------------- /.github/workflows/pre-merge.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | name: Pre-Merge CI Pipeline 5 | 6 | on: 7 | pull_request: 8 | branches: 9 | - main 10 | - release-* 11 | workflow_dispatch: 12 | 13 | permissions: {} 14 | 15 | jobs: 16 | pre-merge: 17 | permissions: 18 | contents: read 19 | uses: open-edge-platform/orch-ci/.github/workflows/pre-merge.yml@37eef2d2a0909dfe8ff26bb0730ab2f13dfbcaf6 # 0.1.25 20 | with: 21 | run_reuse_check: true 22 | run_version_check: true 23 | bootstrap_tools: "go,gotools,nodejs,python,golangci-lint2,helm,shellcheck,hadolint,yq,jq,protolint" 24 | run_dep_version_check: false 25 | cache_go: true 26 | run_build: true 27 | run_lint: true 28 | run_test: true 29 | remove_cache_go: true 30 | run_validate_clean_folder: false 31 | run_docker_build: true 32 | run_helm_build: true 33 | run_artifact: true 34 | artifacts_path: | 35 | ./build/coverage.out 36 | ./build/coverage.xml 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | .idea 5 | .vscode 6 | build/ 7 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | version: "2" 5 | 6 | linters: 7 | # Default set of linters. 8 | # The value can be: `standard`, `all`, `none`, or `fast`. 9 | # Default: standard 10 | default: none 11 | 12 | # Enable specific linter. 13 | # https://golangci-lint.run/usage/linters/#enabled-by-default 14 | enable: 15 | - asasalint 16 | - asciicheck 17 | - bidichk 18 | - bodyclose 19 | - copyloopvar 20 | - decorder 21 | - dogsled 22 | - dupword 23 | - durationcheck 24 | - errcheck 25 | - errchkjson 26 | - errname 27 | - errorlint 28 | - exhaustive 29 | - exptostd 30 | - ginkgolinter 31 | - gocheckcompilerdirectives 32 | - gocritic 33 | - godot 34 | - goprintffuncname 35 | - gosec 36 | - govet 37 | - ineffassign 38 | - interfacebloat 39 | - lll 40 | - makezero 41 | - mirror 42 | - misspell 43 | - musttag 44 | - nakedret 45 | - nestif 46 | - nilerr 47 | - nilnesserr 48 | - nolintlint 49 | - perfsprint 50 | - prealloc 51 | - predeclared 52 | - reassign 53 | - revive 54 | - sqlclosecheck 55 | - staticcheck 56 | - testifylint 57 | - tparallel 58 | - unconvert 59 | - unparam 60 | - unused 61 | - usestdlibvars 62 | - usetesting 63 | - wastedassign 64 | - whitespace 65 | 66 | settings: 67 | errcheck: 68 | # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`. 69 | # Such cases aren't reported by default. 70 | # Default: false 71 | check-blank: true 72 | 73 | # List of functions to exclude from checking, where each entry is a single function to exclude. 74 | # See https://github.com/kisielk/errcheck#excluding-functions for details. 75 | exclude-functions: 76 | - '(*hash/maphash.Hash).Write' 77 | - '(*hash/maphash.Hash).WriteByte' 78 | - '(*hash/maphash.Hash).WriteString' 79 | 80 | exhaustive: 81 | # Presence of "default" case in switch statements satisfies exhaustiveness, 82 | # even if all enum members are not listed. 83 | # Default: false 84 | default-signifies-exhaustive: true 85 | 86 | gocritic: 87 | # Disable all checks. 88 | # Default: false 89 | disable-all: true 90 | # Which checks should be enabled in addition to default checks; can't be combined with 'disabled-checks'. 91 | # By default, list of stable checks is used (https://go-critic.com/overview#checks-overview). 92 | # To see which checks are enabled run `GL_DEBUG=gocritic golangci-lint run --enable=gocritic`. 93 | enabled-checks: 94 | # diagnostic 95 | - argOrder 96 | - badCall 97 | - badCond 98 | - badLock 99 | - badRegexp 100 | - badSorting 101 | - badSyncOnceFunc 102 | - builtinShadowDecl 103 | - caseOrder 104 | - codegenComment 105 | - commentedOutCode 106 | - deferInLoop 107 | - deprecatedComment 108 | - dupArg 109 | - dupBranchBody 110 | - dupCase 111 | - dupSubExpr 112 | - dynamicFmtString 113 | - emptyDecl 114 | - evalOrder 115 | - exitAfterDefer 116 | - externalErrorReassign 117 | - filepathJoin 118 | - flagName 119 | - mapKey 120 | - nilValReturn 121 | - offBy1 122 | - regexpPattern 123 | - sloppyLen 124 | - sloppyReassign 125 | - sloppyTypeAssert 126 | - sortSlice 127 | - sprintfQuotedString 128 | - sqlQuery 129 | - syncMapLoadAndDelete 130 | - truncateCmp 131 | - uncheckedInlineErr 132 | - unnecessaryDefer 133 | - weakCond 134 | # performance 135 | - appendCombine 136 | - equalFold 137 | - hugeParam 138 | - indexAlloc 139 | - preferDecodeRune 140 | - preferFprint 141 | - preferStringWriter 142 | - preferWriteByte 143 | - rangeExprCopy 144 | - rangeValCopy 145 | - sliceClear 146 | - stringXbytes 147 | 148 | # Settings passed to gocritic. 149 | # The settings key is the name of a supported gocritic checker. 150 | # The list of supported checkers can be found at https://go-critic.com/overview. 151 | settings: 152 | hugeParam: 153 | # Size in bytes that makes the warning trigger. 154 | # Default: 80 155 | sizeThreshold: 512 156 | rangeValCopy: 157 | # Size in bytes that makes the warning trigger. 158 | # Default: 128 159 | sizeThreshold: 512 160 | 161 | gosec: 162 | # To select a subset of rules to run. 163 | # Available rules: https://github.com/securego/gosec#available-rules 164 | # Default: [] - means include all rules 165 | includes: 166 | - G101 # Look for hard coded credentials 167 | - G102 # Bind to all interfaces 168 | - G103 # Audit the use of unsafe block 169 | - G106 # Audit the use of ssh.InsecureIgnoreHostKey 170 | - G107 # Url provided to HTTP request as taint input 171 | - G108 # Profiling endpoint automatically exposed on /debug/pprof 172 | - G109 # Potential Integer overflow made by strconv.Atoi result conversion to int16/32 173 | - G110 # Potential DoS vulnerability via decompression bomb 174 | - G111 # Potential directory traversal 175 | - G112 # Potential slowloris attack 176 | - G114 # Use of net/http serve function that has no support for setting timeouts 177 | - G201 # SQL query construction using format string 178 | - G202 # SQL query construction using string concatenation 179 | - G203 # Use of unescaped data in HTML templates 180 | - G301 # Poor file permissions used when creating a directory 181 | - G302 # Poor file permissions used with chmod 182 | - G303 # Creating tempfile using a predictable path 183 | - G305 # File traversal when extracting zip/tar archive 184 | - G306 # Poor file permissions used when writing to a new file 185 | - G401 # Detect the usage of MD5 or SHA1 186 | - G403 # Ensure minimum RSA key length of 2048 bits 187 | - G404 # Insecure random number source (rand) 188 | - G405 # Detect the usage of DES or RC4 189 | - G406 # Detect the usage of MD4 or RIPEMD160 190 | - G501 # Import blocklist: crypto/md5 191 | - G502 # Import blocklist: crypto/des 192 | - G503 # Import blocklist: crypto/rc4 193 | - G505 # Import blocklist: crypto/sha1 194 | - G506 # Import blocklist: golang.org/x/crypto/md4 195 | - G507 # Import blocklist: golang.org/x/crypto/ripemd160 196 | - G601 # Implicit memory aliasing of items from a range statement 197 | - G602 # Slice access out of bounds 198 | # G104, G105, G113, G204, G304, G307, G402, G504 were not enabled intentionally 199 | # TODO: review G115 when reporting false positives is fixed (https://github.com/securego/gosec/issues/1212) 200 | 201 | # To specify the configuration of rules. 202 | config: 203 | # Maximum allowed permissions mode for os.OpenFile and os.Chmod 204 | # Default: "0600" 205 | G302: "0640" 206 | # Maximum allowed permissions mode for os.WriteFile and ioutil.WriteFile 207 | # Default: "0600" 208 | G306: "0640" 209 | 210 | lll: 211 | # Max line length, lines longer will be reported. 212 | # '\t' is counted as 1 character by default, and can be changed with the tab-width option. 213 | # Default: 120. 214 | line-length: 160 215 | # Tab width in spaces. 216 | # Default: 1 217 | tab-width: 4 218 | 219 | nakedret: 220 | # Make an issue if func has more lines of code than this setting, and it has naked returns. 221 | # Default: 30 222 | max-func-lines: 1 223 | 224 | nolintlint: 225 | # Enable to require an explanation of nonzero length after each nolint directive. 226 | # Default: false 227 | require-explanation: true 228 | # Enable to require nolint directives to mention the specific linter being suppressed. 229 | # Default: false 230 | require-specific: true 231 | 232 | prealloc: 233 | # Report pre-allocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them. 234 | # Default: true 235 | simple: false 236 | 237 | revive: 238 | # Sets the default severity. 239 | # See https://github.com/mgechev/revive#configuration 240 | # Default: warning 241 | severity: error 242 | 243 | # Run `GL_DEBUG=revive golangci-lint run --enable-only=revive` to see default, all available rules, and enabled rules. 244 | rules: 245 | - name: argument-limit 246 | arguments: [ 6 ] 247 | - name: atomic 248 | - name: bare-return 249 | - name: blank-imports 250 | - name: bool-literal-in-expr 251 | - name: call-to-gc 252 | - name: comment-spacings 253 | - name: confusing-naming 254 | - name: confusing-results 255 | - name: constant-logical-expr 256 | - name: context-as-argument 257 | - name: context-keys-type 258 | - name: datarace 259 | - name: deep-exit 260 | - name: defer 261 | - name: dot-imports 262 | - name: duplicated-imports 263 | - name: early-return 264 | - name: empty-block 265 | - name: empty-lines 266 | - name: error-naming 267 | - name: error-return 268 | - name: error-strings 269 | - name: errorf 270 | - name: flag-parameter 271 | - name: function-result-limit 272 | arguments: [ 4 ] 273 | - name: get-return 274 | - name: identical-branches 275 | - name: if-return 276 | - name: import-alias-naming 277 | arguments: 278 | - "^[a-z][a-z0-9_]*[a-z0-9]+$" 279 | - name: import-shadowing 280 | - name: increment-decrement 281 | - name: indent-error-flow 282 | - name: max-public-structs 283 | arguments: [ 5 ] 284 | exclude: [ "TEST" ] 285 | - name: modifies-parameter 286 | - name: modifies-value-receiver 287 | - name: optimize-operands-order 288 | - name: package-comments 289 | - name: range 290 | - name: range-val-address 291 | - name: range-val-in-closure 292 | - name: receiver-naming 293 | - name: redefines-builtin-id 294 | - name: redundant-import-alias 295 | - name: string-of-int 296 | - name: struct-tag 297 | - name: superfluous-else 298 | - name: time-equal 299 | - name: time-naming 300 | - name: unconditional-recursion 301 | - name: unexported-naming 302 | - name: unnecessary-stmt 303 | - name: unreachable-code 304 | - name: unused-parameter 305 | - name: unused-receiver 306 | - name: var-declaration 307 | - name: var-naming 308 | - name: waitgroup-by-value 309 | 310 | testifylint: 311 | # Disable all checkers (https://github.com/Antonboom/testifylint#checkers). 312 | # Default: false 313 | disable-all: true 314 | # Enable checkers by name 315 | enable: 316 | - blank-import 317 | - bool-compare 318 | - compares 319 | - contains 320 | - empty 321 | - encoded-compare 322 | - error-is-as 323 | - error-nil 324 | - expected-actual 325 | - float-compare 326 | - formatter 327 | - go-require 328 | - len 329 | - negative-positive 330 | - nil-compare 331 | - regexp 332 | - require-error 333 | - suite-broken-parallel 334 | - suite-dont-use-pkg 335 | - suite-extra-assert-call 336 | - suite-subtest-run 337 | - suite-thelper 338 | - useless-assert 339 | go-require: 340 | # To ignore HTTP handlers (like http.HandlerFunc). 341 | # Default: false 342 | ignore-http-handlers: true 343 | 344 | usetesting: 345 | # Enable/disable `os.TempDir()` detections. 346 | # Default: false 347 | os-temp-dir: true 348 | 349 | # Defines a set of rules to ignore issues. 350 | # It does not skip the analysis, and so does not ignore "typecheck" errors. 351 | exclusions: 352 | # Mode of the generated files analysis. 353 | # 354 | # - `strict`: sources are excluded by strictly following the Go generated file convention. 355 | # Source files that have lines matching only the following regular expression will be excluded: `^// Code generated .* DO NOT EDIT\.$` 356 | # This line must appear before the first non-comment, non-blank text in the file. 357 | # https://go.dev/s/generatedcode 358 | # - `lax`: sources are excluded if they contain lines like `autogenerated file`, `code generated`, `do not edit`, etc. 359 | # - `disable`: disable the generated files exclusion. 360 | # 361 | # Default: strict 362 | generated: lax 363 | 364 | # Excluding configuration per-path, per-linter, per-text and per-source. 365 | rules: 366 | # gosec:G101 367 | - path: _test\.go 368 | text: "Potential hardcoded credentials" 369 | 370 | # revive:dot-imports 371 | - path: (.+)_test\.go 372 | text: should not use dot imports 373 | 374 | # EXC0001 errcheck: Almost all programs ignore errors on these functions, and in most cases it's ok 375 | - path: (.+)\.go$ 376 | text: Error return value of .((os\.)?std(out|err)\..*|.*Close.*|.*close.*|.*Flush|.*Disconnect|.*disconnect|.*Clear|os\.Remove(All)?|.*print(f|ln)?|os\.Setenv|os\.Unsetenv). is not checked 377 | 378 | # EXC0015 revive: Annoying issue about not having a comment. The rare codebase has such comments 379 | - path: (.+)\.go$ 380 | text: should have a package comment 381 | 382 | formatters: 383 | # Enable specific formatter. 384 | # Default: [] (uses standard Go formatting) 385 | enable: 386 | - gci 387 | 388 | # Formatters settings. 389 | settings: 390 | gci: 391 | # Section configuration to compare against. 392 | # Section names are case-insensitive and may contain parameters in (). 393 | # The default order of sections is `standard > default > custom > blank > dot > alias > localmodule`. 394 | # If `custom-order` is `true`, it follows the order of `sections` option. 395 | # Default: ["standard", "default"] 396 | sections: 397 | - standard # Standard section: captures all standard packages. 398 | - default # Default section: contains all imports that could not be matched to another section type. 399 | - localmodule # Local module section: contains all local packages. This section is not present unless explicitly enabled. 400 | 401 | exclusions: 402 | # Mode of the generated files analysis. 403 | # 404 | # - `strict`: sources are excluded by strictly following the Go generated file convention. 405 | # Source files that have lines matching only the following regular expression will be excluded: `^// Code generated .* DO NOT EDIT\.$` 406 | # This line must appear before the first non-comment, non-blank text in the file. 407 | # https://go.dev/s/generatedcode 408 | # - `lax`: sources are excluded if they contain lines like `autogenerated file`, `code generated`, `do not edit`, etc. 409 | # - `disable`: disable the generated files exclusion. 410 | # 411 | # Default: lax 412 | generated: lax 413 | 414 | issues: 415 | # Maximum issues count per one linter. 416 | # Set to 0 to disable. 417 | # Default: 50 418 | max-issues-per-linter: 0 419 | 420 | # Maximum count of issues with the same text. 421 | # Set to 0 to disable. 422 | # Default: 3 423 | max-same-issues: 0 424 | 425 | # Make issues output unique by line. 426 | # Default: true 427 | uniq-by-line: false 428 | 429 | # Output configuration options. 430 | output: 431 | # The formats used to render issues. 432 | formats: 433 | # Prints issues in columns representation separated by tabulations. 434 | tab: 435 | # Output path can be either `stdout`, `stderr` or path to the file to write to. 436 | # Default: stdout 437 | path: stdout 438 | 439 | # Order to use when sorting results. 440 | # Possible values: `file`, `linter`, and `severity`. 441 | # 442 | # If the severity values are inside the following list, they are ordered in this order: 443 | # 1. error 444 | # 2. warning 445 | # 3. high 446 | # 4. medium 447 | # 5. low 448 | # Either they are sorted alphabetically. 449 | # 450 | # Default: ["linter", "file"] 451 | sort-order: 452 | - file # filepath, line, and column. 453 | - linter 454 | 455 | # Show statistics per linter. 456 | # Default: true 457 | show-stats: true 458 | 459 | severity: 460 | # Set the default severity for issues. 461 | # 462 | # If severity rules are defined and the issues do not match or no severity is provided to the rule 463 | # this will be the default severity applied. 464 | # Severities should match the supported severity names of the selected out format. 465 | # - Code climate: https://docs.codeclimate.com/docs/issues#issue-severity 466 | # - Checkstyle: https://checkstyle.sourceforge.io/property_types.html#SeverityLevel 467 | # - GitHub: https://help.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message 468 | # - TeamCity: https://www.jetbrains.com/help/teamcity/service-messages.html#Inspection+Instance 469 | # 470 | # `@linter` can be used as severity value to keep the severity from linters (e.g. revive, gosec, ...) 471 | # 472 | # Default: "" 473 | default: error 474 | -------------------------------------------------------------------------------- /.markdownlint.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | # The rule definition can be found here: 5 | # https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md 6 | 7 | default: true 8 | MD004: 9 | style: dash 10 | MD010: 11 | # Code blocks may have hard tabs. 12 | code_blocks: false 13 | MD013: false # Disable line length checking. 14 | MD024: false # Allow duplicate headers. 15 | MD026: 16 | punctuation: ".,;:!。,;:!" 17 | MD029: 18 | style: one 19 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | ginkgo 2.22.2 2 | golang 1.24.2 3 | golangci-lint 2.1.2 4 | hadolint 2.12.0 5 | helm 3.17.0 6 | jq 1.7.1 7 | kind 0.26.0 8 | markdownlint-cli2 0.17.2 9 | pipx 1.7.1 10 | protoc 29.3 11 | protoc-gen-go 1.36.4 12 | protoc-gen-go-grpc 1.5.1 13 | protolint 0.53.0 14 | shellcheck 0.10.0 15 | yamllint 1.35.1 16 | yq 4.45.1 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | # Observability Tenant Controller Changelog 7 | 8 | ## [v0.5.43](https://github.com/open-edge-platform/o11y-tenant-controller/tree/v0.5.43) 9 | 10 | - Initial release 11 | - Application `observability-tenant-controller` added: 12 | - Monitors [tenancy-datamodel](https://github.com/open-edge-platform/orch-utils/tree/main/tenancy-datamodel) for project (tenant) creation and removal events 13 | - Reconfigures [alerting-monitor]( https://github.com/open-edge-platform/o11y-alerting-monitor), [edgenode-observability](https://github.com/open-edge-platform/o11y-charts/tree/main/charts/edgenode-observability) and [sre-exporter](https://github.com/open-edge-platform/o11y-sre-exporter) (optionally) 14 | - Streams current project data via `gRPC` to [grafana-proxy](https://github.com/open-edge-platform/o11y-charts/tree/main/apps/grafana-proxy) instances 15 | - Exposes project details via `project_metadata` metric in [Prometheus](https://prometheus.io/docs/concepts/data_model/) format 16 | - Enables `strict` or `loose` tenant data removal verification options 17 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | # Contributor Covenant Code of Conduct 7 | 8 | ## Our Pledge 9 | 10 | We as members, contributors, and leaders pledge to make participation in our 11 | community a harassment-free experience for everyone, regardless of age, body 12 | size, visible or invisible disability, ethnicity, sex characteristics, gender 13 | identity and expression, level of experience, education, socio-economic status, 14 | nationality, personal appearance, race, caste, color, religion, or sexual 15 | identity and orientation. 16 | 17 | We pledge to act and interact in ways that contribute to an open, welcoming, 18 | diverse, inclusive, and healthy community. 19 | 20 | ## Our Standards 21 | 22 | Examples of behavior that contributes to a positive environment for our 23 | community include: 24 | 25 | - Demonstrating empathy and kindness toward other people 26 | - Being respectful of differing opinions, viewpoints, and experiences 27 | - Giving and gracefully accepting constructive feedback 28 | - Accepting responsibility and apologizing to those affected by our mistakes, 29 | and learning from the experience 30 | - Focusing on what is best not just for us as individuals, but for the overall 31 | community 32 | 33 | Examples of unacceptable behavior include: 34 | 35 | - The use of sexualized language or imagery, and sexual attention or advances of 36 | any kind 37 | - Trolling, insulting or derogatory comments, and personal or political attacks 38 | - Public or private harassment 39 | - Publishing others' private information, such as a physical or email address, 40 | without their explicit permission 41 | - Other conduct which could reasonably be considered inappropriate in a 42 | professional setting 43 | 44 | ## Enforcement Responsibilities 45 | 46 | Community leaders are responsible for clarifying and enforcing our standards of 47 | acceptable behavior and will take appropriate and fair corrective action in 48 | response to any behavior that they deem inappropriate, threatening, offensive, 49 | or harmful. 50 | 51 | Community leaders have the right and responsibility to remove, edit, or reject 52 | comments, commits, code, wiki edits, issues, and other contributions that are 53 | not aligned to this Code of Conduct, and will communicate reasons for moderation 54 | decisions when appropriate. 55 | 56 | ## Scope 57 | 58 | This Code of Conduct applies within all community spaces, and also applies when 59 | an individual is officially representing the community in public spaces. 60 | Examples of representing our community include using an official e-mail address, 61 | posting via an official social media account, or acting as an appointed 62 | representative at an online or offline event. 63 | 64 | ## Enforcement 65 | 66 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 67 | reported to the community leaders responsible for enforcement at 68 | CommunityCodeOfConduct AT intel DOT com. 69 | All complaints will be reviewed and investigated promptly and fairly. 70 | 71 | All community leaders are obligated to respect the privacy and security of the 72 | reporter of any incident. 73 | 74 | ## Enforcement Guidelines 75 | 76 | Community leaders will follow these Community Impact Guidelines in determining 77 | the consequences for any action they deem in violation of this Code of Conduct: 78 | 79 | ### 1. Correction 80 | 81 | **Community Impact**: Use of inappropriate language or other behavior deemed 82 | unprofessional or unwelcome in the community. 83 | 84 | **Consequence**: A private, written warning from community leaders, providing 85 | clarity around the nature of the violation and an explanation of why the 86 | behavior was inappropriate. A public apology may be requested. 87 | 88 | ### 2. Warning 89 | 90 | **Community Impact**: A violation through a single incident or series of 91 | actions. 92 | 93 | **Consequence**: A warning with consequences for continued behavior. No 94 | interaction with the people involved, including unsolicited interaction with 95 | those enforcing the Code of Conduct, for a specified period of time. This 96 | includes avoiding interactions in community spaces as well as external channels 97 | like social media. Violating these terms may lead to a temporary or permanent 98 | ban. 99 | 100 | ### 3. Temporary Ban 101 | 102 | **Community Impact**: A serious violation of community standards, including 103 | sustained inappropriate behavior. 104 | 105 | **Consequence**: A temporary ban from any sort of interaction or public 106 | communication with the community for a specified period of time. No public or 107 | private interaction with the people involved, including unsolicited interaction 108 | with those enforcing the Code of Conduct, is allowed during this period. 109 | Violating these terms may lead to a permanent ban. 110 | 111 | ### 4. Permanent Ban 112 | 113 | **Community Impact**: Demonstrating a pattern of violation of community 114 | standards, including sustained inappropriate behavior, harassment of an 115 | individual, or aggression toward or disparagement of classes of individuals. 116 | 117 | **Consequence**: A permanent ban from any sort of public interaction within the 118 | community. 119 | 120 | ## Attribution 121 | 122 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 123 | version 2.1, available at 124 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 125 | 126 | Community Impact Guidelines were inspired by 127 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 128 | 129 | For answers to common questions about this code of conduct, see the FAQ at 130 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 131 | [https://www.contributor-covenant.org/translations][translations]. 132 | 133 | [homepage]: https://www.contributor-covenant.org 134 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 135 | [Mozilla CoC]: https://github.com/mozilla/diversity 136 | [FAQ]: https://www.contributor-covenant.org/faq 137 | [translations]: https://www.contributor-covenant.org/translations 138 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | # Building environment 5 | FROM golang:1.24.4-alpine@sha256:68932fa6d4d4059845c8f40ad7e654e626f3ebd3706eef7846f319293ab5cb7a AS build 6 | 7 | WORKDIR /workspace 8 | 9 | RUN apk add --upgrade --no-cache make=~4 bash=~5 10 | 11 | COPY . . 12 | 13 | RUN make build 14 | 15 | # Run tenant controller container 16 | FROM alpine:3.22@sha256:8a1f59ffb675680d47db6337b49d22281a139e9d709335b492be023728e11715 17 | 18 | COPY --from=build /workspace/build/observability-tenant-controller /observability-tenant-controller 19 | 20 | RUN addgroup -S tcontroller && adduser -S tcontroller -G tcontroller 21 | USER tcontroller 22 | 23 | EXPOSE 9273 50051 24 | 25 | ENTRYPOINT ["/observability-tenant-controller"] 26 | -------------------------------------------------------------------------------- /LICENSES/Apache-2.0.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | SHELL := bash -eu -o pipefail 5 | 6 | PROJECT_NAME := observability-tenant-controller 7 | 8 | ## Labels to add Docker/Helm/Service CI meta-data. 9 | LABEL_REVISION = $(shell git rev-parse HEAD) 10 | LABEL_CREATED ?= $(shell date -u "+%Y-%m-%dT%H:%M:%SZ") 11 | 12 | VERSION ?= $(shell cat VERSION | tr -d '[:space:]') 13 | BUILD_DIR ?= ./build 14 | 15 | ## CHART_NAME is specified in Chart.yaml 16 | CHART_NAME ?= $(PROJECT_NAME) 17 | ## CHART_VERSION is specified in Chart.yaml 18 | CHART_VERSION ?= $(shell grep "version:" ./deployments/$(PROJECT_NAME)/Chart.yaml | cut -d ':' -f 2 | tr -d '[:space:]') 19 | ## CHART_APP_VERSION is modified on every commit 20 | CHART_APP_VERSION ?= $(VERSION) 21 | ## CHART_BUILD_DIR is given based on repo structure 22 | CHART_BUILD_DIR ?= $(BUILD_DIR)/chart/ 23 | ## CHART_PATH is given based on repo structure 24 | CHART_PATH ?= "./deployments/$(CHART_NAME)" 25 | ## CHART_NAMESPACE can be modified here 26 | CHART_NAMESPACE ?= orch-platform 27 | ## CHART_TEST_NAMESPACE can be modified here 28 | CHART_TEST_NAMESPACE ?= orch-infra 29 | ## CHART_RELEASE can be modified here 30 | CHART_RELEASE ?= $(PROJECT_NAME) 31 | 32 | REGISTRY ?= 080137407410.dkr.ecr.us-west-2.amazonaws.com 33 | REGISTRY_NO_AUTH ?= edge-orch 34 | REPOSITORY ?= o11y 35 | REPOSITORY_NO_AUTH := $(REGISTRY)/$(REGISTRY_NO_AUTH)/$(REPOSITORY) 36 | DOCKER_IMAGE_NAME ?= $(PROJECT_NAME) 37 | DOCKER_IMAGE_TAG ?= $(VERSION) 38 | 39 | TEST_JOB_NAME ?= observability-tenant-controller-test 40 | DOCKER_FILES_TO_LINT := $(shell find . -type f -name 'Dockerfile*' -print ) 41 | 42 | GOCMD := CGO_ENABLED=0 GOARCH=amd64 GOOS=linux go 43 | GOCMD_TEST := CGO_ENABLED=1 GOARCH=amd64 GOOS=linux go 44 | GOEXTRAFLAGS :=-trimpath -gcflags="all=-spectre=all -N -l" -asmflags="all=-spectre=all" -ldflags="all=-s -w -X main.version=$(shell cat ./VERSION) -X google.golang.org/protobuf/reflect/protoregistry.conflictPolicy=warn" 45 | 46 | .DEFAULT_GOAL := help 47 | .PHONY: build 48 | 49 | ## CI Mandatory Targets start 50 | dependency-check: 51 | @# Help: Unsupported target 52 | @echo '"make $@" is unsupported' 53 | 54 | build: 55 | @# Help: Builds tenant-controller 56 | @echo "---MAKEFILE BUILD---" 57 | $(GOCMD) build $(GOEXTRAFLAGS) -o $(BUILD_DIR)/$(PROJECT_NAME) ./cmd/$(PROJECT_NAME)/$(PROJECT_NAME).go 58 | @echo "---END MAKEFILE BUILD---" 59 | 60 | lint: lint-go lint-markdown lint-yaml lint-proto lint-json lint-shell lint-helm lint-docker 61 | @# Help: Runs all linters 62 | 63 | test: 64 | @# Help: Runs tests and creates a coverage report 65 | @echo "---MAKEFILE TEST---" 66 | $(GOCMD_TEST) test $$(go list ./... | grep -v /cmd/observability-tenant-controller) --race -coverprofile $(BUILD_DIR)/coverage.out -covermode atomic 67 | gocover-cobertura < $(BUILD_DIR)/coverage.out > $(BUILD_DIR)/coverage.xml 68 | @echo "---END MAKEFILE TEST---" 69 | 70 | docker-build: 71 | @# Help: Builds docker image 72 | @echo "---MAKEFILE DOCKER-BUILD---" 73 | docker rmi $(REPOSITORY_NO_AUTH)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG) --force 74 | docker build -f Dockerfile \ 75 | -t $(REPOSITORY_NO_AUTH)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG) \ 76 | --build-arg http_proxy="$(http_proxy)" --build-arg https_proxy="$(https_proxy)" --build-arg no_proxy="$(no_proxy)" \ 77 | --platform linux/amd64 --no-cache . 78 | @echo "---END MAKEFILE DOCKER-BUILD---" 79 | 80 | helm-build: helm-clean 81 | @# Help: Builds the helm chart 82 | @echo "---MAKEFILE HELM-BUILD---" 83 | yq eval -i '.version = "$(VERSION)"' $(CHART_PATH)/Chart.yaml 84 | yq eval -i '.appVersion = "$(VERSION)"' $(CHART_PATH)/Chart.yaml 85 | yq eval -i '.annotations.revision = "$(LABEL_REVISION)"' $(CHART_PATH)/Chart.yaml 86 | yq eval -i '.annotations.created = "$(LABEL_CREATED)"' $(CHART_PATH)/Chart.yaml 87 | helm package \ 88 | --app-version=$(CHART_APP_VERSION) \ 89 | --debug \ 90 | --dependency-update \ 91 | --destination $(CHART_BUILD_DIR) \ 92 | $(CHART_PATH) 93 | 94 | @echo "---END MAKEFILE HELM-BUILD---" 95 | 96 | docker-push: 97 | @# Help: Pushes the docker image 98 | @echo "---MAKEFILE DOCKER-PUSH---" 99 | aws ecr create-repository --region us-west-2 --repository-name $(REGISTRY_NO_AUTH)/$(REPOSITORY)/$(DOCKER_IMAGE_NAME) || true 100 | docker push $(REPOSITORY_NO_AUTH)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG) 101 | @echo "---END MAKEFILE DOCKER-PUSH---" 102 | 103 | helm-push: 104 | @# Help: Pushes the helm chart 105 | @echo "---MAKEFILE HELM-PUSH---" 106 | aws ecr create-repository --region us-west-2 --repository-name $(REGISTRY_NO_AUTH)/$(REPOSITORY)/charts/$(CHART_NAME) || true 107 | helm push $(CHART_BUILD_DIR)$(CHART_NAME)*.tgz oci://$(REPOSITORY_NO_AUTH)/charts 108 | @echo "---END MAKEFILE HELM-PUSH---" 109 | 110 | docker-list: ## Print name of docker container image 111 | @echo "images:" 112 | @echo " $(DOCKER_IMAGE_NAME):" 113 | @echo " name: '$(REPOSITORY_NO_AUTH)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG)'" 114 | @echo " version: '$(DOCKER_IMAGE_TAG)'" 115 | @echo " gitTagPrefix: 'v'" 116 | @echo " buildTarget: 'docker-build'" 117 | 118 | helm-list: ## List helm charts, tag format, and versions in YAML format 119 | @echo "charts:" ;\ 120 | echo " $(CHART_NAME):" ;\ 121 | echo -n " "; grep "^version" "${CHART_PATH}/Chart.yaml" ;\ 122 | echo " gitTagPrefix: 'v'" ;\ 123 | echo " outDir: '${CHART_BUILD_DIR}'" ;\ 124 | 125 | ## CI Mandatory Targets end 126 | 127 | ## Helper Targets start 128 | all: clean build lint test 129 | @# Help: Runs clean, build, lint, test targets 130 | 131 | clean: 132 | @# Help: Deletes build directory 133 | @echo "---MAKEFILE CLEAN---" 134 | rm -rf $(BUILD_DIR) 135 | @echo "---END MAKEFILE CLEAN---" 136 | 137 | helm-clean: 138 | @# Help: Cleans the build directory of the helm chart 139 | @echo "---MAKEFILE HELM-CLEAN---" 140 | rm -rf $(CHART_BUILD_DIR) 141 | @echo "---END MAKEFILE HELM-CLEAN---" 142 | 143 | lint-go: 144 | @# Help: Runs linters for golang source code files 145 | @echo "---MAKEFILE LINT-GO---" 146 | golangci-lint -v run 147 | @echo "---END MAKEFILE LINT-GO---" 148 | 149 | lint-markdown: 150 | @# Help: Runs linter for markdown files 151 | @echo "---MAKEFILE LINT-MARKDOWN---" 152 | markdownlint-cli2 '**/*.md' "!.github" "!**/ci/*" 153 | @echo "---END MAKEFILE LINT-MARKDOWN---" 154 | 155 | lint-yaml: 156 | @# Help: Runs linter for for yaml files 157 | @echo "---MAKEFILE LINT-YAML---" 158 | yamllint -v 159 | yamllint -f parsable -c yamllint-conf.yaml . 160 | @echo "---END MAKEFILE LINT-YAML---" 161 | 162 | lint-proto: 163 | @# Help: Runs linter for for proto files 164 | @echo "---MAKEFILE LINT-PROTO---" 165 | protolint version 166 | protolint lint -reporter unix api/ 167 | @echo "---END MAKEFILE LINT-PROTO---" 168 | 169 | lint-json: 170 | @# Help: Runs linter for json files 171 | @echo "---MAKEFILE LINT-JSON---" 172 | ./scripts/lintJsons.sh 173 | @echo "---END MAKEFILE LINT-JSON---" 174 | 175 | lint-shell: 176 | @# Help: Runs linter for shell scripts 177 | @echo "---MAKEFILE LINT-SHELL---" 178 | shellcheck --version 179 | shellcheck ***/*.sh 180 | @echo "---END MAKEFILE LINT-SHELL---" 181 | 182 | lint-helm: 183 | @# Help: Runs linter for helm chart 184 | @echo "---MAKEFILE LINT-HELM---" 185 | helm version 186 | helm lint --strict $(CHART_PATH) --values $(CHART_PATH)/values.yaml 187 | @echo "---END MAKEFILE LINT-HELM---" 188 | 189 | lint-docker: 190 | @# Help: Runs linter for docker files 191 | @echo "---MAKEFILE LINT-DOCKER---" 192 | hadolint --version 193 | hadolint $(DOCKER_FILES_TO_LINT) 194 | @echo "---END MAKEFILE LINT-DOCKER---" 195 | 196 | lint-license: 197 | @# Help: Runs license check 198 | @echo "---MAKEFILE LINT-LICENSE---" 199 | reuse --version 200 | reuse --root . lint 201 | @echo "---END MAKEFILE LINT-LICENSE---" 202 | 203 | kind-all: helm-clean docker-build kind-load helm-build 204 | @# Help: Builds docker image and loads it into the kind cluster and builds the helm chart 205 | 206 | kind-load: 207 | @# Help: Loads docker image into the kind cluster 208 | @echo "---MAKEFILE KIND-LOAD---" 209 | kind load docker-image $(DOCKER_REGISTRY)$(DOCKER_REPOSITORY)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG) 210 | @echo "---END MAKEFILE KIND-LOAD---" 211 | 212 | proto: 213 | @# Help: Regenerates proto-based code 214 | @echo "---MAKEFILE PROTO---" 215 | # Requires installed: protoc, protoc-gen-go and protoc-gen-go-grpc 216 | # See: https://grpc.io/docs/languages/go/quickstart/ 217 | protoc api/*.proto --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative --proto_path=. 218 | @echo "---END-MAKEFILE PROTO---" 219 | 220 | install-tools: 221 | @# Help: Installs tools required for the project 222 | # Requires installed: asdf 223 | @echo "---MAKEFILE INSTALL-TOOLS---" 224 | ./scripts/installTools.sh .tool-versions 225 | @echo "---END MAKEFILE INSTALL-TOOLS---" 226 | ## Helper Targets end 227 | 228 | list: help 229 | @# Help: Displays make targets 230 | 231 | help: 232 | @# Help: Displays make targets 233 | @printf "%-35s %s\n" "Target" "Description" 234 | @printf "%-35s %s\n" "------" "-----------" 235 | @grep -E '^[a-zA-Z0-9_%-]+:|^[[:space:]]+@# Help:' Makefile | \ 236 | awk '\ 237 | /^[a-zA-Z0-9_%-]+:/ { \ 238 | target = $$1; \ 239 | sub(":", "", target); \ 240 | } \ 241 | /^[[:space:]]+@# Help:/ { \ 242 | if (target != "") { \ 243 | help_line = $$0; \ 244 | sub("^[[:space:]]+@# Help: ", "", help_line); \ 245 | printf "%-35s %s\n", target, help_line; \ 246 | target = ""; \ 247 | } \ 248 | }' | sort -k1,1 249 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | # Edge Orchestrator Observability Tenant Controller 7 | 8 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 9 | [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/open-edge-platform/o11y-tenant-controller/badge)](https://scorecard.dev/viewer/?uri=github.com/open-edge-platform/o11y-tenant-controller) 10 | 11 | [alerting-monitor]: https://github.com/open-edge-platform/o11y-alerting-monitor 12 | [edgenode-observability]: https://github.com/open-edge-platform/o11y-charts/tree/main/charts/edgenode-observability 13 | [grafana-proxy]: https://github.com/open-edge-platform/o11y-charts/tree/main/apps/grafana-proxy 14 | [orchestrator-observability]: https://github.com/open-edge-platform/o11y-charts/tree/main/charts/orchestrator-observability 15 | 16 | [prometheus-agent]: https://github.com/open-edge-platform/edge-manageability-framework/blob/main/argocd/applications/templates/orchestrator-prometheus-agent.yaml 17 | [sre-exporter]: https://github.com/open-edge-platform/o11y-sre-exporter 18 | [tenancy-datamodel]: https://github.com/open-edge-platform/orch-utils/tree/main/tenancy-datamodel 19 | 20 | [Documentation]: https://docs.openedgeplatform.intel.com/edge-manage-docs/main/developer_guide/observability/arch/index.html 21 | [Edge Orchestrator Community]: https://github.com/open-edge-platform 22 | [Troubleshooting]: https://docs.openedgeplatform.intel.com/edge-manage-docs/main/developer_guide/troubleshooting/index.html 23 | [Contact us]: https://github.com/open-edge-platform 24 | 25 | [Apache 2.0 License]: LICENSES/Apache-2.0.txt 26 | [Contributor's Guide]: https://docs.openedgeplatform.intel.com/edge-manage-docs/main/developer_guide/contributor_guide/index.html 27 | 28 | ## Overview 29 | 30 | Edge Orchestrator Observability Tenant Controller is responsible for reconfiguration of the following components upon tenant creation and removal: 31 | 32 | - [alerting-monitor] (via a dedicated gRPC management interface) 33 | - [sre-exporter] (via a dedicated gRPC management interface) 34 | - [edgenode-observability] (Grafana Mimir & Grafana Loki) 35 | 36 | This service also provides tenant data via: 37 | 38 | - a dedicated gRPC interface to [grafana-proxy] instances running in [edgenode-observability] and [orchestrator-observability] 39 | - `project_metadata` metric exposed for scraping by [edgenode-observability] and [prometheus-agent] (which feeds it to [orchestrator-observability]) 40 | 41 | The multi-tenancy approach considers a `project` being a representation of a `tenant` as exposed by [tenancy-datamodel]. 42 | 43 | Read more about Edge Orchestrator Observability Tenant Controller in the [Documentation]. 44 | 45 | ## Get Started 46 | 47 | To set up the development environment and work on this project, follow the steps below. 48 | All necessary tools will be installed using the `install-tools` target. 49 | Note that `docker` and `asdf` must be installed beforehand. 50 | 51 | ### Install Tools 52 | 53 | To install all the necessary tools needed for development the project, run: 54 | 55 | ```sh 56 | make install-tools 57 | ``` 58 | 59 | ### Build 60 | 61 | To build the project, use the following command: 62 | 63 | ```sh 64 | make build 65 | ``` 66 | 67 | ### Lint 68 | 69 | To lint the code and ensure it adheres to the coding standards, run: 70 | 71 | ```sh 72 | make lint 73 | ``` 74 | 75 | ### Test 76 | 77 | To run the tests and verify the functionality of the project, use: 78 | 79 | ```sh 80 | make test 81 | ``` 82 | 83 | ### Docker Build 84 | 85 | To build the Docker image for the project, run: 86 | 87 | ```sh 88 | make docker-build 89 | ``` 90 | 91 | ### Helm Build 92 | 93 | To package the Helm chart for the project, use: 94 | 95 | ```sh 96 | make helm-build 97 | ``` 98 | 99 | ### Docker Push 100 | 101 | To push the Docker image to the registry, run: 102 | 103 | ```sh 104 | make docker-push 105 | ``` 106 | 107 | ### Helm Push 108 | 109 | To push the Helm chart to the repository, use: 110 | 111 | ```sh 112 | make helm-push 113 | ``` 114 | 115 | ### Kind All 116 | 117 | To load the Docker image into a local Kind cluster, run: 118 | 119 | ```sh 120 | make kind-all 121 | ``` 122 | 123 | ### Proto 124 | 125 | To generate code from protobuf definitions, use: 126 | 127 | ```sh 128 | make proto 129 | ``` 130 | 131 | ## Develop 132 | 133 | It is recommended to develop the `observability-tenant-controller` application by deploying and testing it as a part of the Edge Orchestrator cluster. 134 | 135 | The code of this project is maintained and released in CI using the `VERSION` file. 136 | In addition, the chart is versioned with the same tag as the `VERSION` file. 137 | 138 | This is mandatory to keep all chart versions and app versions coherent. 139 | 140 | To bump the version, increment the version in the `VERSION` file and run the following command 141 | (to set `version` and `appVersion` in the `Chart.yaml` automatically): 142 | 143 | ```sh 144 | make helm-build 145 | ``` 146 | 147 | ## Contribute 148 | 149 | To learn how to contribute to the project, see the [Contributor's Guide]. 150 | 151 | ## Community and Support 152 | 153 | To learn more about the project, its community, and governance, visit the [Edge Orchestrator Community]. 154 | 155 | For support, start with [Troubleshooting] or [Contact us]. 156 | 157 | ## License 158 | 159 | Edge Orchestrator Observability Charts are licensed under [Apache 2.0 License]. 160 | 161 | Last Updated Date: {March 28, 2025} 162 | -------------------------------------------------------------------------------- /REUSE.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 Intel Corporation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | version = 1 5 | 6 | [[annotations]] 7 | path = [ 8 | ".tool-versions", 9 | "VERSION", 10 | "deployments/observability-tenant-controller/templates/**.tpl", 11 | "go.mod", 12 | "go.sum", 13 | ".cache/**", 14 | "ci/**" 15 | ] 16 | SPDX-FileCopyrightText = "2025 Intel Corporation" 17 | SPDX-License-Identifier = "Apache-2.0" 18 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | # Security Policy 7 | 8 | Intel is committed to rapidly addressing security vulnerabilities affecting our customers and providing clear guidance on the solution, impact, severity and mitigation. 9 | 10 | ## Reporting a Vulnerability 11 | 12 | Please report any security vulnerabilities in this project utilizing the guidelines [here](https://www.intel.com/content/www/us/en/security-center/vulnerability-handling-guidelines.html). 13 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.6.1-dev 2 | -------------------------------------------------------------------------------- /api/projectstream.pb.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Code generated by protoc-gen-go. DO NOT EDIT. 5 | // versions: 6 | // protoc-gen-go v1.36.4 7 | // protoc v5.29.3 8 | // source: api/projectstream.proto 9 | 10 | package proto 11 | 12 | import ( 13 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 14 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 15 | reflect "reflect" 16 | sync "sync" 17 | unsafe "unsafe" 18 | ) 19 | 20 | const ( 21 | // Verify that this generated code is sufficiently up-to-date. 22 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 23 | // Verify that runtime/protoimpl is sufficiently up-to-date. 24 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 25 | ) 26 | 27 | type EmptyRequest struct { 28 | state protoimpl.MessageState `protogen:"open.v1"` 29 | unknownFields protoimpl.UnknownFields 30 | sizeCache protoimpl.SizeCache 31 | } 32 | 33 | func (x *EmptyRequest) Reset() { 34 | *x = EmptyRequest{} 35 | mi := &file_api_projectstream_proto_msgTypes[0] 36 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 37 | ms.StoreMessageInfo(mi) 38 | } 39 | 40 | func (x *EmptyRequest) String() string { 41 | return protoimpl.X.MessageStringOf(x) 42 | } 43 | 44 | func (*EmptyRequest) ProtoMessage() {} 45 | 46 | func (x *EmptyRequest) ProtoReflect() protoreflect.Message { 47 | mi := &file_api_projectstream_proto_msgTypes[0] 48 | if x != nil { 49 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 50 | if ms.LoadMessageInfo() == nil { 51 | ms.StoreMessageInfo(mi) 52 | } 53 | return ms 54 | } 55 | return mi.MessageOf(x) 56 | } 57 | 58 | // Deprecated: Use EmptyRequest.ProtoReflect.Descriptor instead. 59 | func (*EmptyRequest) Descriptor() ([]byte, []int) { 60 | return file_api_projectstream_proto_rawDescGZIP(), []int{0} 61 | } 62 | 63 | type ProjectData struct { 64 | state protoimpl.MessageState `protogen:"open.v1"` 65 | ProjectName string `protobuf:"bytes,1,opt,name=project_name,json=projectName,proto3" json:"project_name,omitempty"` 66 | OrgName string `protobuf:"bytes,2,opt,name=org_name,json=orgName,proto3" json:"org_name,omitempty"` 67 | Status string `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"` 68 | unknownFields protoimpl.UnknownFields 69 | sizeCache protoimpl.SizeCache 70 | } 71 | 72 | func (x *ProjectData) Reset() { 73 | *x = ProjectData{} 74 | mi := &file_api_projectstream_proto_msgTypes[1] 75 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 76 | ms.StoreMessageInfo(mi) 77 | } 78 | 79 | func (x *ProjectData) String() string { 80 | return protoimpl.X.MessageStringOf(x) 81 | } 82 | 83 | func (*ProjectData) ProtoMessage() {} 84 | 85 | func (x *ProjectData) ProtoReflect() protoreflect.Message { 86 | mi := &file_api_projectstream_proto_msgTypes[1] 87 | if x != nil { 88 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 89 | if ms.LoadMessageInfo() == nil { 90 | ms.StoreMessageInfo(mi) 91 | } 92 | return ms 93 | } 94 | return mi.MessageOf(x) 95 | } 96 | 97 | // Deprecated: Use ProjectData.ProtoReflect.Descriptor instead. 98 | func (*ProjectData) Descriptor() ([]byte, []int) { 99 | return file_api_projectstream_proto_rawDescGZIP(), []int{1} 100 | } 101 | 102 | func (x *ProjectData) GetProjectName() string { 103 | if x != nil { 104 | return x.ProjectName 105 | } 106 | return "" 107 | } 108 | 109 | func (x *ProjectData) GetOrgName() string { 110 | if x != nil { 111 | return x.OrgName 112 | } 113 | return "" 114 | } 115 | 116 | func (x *ProjectData) GetStatus() string { 117 | if x != nil { 118 | return x.Status 119 | } 120 | return "" 121 | } 122 | 123 | type ProjectUpdate struct { 124 | state protoimpl.MessageState `protogen:"open.v1"` 125 | Projects []*ProjectEntry `protobuf:"bytes,1,rep,name=projects,proto3" json:"projects,omitempty"` 126 | unknownFields protoimpl.UnknownFields 127 | sizeCache protoimpl.SizeCache 128 | } 129 | 130 | func (x *ProjectUpdate) Reset() { 131 | *x = ProjectUpdate{} 132 | mi := &file_api_projectstream_proto_msgTypes[2] 133 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 134 | ms.StoreMessageInfo(mi) 135 | } 136 | 137 | func (x *ProjectUpdate) String() string { 138 | return protoimpl.X.MessageStringOf(x) 139 | } 140 | 141 | func (*ProjectUpdate) ProtoMessage() {} 142 | 143 | func (x *ProjectUpdate) ProtoReflect() protoreflect.Message { 144 | mi := &file_api_projectstream_proto_msgTypes[2] 145 | if x != nil { 146 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 147 | if ms.LoadMessageInfo() == nil { 148 | ms.StoreMessageInfo(mi) 149 | } 150 | return ms 151 | } 152 | return mi.MessageOf(x) 153 | } 154 | 155 | // Deprecated: Use ProjectUpdate.ProtoReflect.Descriptor instead. 156 | func (*ProjectUpdate) Descriptor() ([]byte, []int) { 157 | return file_api_projectstream_proto_rawDescGZIP(), []int{2} 158 | } 159 | 160 | func (x *ProjectUpdate) GetProjects() []*ProjectEntry { 161 | if x != nil { 162 | return x.Projects 163 | } 164 | return nil 165 | } 166 | 167 | type ProjectEntry struct { 168 | state protoimpl.MessageState `protogen:"open.v1"` 169 | Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` 170 | Data *ProjectData `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` 171 | unknownFields protoimpl.UnknownFields 172 | sizeCache protoimpl.SizeCache 173 | } 174 | 175 | func (x *ProjectEntry) Reset() { 176 | *x = ProjectEntry{} 177 | mi := &file_api_projectstream_proto_msgTypes[3] 178 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 179 | ms.StoreMessageInfo(mi) 180 | } 181 | 182 | func (x *ProjectEntry) String() string { 183 | return protoimpl.X.MessageStringOf(x) 184 | } 185 | 186 | func (*ProjectEntry) ProtoMessage() {} 187 | 188 | func (x *ProjectEntry) ProtoReflect() protoreflect.Message { 189 | mi := &file_api_projectstream_proto_msgTypes[3] 190 | if x != nil { 191 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 192 | if ms.LoadMessageInfo() == nil { 193 | ms.StoreMessageInfo(mi) 194 | } 195 | return ms 196 | } 197 | return mi.MessageOf(x) 198 | } 199 | 200 | // Deprecated: Use ProjectEntry.ProtoReflect.Descriptor instead. 201 | func (*ProjectEntry) Descriptor() ([]byte, []int) { 202 | return file_api_projectstream_proto_rawDescGZIP(), []int{3} 203 | } 204 | 205 | func (x *ProjectEntry) GetKey() string { 206 | if x != nil { 207 | return x.Key 208 | } 209 | return "" 210 | } 211 | 212 | func (x *ProjectEntry) GetData() *ProjectData { 213 | if x != nil { 214 | return x.Data 215 | } 216 | return nil 217 | } 218 | 219 | var File_api_projectstream_proto protoreflect.FileDescriptor 220 | 221 | var file_api_projectstream_proto_rawDesc = string([]byte{ 222 | 0x0a, 0x17, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x74, 0x72, 223 | 0x65, 0x61, 0x6d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0d, 0x70, 0x72, 0x6f, 0x6a, 0x65, 224 | 0x63, 0x74, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x22, 0x0e, 0x0a, 0x0c, 0x45, 0x6d, 0x70, 0x74, 225 | 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x63, 0x0a, 0x0b, 0x50, 0x72, 0x6f, 0x6a, 226 | 0x65, 0x63, 0x74, 0x44, 0x61, 0x74, 0x61, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x72, 0x6f, 0x6a, 0x65, 227 | 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x70, 228 | 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x6f, 0x72, 229 | 0x67, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6f, 0x72, 230 | 0x67, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 231 | 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x48, 0x0a, 232 | 0x0d, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x37, 233 | 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 234 | 0x32, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 235 | 0x2e, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x70, 236 | 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x22, 0x50, 0x0a, 0x0c, 0x50, 0x72, 0x6f, 0x6a, 0x65, 237 | 0x63, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 238 | 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2e, 0x0a, 0x04, 0x64, 0x61, 0x74, 239 | 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 240 | 0x74, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x2e, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x44, 241 | 0x61, 0x74, 0x61, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x32, 0x65, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 242 | 0x6a, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x53, 0x0a, 0x14, 0x53, 243 | 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x55, 0x70, 0x64, 0x61, 244 | 0x74, 0x65, 0x73, 0x12, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x74, 0x72, 245 | 0x65, 0x61, 0x6d, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 246 | 0x1a, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 247 | 0x2e, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x30, 0x01, 248 | 0x42, 0x08, 0x5a, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 249 | 0x6f, 0x33, 250 | }) 251 | 252 | var ( 253 | file_api_projectstream_proto_rawDescOnce sync.Once 254 | file_api_projectstream_proto_rawDescData []byte 255 | ) 256 | 257 | func file_api_projectstream_proto_rawDescGZIP() []byte { 258 | file_api_projectstream_proto_rawDescOnce.Do(func() { 259 | file_api_projectstream_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_api_projectstream_proto_rawDesc), len(file_api_projectstream_proto_rawDesc))) 260 | }) 261 | return file_api_projectstream_proto_rawDescData 262 | } 263 | 264 | var file_api_projectstream_proto_msgTypes = make([]protoimpl.MessageInfo, 4) 265 | var file_api_projectstream_proto_goTypes = []any{ 266 | (*EmptyRequest)(nil), // 0: projectstream.EmptyRequest 267 | (*ProjectData)(nil), // 1: projectstream.ProjectData 268 | (*ProjectUpdate)(nil), // 2: projectstream.ProjectUpdate 269 | (*ProjectEntry)(nil), // 3: projectstream.ProjectEntry 270 | } 271 | var file_api_projectstream_proto_depIdxs = []int32{ 272 | 3, // 0: projectstream.ProjectUpdate.projects:type_name -> projectstream.ProjectEntry 273 | 1, // 1: projectstream.ProjectEntry.data:type_name -> projectstream.ProjectData 274 | 0, // 2: projectstream.ProjectService.StreamProjectUpdates:input_type -> projectstream.EmptyRequest 275 | 2, // 3: projectstream.ProjectService.StreamProjectUpdates:output_type -> projectstream.ProjectUpdate 276 | 3, // [3:4] is the sub-list for method output_type 277 | 2, // [2:3] is the sub-list for method input_type 278 | 2, // [2:2] is the sub-list for extension type_name 279 | 2, // [2:2] is the sub-list for extension extendee 280 | 0, // [0:2] is the sub-list for field type_name 281 | } 282 | 283 | func init() { file_api_projectstream_proto_init() } 284 | func file_api_projectstream_proto_init() { 285 | if File_api_projectstream_proto != nil { 286 | return 287 | } 288 | type x struct{} 289 | out := protoimpl.TypeBuilder{ 290 | File: protoimpl.DescBuilder{ 291 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 292 | RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_projectstream_proto_rawDesc), len(file_api_projectstream_proto_rawDesc)), 293 | NumEnums: 0, 294 | NumMessages: 4, 295 | NumExtensions: 0, 296 | NumServices: 1, 297 | }, 298 | GoTypes: file_api_projectstream_proto_goTypes, 299 | DependencyIndexes: file_api_projectstream_proto_depIdxs, 300 | MessageInfos: file_api_projectstream_proto_msgTypes, 301 | }.Build() 302 | File_api_projectstream_proto = out.File 303 | file_api_projectstream_proto_goTypes = nil 304 | file_api_projectstream_proto_depIdxs = nil 305 | } 306 | -------------------------------------------------------------------------------- /api/projectstream.proto: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | syntax = "proto3"; 5 | 6 | package projectstream; 7 | option go_package = "proto/"; 8 | 9 | service ProjectService { 10 | rpc StreamProjectUpdates(EmptyRequest) returns (stream ProjectUpdate); 11 | } 12 | 13 | message EmptyRequest { 14 | } 15 | 16 | message ProjectData { 17 | string project_name = 1; 18 | string org_name = 2; 19 | string status = 3; 20 | } 21 | 22 | message ProjectUpdate { 23 | repeated ProjectEntry projects = 1; 24 | } 25 | 26 | message ProjectEntry { 27 | string key = 1; 28 | ProjectData data = 2; 29 | } 30 | -------------------------------------------------------------------------------- /api/projectstream_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 5 | // versions: 6 | // - protoc-gen-go-grpc v1.5.1 7 | // - protoc v5.29.3 8 | // source: api/projectstream.proto 9 | 10 | package proto 11 | 12 | import ( 13 | context "context" 14 | grpc "google.golang.org/grpc" 15 | codes "google.golang.org/grpc/codes" 16 | status "google.golang.org/grpc/status" 17 | ) 18 | 19 | // This is a compile-time assertion to ensure that this generated file 20 | // is compatible with the grpc package it is being compiled against. 21 | // Requires gRPC-Go v1.64.0 or later. 22 | const _ = grpc.SupportPackageIsVersion9 23 | 24 | const ( 25 | ProjectService_StreamProjectUpdates_FullMethodName = "/projectstream.ProjectService/StreamProjectUpdates" 26 | ) 27 | 28 | // ProjectServiceClient is the client API for ProjectService service. 29 | // 30 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 31 | type ProjectServiceClient interface { 32 | StreamProjectUpdates(ctx context.Context, in *EmptyRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ProjectUpdate], error) 33 | } 34 | 35 | type projectServiceClient struct { 36 | cc grpc.ClientConnInterface 37 | } 38 | 39 | func NewProjectServiceClient(cc grpc.ClientConnInterface) ProjectServiceClient { 40 | return &projectServiceClient{cc} 41 | } 42 | 43 | func (c *projectServiceClient) StreamProjectUpdates(ctx context.Context, in *EmptyRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ProjectUpdate], error) { 44 | cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 45 | stream, err := c.cc.NewStream(ctx, &ProjectService_ServiceDesc.Streams[0], ProjectService_StreamProjectUpdates_FullMethodName, cOpts...) 46 | if err != nil { 47 | return nil, err 48 | } 49 | x := &grpc.GenericClientStream[EmptyRequest, ProjectUpdate]{ClientStream: stream} 50 | if err := x.ClientStream.SendMsg(in); err != nil { 51 | return nil, err 52 | } 53 | if err := x.ClientStream.CloseSend(); err != nil { 54 | return nil, err 55 | } 56 | return x, nil 57 | } 58 | 59 | // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. 60 | type ProjectService_StreamProjectUpdatesClient = grpc.ServerStreamingClient[ProjectUpdate] 61 | 62 | // ProjectServiceServer is the server API for ProjectService service. 63 | // All implementations must embed UnimplementedProjectServiceServer 64 | // for forward compatibility. 65 | type ProjectServiceServer interface { 66 | StreamProjectUpdates(*EmptyRequest, grpc.ServerStreamingServer[ProjectUpdate]) error 67 | mustEmbedUnimplementedProjectServiceServer() 68 | } 69 | 70 | // UnimplementedProjectServiceServer must be embedded to have 71 | // forward compatible implementations. 72 | // 73 | // NOTE: this should be embedded by value instead of pointer to avoid a nil 74 | // pointer dereference when methods are called. 75 | type UnimplementedProjectServiceServer struct{} 76 | 77 | func (UnimplementedProjectServiceServer) StreamProjectUpdates(*EmptyRequest, grpc.ServerStreamingServer[ProjectUpdate]) error { 78 | return status.Errorf(codes.Unimplemented, "method StreamProjectUpdates not implemented") 79 | } 80 | func (UnimplementedProjectServiceServer) mustEmbedUnimplementedProjectServiceServer() {} 81 | func (UnimplementedProjectServiceServer) testEmbeddedByValue() {} 82 | 83 | // UnsafeProjectServiceServer may be embedded to opt out of forward compatibility for this service. 84 | // Use of this interface is not recommended, as added methods to ProjectServiceServer will 85 | // result in compilation errors. 86 | type UnsafeProjectServiceServer interface { 87 | mustEmbedUnimplementedProjectServiceServer() 88 | } 89 | 90 | func RegisterProjectServiceServer(s grpc.ServiceRegistrar, srv ProjectServiceServer) { 91 | // If the following call pancis, it indicates UnimplementedProjectServiceServer was 92 | // embedded by pointer and is nil. This will cause panics if an 93 | // unimplemented method is ever invoked, so we test this at initialization 94 | // time to prevent it from happening at runtime later due to I/O. 95 | if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { 96 | t.testEmbeddedByValue() 97 | } 98 | s.RegisterService(&ProjectService_ServiceDesc, srv) 99 | } 100 | 101 | func _ProjectService_StreamProjectUpdates_Handler(srv interface{}, stream grpc.ServerStream) error { 102 | m := new(EmptyRequest) 103 | if err := stream.RecvMsg(m); err != nil { 104 | return err 105 | } 106 | return srv.(ProjectServiceServer).StreamProjectUpdates(m, &grpc.GenericServerStream[EmptyRequest, ProjectUpdate]{ServerStream: stream}) 107 | } 108 | 109 | // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. 110 | type ProjectService_StreamProjectUpdatesServer = grpc.ServerStreamingServer[ProjectUpdate] 111 | 112 | // ProjectService_ServiceDesc is the grpc.ServiceDesc for ProjectService service. 113 | // It's only intended for direct use with grpc.RegisterService, 114 | // and not to be introspected or modified (even as a copy) 115 | var ProjectService_ServiceDesc = grpc.ServiceDesc{ 116 | ServiceName: "projectstream.ProjectService", 117 | HandlerType: (*ProjectServiceServer)(nil), 118 | Methods: []grpc.MethodDesc{}, 119 | Streams: []grpc.StreamDesc{ 120 | { 121 | StreamName: "StreamProjectUpdates", 122 | Handler: _ProjectService_StreamProjectUpdates_Handler, 123 | ServerStreams: true, 124 | }, 125 | }, 126 | Metadata: "api/projectstream.proto", 127 | } 128 | -------------------------------------------------------------------------------- /cmd/observability-tenant-controller/observability-tenant-controller.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "context" 8 | "flag" 9 | "log" 10 | "net" 11 | "os" 12 | "os/signal" 13 | "strconv" 14 | "sync" 15 | "syscall" 16 | "time" 17 | 18 | "google.golang.org/grpc" 19 | "google.golang.org/grpc/backoff" 20 | "google.golang.org/grpc/credentials/insecure" 21 | 22 | pb "github.com/open-edge-platform/o11y-tenant-controller/api" 23 | "github.com/open-edge-platform/o11y-tenant-controller/internal/config" 24 | "github.com/open-edge-platform/o11y-tenant-controller/internal/controller" 25 | "github.com/open-edge-platform/o11y-tenant-controller/internal/jobs" 26 | "github.com/open-edge-platform/o11y-tenant-controller/internal/projects" 27 | ) 28 | 29 | func main() { 30 | cfgFilePath := flag.String("config", "", "path to the config file") 31 | flag.Parse() 32 | 33 | cfg, err := config.ReadConfig(*cfgFilePath) 34 | if err != nil { 35 | log.Panicf("Failed to load config: %v", err) 36 | } 37 | 38 | ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) 39 | defer stop() 40 | 41 | grpcServer := projects.Server{ 42 | GrpcServer: grpc.NewServer(), 43 | Port: 50051, 44 | Mu: &sync.RWMutex{}, 45 | Projects: make(map[string]projects.ProjectData), 46 | Clients: &sync.Map{}, 47 | } 48 | 49 | lis, err := net.Listen("tcp", ":"+strconv.Itoa(grpcServer.Port)) 50 | if err != nil { 51 | log.Panicf("Failed to listen: %v", err) 52 | } 53 | pb.RegisterProjectServiceServer(grpcServer.GrpcServer, &grpcServer) 54 | 55 | go func() { 56 | if err := grpcServer.GrpcServer.Serve(lis); err != nil { 57 | log.Printf("gRPC server failed to serve: %v", err) 58 | stop() 59 | } 60 | }() 61 | log.Printf("gRPC server listening on port %d", grpcServer.Port) 62 | 63 | tenantCtrl, err := controller.New(cfg.Controller.Channel.MaxInflightRequests, cfg.Controller.CreateDeleteWatcherTimeout, &grpcServer) 64 | if err != nil { 65 | log.Panicf("Failed to create tenant controller: %v", err) 66 | } 67 | 68 | amConn, err := grpc.NewClient(cfg.Endpoints.AlertingMonitor, 69 | grpc.WithTransportCredentials(insecure.NewCredentials()), 70 | grpc.WithConnectParams(grpc.ConnectParams{Backoff: backoff.DefaultConfig}), 71 | ) 72 | if err != nil { 73 | log.Panicf("Failed to create alerting monitor gRPC client: %v", err) 74 | } 75 | defer amConn.Close() 76 | 77 | sreConn, err := grpc.NewClient(cfg.Endpoints.Sre, 78 | grpc.WithTransportCredentials(insecure.NewCredentials()), 79 | grpc.WithConnectParams(grpc.ConnectParams{Backoff: backoff.DefaultConfig}), 80 | ) 81 | if err != nil { 82 | log.Panicf("Failed to create sre-exporter gRPC client: %v", err) 83 | } 84 | 85 | err = tenantCtrl.Start() 86 | // defer before checking error done on purpose - to ensure cleanup (Start may fail for reasons other than an error at addProjectWatcher). 87 | defer tenantCtrl.Stop() 88 | if err != nil { 89 | log.Panicf("Failed to start tenant controller: %v", err) 90 | } 91 | 92 | ticker := time.NewTicker(cfg.Job.Manager.Deletion.Rate) 93 | defer ticker.Stop() 94 | 95 | jobManager := jobs.New(tenantCtrl.ComSig, cfg.Job, cfg.Endpoints, amConn, sreConn) 96 | jobManager.Start(ticker) 97 | 98 | <-ctx.Done() 99 | jobManager.Stop() 100 | } 101 | -------------------------------------------------------------------------------- /deployments/observability-tenant-controller/Chart.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | apiVersion: v2 5 | name: observability-tenant-controller 6 | description: A Helm chart for Observability Tenant Controller 7 | type: application 8 | version: 0.6.1-dev 9 | appVersion: 0.6.1-dev 10 | annotations: 11 | revision: 8bc9bd4b49f5fc50ae4b6a529ce6992d4c48c3ce 12 | created: "2025-04-02T10:03:19Z" 13 | -------------------------------------------------------------------------------- /deployments/observability-tenant-controller/files/config/config.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | endpoints: 5 | alertingmonitor: alerting-monitor-management.{{ .Values.namespaces.edgenode }}.svc.cluster.local:51001 6 | sre: sre-config-reloader-service.{{ .Values.namespaces.sre }}.svc.cluster.local:50051 7 | mimir: 8 | ingester: "http://edgenode-observability-mimir-ingester.{{ .Values.namespaces.edgenode }}.svc.cluster.local:8080" 9 | compactor: "http://edgenode-observability-mimir-compactor.{{ .Values.namespaces.edgenode }}.svc.cluster.local:8080" 10 | pollingRate: 20s 11 | # Verify mode can be strict or loose 12 | deleteVerifyMode: {{ .Values.loki.deleteVerifyMode }} 13 | loki: 14 | write: "http://loki-write.{{ .Values.namespaces.edgenode }}.svc.cluster.local:3100" 15 | backend: "http://loki-backend.{{ .Values.namespaces.edgenode }}.svc.cluster.local:3100" 16 | pollingRate: 20s 17 | maxPollingRate: 1m 18 | # Verify mode can be "strict" or "loose" 19 | deleteVerifyMode: {{ .Values.mimir.deleteVerifyMode }} 20 | 21 | controller: 22 | channel: 23 | maxInflightRequests: 1000 24 | createDeleteWatcherTimeout: 10m 25 | 26 | job: 27 | manager: 28 | deletion: 29 | rate: "1m" 30 | backoff: 31 | initial: "3s" 32 | max: "10m" 33 | timeMultiplier: 1.6 34 | timeout: "30m" 35 | sre: 36 | enabled: {{ .Values.sre.enabled }} 37 | -------------------------------------------------------------------------------- /deployments/observability-tenant-controller/templates/_checks.tpl: -------------------------------------------------------------------------------- 1 | {{ define "checks" }} 2 | {{- $verifyModes := list "loose" "strict" }} 3 | {{ if not (mustHas .Values.loki.deleteVerifyMode $verifyModes) }} 4 | {{ fail "please provide correct .Values.loki.deleteVerifyMode value" }} 5 | {{ end }} 6 | {{ if not (mustHas .Values.mimir.deleteVerifyMode $verifyModes) }} 7 | {{ fail "please provide correct .Values.mimir.deleteVerifyMode value" }} 8 | {{ end }} 9 | {{ end }} 10 | -------------------------------------------------------------------------------- /deployments/observability-tenant-controller/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Create chart name and version as used by the chart label. 3 | */}} 4 | {{- define "observability-tenant-controller.chart" -}} 5 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | 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 "observability-tenant-controller.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 | Selector labels 28 | */}} 29 | {{- define "observability-tenant-controller.selectorLabels" -}} 30 | app.kubernetes.io/name: {{ .Chart.Name }} 31 | app.kubernetes.io/instance: {{ .Release.Name }} 32 | {{- end }} 33 | 34 | {{/* 35 | Common labels 36 | */}} 37 | {{- define "observability-tenant-controller.labels" -}} 38 | helm.sh/chart: {{ include "observability-tenant-controller.chart" . }} 39 | {{ include "observability-tenant-controller.selectorLabels" . }} 40 | {{- if .Chart.AppVersion }} 41 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 42 | {{- end }} 43 | app.kubernetes.io/managed-by: {{ .Release.Service }} 44 | {{- end }} 45 | 46 | {{/* 47 | gRPC port definition 48 | */}} 49 | {{- define "observability-tenant-controller.ports.grpc" -}} 50 | 50051 51 | {{- end -}} 52 | 53 | {{/* 54 | Prometheus port definition 55 | */}} 56 | {{- define "observability-tenant-controller.ports.prometheus" -}} 57 | 9273 58 | {{- end -}} 59 | -------------------------------------------------------------------------------- /deployments/observability-tenant-controller/templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | {{ include "checks" . }} 5 | 6 | --- 7 | apiVersion: v1 8 | kind: ConfigMap 9 | metadata: 10 | name: "observability-tenant-controller-config" 11 | namespace: {{ .Release.Namespace }} 12 | data: 13 | config.yaml: | 14 | {{- tpl (.Files.Get "files/config/config.yaml") . | nindent 4 }} 15 | -------------------------------------------------------------------------------- /deployments/observability-tenant-controller/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | apiVersion: apps/v1 5 | kind: Deployment 6 | metadata: 7 | name: {{ include "observability-tenant-controller.fullname" . }} 8 | labels: 9 | {{- include "observability-tenant-controller.labels" . | nindent 4 }} 10 | spec: 11 | selector: 12 | matchLabels: 13 | {{- include "observability-tenant-controller.selectorLabels" . | nindent 6 }} 14 | template: 15 | metadata: 16 | labels: 17 | {{- include "observability-tenant-controller.selectorLabels" . | nindent 8 }} 18 | spec: 19 | {{- with .Values.imagePullSecrets }} 20 | imagePullSecrets: 21 | {{- toYaml . | nindent 8 }} 22 | {{- end }} 23 | containers: 24 | - name: observability-tenant-controller 25 | image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 26 | imagePullPolicy: {{ .Values.image.pullPolicy }} 27 | ports: 28 | - containerPort: {{ include "observability-tenant-controller.ports.prometheus" . }} 29 | - containerPort: {{ include "observability-tenant-controller.ports.grpc" . }} 30 | args: 31 | - "--config={{ .Values.configmap.mountPath }}/config.yaml" 32 | resources: 33 | requests: 34 | cpu: 50m 35 | memory: 64Mi 36 | limits: 37 | cpu: 500m 38 | memory: 256Mi 39 | volumeMounts: 40 | - name: config 41 | mountPath: {{ .Values.configmap.mountPath }} 42 | readOnly: true 43 | securityContext: 44 | capabilities: 45 | drop: 46 | - ALL 47 | allowPrivilegeEscalation: false 48 | readOnlyRootFilesystem: true 49 | securityContext: 50 | runAsNonRoot: true 51 | runAsUser: 1000 52 | seccompProfile: 53 | type: RuntimeDefault 54 | serviceAccountName: observability-tenant-controller 55 | volumes: 56 | - name: config 57 | configMap: 58 | name: "observability-tenant-controller-config" 59 | items: 60 | - key: config.yaml 61 | path: config.yaml 62 | -------------------------------------------------------------------------------- /deployments/observability-tenant-controller/templates/network_policy.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | apiVersion: networking.k8s.io/v1 5 | kind: NetworkPolicy 6 | metadata: 7 | name: observability-tenant-controller-allowed-traffic 8 | labels: 9 | {{- include "observability-tenant-controller.labels" . | nindent 4 }} 10 | spec: 11 | ingress: 12 | - from: 13 | - namespaceSelector: 14 | matchLabels: 15 | kubernetes.io/metadata.name: {{ .Values.namespaces.edgenode }} 16 | podSelector: 17 | matchLabels: 18 | app.kubernetes.io/name: opentelemetry-collector 19 | ports: 20 | - port: {{ include "observability-tenant-controller.ports.prometheus" . }} 21 | protocol: TCP 22 | 23 | - from: 24 | - namespaceSelector: 25 | matchLabels: 26 | kubernetes.io/metadata.name: {{ .Values.namespaces.platform }} 27 | podSelector: 28 | matchLabels: 29 | app.kubernetes.io/name: prometheus 30 | ports: 31 | - port: {{ include "observability-tenant-controller.ports.prometheus" . }} 32 | protocol: TCP 33 | 34 | - from: 35 | - namespaceSelector: 36 | matchLabels: 37 | kubernetes.io/metadata.name: {{ .Values.namespaces.edgenode }} 38 | podSelector: 39 | matchLabels: 40 | app.kubernetes.io/name: grafana 41 | ports: 42 | - port: {{ include "observability-tenant-controller.ports.grpc" . }} 43 | protocol: TCP 44 | 45 | - from: 46 | - namespaceSelector: 47 | matchLabels: 48 | kubernetes.io/metadata.name: {{ .Values.namespaces.gateway }} 49 | podSelector: 50 | matchLabels: 51 | app.kubernetes.io/name: auth-service 52 | ports: 53 | - port: {{ include "observability-tenant-controller.ports.grpc" . }} 54 | protocol: TCP 55 | 56 | - from: 57 | - namespaceSelector: 58 | matchLabels: 59 | kubernetes.io/metadata.name: {{ .Values.namespaces.platform }} 60 | podSelector: 61 | matchLabels: 62 | app.kubernetes.io/name: grafana 63 | ports: 64 | - port: {{ include "observability-tenant-controller.ports.grpc" . }} 65 | protocol: TCP 66 | 67 | podSelector: 68 | matchLabels: 69 | {{- include "observability-tenant-controller.selectorLabels" . | nindent 6 }} 70 | policyTypes: 71 | - Ingress 72 | -------------------------------------------------------------------------------- /deployments/observability-tenant-controller/templates/service.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # SPDX-License-Identifier: Apache-2.0 3 | --- 4 | apiVersion: v1 5 | kind: Service 6 | metadata: 7 | name: {{ include "observability-tenant-controller.fullname" . }} 8 | labels: 9 | {{- include "observability-tenant-controller.labels" . | nindent 4 }} 10 | spec: 11 | selector: 12 | {{- include "observability-tenant-controller.selectorLabels" . | nindent 6 }} 13 | ports: 14 | - port: {{ include "observability-tenant-controller.ports.prometheus" . }} 15 | protocol: TCP 16 | targetPort: {{ include "observability-tenant-controller.ports.prometheus" . }} 17 | name: metrics 18 | - port: {{ include "observability-tenant-controller.ports.grpc" . }} 19 | protocol: TCP 20 | name: grpc 21 | type: ClusterIP 22 | --- 23 | apiVersion: monitoring.coreos.com/v1 24 | kind: ServiceMonitor 25 | metadata: 26 | name: {{ include "observability-tenant-controller.fullname" . }} 27 | labels: 28 | {{- include "observability-tenant-controller.labels" . | nindent 4 }} 29 | spec: 30 | endpoints: 31 | - port: metrics 32 | scheme: http 33 | path: /metrics 34 | namespaceSelector: 35 | matchNames: 36 | - {{ .Release.Namespace }} 37 | selector: 38 | matchExpressions: 39 | - key: prometheus.io/service-monitor 40 | operator: NotIn 41 | values: 42 | - "false" 43 | matchLabels: 44 | {{- include "observability-tenant-controller.labels" . | nindent 6 }} 45 | -------------------------------------------------------------------------------- /deployments/observability-tenant-controller/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | apiVersion: rbac.authorization.k8s.io/v1 5 | kind: ClusterRole 6 | metadata: 7 | name: observability-tenant-controller 8 | rules: 9 | - apiGroups: 10 | - "runtimeproject.edge-orchestrator.intel.com" 11 | - "projectactivewatcher.edge-orchestrator.intel.com" 12 | - "projectwatcher.edge-orchestrator.intel.com" 13 | - "runtimefolder.edge-orchestrator.intel.com" 14 | - "runtimeorg.edge-orchestrator.intel.com" 15 | - "runtime.edge-orchestrator.intel.com" 16 | - "tenancy.edge-orchestrator.intel.com" 17 | - "config.edge-orchestrator.intel.com" 18 | resources: 19 | - runtimeprojects 20 | - projectactivewatchers 21 | - projectwatchers 22 | - runtimefolders 23 | - runtimeorgs 24 | - runtimes 25 | - multitenancies 26 | - configs 27 | verbs: [ "*" ] 28 | 29 | --- 30 | 31 | apiVersion: rbac.authorization.k8s.io/v1 32 | kind: ClusterRoleBinding 33 | metadata: 34 | name: observability-tenant-controller 35 | subjects: 36 | - kind: ServiceAccount 37 | name: observability-tenant-controller 38 | namespace: {{ .Release.Namespace }} 39 | roleRef: 40 | kind: ClusterRole 41 | name: observability-tenant-controller 42 | apiGroup: rbac.authorization.k8s.io 43 | 44 | --- 45 | 46 | apiVersion: v1 47 | kind: ServiceAccount 48 | metadata: 49 | name: observability-tenant-controller 50 | namespace: {{ .Release.Namespace }} 51 | -------------------------------------------------------------------------------- /deployments/observability-tenant-controller/values.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | image: 5 | registry: registry-rs.edgeorchestration.intel.com/edge-orch 6 | repository: o11y/observability-tenant-controller 7 | pullPolicy: IfNotPresent 8 | 9 | configmap: 10 | mountPath: "/etc/config" 11 | 12 | sre: 13 | enabled: true 14 | 15 | loki: 16 | # Verify mode can be "strict" or "loose" 17 | deleteVerifyMode: loose 18 | mimir: 19 | # Verify mode can be "strict" or "loose" 20 | deleteVerifyMode: loose 21 | 22 | namespaces: 23 | # Where edgenode observability is 24 | edgenode: orch-infra 25 | # Where platform observability is 26 | platform: orch-platform 27 | # Where auth-service is 28 | gateway: orch-gateway 29 | # Where sre exporter is 30 | sre: orch-sre 31 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/open-edge-platform/o11y-tenant-controller 2 | 3 | go 1.24.0 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/onsi/ginkgo/v2 v2.23.4 9 | github.com/onsi/gomega v1.37.0 10 | github.com/open-edge-platform/o11y-alerting-monitor v1.7.1 11 | github.com/open-edge-platform/o11y-sre-exporter v0.9.0 12 | github.com/open-edge-platform/orch-utils/tenancy-datamodel v1.2.0 13 | github.com/prometheus/client_golang v1.22.0 14 | github.com/stretchr/testify v1.10.0 15 | golang.org/x/sync v0.15.0 16 | google.golang.org/grpc v1.73.0 17 | google.golang.org/protobuf v1.36.6 18 | gopkg.in/yaml.v3 v3.0.1 19 | k8s.io/apimachinery v0.33.1 20 | k8s.io/client-go v0.33.1 21 | ) 22 | 23 | require ( 24 | github.com/beorn7/perks v1.0.1 // indirect 25 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 26 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 27 | github.com/elliotchance/orderedmap v1.8.0 // indirect 28 | github.com/emicklei/go-restful/v3 v3.12.2 // indirect 29 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 30 | github.com/go-logr/logr v1.4.2 // indirect 31 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 32 | github.com/go-openapi/jsonreference v0.21.0 // indirect 33 | github.com/go-openapi/swag v0.23.0 // indirect 34 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 35 | github.com/gogo/protobuf v1.3.2 // indirect 36 | github.com/google/gnostic-models v0.6.9 // indirect 37 | github.com/google/go-cmp v0.7.0 // indirect 38 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect 39 | github.com/google/uuid v1.6.0 // indirect 40 | github.com/josharian/intern v1.0.0 // indirect 41 | github.com/json-iterator/go v1.1.12 // indirect 42 | github.com/mailru/easyjson v0.9.0 // indirect 43 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 44 | github.com/modern-go/reflect2 v1.0.2 // indirect 45 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 46 | github.com/pkg/errors v0.9.1 // indirect 47 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 48 | github.com/prometheus/client_model v0.6.2 // indirect 49 | github.com/prometheus/common v0.63.0 // indirect 50 | github.com/prometheus/procfs v0.15.1 // indirect 51 | github.com/sirupsen/logrus v1.9.3 // indirect 52 | github.com/x448/float16 v0.8.4 // indirect 53 | go.uber.org/automaxprocs v1.6.0 // indirect 54 | golang.org/x/net v0.40.0 // indirect 55 | golang.org/x/oauth2 v0.29.0 // indirect 56 | golang.org/x/sys v0.33.0 // indirect 57 | golang.org/x/term v0.32.0 // indirect 58 | golang.org/x/text v0.25.0 // indirect 59 | golang.org/x/time v0.11.0 // indirect 60 | golang.org/x/tools v0.32.0 // indirect 61 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e // indirect 62 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 63 | gopkg.in/inf.v0 v0.9.1 // indirect 64 | k8s.io/api v0.33.1 // indirect 65 | k8s.io/klog/v2 v2.130.1 // indirect 66 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect 67 | k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect 68 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect 69 | sigs.k8s.io/randfill v1.0.0 // indirect 70 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect 71 | sigs.k8s.io/yaml v1.4.0 // indirect 72 | ) 73 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 2 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 3 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 4 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 8 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/elliotchance/orderedmap v1.8.0 h1:TrOREecvh3JbS+NCgwposXG5ZTFHtEsQiCGOhPElnMw= 10 | github.com/elliotchance/orderedmap v1.8.0/go.mod h1:wsDwEaX5jEoyhbs7x93zk2H/qv0zwuhg4inXhDkYqys= 11 | github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= 12 | github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 13 | github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= 14 | github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= 15 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 16 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 17 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 18 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 19 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 20 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 21 | github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= 22 | github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= 23 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 24 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 25 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 26 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 27 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 28 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 29 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 30 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 31 | github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= 32 | github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= 33 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 34 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 35 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 36 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 37 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= 38 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 39 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 40 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 41 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 42 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 43 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 44 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 45 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 46 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 47 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 48 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 49 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 50 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 51 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 52 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 53 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 54 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 55 | github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= 56 | github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= 57 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 58 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 59 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 60 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 61 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 62 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 63 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 64 | github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= 65 | github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= 66 | github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= 67 | github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= 68 | github.com/open-edge-platform/o11y-alerting-monitor v1.7.1 h1:92AAJe7K3ByxG+2J13qGG+earOSzTgZw0goqIYyuGuM= 69 | github.com/open-edge-platform/o11y-alerting-monitor v1.7.1/go.mod h1:G1yDtBZHpMnsUTz5OXLdOMm49kkvzIue+cEnnhyR3A4= 70 | github.com/open-edge-platform/o11y-sre-exporter v0.9.0 h1:LYDXVtZVPbWtMZn2wP3NOumHS7/8RD7gBEo0Gyv8/sI= 71 | github.com/open-edge-platform/o11y-sre-exporter v0.9.0/go.mod h1:an3relsimf7XxbqmNyn+Vj9XOTE2ni5avS2w9pHymc0= 72 | github.com/open-edge-platform/orch-utils/tenancy-datamodel v1.2.0 h1:vJFShs8dXooCwQWePAE9aCOYLjngQNmSZKb7iBjj7jk= 73 | github.com/open-edge-platform/orch-utils/tenancy-datamodel v1.2.0/go.mod h1:dshRhRSIazosRzSsjP0uddwKbQuq6ObGlDoBeBHY164= 74 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 75 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 76 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 77 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 78 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 79 | github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= 80 | github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= 81 | github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= 82 | github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= 83 | github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 84 | github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 85 | github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= 86 | github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= 87 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 88 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 89 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 90 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 91 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 92 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 93 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 94 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 95 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 96 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 97 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 98 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 99 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 100 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 101 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 102 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 103 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 104 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 105 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 106 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 107 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 108 | go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= 109 | go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= 110 | go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= 111 | go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= 112 | go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= 113 | go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= 114 | go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= 115 | go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= 116 | go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= 117 | go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= 118 | go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= 119 | go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= 120 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 121 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 122 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 123 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 124 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 125 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 126 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 127 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 128 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 129 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 130 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 131 | golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= 132 | golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= 133 | golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= 134 | golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 135 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 136 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 137 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 138 | golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= 139 | golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 140 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 141 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 142 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 143 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 144 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 145 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 146 | golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= 147 | golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= 148 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 149 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 150 | golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= 151 | golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= 152 | golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= 153 | golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 154 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 155 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 156 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 157 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 158 | golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= 159 | golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= 160 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 161 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 162 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 163 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 164 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e h1:ztQaXfzEXTmCBvbtWYRhJxW+0iJcz2qXfd38/e9l7bA= 165 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= 166 | google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= 167 | google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= 168 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 169 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 170 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 171 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 172 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 173 | gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= 174 | gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 175 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 176 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 177 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 178 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 179 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 180 | k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw= 181 | k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw= 182 | k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4= 183 | k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= 184 | k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4= 185 | k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA= 186 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 187 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 188 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= 189 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= 190 | k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0= 191 | k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 192 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= 193 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= 194 | sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 195 | sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= 196 | sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 197 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= 198 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= 199 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 200 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 201 | -------------------------------------------------------------------------------- /internal/alertingmonitor/alertingmonitor.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package alertingmonitor 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "log" 10 | 11 | proto "github.com/open-edge-platform/o11y-alerting-monitor/api/v1/management" 12 | "google.golang.org/grpc/codes" 13 | "google.golang.org/grpc/status" 14 | 15 | "github.com/open-edge-platform/o11y-tenant-controller/internal/util" 16 | ) 17 | 18 | func InitializeTenant(ctx context.Context, am proto.ManagementClient) error { 19 | tenantID, ok := ctx.Value(util.ContextKeyTenantID).(string) 20 | if !ok { 21 | return fmt.Errorf("failed to retrieve %q from context", util.ContextKeyTenantID) 22 | } 23 | 24 | log.Printf("Creating tenantID %q in alerting monitor", tenantID) 25 | _, err := am.InitializeTenant(ctx, &proto.TenantRequest{Tenant: tenantID}) 26 | if status.Code(err) == codes.AlreadyExists { 27 | log.Printf("TenantID %q already initialized in alerting monitor", tenantID) 28 | return nil 29 | } else if err != nil { 30 | return fmt.Errorf("failed to initialize tenantID %q in alerting monitor: %w", tenantID, err) 31 | } 32 | 33 | log.Printf("TenantID %q initialized in alerting monitor", tenantID) 34 | return nil 35 | } 36 | 37 | func CleanupTenant(ctx context.Context, am proto.ManagementClient) error { 38 | tenantID, ok := ctx.Value(util.ContextKeyTenantID).(string) 39 | if !ok { 40 | return fmt.Errorf("failed to retrieve %q from context", util.ContextKeyTenantID) 41 | } 42 | 43 | log.Printf("Deleting tenantID %q in alerting monitor", tenantID) 44 | _, err := am.CleanupTenant(ctx, &proto.TenantRequest{Tenant: tenantID}) 45 | if status.Code(err) == codes.NotFound { 46 | log.Printf("TenantID %q already deleted in alerting monitor", tenantID) 47 | return nil 48 | } else if err != nil { 49 | return fmt.Errorf("failed to delete tenantID %q in alerting monitor: %w", tenantID, err) 50 | } 51 | 52 | log.Printf("TenantID %q deleted in alerting monitor", tenantID) 53 | return nil 54 | } 55 | -------------------------------------------------------------------------------- /internal/alertingmonitor/alertingmonitor_suite_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package alertingmonitor_test 5 | 6 | import ( 7 | "testing" 8 | 9 | . "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | ) 12 | 13 | func TestManagement(t *testing.T) { 14 | RegisterFailHandler(Fail) 15 | RunSpecs(t, "Alerting Monitor Suite") 16 | } 17 | -------------------------------------------------------------------------------- /internal/alertingmonitor/alertingmonitor_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package alertingmonitor_test 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "log" 10 | "net" 11 | 12 | . "github.com/onsi/ginkgo/v2" 13 | . "github.com/onsi/gomega" 14 | proto "github.com/open-edge-platform/o11y-alerting-monitor/api/v1/management" 15 | "google.golang.org/grpc" 16 | "google.golang.org/grpc/codes" 17 | "google.golang.org/grpc/credentials/insecure" 18 | "google.golang.org/grpc/status" 19 | "google.golang.org/grpc/test/bufconn" 20 | "google.golang.org/protobuf/types/known/emptypb" 21 | 22 | "github.com/open-edge-platform/o11y-tenant-controller/internal/alertingmonitor" 23 | "github.com/open-edge-platform/o11y-tenant-controller/internal/util" 24 | ) 25 | 26 | var ( 27 | lis *bufconn.Listener 28 | client proto.ManagementClient 29 | server *grpc.Server 30 | mockServer *mockManagementServer 31 | ) 32 | 33 | type mockManagementServer struct { 34 | proto.UnimplementedManagementServer 35 | tenants map[string]bool 36 | simulateError bool 37 | errorToReturn error 38 | } 39 | 40 | func newMockManagementServer() *mockManagementServer { 41 | return &mockManagementServer{ 42 | tenants: make(map[string]bool), 43 | } 44 | } 45 | 46 | func (m *mockManagementServer) InitializeTenant(_ context.Context, req *proto.TenantRequest) (*emptypb.Empty, error) { 47 | if m.simulateError { 48 | return nil, m.errorToReturn 49 | } 50 | tenantID := req.GetTenant() 51 | 52 | if tenantID == "" { 53 | return nil, status.Error(codes.InvalidArgument, "invalid tenant name") 54 | } 55 | 56 | if m.tenants[tenantID] { 57 | return nil, status.Error(codes.AlreadyExists, "tenant already exists") 58 | } 59 | 60 | m.tenants[tenantID] = true 61 | return nil, nil 62 | } 63 | 64 | func (m *mockManagementServer) CleanupTenant(_ context.Context, req *proto.TenantRequest) (*emptypb.Empty, error) { 65 | if m.simulateError { 66 | return nil, m.errorToReturn 67 | } 68 | tenantID := req.GetTenant() 69 | 70 | if tenantID == "" { 71 | return nil, status.Error(codes.InvalidArgument, "invalid tenant name") 72 | } 73 | 74 | if !m.tenants[tenantID] { 75 | return nil, status.Error(codes.NotFound, "tenant not found") 76 | } 77 | 78 | delete(m.tenants, tenantID) 79 | return nil, nil 80 | } 81 | 82 | var _ = Describe("AlertingMonitor", Ordered, func() { 83 | BeforeAll(func() { 84 | lis = bufconn.Listen(1024 * 1024) 85 | 86 | // Create and register the mock server 87 | server = grpc.NewServer() 88 | mockServer = newMockManagementServer() 89 | proto.RegisterManagementServer(server, mockServer) 90 | 91 | go func() { 92 | defer GinkgoRecover() 93 | if err := server.Serve(lis); err != nil && !errors.Is(err, grpc.ErrServerStopped) { 94 | log.Printf("Error serving server: %v", err) 95 | } 96 | }() 97 | 98 | conn, err := grpc.NewClient( 99 | "passthrough://bufnet", 100 | grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { 101 | return lis.Dial() 102 | }), 103 | grpc.WithTransportCredentials(insecure.NewCredentials()), 104 | ) 105 | Expect(err).ToNot(HaveOccurred()) 106 | client = proto.NewManagementClient(conn) 107 | }) 108 | 109 | AfterAll(func() { 110 | server.Stop() 111 | Expect(lis.Close()).To(Succeed()) 112 | }) 113 | 114 | BeforeEach(func() { 115 | // Reset mock server state 116 | mockServer.tenants = make(map[string]bool) 117 | mockServer.simulateError = false 118 | mockServer.errorToReturn = nil 119 | }) 120 | 121 | It("Initialize tenant - context has no tenant value", func() { 122 | err := alertingmonitor.InitializeTenant(context.Background(), client) 123 | Expect(err).Should(HaveOccurred()) 124 | }) 125 | 126 | It("Initialize tenant with valid name", func() { 127 | ctx := context.WithValue(context.Background(), util.ContextKeyTenantID, "NewTenant") 128 | err := alertingmonitor.InitializeTenant(ctx, client) 129 | Expect(err).ShouldNot(HaveOccurred()) 130 | }) 131 | 132 | It("Initialize tenant with valid name - tenant already exists", func() { 133 | ctx := context.WithValue(context.Background(), util.ContextKeyTenantID, "ExistingTenant") 134 | 135 | // First initialization should succeed 136 | err := alertingmonitor.InitializeTenant(ctx, client) 137 | Expect(err).ShouldNot(HaveOccurred()) 138 | 139 | // Second initialization should also succeed, as AlreadyExists is marked as success 140 | err = alertingmonitor.InitializeTenant(ctx, client) 141 | Expect(err).ShouldNot(HaveOccurred()) 142 | }) 143 | 144 | It("Initialize tenant with invalid (empty) name - error returned when server validates tenant name", func() { 145 | tenant := "" 146 | 147 | ctx := context.WithValue(context.Background(), util.ContextKeyTenantID, tenant) 148 | err := alertingmonitor.InitializeTenant(ctx, client) 149 | Expect(err).Should(HaveOccurred()) 150 | 151 | // Check that the error code is InvalidArgument 152 | grpcStatus, ok := status.FromError(err) 153 | Expect(ok).To(BeTrue()) 154 | Expect(grpcStatus.Code()).To(Equal(codes.InvalidArgument)) 155 | Expect(grpcStatus.Message()).To(ContainSubstring("invalid tenant name")) 156 | }) 157 | 158 | It("Initialize tenant with valid name - unexpected error", func() { 159 | // Simulate an error during initialization 160 | mockServer.simulateError = true 161 | mockServer.errorToReturn = status.Error(codes.Internal, "unexpected server error") 162 | 163 | ctx := context.WithValue(context.Background(), util.ContextKeyTenantID, "NewTenant") 164 | err := alertingmonitor.InitializeTenant(ctx, client) 165 | Expect(err).Should(HaveOccurred()) 166 | 167 | // Check that the error code is Internal 168 | grpcStatus, ok := status.FromError(err) 169 | Expect(ok).To(BeTrue()) 170 | Expect(grpcStatus.Code()).To(Equal(codes.Internal)) 171 | Expect(grpcStatus.Message()).To(ContainSubstring("unexpected server error")) 172 | }) 173 | 174 | It("Cleanup tenant - context has no tenant value", func() { 175 | err := alertingmonitor.CleanupTenant(context.Background(), client) 176 | Expect(err).Should(HaveOccurred()) 177 | }) 178 | 179 | It("Cleanup tenant with valid name", func() { 180 | ctx := context.WithValue(context.Background(), util.ContextKeyTenantID, "NewTenant") 181 | // Initialize first so the map is not empty 182 | err := alertingmonitor.InitializeTenant(ctx, client) 183 | Expect(err).ShouldNot(HaveOccurred()) 184 | 185 | // Now, cleanup the tenant 186 | err = alertingmonitor.CleanupTenant(ctx, client) 187 | Expect(err).ShouldNot(HaveOccurred()) 188 | }) 189 | 190 | It("Cleanup tenant with valid name - tenant has already been deleted", func() { 191 | ctx := context.WithValue(context.Background(), util.ContextKeyTenantID, "NewTenant") 192 | err := alertingmonitor.InitializeTenant(ctx, client) 193 | Expect(err).ShouldNot(HaveOccurred()) 194 | 195 | err = alertingmonitor.CleanupTenant(ctx, client) 196 | Expect(err).ShouldNot(HaveOccurred()) 197 | 198 | // A second cleanup should also result in success 199 | err = alertingmonitor.CleanupTenant(ctx, client) 200 | Expect(err).ShouldNot(HaveOccurred()) 201 | }) 202 | 203 | It("Cleanup tenant with invalid (empty) name - error returned when server validates tenant name", func() { 204 | tenant := "" 205 | 206 | ctx := context.WithValue(context.Background(), util.ContextKeyTenantID, tenant) 207 | err := alertingmonitor.CleanupTenant(ctx, client) 208 | Expect(err).Should(HaveOccurred()) 209 | 210 | // Check that the error code is InvalidArgument 211 | grpcStatus, ok := status.FromError(err) 212 | Expect(ok).To(BeTrue()) 213 | Expect(grpcStatus.Code()).To(Equal(codes.InvalidArgument)) 214 | Expect(grpcStatus.Message()).To(ContainSubstring("invalid tenant name")) 215 | }) 216 | 217 | It("Cleanup tenant with valid name - unexpected error", func() { 218 | // Simulate an error during initialization 219 | mockServer.simulateError = true 220 | mockServer.errorToReturn = status.Error(codes.Internal, "unexpected server error") 221 | 222 | ctx := context.WithValue(context.Background(), util.ContextKeyTenantID, "NewTenant") 223 | err := alertingmonitor.CleanupTenant(ctx, client) 224 | Expect(err).Should(HaveOccurred()) 225 | 226 | // Check that the error code is Internal 227 | grpcStatus, ok := status.FromError(err) 228 | Expect(ok).To(BeTrue()) 229 | Expect(grpcStatus.Code()).To(Equal(codes.Internal)) 230 | Expect(grpcStatus.Message()).To(ContainSubstring("unexpected server error")) 231 | }) 232 | }) 233 | -------------------------------------------------------------------------------- /internal/config/model.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package config 5 | 6 | import ( 7 | "time" 8 | 9 | "github.com/open-edge-platform/o11y-tenant-controller/internal/util" 10 | ) 11 | 12 | type Config struct { 13 | Endpoints Endpoints `yaml:"endpoints"` 14 | Controller struct { 15 | Channel struct { 16 | MaxInflightRequests int `yaml:"maxInflightRequests"` 17 | } `yaml:"channel"` 18 | CreateDeleteWatcherTimeout time.Duration `yaml:"createDeleteWatcherTimeout"` 19 | } `yaml:"controller"` 20 | Job Job `yaml:"job"` 21 | } 22 | 23 | type Job struct { 24 | Manager struct { 25 | Deletion struct { 26 | Rate time.Duration `yaml:"rate"` 27 | } `yaml:"deletion"` 28 | } `yaml:"manager"` 29 | Backoff struct { 30 | Initial time.Duration `yaml:"initial"` 31 | Max time.Duration `yaml:"max"` 32 | TimeMultiplier float64 `yaml:"timeMultiplier"` 33 | } `yaml:"backoff"` 34 | Timeout time.Duration `yaml:"timeout"` 35 | Sre struct { 36 | Enabled bool `yaml:"enabled"` 37 | } `yaml:"sre"` 38 | } 39 | 40 | type Mimir struct { 41 | Ingester string `yaml:"ingester"` 42 | Compactor string `yaml:"compactor"` 43 | PollingRate time.Duration `yaml:"pollingRate"` 44 | DeleteVerifyMode util.VerifyMode `yaml:"deleteVerifyMode"` 45 | } 46 | 47 | type Loki struct { 48 | Write string `yaml:"write"` 49 | Backend string `yaml:"backend"` 50 | PollingRate time.Duration `yaml:"pollingRate"` 51 | MaxPollingRate time.Duration `yaml:"maxPollingRate"` 52 | DeleteVerifyMode util.VerifyMode `yaml:"deleteVerifyMode"` 53 | } 54 | 55 | type Endpoints struct { 56 | AlertingMonitor string `yaml:"alertingmonitor"` 57 | Sre string `yaml:"sre"` 58 | Mimir Mimir `yaml:"mimir"` 59 | Loki Loki `yaml:"loki"` 60 | } 61 | -------------------------------------------------------------------------------- /internal/config/reader.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package config 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | 10 | "gopkg.in/yaml.v3" 11 | ) 12 | 13 | func ReadConfig(path string) (Config, error) { 14 | file, err := os.ReadFile(path) 15 | if err != nil { 16 | return Config{}, fmt.Errorf("failed to read file %q: %w", file, err) 17 | } 18 | 19 | var cfg Config 20 | if err = yaml.Unmarshal(file, &cfg); err != nil { 21 | return Config{}, fmt.Errorf("failed to unmarshal: %w", err) 22 | } 23 | 24 | return cfg, nil 25 | } 26 | -------------------------------------------------------------------------------- /internal/config/reader_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package config 5 | 6 | import ( 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/require" 11 | 12 | "github.com/open-edge-platform/o11y-tenant-controller/internal/util" 13 | ) 14 | 15 | func TestReadConfig(t *testing.T) { 16 | t.Run("Valid config file", func(t *testing.T) { 17 | configFile, err := ReadConfig("testdata/test_config.yaml") 18 | require.NoError(t, err) 19 | require.Equal(t, 20, configFile.Controller.Channel.MaxInflightRequests, "Config value different from expected") 20 | require.Equal(t, 30*time.Minute, configFile.Job.Timeout, "Config value different from expected") 21 | require.Equal(t, time.Minute, configFile.Job.Manager.Deletion.Rate, "Config value different from expected") 22 | require.Equal(t, 10*time.Second, configFile.Job.Backoff.Initial, "Config value different from expected") 23 | require.Equal(t, 10*time.Minute, configFile.Job.Backoff.Max, "Config value different from expected") 24 | require.InEpsilon(t, 1.6, configFile.Job.Backoff.TimeMultiplier, 0, "Config value different from expected") 25 | require.Equal(t, "http://localhost:3100", configFile.Endpoints.Loki.Write, "Config value different from expected") 26 | require.Equal(t, "http://localhost:3100", configFile.Endpoints.Loki.Backend, "Config value different from expected") 27 | require.Equal(t, 20*time.Second, configFile.Endpoints.Loki.PollingRate, "Config value different from expected") 28 | require.Equal(t, time.Minute, configFile.Endpoints.Loki.MaxPollingRate, "Config value different from expected") 29 | require.Equal(t, util.LooseMode, configFile.Endpoints.Loki.DeleteVerifyMode, "Config value different from expected") 30 | require.Equal(t, "http://localhost:8080", configFile.Endpoints.Mimir.Compactor, "Config value different from expected") 31 | require.Equal(t, "http://localhost:8080", configFile.Endpoints.Mimir.Ingester, "Config value different from expected") 32 | require.Equal(t, 20*time.Second, configFile.Endpoints.Mimir.PollingRate, "Config value different from expected") 33 | require.Equal(t, util.LooseMode, configFile.Endpoints.Mimir.DeleteVerifyMode, "Config value different from expected") 34 | require.Equal(t, "http://localhost:8080", configFile.Endpoints.AlertingMonitor, "Config value different from expected") 35 | require.Equal(t, 10*time.Minute, configFile.Controller.CreateDeleteWatcherTimeout, "Config value different from expected") 36 | require.Equal(t, "http://localhost:8080", configFile.Endpoints.Sre, "Config value different from expected") 37 | require.True(t, configFile.Job.Sre.Enabled, "Config value different from expected") 38 | }) 39 | t.Run("Invalid config file name", func(t *testing.T) { 40 | _, err := ReadConfig("testdata/invalid_file_name.yaml") 41 | require.Error(t, err) 42 | }) 43 | t.Run("Invalid config file", func(t *testing.T) { 44 | _, err := ReadConfig("testdata/test_config_malformed.yaml") 45 | require.Error(t, err) 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /internal/config/testdata/test_config.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | endpoints: 5 | alertingmonitor: "http://localhost:8080" 6 | sre: "http://localhost:8080" 7 | mimir: 8 | ingester: "http://localhost:8080" 9 | compactor: "http://localhost:8080" 10 | pollingRate: 20s 11 | deleteVerifyMode: loose 12 | loki: 13 | write: "http://localhost:3100" 14 | backend: "http://localhost:3100" 15 | pollingRate: 20s 16 | maxPollingRate: 1m 17 | deleteVerifyMode: loose 18 | 19 | controller: 20 | channel: 21 | maxInflightRequests: 20 22 | createDeleteWatcherTimeout: 10m 23 | 24 | job: 25 | manager: 26 | deletion: 27 | rate: "1m" 28 | backoff: 29 | initial: "10s" 30 | max: "10m" 31 | timeMultiplier: 1.6 32 | timeout: "30m" 33 | sre: 34 | enabled: true 35 | -------------------------------------------------------------------------------- /internal/config/testdata/test_config_malformed.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | []] 5 | -------------------------------------------------------------------------------- /internal/controller/controller.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package controller 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "fmt" 10 | "log" 11 | "net/http" 12 | "time" 13 | 14 | projectwatcherv1 "github.com/open-edge-platform/orch-utils/tenancy-datamodel/build/apis/projectwatcher.edge-orchestrator.intel.com/v1" 15 | nexus "github.com/open-edge-platform/orch-utils/tenancy-datamodel/build/nexus-client" 16 | "github.com/prometheus/client_golang/prometheus/promhttp" 17 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 18 | "k8s.io/client-go/rest" 19 | 20 | "github.com/open-edge-platform/o11y-tenant-controller/internal/projects" 21 | "github.com/open-edge-platform/o11y-tenant-controller/internal/util" 22 | ) 23 | 24 | type Action int 25 | 26 | const ( 27 | InitializeTenant Action = iota 28 | CleanupTenant 29 | ) 30 | 31 | type TenantController struct { 32 | ComSig chan CommChannel 33 | client *nexus.Clientset 34 | server *http.Server 35 | watcherTimeout time.Duration 36 | 37 | grpcServer projects.Server 38 | } 39 | 40 | type CommChannel struct { 41 | Project *nexus.RuntimeprojectRuntimeProject 42 | Status Action 43 | } 44 | 45 | func New(buffer int, watcherTimeout time.Duration, grpcServer *projects.Server) (*TenantController, error) { 46 | c, err := rest.InClusterConfig() 47 | if err != nil { 48 | return nil, fmt.Errorf("failed to read kubernetes service account token: %w", err) 49 | } 50 | 51 | client, err := nexus.NewForConfig(c) 52 | if err != nil { 53 | return nil, fmt.Errorf("failed to open communication with the kubernetes server: %w", err) 54 | } 55 | 56 | mux := http.NewServeMux() 57 | mux.Handle("/metrics", promhttp.Handler()) 58 | 59 | server := &http.Server{ 60 | Addr: ":9273", 61 | Handler: mux, 62 | ReadTimeout: 5 * time.Second, 63 | WriteTimeout: 10 * time.Second, 64 | IdleTimeout: 15 * time.Second, 65 | } 66 | 67 | return &TenantController{ 68 | ComSig: make(chan CommChannel, buffer), 69 | client: client, 70 | server: server, 71 | watcherTimeout: watcherTimeout, 72 | grpcServer: *grpcServer, 73 | }, nil 74 | } 75 | 76 | func (tc *TenantController) Start() error { 77 | if err := tc.addProjectWatcher(); err != nil { 78 | return fmt.Errorf("failed to create project watcher: %w", err) 79 | } 80 | 81 | if _, err := tc.client.TenancyMultiTenancy().Runtime().Orgs("*").Folders("*").Projects("*").RegisterAddCallback(tc.addHandler); err != nil { 82 | return fmt.Errorf("unable to register project creation callback: %w", err) 83 | } 84 | 85 | if _, err := tc.client.TenancyMultiTenancy().Runtime().Orgs("*").Folders("*").Projects("*").RegisterUpdateCallback(tc.updateHandler); err != nil { 86 | return fmt.Errorf("unable to register project update callback: %w", err) 87 | } 88 | 89 | // Callback for project watcher deletion is safeguard for unintended project watcher deletion eg. during tenant controller update. 90 | if _, err := tc.client.TenancyMultiTenancy().Config().ProjectWatchers(util.AppName).RegisterDeleteCallback(tc.projectWatcherDeleteHandler); err != nil { 91 | return fmt.Errorf("unable to register project watcher delete callback: %w", err) 92 | } 93 | 94 | go func() { 95 | if err := tc.server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { 96 | log.Printf("Prometheus server error: %v", err) 97 | } 98 | }() 99 | 100 | log.Print("Tenant controller starting") 101 | return nil 102 | } 103 | 104 | func (tc *TenantController) Stop() { 105 | log.Print("Tenant controller stopping") 106 | tc.client.UnsubscribeAll() 107 | 108 | if err := tc.deleteProjectWatcher(); err != nil { 109 | log.Printf("Failed to delete watcher: %v", err) 110 | } 111 | stopped := make(chan struct{}) 112 | go func() { 113 | tc.grpcServer.GrpcServer.GracefulStop() 114 | close(stopped) 115 | }() 116 | 117 | dur := 5 * time.Second 118 | t := time.NewTimer(dur) 119 | select { 120 | case <-t.C: 121 | log.Printf("gRPC server did not stop in %v, stopping forcefully", dur) 122 | tc.grpcServer.GrpcServer.Stop() 123 | case <-stopped: 124 | t.Stop() 125 | } 126 | 127 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 128 | defer cancel() 129 | if err := tc.server.Shutdown(ctx); err != nil { 130 | log.Printf("Prometheus server shutdown error: %v", err) 131 | } 132 | 133 | close(tc.ComSig) 134 | } 135 | 136 | func (a Action) String() string { 137 | return [...]string{"InitializeTenant", "CleanupTenant"}[a] 138 | } 139 | 140 | // Callback for project watcher deletion is safeguard for unintended project watcher deletion eg. during tenant controller update. 141 | func (tc *TenantController) projectWatcherDeleteHandler(_ *nexus.ProjectwatcherProjectWatcher) { 142 | err := tc.addProjectWatcher() 143 | if err != nil { 144 | log.Print(err) 145 | } 146 | } 147 | 148 | func (tc *TenantController) addProjectWatcher() error { 149 | ctx, cancel := context.WithTimeout(context.Background(), tc.watcherTimeout) 150 | defer cancel() 151 | 152 | _, err := tc.client.TenancyMultiTenancy().Config().AddProjectWatchers(ctx, &projectwatcherv1.ProjectWatcher{ObjectMeta: metav1.ObjectMeta{ 153 | Name: util.AppName, 154 | }}) 155 | 156 | if nexus.IsAlreadyExists(err) { 157 | log.Print("Project watcher already exists") 158 | } else if err != nil { 159 | return err 160 | } 161 | return nil 162 | } 163 | 164 | func (tc *TenantController) deleteProjectWatcher() error { 165 | ctx, cancel := context.WithTimeout(context.Background(), tc.watcherTimeout) 166 | defer cancel() 167 | 168 | err := tc.client.TenancyMultiTenancy().Config().DeleteProjectWatchers(ctx, util.AppName) 169 | 170 | if nexus.IsChildNotFound(err) { 171 | log.Print("Project watcher already deleted") 172 | } else if err != nil { 173 | return err 174 | } 175 | return nil 176 | } 177 | 178 | func (tc *TenantController) addHandler(project *nexus.RuntimeprojectRuntimeProject) { 179 | log.Printf("Project %q added", project.UID) 180 | pd := projects.ProjectData{ 181 | ProjectName: project.DisplayName(), 182 | OrgID: project.GetLabels()[util.OrgNameLabel], 183 | } 184 | 185 | if project.Spec.Deleted { 186 | tc.ComSig <- CommChannel{project, CleanupTenant} 187 | pd.Status = projects.ProjectDeleted 188 | } else { 189 | tc.ComSig <- CommChannel{project, InitializeTenant} 190 | pd.Status = projects.ProjectCreated 191 | } 192 | 193 | tc.grpcServer.Mu.Lock() 194 | tc.grpcServer.Projects[string(project.UID)] = pd 195 | tc.grpcServer.Mu.Unlock() 196 | tc.grpcServer.BroadcastUpdate() 197 | } 198 | 199 | func (tc *TenantController) updateHandler(_, project *nexus.RuntimeprojectRuntimeProject) { 200 | log.Printf("Project %q updated", project.UID) 201 | pd := projects.ProjectData{ 202 | ProjectName: project.DisplayName(), 203 | OrgID: project.GetLabels()[util.OrgNameLabel], 204 | } 205 | 206 | if project.Spec.Deleted { 207 | tc.ComSig <- CommChannel{project, CleanupTenant} 208 | pd.Status = projects.ProjectDeleted 209 | } else { 210 | tc.ComSig <- CommChannel{project, InitializeTenant} 211 | pd.Status = projects.ProjectCreated 212 | } 213 | 214 | tc.grpcServer.Mu.Lock() 215 | tc.grpcServer.Projects[string(project.UID)] = pd 216 | tc.grpcServer.Mu.Unlock() 217 | tc.grpcServer.BroadcastUpdate() 218 | } 219 | -------------------------------------------------------------------------------- /internal/jobs/jobs.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package jobs 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "fmt" 10 | "log" 11 | "math" 12 | "sync/atomic" 13 | "time" 14 | 15 | amproto "github.com/open-edge-platform/o11y-alerting-monitor/api/v1/management" 16 | sreproto "github.com/open-edge-platform/o11y-sre-exporter/api/config-reloader" 17 | projectwatchv1 "github.com/open-edge-platform/orch-utils/tenancy-datamodel/build/apis/projectactivewatcher.edge-orchestrator.intel.com/v1" 18 | nexus "github.com/open-edge-platform/orch-utils/tenancy-datamodel/build/nexus-client" 19 | "github.com/prometheus/client_golang/prometheus" 20 | "github.com/prometheus/client_golang/prometheus/promauto" 21 | "golang.org/x/sync/errgroup" 22 | "google.golang.org/grpc" 23 | "k8s.io/apimachinery/pkg/types" 24 | 25 | "github.com/open-edge-platform/o11y-tenant-controller/internal/alertingmonitor" 26 | "github.com/open-edge-platform/o11y-tenant-controller/internal/config" 27 | "github.com/open-edge-platform/o11y-tenant-controller/internal/controller" 28 | "github.com/open-edge-platform/o11y-tenant-controller/internal/loki" 29 | "github.com/open-edge-platform/o11y-tenant-controller/internal/mimir" 30 | "github.com/open-edge-platform/o11y-tenant-controller/internal/projects" 31 | "github.com/open-edge-platform/o11y-tenant-controller/internal/sre" 32 | "github.com/open-edge-platform/o11y-tenant-controller/internal/util" 33 | "github.com/open-edge-platform/o11y-tenant-controller/internal/watcher" 34 | ) 35 | 36 | type jobStatus int 37 | 38 | var projectIDs = promauto.NewGaugeVec( 39 | prometheus.GaugeOpts{ 40 | Name: "project_metadata", 41 | Help: "Exposes project metadata", 42 | }, []string{"projectId", "projectName", "orgName", "status"}, 43 | ) 44 | 45 | const ( 46 | jobCreated jobStatus = iota 47 | jobInProgress 48 | jobCancelled 49 | tenantCreated 50 | tenantDeleted 51 | tenantIDsNotMatch 52 | ) 53 | 54 | type JobManager struct { 55 | comSig chan controller.CommChannel 56 | jobList map[types.UID]*job 57 | jobCfg config.Job 58 | endpointsCfg config.Endpoints 59 | cancelFn context.CancelFunc 60 | done chan struct{} 61 | 62 | amClient amproto.ManagementClient 63 | sreClient sreproto.ManagementClient 64 | } 65 | 66 | type job struct { 67 | project *nexus.RuntimeprojectRuntimeProject 68 | status atomic.Int32 69 | jobCfg config.Job 70 | endpointsCfg config.Endpoints 71 | cancelFn context.CancelFunc 72 | 73 | amClient amproto.ManagementClient 74 | sreClient sreproto.ManagementClient 75 | } 76 | 77 | func New(channel chan controller.CommChannel, jCfg config.Job, endpoints config.Endpoints, amConn, sreConn *grpc.ClientConn) *JobManager { 78 | return &JobManager{ 79 | comSig: channel, 80 | jobList: map[types.UID]*job{}, 81 | jobCfg: jCfg, 82 | endpointsCfg: endpoints, 83 | done: make(chan struct{}), 84 | amClient: amproto.NewManagementClient(amConn), 85 | sreClient: sreproto.NewManagementClient(sreConn), 86 | } 87 | } 88 | 89 | func (jm *JobManager) Start(ticker *time.Ticker) { 90 | ctx, cancel := context.WithCancel(context.Background()) 91 | jm.cancelFn = cancel 92 | go func() { 93 | for { 94 | select { 95 | case <-jm.done: 96 | return 97 | case v := <-jm.comSig: 98 | switch v.Status { 99 | case controller.InitializeTenant: 100 | jm.startJob(ctx, v.Project, controller.InitializeTenant) 101 | case controller.CleanupTenant: 102 | jm.startJob(ctx, v.Project, controller.CleanupTenant) 103 | } 104 | case <-ticker.C: 105 | for k, job := range jm.jobList { 106 | status := jobStatus(job.status.Load()) 107 | //nolint:staticcheck // Linter suggests: "QF1003: could use tagged switch on status" 108 | // If this suggestion were to be applied, then exhaustive linter would be unhappy 109 | // that there are missing cases in switch of iota type jobs.jobStatus 110 | if status == tenantDeleted { 111 | removeProjectMetadata(job.project) 112 | delete(jm.jobList, k) 113 | } else if status == tenantIDsNotMatch { 114 | removeProjectMetadataByID(string(k)) 115 | delete(jm.jobList, k) 116 | } 117 | } 118 | } 119 | } 120 | }() 121 | } 122 | 123 | func (jm *JobManager) Stop() { 124 | if jm.cancelFn != nil { 125 | jm.cancelFn() 126 | } 127 | close(jm.done) 128 | } 129 | 130 | func (jm *JobManager) startJob(ctx context.Context, project *nexus.RuntimeprojectRuntimeProject, action controller.Action) { 131 | setProjectMetadata(project, action) 132 | job, exists := jm.jobList[project.UID] 133 | if exists { 134 | job.cancel() 135 | job.run(ctx, action) 136 | } else { 137 | job = newJob(project, jm.jobCfg, jm.endpointsCfg, jm.amClient, jm.sreClient) 138 | jm.jobList[project.UID] = job 139 | job.run(ctx, action) 140 | } 141 | } 142 | 143 | func newJob(project *nexus.RuntimeprojectRuntimeProject, jCfg config.Job, 144 | endpoints config.Endpoints, am amproto.ManagementClient, sreCl sreproto.ManagementClient) *job { 145 | return &job{ 146 | project: project, 147 | jobCfg: jCfg, 148 | endpointsCfg: endpoints, 149 | amClient: am, 150 | sreClient: sreCl, 151 | } 152 | } 153 | 154 | func (j *job) run(parentCtx context.Context, action controller.Action) { 155 | j.status.Store(int32(jobInProgress)) 156 | 157 | go func() { 158 | ctx, cancel := context.WithCancel(parentCtx) 159 | j.cancelFn = cancel 160 | defer cancel() 161 | 162 | switch action { 163 | case controller.InitializeTenant: 164 | j.manageTenant(ctx, j.initializeTenant, controller.InitializeTenant) 165 | if errors.Is(ctx.Err(), context.Canceled) { 166 | j.status.Store(int32(jobCancelled)) 167 | return 168 | } 169 | j.status.Store(int32(tenantCreated)) 170 | case controller.CleanupTenant: 171 | j.manageTenant(ctx, j.cleanupTenant, controller.CleanupTenant) 172 | if errors.Is(ctx.Err(), context.Canceled) { 173 | j.status.Store(int32(jobCancelled)) 174 | return 175 | } 176 | if jobStatus(j.status.Load()) != tenantIDsNotMatch { 177 | j.status.Store(int32(tenantDeleted)) 178 | } 179 | } 180 | }() 181 | } 182 | 183 | func (j *job) cancel() { 184 | if j.cancelFn != nil { 185 | j.cancelFn() 186 | } 187 | } 188 | 189 | func (j *job) manageTenant(parentCtx context.Context, tenantAction func(context.Context) error, action controller.Action) { 190 | cnt := 0 191 | id := j.project.UID 192 | ctx := context.WithValue(parentCtx, util.ContextKeyTenantID, string(id)) 193 | 194 | for { 195 | err := tenantAction(ctx) 196 | if err == nil { 197 | log.Printf("%v action for tenantID %q completed successfully", action.String(), id) 198 | break 199 | } 200 | 201 | if errors.As(err, &watcher.IDsDoNotMatchError{}) { 202 | log.Printf("%v action for tenantID %q completed successfully - watcher deleted manually", action.String(), id) 203 | j.status.Store(int32(tenantIDsNotMatch)) 204 | break 205 | } 206 | 207 | log.Printf("Failed to %s: %v", action.String(), err) 208 | 209 | sleepTime := j.jobCfg.Backoff.Max 210 | 211 | calcTime := math.Pow(j.jobCfg.Backoff.TimeMultiplier, float64(cnt)) * float64(j.jobCfg.Backoff.Initial) 212 | if calcTime < float64(j.jobCfg.Backoff.Max) { 213 | sleepTime = time.Duration(calcTime) 214 | cnt++ 215 | } 216 | 217 | err = util.SleepWithContext(ctx, sleepTime) 218 | if errors.Is(err, context.Canceled) { 219 | log.Printf("%v action for tenantID %q cancelled", action.String(), id) 220 | break 221 | } 222 | } 223 | } 224 | 225 | func (j *job) initializeTenant(parentCtx context.Context) error { 226 | timedOutCtx, cancel := context.WithTimeout(parentCtx, j.jobCfg.Timeout) 227 | defer cancel() 228 | err := watcher.CreateUpdateWatcher(parentCtx, j.project, 229 | projectwatchv1.StatusIndicationInProgress, fmt.Sprintf("Creating tenant %q", j.project.UID)) 230 | if err != nil { 231 | return err 232 | } 233 | 234 | g, ctx := errgroup.WithContext(timedOutCtx) 235 | 236 | g.Go(func() error { return alertingmonitor.InitializeTenant(ctx, j.amClient) }) 237 | if j.jobCfg.Sre.Enabled { 238 | g.Go(func() error { return sre.InitializeTenant(ctx, j.sreClient) }) 239 | } 240 | 241 | if err := g.Wait(); err != nil { 242 | return err 243 | } 244 | 245 | return watcher.CreateUpdateWatcher(parentCtx, j.project, 246 | projectwatchv1.StatusIndicationIdle, fmt.Sprintf("Tenant %q created", j.project.UID)) 247 | } 248 | 249 | func (j *job) cleanupTenant(parentCtx context.Context) error { 250 | timedOutCtx, cancel := context.WithTimeout(parentCtx, j.jobCfg.Timeout) 251 | defer cancel() 252 | err := watcher.CreateUpdateWatcher(parentCtx, j.project, 253 | projectwatchv1.StatusIndicationInProgress, fmt.Sprintf("Deleting tenant %q", j.project.UID)) 254 | if err != nil { 255 | return err 256 | } 257 | 258 | g, ctx := errgroup.WithContext(timedOutCtx) 259 | 260 | g.Go(func() error { return alertingmonitor.CleanupTenant(ctx, j.amClient) }) 261 | if j.jobCfg.Sre.Enabled { 262 | g.Go(func() error { return sre.CleanupTenant(ctx, j.sreClient) }) 263 | } 264 | g.Go(func() error { return loki.CleanupTenant(ctx, j.endpointsCfg.Loki) }) 265 | g.Go(func() error { return mimir.CleanupTenant(ctx, j.endpointsCfg.Mimir) }) 266 | 267 | if err := g.Wait(); err != nil { 268 | return err 269 | } 270 | 271 | return watcher.DeleteWatcher(parentCtx, j.project) 272 | } 273 | 274 | func setProjectMetadata(project *nexus.RuntimeprojectRuntimeProject, action controller.Action) { 275 | projectID, projectName, orgName := extractLabelsFrom(project) 276 | status := projects.ProjectCreated 277 | if action == controller.CleanupTenant { 278 | status = projects.ProjectDeleted 279 | } 280 | 281 | labels := prometheus.Labels{ 282 | "projectId": projectID, 283 | "projectName": projectName, 284 | "orgName": orgName, 285 | "status": string(status), 286 | } 287 | 288 | projectIDs.With(labels).Set(1) 289 | log.Printf("Added project metadata: %q", labels) 290 | } 291 | 292 | func removeProjectMetadata(project *nexus.RuntimeprojectRuntimeProject) { 293 | projectID, projectName, orgName := extractLabelsFrom(project) 294 | 295 | labels := prometheus.Labels{ 296 | "projectId": projectID, 297 | "projectName": projectName, 298 | "orgName": orgName, 299 | } 300 | 301 | numberDeleted := projectIDs.DeletePartialMatch(labels) 302 | if numberDeleted > 0 { 303 | log.Printf("Removed project metadata: %q", labels) 304 | } else { 305 | log.Printf("Failed to remove project metadata: %q", labels) 306 | } 307 | } 308 | 309 | func removeProjectMetadataByID(projectID string) { 310 | labels := prometheus.Labels{ 311 | "projectId": projectID, 312 | } 313 | 314 | numberDeleted := projectIDs.DeletePartialMatch(labels) 315 | if numberDeleted > 0 { 316 | log.Printf("Removed project metadata: %q", labels) 317 | } else { 318 | log.Printf("Failed to remove project metadata: %q", labels) 319 | } 320 | } 321 | 322 | func extractLabelsFrom(project *nexus.RuntimeprojectRuntimeProject) (projectID, projectName, orgName string) { 323 | orgName = "" 324 | if project.GetLabels() != nil { 325 | orgName = project.GetLabels()[util.OrgNameLabel] 326 | } 327 | 328 | return string(project.UID), project.DisplayName(), orgName 329 | } 330 | -------------------------------------------------------------------------------- /internal/loki/loki.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package loki 5 | 6 | import ( 7 | "context" 8 | "encoding/json" 9 | "fmt" 10 | "log" 11 | "time" 12 | 13 | "github.com/open-edge-platform/o11y-tenant-controller/internal/config" 14 | "github.com/open-edge-platform/o11y-tenant-controller/internal/util" 15 | ) 16 | 17 | type DeleteLogRequest []struct { 18 | RequestID string `json:"request_id"` 19 | StartTime float64 `json:"start_time"` 20 | EndTime float64 `json:"end_time"` 21 | Query string `json:"query"` 22 | Status string `json:"status"` 23 | CreatedAt float64 `json:"created_at"` 24 | } 25 | 26 | func CleanupTenant(ctx context.Context, urlCfg config.Loki) error { 27 | tenantID, ok := ctx.Value(util.ContextKeyTenantID).(string) 28 | if !ok { 29 | return fmt.Errorf("failed to retrieve %q from context", util.ContextKeyTenantID) 30 | } 31 | log.Printf("Deleting tenantID %q logs", tenantID) 32 | 33 | if err := flushIngesters(ctx, urlCfg, tenantID); err != nil { 34 | return fmt.Errorf("failed to flush ingesters for tenantID %q: %w", tenantID, err) 35 | } 36 | 37 | if err := deleteLogsRequest(ctx, urlCfg, tenantID); err != nil { 38 | return fmt.Errorf("failed to delete logs for tenantID %q: %w", tenantID, err) 39 | } 40 | 41 | if err := checkDeletionStatus(ctx, urlCfg, tenantID); err != nil { 42 | return fmt.Errorf("failed to check deletion status for tenantID %q: %w", tenantID, err) 43 | } 44 | 45 | log.Printf("TenantID %q logs deleted", tenantID) 46 | return nil 47 | } 48 | 49 | func flushIngesters(ctx context.Context, urlCfg config.Loki, tenantID string) error { 50 | urlRaw := fmt.Sprintf("%v/flush", urlCfg.Write) 51 | return util.PostReq(ctx, urlRaw, tenantID) 52 | } 53 | 54 | func deleteLogsRequest(ctx context.Context, urlCfg config.Loki, tenantID string) error { 55 | urlRaw := fmt.Sprintf("%v/loki/api/v1/delete?query={__tenant_id__=\"%v\"}&start=0000000001", urlCfg.Backend, tenantID) 56 | return util.PostReq(ctx, urlRaw, tenantID) 57 | } 58 | 59 | func checkDeletionStatus(ctx context.Context, urlCfg config.Loki, tenantID string) error { 60 | var deletionLogBody DeleteLogRequest 61 | 62 | cnt := 0 63 | sleepTime := urlCfg.PollingRate 64 | urlRaw := fmt.Sprintf("%v/loki/api/v1/delete", urlCfg.Backend) 65 | 66 | log.Printf("Waiting for tenantID %q logs deletion in Loki...", tenantID) 67 | for { 68 | if err := util.SleepWithContext(ctx, sleepTime); err != nil { 69 | return err 70 | } 71 | 72 | body, err := util.GetReq(ctx, urlRaw, tenantID) 73 | if err != nil { 74 | return err 75 | } 76 | 77 | if err := json.Unmarshal(body, &deletionLogBody); err != nil { 78 | return fmt.Errorf("failed to unmarshal response body: %w", err) 79 | } 80 | 81 | if len(deletionLogBody) != 0 { 82 | newestDelReq := deletionLogBody[len(deletionLogBody)-1] 83 | if urlCfg.DeleteVerifyMode == util.LooseMode { 84 | break 85 | } 86 | 87 | if newestDelReq.Status == "processed" { 88 | break 89 | } 90 | continue 91 | } 92 | 93 | // In case of empty response from loki, add new deletion request, and try checking again. 94 | // Every retry have longer waiting period - up to MaxPolling rate. 95 | log.Printf("Loki: empty deletion status response for tenant %q, retrying...", tenantID) 96 | 97 | err = deleteLogsRequest(ctx, urlCfg, tenantID) 98 | if err != nil { 99 | return err 100 | } 101 | 102 | sleepTime = urlCfg.MaxPollingRate 103 | calcTime := time.Duration(1<&2 13 | exit 1 14 | fi 15 | 16 | TOOL_VERSIONS_FILE="$1" 17 | 18 | # Function to install a tool using asdf 19 | install_tool() { 20 | local tool=$1 21 | local version=$2 22 | 23 | # Check if the plugin is installed, if not, install it 24 | if ! asdf plugin-list | grep -q "^$tool\$"; then 25 | echo "Installing plugin for $tool..." 26 | if ! asdf plugin-add "$tool"; then 27 | echo "Error: Failed to install plugin for $tool" >&2 28 | return 29 | else 30 | echo "Successfully installed plugin for $tool" 31 | fi 32 | fi 33 | 34 | # Check if the tool version is installed, if not, install it 35 | if asdf list "$tool" | grep -q "$version"; then 36 | echo "$tool $version is already installed." 37 | else 38 | echo "Installing $tool $version..." 39 | if ! asdf install "$tool" "$version"; then 40 | echo "Error: Failed to install $tool $version" >&2 41 | return 42 | else 43 | echo "Successfully installed $tool $version" 44 | fi 45 | fi 46 | } 47 | 48 | # Function to install pipx packages 49 | install_pipx_packages() { 50 | for package in "${PIPX_PACKAGES[@]}"; do 51 | if pipx install "$package"; then 52 | echo "Successfully installed $package" 53 | else 54 | echo "Error installing $package" >&2 55 | fi 56 | done 57 | } 58 | 59 | # Read the .tool-versions file and install each tool 60 | while read -r line; do 61 | # Skip empty lines 62 | if [[ -z "$line" ]]; then 63 | continue 64 | fi 65 | 66 | tool=$(echo "$line" | awk '{print $1}') 67 | version=$(echo "$line" | awk '{print $2}') 68 | install_tool "$tool" "$version" 69 | done < "$TOOL_VERSIONS_FILE" 70 | 71 | # Install go packages 72 | go install github.com/boumenot/gocover-cobertura@v1.3.0 73 | 74 | # Call the function to install pipx packages 75 | install_pipx_packages 76 | 77 | # Ensure the installation path is in $PATH 78 | pipx ensurepath 79 | 80 | -------------------------------------------------------------------------------- /scripts/lintJsons.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | PATHS_TO_EXCLUDE=$(cat < /dev/null; then 23 | echo "\"$json_file\" is not pretty printed. To pretty-print: jq . \"$json_file\" > tmp && mv tmp \"$json_file\"" 24 | error_found=1 25 | fi 26 | done 27 | 28 | # Return error code 1 if any errors were found 29 | if [ $error_found -eq 1 ]; then 30 | exit 1 31 | fi 32 | -------------------------------------------------------------------------------- /yamllint-conf.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | # yamllint config 5 | 6 | extends: default 7 | 8 | rules: 9 | empty-lines: 10 | max-end: 1 11 | line-length: 12 | max: 160 13 | braces: 14 | min-spaces-inside: 0 15 | max-spaces-inside: 1 16 | brackets: 17 | min-spaces-inside: 0 18 | max-spaces-inside: 1 19 | document-start: disable 20 | 21 | ignore: 22 | - ci/ 23 | - trivy/ 24 | - .github/ 25 | - .git/ 26 | - .golangci.yml 27 | # ignore files with Helm template syntax (yamllint can't parse them) and deliberately malformed 28 | - deployments/observability-tenant-controller/templates/deployment.yaml 29 | - deployments/observability-tenant-controller/templates/service.yaml 30 | - deployments/observability-tenant-controller/templates/network_policy.yaml 31 | - internal/config/testdata/test_config_malformed.yaml 32 | --------------------------------------------------------------------------------