├── .chglog ├── CHANGELOG.tpl.md └── config.yml ├── .githooks ├── commit-msg ├── pre-commit └── pre-push ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── objective.md ├── renovate.json ├── stale.yml └── workflows │ ├── compile.yml │ ├── release.yml │ ├── security.yml │ ├── snapshot.yml │ └── test.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── PROJECT ├── README.md ├── alerts.go ├── api └── v1 │ ├── alerts_apmcondition_types.go │ ├── alerts_apmcondition_types_test.go │ ├── alerts_apmcondition_webhook.go │ ├── alerts_apmcondition_webhook_test.go │ ├── alerts_nrqlcondition_types.go │ ├── alerts_nrqlcondition_types_test.go │ ├── alerts_nrqlcondition_webhook.go │ ├── alerts_nrqlcondition_webhook_test.go │ ├── alerts_policy_types.go │ ├── alerts_policy_types_test.go │ ├── alerts_policy_webhook.go │ ├── alerts_policy_webhook_test.go │ ├── alertschannel_types.go │ ├── alertschannel_types_test.go │ ├── alertschannel_webhook.go │ ├── alertschannel_webhook_test.go │ ├── apmalertcondition_types.go │ ├── apmalertcondition_types_test.go │ ├── apmalertcondition_webhook.go │ ├── apmalertcondition_webhook_test.go │ ├── common.go │ ├── groupversion_info.go │ ├── nrqlalertcondition_types.go │ ├── nrqlalertcondition_types_integration_test.go │ ├── nrqlalertcondition_types_test.go │ ├── nrqlalertcondition_webhook.go │ ├── nrqlalertcondition_webhook_test.go │ ├── policy_types.go │ ├── policy_types_test.go │ ├── policy_webhook.go │ ├── policy_webhook_test.go │ ├── suite_test.go │ └── zz_generated.deepcopy.go ├── build ├── .keep ├── compile.mk ├── docker.mk ├── document.mk ├── generate.mk ├── generate │ └── boilerplate.go.txt ├── kube.mk ├── lint.mk ├── package │ └── Dockerfile ├── release.mk ├── test.mk ├── tools.mk └── util.mk ├── cla.md ├── config ├── certmanager │ ├── certificate.yaml │ ├── kustomization.yaml │ └── kustomizeconfig.yaml ├── crd │ ├── bases │ │ ├── nr.k8s.newrelic.com_alertsapmconditions.yaml │ │ ├── nr.k8s.newrelic.com_alertschannels.yaml │ │ ├── nr.k8s.newrelic.com_alertsnrqlconditions.yaml │ │ ├── nr.k8s.newrelic.com_alertspolicies.yaml │ │ ├── nr.k8s.newrelic.com_apmalertconditions.yaml │ │ ├── nr.k8s.newrelic.com_nrqlalertconditions.yaml │ │ └── nr.k8s.newrelic.com_policies.yaml │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ └── patches │ │ ├── cainjection_in_alertsapmconditions.yaml │ │ ├── cainjection_in_alertschannels.yaml │ │ ├── cainjection_in_alertsnrqlconditions.yaml │ │ ├── cainjection_in_alertspolicies.yaml │ │ ├── cainjection_in_apmalertconditions.yaml │ │ ├── cainjection_in_newrelicpolicies.yaml │ │ ├── cainjection_in_nrqlalertconditions.yaml │ │ ├── webhook_in_alertsapmconditions.yaml │ │ ├── webhook_in_alertschannels.yaml │ │ ├── webhook_in_alertsnrqlconditions.yaml │ │ ├── webhook_in_alertspolicies.yaml │ │ ├── webhook_in_apmalertconditions.yaml │ │ ├── webhook_in_newrelicpolicies.yaml │ │ └── webhook_in_nrqlalertconditions.yaml ├── default │ ├── kustomization.yaml │ ├── manager_auth_proxy_patch.yaml │ ├── manager_webhook_patch.yaml │ └── webhookcainjection_patch.yaml ├── development │ └── kustomization.yaml ├── manager │ ├── kustomization.yaml │ ├── manager.yaml │ └── new_relic_agent_patch.yaml ├── prometheus │ ├── kustomization.yaml │ └── monitor.yaml ├── rbac │ ├── alertsapmcondition_editor_role.yaml │ ├── alertsapmcondition_viewer_role.yaml │ ├── alertschannel_editor_role.yaml │ ├── alertschannel_viewer_role.yaml │ ├── alertsnrqlcondition_editor_role.yaml │ ├── alertsnrqlcondition_viewer_role.yaml │ ├── alertspolicy_editor_role.yaml │ ├── alertspolicy_viewer_role.yaml │ ├── apmalertcondition_editor_role.yaml │ ├── apmalertcondition_viewer_role.yaml │ ├── auth_proxy_role.yaml │ ├── auth_proxy_role_binding.yaml │ ├── auth_proxy_service.yaml │ ├── kustomization.yaml │ ├── leader_election_role.yaml │ ├── leader_election_role_binding.yaml │ ├── newrelicpolicy_editor_role.yaml │ ├── newrelicpolicy_viewer_role.yaml │ ├── nrqlalertcondition_editor_role.yaml │ ├── nrqlalertcondition_viewer_role.yaml │ ├── role.yaml │ ├── role_binding.yaml │ ├── secrets_role.yaml │ └── secrets_role_binding.yaml ├── samples │ ├── nr-alerts_v1_nrqlalertcondition.yaml │ ├── nr-alerts_v1beta1_nrqlalertcondition.yaml │ ├── nr_v1_apmalertcondition.yaml │ └── nr_v1_newrelicpolicy.yaml └── webhook │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ ├── manifests.yaml │ └── service.yaml ├── controllers ├── alerts_apmcondition_controller.go ├── alerts_apmcondition_controller_integration_test.go ├── alerts_nrqlcondition_controller.go ├── alerts_nrqlcondition_controller_integration_test.go ├── alerts_policy_controller.go ├── alerts_policy_controller_integration_api_test.go ├── alerts_policy_controller_integration_test.go ├── alertschannel_controller.go ├── alertschannel_controller_test.go ├── apmalertcondition_controller.go ├── apmalertcondition_controller_integration_test.go ├── nrqlalertcondition_controller.go ├── nrqlalertcondition_controller_integration_test.go ├── policy_controller.go ├── policy_controller_integration_api_test.go ├── policy_controller_integration_test.go └── suite_test.go ├── errors └── collector.go ├── examples ├── development │ ├── personal-api-key-patch.yaml │ ├── personalized-policy-name-patch.yaml │ └── use-staging-patch.yaml ├── example_alerts_channel_email.yaml ├── example_alerts_channel_webhook.yaml ├── example_apm_alert_condition.yaml ├── example_new_relic_agent_config.yaml ├── example_nrql_alert_condition.yaml ├── example_policy.yaml ├── example_policy_apm.yaml ├── example_secret.yaml └── kustomization.yaml ├── go.mod ├── go.sum ├── hack └── boilerplate.go.txt ├── interfaces ├── interfacesfakes │ ├── fake_k8s_client.go │ └── fake_new_relic_alerts_client.go └── new_relic_alert_client.go ├── internal ├── info │ └── main.go └── testutil │ └── alerts.go ├── main.go ├── nrgoagent.go ├── scripts └── release.sh └── tools └── tools.go /.chglog/CHANGELOG.tpl.md: -------------------------------------------------------------------------------- 1 | {{ if .Versions -}} 2 | {{ if .Unreleased.CommitGroups -}} 3 | 4 | ## [Unreleased] 5 | 6 | {{ range .Unreleased.CommitGroups -}} 7 | ### {{ .Title }} 8 | {{ range .Commits -}} 9 | - {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }} 10 | {{ end -}} 11 | {{ end -}} 12 | {{ end -}} 13 | 14 | {{ range .Versions -}} 15 | 16 | ## {{ if .Tag.Previous }}[{{ .Tag.Name }}]{{ else }}{{ .Tag.Name }}{{ end }} - {{ datetime "2006-01-02" .Tag.Date }} 17 | {{ range .CommitGroups -}} 18 | ### {{ .Title }} 19 | {{ range .Commits -}} 20 | - {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }} 21 | {{ end }} 22 | {{ end -}} 23 | 24 | {{ if .NoteGroups -}} 25 | {{ range .NoteGroups -}} 26 | ### {{ .Title }} 27 | {{ range .Notes }} 28 | {{ .Body }} 29 | {{ end }} 30 | {{ end -}} 31 | {{ end -}} 32 | {{ end -}} 33 | 34 | [Unreleased]: {{ .Info.RepositoryURL }}/compare/{{ $latest := index .Versions 0 }}{{ $latest.Tag.Name }}...HEAD 35 | {{ range .Versions -}} 36 | {{ if .Tag.Previous -}} 37 | [{{ .Tag.Name }}]: {{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }} 38 | {{ end -}} 39 | {{ end -}} 40 | {{ end -}} 41 | -------------------------------------------------------------------------------- /.chglog/config.yml: -------------------------------------------------------------------------------- 1 | style: github 2 | template: CHANGELOG.tpl.md 3 | info: 4 | title: CHANGELOG 5 | repository_url: https://github.com/newrelic/newrelic-kubernetes-operator 6 | options: 7 | commits: 8 | filters: 9 | Type: 10 | - docs 11 | - feat 12 | - fix 13 | 14 | commit_groups: 15 | title_maps: 16 | docs: Documentation Updates 17 | feat: Features 18 | fix: Bug Fixes 19 | 20 | refs: 21 | actions: 22 | - Closes 23 | - Fixes 24 | - Resolves 25 | 26 | issues: 27 | prefix: 28 | - # 29 | 30 | header: 31 | pattern: "^(\\w*)(?:\\(([\\/\\w\\$\\.\\-\\*\\s]*)\\))?\\:\\s(.*)$" 32 | pattern_maps: 33 | - Type 34 | - Scope 35 | - Subject 36 | 37 | notes: 38 | keywords: 39 | - BREAKING CHANGE 40 | -------------------------------------------------------------------------------- /.githooks/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo 4 | echo "Checking commit message..." 5 | echo 6 | 7 | make lint-commit COMMIT_MSG_FILE=$1 8 | 9 | -------------------------------------------------------------------------------- /.githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo 3 | echo "Running pre-commit checks..." 4 | echo 5 | 6 | make lint 7 | -------------------------------------------------------------------------------- /.githooks/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo 3 | echo "Running tests before pushing..." 4 | echo 5 | 6 | make lint test-unit 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: 'Report a bug ' 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Description 11 | A clear and concise description of what the problem is. 12 | 13 | ### Go Version 14 | Provide the output of `go version` here please 15 | 16 | ### Current behavior 17 | A clear and concise description of what you're currently experiencing. 18 | 19 | ### Expected behavior 20 | A clear and concise description of what you expected to happen. 21 | 22 | ### Steps To Reproduce 23 | Steps to reproduce the behavior: 24 | 1. Do this... 25 | 2. Then do this... etc 26 | 3. See error 27 | 28 | ### Debug Output (if applicable) 29 | If applicable, add associated log output to help explain the problem. 30 | 31 | ### Additional Context 32 | Add any other context about the problem here. 33 | 34 | ### References or Related Issues 35 | Are there any other related GitHub issues (open or closed) or Pull Requests that should be linked here? 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Feature Description 11 | A clear and concise description of the feature you want or need. 12 | 13 | ### Describe Alternatives 14 | A clear and concise description of any alternative solutions or features you've considered. Are there examples you could link us to? 15 | 16 | ### Additional context 17 | Add any other context here. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/objective.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Objective 3 | about: Custom objective template 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Objective 11 | State the desired outcome of this ticket. 12 | 13 | ### Acceptance Criteria 14 | - Acceptance criterion 1 15 | - Acceptance criterion 2 16 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "gomodTidy": true, 4 | "ignoreDeps": [ 5 | "golang.org/x/crypto", 6 | "golang.org/x/net", 7 | "golang.org/x/sys", 8 | "golang.org/x/tools" 9 | ], 10 | "commitMessagePrefix": "chore(deps):", 11 | "reviewers": ["team:developer-toolkit"], 12 | "labels": ["dependencies"], 13 | "prConcurrentLimit": 4, 14 | "postUpdateOptions": ["gomodTidy"] 15 | } 16 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-stale - https://github.com/probot/stale 2 | 3 | # Number of days of inactivity before an Issue or Pull Request becomes stale 4 | daysUntilStale: 30 5 | 6 | # Number of days of inactivity before an Issue or Pull Request with the stale label is closed. 7 | # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. 8 | daysUntilClose: 14 9 | 10 | # Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled) 11 | onlyLabels: [] 12 | 13 | # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable 14 | exemptLabels: 15 | - enhancement 16 | - help wanted 17 | - priority:low 18 | - priority:medium 19 | - priority:high 20 | - priority:blocker 21 | - good first issue 22 | - pinned 23 | 24 | # Set to true to ignore issues in a project (defaults to false) 25 | exemptProjects: false 26 | 27 | # Set to true to ignore issues in a milestone (defaults to false) 28 | exemptMilestones: false 29 | 30 | # Set to true to ignore issues with an assignee (defaults to false) 31 | exemptAssignees: false 32 | 33 | # Label to use when marking as stale 34 | staleLabel: stale 35 | 36 | # Comment to post when marking as stale. Set to `false` to disable 37 | markComment: > 38 | This issue has been automatically marked as stale because it has not had 39 | any recent activity. It will be closed if no further activity occurs. 40 | 41 | # Comment to post when removing the stale label. 42 | # unmarkComment: > 43 | # Your comment here. 44 | 45 | # Comment to post when closing a stale Issue or Pull Request. 46 | closeComment: > 47 | This issue has been automatically closed due to a lack of activity 48 | for an extended period of time. 49 | 50 | # Limit to only `issues` or `pulls` 51 | only: issues 52 | -------------------------------------------------------------------------------- /.github/workflows/compile.yml: -------------------------------------------------------------------------------- 1 | name: Compiling 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | # Compile on all supported OSes 11 | compile: 12 | strategy: 13 | matrix: 14 | go-version: 15 | - 1.14.x 16 | platform: 17 | - ubuntu-latest 18 | - macos-latest 19 | - windows-latest 20 | runs-on: ${{ matrix.platform }} 21 | steps: 22 | - name: Install Go 23 | uses: actions/setup-go@v1 24 | with: 25 | go-version: ${{ matrix.go-version }} 26 | 27 | - name: Add GOBIN to PATH 28 | run: echo "$(go env GOPATH)/bin" >> $GITHUB_PATH 29 | shell: bash 30 | 31 | - name: Checkout code 32 | uses: actions/checkout@v2 33 | 34 | - name: Compile 35 | run: make compile-only 36 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Install Go 13 | uses: actions/setup-go@v1 14 | with: 15 | go-version: ${{ matrix.go-version }} 16 | 17 | - name: Add GOBIN to PATH 18 | run: echo "$(go env GOPATH)/bin" >> $GITHUB_PATH 19 | shell: bash 20 | 21 | - name: Checkout code 22 | uses: actions/checkout@v2 23 | with: 24 | # Needed for release notes 25 | fetch-depth: 0 26 | 27 | - name: Publish Release 28 | shell: bash 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.DEV_TOOLKIT_TOKEN }} 31 | DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} 32 | DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} 33 | run: make release-publish 34 | -------------------------------------------------------------------------------- /.github/workflows/security.yml: -------------------------------------------------------------------------------- 1 | name: Security Scan 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | schedule: 7 | - cron: '0 6 * * *' 8 | 9 | jobs: 10 | security: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@master 14 | - name: Run Snyk to check for vulnerabilities 15 | uses: snyk/actions/golang@master 16 | env: 17 | SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} 18 | with: 19 | args: --severity-threshold=high 20 | 21 | -------------------------------------------------------------------------------- /.github/workflows/snapshot.yml: -------------------------------------------------------------------------------- 1 | name: Snapshot 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | 7 | jobs: 8 | snapshot: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Install Go 12 | uses: actions/setup-go@v1 13 | with: 14 | go-version: ${{ matrix.go-version }} 15 | 16 | - name: Add GOBIN to PATH 17 | run: echo "$(go env GOPATH)/bin" >> $GITHUB_PATH 18 | shell: bash 19 | 20 | - name: Checkout code 21 | uses: actions/checkout@v2 22 | 23 | - name: Publish Snapshot to Docker 24 | shell: bash 25 | run: make docker-push 26 | env: 27 | DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} 28 | DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} 29 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Testing 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | lint: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Install Go 14 | uses: actions/setup-go@v1 15 | with: 16 | go-version: 1.14.x 17 | 18 | - name: Add GOBIN to PATH 19 | run: echo "$(go env GOPATH)/bin" >> $GITHUB_PATH 20 | shell: bash 21 | 22 | - name: Checkout code 23 | uses: actions/checkout@v2 24 | with: 25 | fetch-depth: 0 26 | 27 | - name: Cache deps 28 | uses: actions/cache@v1 29 | with: 30 | path: ~/go/pkg/mod 31 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 32 | restore-keys: | 33 | ${{ runner.os }}-go- 34 | 35 | - name: Lint 36 | run: make lint 37 | 38 | test-unit: 39 | needs: lint 40 | runs-on: ubuntu-latest 41 | steps: 42 | - name: Install Go 43 | uses: actions/setup-go@v1 44 | with: 45 | go-version: 1.14.x 46 | 47 | - name: Add GOBIN to PATH 48 | run: echo "$(go env GOPATH)/bin" >> $GITHUB_PATH 49 | shell: bash 50 | 51 | - name: Install kubebuilder 52 | run: | 53 | version=2.3.1 54 | arch=amd64 55 | curl -L -O https://github.com/kubernetes-sigs/kubebuilder/releases/download/v${version}/kubebuilder_${version}_linux_${arch}.tar.gz 56 | tar -zxvf kubebuilder_${version}_linux_${arch}.tar.gz 57 | sudo mv kubebuilder_${version}_linux_${arch} /usr/local/kubebuilder 58 | shell: bash 59 | 60 | - name: Add Kubebuilder to PATH 61 | run: echo "/usr/local/kubebuilder/bin" >> $GITHUB_PATH 62 | shell: bash 63 | 64 | - name: Checkout code 65 | uses: actions/checkout@v2 66 | 67 | - name: Cache deps 68 | uses: actions/cache@v1 69 | with: 70 | path: ~/go/pkg/mod 71 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 72 | restore-keys: | 73 | ${{ runner.os }}-go- 74 | 75 | - name: Unit Tests 76 | run: make test-unit 77 | 78 | test-integration: 79 | needs: lint 80 | runs-on: ubuntu-latest 81 | steps: 82 | - name: Install Go 83 | uses: actions/setup-go@v1 84 | with: 85 | go-version: 1.14.x 86 | 87 | - name: Add GOBIN to PATH 88 | run: echo "$(go env GOPATH)/bin" >> $GITHUB_PATH 89 | shell: bash 90 | 91 | - name: Install kubebuilder 92 | run: | 93 | version=2.3.1 94 | arch=amd64 95 | curl -L -O https://github.com/kubernetes-sigs/kubebuilder/releases/download/v${version}/kubebuilder_${version}_linux_${arch}.tar.gz 96 | tar -zxvf kubebuilder_${version}_linux_${arch}.tar.gz 97 | sudo mv kubebuilder_${version}_linux_${arch} /usr/local/kubebuilder 98 | shell: bash 99 | 100 | - name: Add Kubebuilder to PATH 101 | run: echo "/usr/local/kubebuilder/bin" >> $GITHUB_PATH 102 | shell: bash 103 | 104 | - name: Checkout code 105 | uses: actions/checkout@v2 106 | 107 | - name: Cache deps 108 | uses: actions/cache@v1 109 | with: 110 | path: ~/go/pkg/mod 111 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 112 | restore-keys: | 113 | ${{ runner.os }}-go- 114 | 115 | - name: Integration Tests 116 | run: make test-integration 117 | env: 118 | NEW_RELIC_ACCOUNT_ID: ${{ secrets.NEW_RELIC_ACCOUNT_ID }} 119 | NEW_RELIC_API_KEY: ${{ secrets.NEW_RELIC_API_KEY }} 120 | NEW_RELIC_LICENSE_KEY: ${{ secrets.NEW_RELIC_LICENSE_KEY }} 121 | NEW_RELIC_REGION: ${{ secrets.NEW_RELIC_REGION }} 122 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | bin/ 8 | coverage/ 9 | dist/ 10 | tmp 11 | 12 | # Test binary, build with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | # Misc 19 | .DS_Store 20 | .vscode/ 21 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # options for analysis running 2 | run: 3 | # timeout for analysis, e.g. 30s, 5m, default is 1m 4 | deadline: 5m 5 | 6 | # exit code when at least one issue was found, default is 1 7 | issues-exit-code: 1 8 | 9 | # include test files or not, default is true 10 | tests: true 11 | 12 | # all available settings of specific linters 13 | linters-settings: 14 | govet: 15 | # report about shadowed variables 16 | check-shadowing: true 17 | golint: 18 | # minimal confidence for issues, default is 0.8 19 | min-confidence: 0.5 20 | gocyclo: 21 | # minimal code complexity to report, 30 by default (but we recommend 10-20) 22 | min-complexity: 20 23 | maligned: 24 | # print struct with more effective memory layout or not, false by default 25 | suggest-new: true 26 | depguard: 27 | list-type: blacklist 28 | include-go-root: false 29 | packages: 30 | - github.com/davecgh/go-spew/spew 31 | misspell: 32 | ignore-words: 33 | - newrelic 34 | lll: 35 | # max line length, lines longer will be reported. Default is 120. 36 | # '\t' is counted as 1 character by default, and can be changed with the tab-width option 37 | line-length: 150 38 | 39 | linters: 40 | disable-all: true 41 | enable: 42 | - deadcode 43 | - errcheck 44 | - gocyclo 45 | - gofmt 46 | - golint 47 | - gosimple 48 | - govet 49 | - ineffassign 50 | - maligned 51 | - misspell 52 | - staticcheck 53 | - structcheck 54 | - unconvert 55 | - unused 56 | - varcheck 57 | - vet 58 | 59 | issues: 60 | # disable limits on issue reporting 61 | max-per-linter: 0 62 | max-same-issues: 0 63 | 64 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: newrelic-kubernetes-operator 2 | 3 | env: 4 | - GO111MODULE=on 5 | 6 | before: 7 | hooks: 8 | - go mod download 9 | 10 | builds: 11 | - 12 | id: newrelic 13 | dir: . 14 | binary: manager 15 | env: 16 | - CGO_ENABLED=0 17 | goos: 18 | - linux 19 | goarch: 20 | - amd64 21 | ldflags: 22 | - -s -w -X main.version={{.Version}} -X main.appName={{.Binary}} 23 | 24 | release: 25 | # Mark as a pre-release for now 26 | prerelease: true 27 | name_template: "{{.ProjectName}} v{{.Version}}" 28 | 29 | archives: 30 | - 31 | id: "default" 32 | builds: 33 | - newrelic 34 | replacements: 35 | linux: Linux 36 | amd64: x86_64 37 | files: 38 | - CHANGELOG.md 39 | - LICENSE 40 | - README.md 41 | 42 | dockers: 43 | - 44 | dockerfile: build/package/Dockerfile 45 | image_templates: 46 | - 'newrelic/kubernetes-operator:{{ .Tag }}' 47 | - 'newrelic/kubernetes-operator:v{{ .Major }}.{{ .Minor }}' 48 | - 'newrelic/kubernetes-operator:latest' 49 | binaries: 50 | - manager 51 | build_flag_templates: 52 | - "--pull" 53 | - "--label=repository=http://github.com/newrelic/newrelic-kubernetes-operator" 54 | - "--label=homepage=https://developer.newrelic.com/" 55 | - "--label=maintainer=Developer Toolkit " 56 | 57 | # Already using git-chglog 58 | changelog: 59 | skip: true 60 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## [v0.0.8] - 2021-07-09 3 | ### Bug Fixes 4 | - namespace's prefix deleted(manager.yaml) 5 | - **float:** check type 6 | - **float:** typo 7 | 8 | ### Features 9 | - support for managing channels from policy 10 | - **alertCondition:** support for lost signal 11 | - **nrqlCondition:** add ability to create Baseline NRQL Alerts 12 | - **types:** keeping fields as pointers 13 | 14 | 15 | ## [v0.0.7] - 2020-10-15 16 | ### Features 17 | - add parent kubernetes object references 18 | 19 | 20 | ## [v0.0.6] - 2020-09-09 21 | ### Bug Fixes 22 | - rollback ns change 23 | - update manager namespace created 24 | 25 | ### Documentation Updates 26 | - remove hardcoded examples in README 27 | 28 | 29 | ## [v0.0.5] - 2020-09-01 30 | ### Features 31 | - support for secrets in the headers of webhook alerts channels 32 | - add schema for headers and payload definition 33 | 34 | 35 | ## [v0.0.4] - 2020-08-10 36 | 37 | ## [v0.0.3] - 2020-08-05 38 | ### Bug Fixes 39 | - **buikd:** match release manager docker image tag with goreleaser 40 | 41 | ### Documentation Updates 42 | - add instructions for monitoring with the go agent 43 | 44 | ### Features 45 | - **alertChannel:** add initial support for alertChannel CRD 46 | 47 | 48 | ## [v0.0.2] - 2020-06-11 49 | ### Bug Fixes 50 | - set LeaderElectionID 51 | - use per-function contexts and printer package 52 | - revert gnostic version to pre-case-change 53 | - try go 1.14 54 | - go.mod wrangling 55 | - skip go mod tidy 56 | 57 | ### Documentation Updates 58 | - update the README 59 | - update the other examples 60 | - update policy example 61 | - revert go version change 62 | 63 | ### Features 64 | - support kubectl diff 65 | - **ci:** add release workflow + goreleaser 66 | 67 | 68 | ## v0.0.1 - 2020-05-29 69 | ### Bug Fixes 70 | - log only when error occurs 71 | - **build:** Fix manifest generation with renamed configs dir 72 | - **build:** Disable CGO for all compiling operations 73 | - **build:** ensure we generate the interfaces 74 | - **build:** no trailing slash for BUILD_DIR, use updated CONFIG_ROOT for make deploy 75 | - **build:** Correct spelling of DOCKER_IMAGE 76 | - **changelog:** reference correct repo in git-chglog config 77 | - **config:** increase memory for operator 78 | - **rbac:** update rbac permissions 79 | 80 | ### Documentation Updates 81 | - **README:** cleaned up instructions for running 82 | - **build:** Recommend kustomize build ... | kubectl apply -f - 83 | - **build:** Need to install cert-manager 84 | - **build:** Documentation driven development 85 | - **build:** Correct url for kustomize build in quick start 86 | - **policy:** change example policy to create nrql & apm conditions 87 | - **readme:** reorganize the README a bit 88 | - **readme:** update table of contents 89 | - **readme:** update examples, update helpful commands, other minor updates 90 | 91 | ### Features 92 | - **alerts:** add apm alerts methods to interface 93 | - **api:** merged upstream changes and revved API version 94 | - **api:** extend the CRD to include API key and region 95 | - **api:** fixed the tests 96 | - **api:** refactored alerts client behavior to read from condition 97 | - **api:** added webhook tests 98 | - **api:** fixing linting 99 | - **api:** fixing more linting 100 | - **api:** added kubbernetes secrets support 101 | - **api:** added secrets rbac bindings 102 | - **ci:** add release 103 | - **conditions:** adds initial template for APM conditions 104 | - **examples:** add secrets example yaml, continue doc updates 105 | - **manager:** add switch for dev logging 106 | - **manager:** Add custom version / service info headers 107 | - **manager:** Add version flag, and version / appName vars to pass through 108 | - **policy:** added update logic to conditions created by policy controller 109 | - **policy:** added tests and default webhook for policy 110 | - **policy:** Adds policy scaffolding. 111 | - **policy:** add defaulting and validation logic 112 | - **policy:** added policy condition creation and deletion 113 | - **policy:** Added most of reconcile function 114 | 115 | [Unreleased]: https://github.com/newrelic/newrelic-kubernetes-operator/compare/v0.0.8...HEAD 116 | [v0.0.8]: https://github.com/newrelic/newrelic-kubernetes-operator/compare/v0.0.7...v0.0.8 117 | [v0.0.7]: https://github.com/newrelic/newrelic-kubernetes-operator/compare/v0.0.6...v0.0.7 118 | [v0.0.6]: https://github.com/newrelic/newrelic-kubernetes-operator/compare/v0.0.5...v0.0.6 119 | [v0.0.5]: https://github.com/newrelic/newrelic-kubernetes-operator/compare/v0.0.4...v0.0.5 120 | [v0.0.4]: https://github.com/newrelic/newrelic-kubernetes-operator/compare/v0.0.3...v0.0.4 121 | [v0.0.3]: https://github.com/newrelic/newrelic-kubernetes-operator/compare/v0.0.2...v0.0.3 122 | [v0.0.2]: https://github.com/newrelic/newrelic-kubernetes-operator/compare/v0.0.1...v0.0.2 123 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are always welcome. Before contributing please read the 4 | [code of conduct](blog/master/CODE_OF_CONDUCT.md) and [search the issue tracker](issues); your issue may have already been discussed or fixed in `master`. To contribute, 5 | [fork](https://help.github.com/articles/fork-a-repo/) this repository, commit your changes, and [send a Pull Request](https://help.github.com/articles/using-pull-requests/). 6 | 7 | Note that our [code of conduct](blog/master/CODE_OF_CONDUCT.md) applies to all platforms and venues related to this project; please follow it in all your interactions with the project and its participants. 8 | 9 | ## Feature Requests 10 | 11 | Feature requests should be submitted in the [Issue tracker](issues), with a description of the expected behavior & use case, where they’ll remain closed until sufficient interest, [e.g. :+1: reactions](https://help.github.com/articles/about-discussions-in-issues-and-pull-requests/), has been [shown by the community](issues?q=label%3A%22votes+needed%22+sort%3Areactions-%2B1-desc). 12 | Before submitting an Issue, please search for similar ones in the 13 | [closed issues](issues?q=is%3Aissue+is%3Aclosed+label%3Aenhancement). 14 | 15 | ## Pull Requests 16 | 17 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a build. 18 | 2. Increase the version numbers in any examples files and the README.md to the new version that this Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). 19 | 3. You may merge the Pull Request in once you have the sign-off of two other developers, or if you do not have permission to do that, you may request the second reviewer to merge it for you. 20 | 21 | ## Contributor License Agreement 22 | 23 | Keep in mind that when you submit your Pull Request, you'll need to sign the CLA via the click-through using CLA-Assistant. If you'd like to execute our corporate CLA, or if you have any questions, please drop us an email at opensource@newrelic.com. 24 | 25 | For more information about CLAs, please check out Alex Russell’s excellent post, 26 | [“Why Do I Need to Sign This?”](https://infrequently.org/2008/06/why-do-i-need-to-sign-this/). 27 | 28 | # Slack 29 | 30 | For contributors and maintainers of open source projects hosted by New Relic, we host a public Slack with a channel dedicated to this project. If you are contributing to this project, you're welcome to request access to that community space. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ############################# 2 | # Global vars 3 | ############################# 4 | PROJECT_NAME := $(shell basename $(shell pwd)) 5 | PROJECT_VER ?= $(shell git describe --tags --always --dirty | sed -e '/^v/s/^v\(.*\)$$/\1/g') # Strip leading 'v' if found 6 | # Last released version (not dirty) 7 | PROJECT_VER_TAGGED ?= $(shell git describe --tags --always --abbrev=0 | sed -e '/^v/s/^v\(.*\)$$/\1/g') # Strip leading 'v' if found 8 | 9 | SRCDIR ?= . 10 | GO = go 11 | 12 | # The root module (from go.mod) 13 | PROJECT_MODULE ?= $(shell $(GO) list -m) 14 | 15 | ############################# 16 | # Targets 17 | ############################# 18 | all: build 19 | 20 | # Humans running make: 21 | build: git-hooks check-version clean generate lint test cover-report compile 22 | 23 | # Build command for CI tooling 24 | build-ci: check-version clean lint test compile-only 25 | 26 | # All clean commands 27 | clean: cover-clean compile-clean release-clean 28 | 29 | # Import fragments 30 | include build/compile.mk 31 | include build/docker.mk 32 | include build/document.mk 33 | include build/generate.mk 34 | include build/kube.mk 35 | include build/lint.mk 36 | include build/release.mk 37 | include build/test.mk 38 | include build/tools.mk 39 | include build/util.mk 40 | 41 | .PHONY: all build build-ci clean 42 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | domain: k8s.newrelic.com 2 | repo: github.com/newrelic/newrelic-kubernetes-operator 3 | resources: 4 | - group: nr 5 | kind: NrqlAlertCondition 6 | version: v1 7 | - group: nr 8 | kind: Policy 9 | version: v1 10 | - group: nr 11 | kind: ApmAlertCondition 12 | version: v1 13 | - group: nr 14 | kind: AlertsChannel 15 | version: v1 16 | - group: nr 17 | kind: AlertsNrqlCondition 18 | version: v1 19 | - group: nr 20 | kind: AlertsPolicy 21 | version: v1 22 | - group: nr 23 | kind: AlertsAPMCondition 24 | version: v1 25 | version: "2" 26 | -------------------------------------------------------------------------------- /alerts.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package main 17 | 18 | import ( 19 | "os" 20 | 21 | "github.com/newrelic/go-agent/v3/newrelic" 22 | _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" 23 | ctrl "sigs.k8s.io/controller-runtime" 24 | 25 | nrv1 "github.com/newrelic/newrelic-kubernetes-operator/api/v1" 26 | "github.com/newrelic/newrelic-kubernetes-operator/controllers" 27 | "github.com/newrelic/newrelic-kubernetes-operator/interfaces" 28 | // +kubebuilder:scaffold:imports 29 | ) 30 | 31 | func registerAlerts(mgr *ctrl.Manager, nrApp *newrelic.Application) error { 32 | 33 | // nrqlalertcondition 34 | nrqlAlertConditionReconciler := &controllers.NrqlAlertConditionReconciler{ 35 | Client: (*mgr).GetClient(), 36 | Log: ctrl.Log.WithName("controllers").WithName("NrqlAlertCondition"), 37 | Scheme: (*mgr).GetScheme(), 38 | AlertClientFunc: interfaces.InitializeAlertsClient, 39 | NewRelicAgent: *nrApp, 40 | } 41 | 42 | if err := nrqlAlertConditionReconciler.SetupWithManager(*mgr); err != nil { 43 | setupLog.Error(err, "unable to create controller", "controller", "NrqlAlertCondition") 44 | os.Exit(1) 45 | } 46 | 47 | nrqlAlertCondition := &nrv1.NrqlAlertCondition{} 48 | if err := nrqlAlertCondition.SetupWebhookWithManager(*mgr); err != nil { 49 | setupLog.Error(err, "unable to create webhook", "webhook", "NrqlAlertCondition") 50 | os.Exit(1) 51 | } 52 | 53 | // alertsnrqlcondition 54 | alertsNrqlConditionReconciler := &controllers.AlertsNrqlConditionReconciler{ 55 | Client: (*mgr).GetClient(), 56 | Log: ctrl.Log.WithName("controllers").WithName("AlertsNrqlCondition"), 57 | Scheme: (*mgr).GetScheme(), 58 | AlertClientFunc: interfaces.InitializeAlertsClient, 59 | NewRelicAgent: *nrApp, 60 | } 61 | 62 | if err := alertsNrqlConditionReconciler.SetupWithManager(*mgr); err != nil { 63 | setupLog.Error(err, "unable to create controller", "controller", "AlertsNrqlCondition") 64 | os.Exit(1) 65 | } 66 | 67 | alertsNrqlCondition := &nrv1.AlertsNrqlCondition{} 68 | if err := alertsNrqlCondition.SetupWebhookWithManager(*mgr); err != nil { 69 | setupLog.Error(err, "unable to create webhook", "webhook", "AlertsNrqlCondition") 70 | os.Exit(1) 71 | } 72 | 73 | // apmalertcondition 74 | apmReconciler := &controllers.ApmAlertConditionReconciler{ 75 | Client: (*mgr).GetClient(), 76 | Log: ctrl.Log.WithName("controllers").WithName("ApmAlertCondition"), 77 | Scheme: (*mgr).GetScheme(), 78 | AlertClientFunc: interfaces.InitializeAlertsClient, 79 | NewRelicAgent: *nrApp, 80 | } 81 | 82 | if err := apmReconciler.SetupWithManager(*mgr); err != nil { 83 | setupLog.Error(err, "unable to create controller", "controller", "ApmAlertCondition") 84 | os.Exit(1) 85 | } 86 | 87 | apmAlertCondition := &nrv1.ApmAlertCondition{} 88 | if err := apmAlertCondition.SetupWebhookWithManager(*mgr); err != nil { 89 | setupLog.Error(err, "unable to create webhook", "webhook", "ApmAlertCondition") 90 | os.Exit(1) 91 | } 92 | 93 | // alertsapmcondition 94 | alertsAPMReconciler := &controllers.AlertsAPMConditionReconciler{ 95 | Client: (*mgr).GetClient(), 96 | Log: ctrl.Log.WithName("controllers").WithName("AlertsAPMCondition"), 97 | Scheme: (*mgr).GetScheme(), 98 | AlertClientFunc: interfaces.InitializeAlertsClient, 99 | NewRelicAgent: *nrApp, 100 | } 101 | 102 | if err := alertsAPMReconciler.SetupWithManager(*mgr); err != nil { 103 | setupLog.Error(err, "unable to create controller", "controller", "AlertsAPMCondition") 104 | os.Exit(1) 105 | } 106 | 107 | alertsAPMCondition := &nrv1.AlertsAPMCondition{} 108 | if err := alertsAPMCondition.SetupWebhookWithManager(*mgr); err != nil { 109 | setupLog.Error(err, "unable to create webhook", "webhook", "AlertsAPMCondition") 110 | os.Exit(1) 111 | } 112 | 113 | // policy 114 | policyReconciler := &controllers.PolicyReconciler{ 115 | Client: (*mgr).GetClient(), 116 | Log: ctrl.Log.WithName("controllers").WithName("Policy"), 117 | Scheme: (*mgr).GetScheme(), 118 | AlertClientFunc: interfaces.InitializeAlertsClient, 119 | NewRelicAgent: *nrApp, 120 | } 121 | 122 | if err := policyReconciler.SetupWithManager(*mgr); err != nil { 123 | setupLog.Error(err, "unable to create controller", "controller", "Policy") 124 | os.Exit(1) 125 | } 126 | 127 | policy := &nrv1.Policy{} 128 | if err := policy.SetupWebhookWithManager(*mgr); err != nil { 129 | setupLog.Error(err, "unable to create webhook", "webhook", "Policy") 130 | os.Exit(1) 131 | } 132 | 133 | //alertsChannel 134 | alertsChannelReconciler := &controllers.AlertsChannelReconciler{ 135 | Client: (*mgr).GetClient(), 136 | Log: ctrl.Log.WithName("controllers").WithName("alertsChannel"), 137 | Scheme: (*mgr).GetScheme(), 138 | AlertClientFunc: interfaces.InitializeAlertsClient, 139 | NewRelicAgent: *nrApp, 140 | } 141 | 142 | if err := alertsChannelReconciler.SetupWithManager(*mgr); err != nil { 143 | setupLog.Error(err, "unable to create controller", "controller", "alertsChannel") 144 | os.Exit(1) 145 | } 146 | 147 | alertsChannel := &nrv1.AlertsChannel{} 148 | if err := alertsChannel.SetupWebhookWithManager(*mgr); err != nil { 149 | setupLog.Error(err, "unable to create webhook", "webhook", "AlertsChannel") 150 | os.Exit(1) 151 | } 152 | 153 | // alertspolicy 154 | alertsPolicyReconciler := &controllers.AlertsPolicyReconciler{ 155 | Client: (*mgr).GetClient(), 156 | Log: ctrl.Log.WithName("controllers").WithName("AlertsPolicy"), 157 | Scheme: (*mgr).GetScheme(), 158 | AlertClientFunc: interfaces.InitializeAlertsClient, 159 | NewRelicAgent: *nrApp, 160 | } 161 | if err := alertsPolicyReconciler.SetupWithManager(*mgr); err != nil { 162 | setupLog.Error(err, "unable to create controller", "controller", "AlertsPolicy") 163 | os.Exit(1) 164 | } 165 | 166 | alertsPolicy := &nrv1.AlertsPolicy{} 167 | if err := alertsPolicy.SetupWebhookWithManager(*mgr); err != nil { 168 | setupLog.Error(err, "unable to create webhook", "webhook", "AlertsPolicy") 169 | os.Exit(1) 170 | } 171 | 172 | return nil 173 | } 174 | -------------------------------------------------------------------------------- /api/v1/alerts_apmcondition_types.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/newrelic/newrelic-client-go/pkg/alerts" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | ) 9 | 10 | // AlertsAPMConditionSpec defines the desired state of AlertsAPMCondition 11 | type AlertsAPMConditionSpec struct { 12 | AlertsGenericConditionSpec `json:",inline"` 13 | AlertsAPMSpecificSpec `json:",inline"` 14 | } 15 | 16 | type AlertsAPMSpecificSpec struct { 17 | Metric string `json:"metric,omitempty"` 18 | UserDefined alerts.ConditionUserDefined `json:"user_defined,omitempty"` 19 | Scope string `json:"condition_scope,omitempty"` 20 | Entities []string `json:"entities,omitempty"` 21 | GCMetric string `json:"gc_metric,omitempty"` 22 | ViolationCloseTimer int `json:"violation_close_timer,omitempty"` 23 | } 24 | 25 | // AlertsAPMConditionStatus defines the observed state of AlertsAPMCondition 26 | type AlertsAPMConditionStatus struct { 27 | AppliedSpec *AlertsAPMConditionSpec `json:"applied_spec"` 28 | ConditionID int `json:"condition_id"` 29 | } 30 | 31 | // +kubebuilder:object:root=true 32 | // +kubebuilder:printcolumn:name="Created",type="boolean",JSONPath=".status.created" 33 | 34 | // AlertsAPMCondition is the Schema for the alertsapmconditions API 35 | type AlertsAPMCondition struct { 36 | metav1.TypeMeta `json:",inline"` 37 | metav1.ObjectMeta `json:"metadata,omitempty"` 38 | 39 | Spec AlertsAPMConditionSpec `json:"spec,omitempty"` 40 | Status AlertsAPMConditionStatus `json:"status,omitempty"` 41 | } 42 | 43 | // +kubebuilder:object:root=true 44 | 45 | // AlertsAPMConditionList contains a list of AlertsAPMCondition 46 | type AlertsAPMConditionList struct { 47 | metav1.TypeMeta `json:",inline"` 48 | metav1.ListMeta `json:"metadata,omitempty"` 49 | Items []AlertsAPMCondition `json:"items"` 50 | } 51 | 52 | func init() { 53 | SchemeBuilder.Register(&AlertsAPMCondition{}, &AlertsAPMConditionList{}) 54 | } 55 | 56 | func (in AlertsAPMConditionSpec) APICondition() alerts.Condition { 57 | jsonString, _ := json.Marshal(in) 58 | var APICondition alerts.Condition 59 | json.Unmarshal(jsonString, &APICondition) //nolint 60 | 61 | // Set the condition terms from the APITerms slice, not the Terms slice. 62 | // This is due to the fact that the type definitions are different 63 | // between the REST API and Nerdgraph. 64 | for _, term := range in.AlertsGenericConditionSpec.APMTerms { 65 | jsonString, _ := json.Marshal(term) 66 | var APITerm alerts.ConditionTerm 67 | json.Unmarshal(jsonString, &APITerm) //nolint 68 | 69 | APICondition.Terms = append(APICondition.Terms, APITerm) 70 | } 71 | 72 | return APICondition 73 | } 74 | -------------------------------------------------------------------------------- /api/v1/alerts_apmcondition_types_test.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | 7 | "github.com/newrelic/newrelic-client-go/pkg/alerts" 8 | 9 | . "github.com/onsi/ginkgo" 10 | . "github.com/onsi/gomega" 11 | ) 12 | 13 | var _ = Describe("AlertsAPMConditionSpec", func() { 14 | var condition AlertsAPMConditionSpec 15 | 16 | BeforeEach(func() { 17 | condition = AlertsAPMConditionSpec{ 18 | AlertsGenericConditionSpec{ 19 | APMTerms: []AlertConditionTerm{ 20 | { 21 | Duration: "30", 22 | Operator: "above", 23 | Priority: "critical", 24 | Threshold: "1.5", 25 | TimeFunction: "all", 26 | }, 27 | }, 28 | Type: "apm_app_metric", 29 | Name: "APM Condition", 30 | RunbookURL: "http://test.com/runbook", 31 | PolicyID: 0, 32 | ID: 888, 33 | Enabled: true, 34 | ExistingPolicyID: "42", 35 | }, 36 | AlertsAPMSpecificSpec{ 37 | Metric: "apdex", 38 | Entities: []string{"333"}, 39 | UserDefined: alerts.ConditionUserDefined{ 40 | Metric: "Custom/foo", 41 | ValueFunction: "average", 42 | }, 43 | Scope: "application", 44 | GCMetric: "", 45 | ViolationCloseTimer: 60, 46 | }, 47 | } 48 | }) 49 | 50 | Describe("APICondition", func() { 51 | It("converts AlertsAPMConditionSpec object to Condition object from go client, retaining field values", func() { 52 | apiCondition := condition.APICondition() 53 | 54 | Expect(fmt.Sprint(reflect.TypeOf(apiCondition))).To(Equal("alerts.Condition")) 55 | 56 | Expect(apiCondition.Type).To(Equal(alerts.ConditionTypes.APMApplicationMetric)) 57 | Expect(apiCondition.Name).To(Equal("APM Condition")) 58 | Expect(apiCondition.RunbookURL).To(Equal("http://test.com/runbook")) 59 | Expect(apiCondition.Metric).To(Equal(alerts.MetricTypes.Apdex)) 60 | Expect(apiCondition.Entities[0]).To(Equal("333")) 61 | Expect(apiCondition.Scope).To(Equal("application")) 62 | Expect(apiCondition.GCMetric).To(Equal("")) 63 | 64 | Expect(apiCondition.ID).To(Equal(888)) 65 | Expect(apiCondition.ViolationCloseTimer).To(Equal(60)) 66 | Expect(apiCondition.Enabled).To(Equal(true)) 67 | 68 | apiTerm := apiCondition.Terms[0] 69 | 70 | Expect(fmt.Sprint(reflect.TypeOf(apiTerm))).To(Equal("alerts.ConditionTerm")) 71 | 72 | Expect(apiTerm.Duration).To(Equal(30)) 73 | Expect(apiTerm.Operator).To(Equal(alerts.OperatorTypes.Above)) 74 | Expect(apiTerm.Priority).To(Equal(alerts.PriorityTypes.Critical)) 75 | Expect(apiTerm.Threshold).To(Equal(float64(1.5))) 76 | Expect(apiTerm.TimeFunction).To(Equal(alerts.TimeFunctionTypes.All)) 77 | 78 | userDefinedCondition := apiCondition.UserDefined 79 | Expect(fmt.Sprint(reflect.TypeOf(userDefinedCondition))).To(Equal("alerts.ConditionUserDefined")) 80 | 81 | Expect(userDefinedCondition.Metric).To(Equal("Custom/foo")) 82 | Expect(userDefinedCondition.ValueFunction).To(Equal(alerts.ValueFunctionTypes.Average)) 83 | }) 84 | }) 85 | }) 86 | -------------------------------------------------------------------------------- /api/v1/alerts_apmcondition_webhook_test.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/newrelic/newrelic-client-go/pkg/alerts" 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/gomega" 10 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | 12 | "github.com/newrelic/newrelic-kubernetes-operator/interfaces" 13 | "github.com/newrelic/newrelic-kubernetes-operator/interfaces/interfacesfakes" 14 | ) 15 | 16 | var _ = Describe("alertsAPMCondition_webhook", func() { 17 | var ( 18 | r AlertsAPMCondition 19 | alertsClient *interfacesfakes.FakeNewRelicAlertsClient 20 | ) 21 | 22 | BeforeEach(func() { 23 | k8Client = testk8sClient 24 | alertsClient = &interfacesfakes.FakeNewRelicAlertsClient{} 25 | fakeAlertFunc := func(string, string) (interfaces.NewRelicAlertsClient, error) { 26 | return alertsClient, nil 27 | } 28 | alertClientFunc = fakeAlertFunc 29 | r = AlertsAPMCondition{ 30 | ObjectMeta: v1.ObjectMeta{ 31 | Name: "test apm condition", 32 | }, 33 | Spec: AlertsAPMConditionSpec{ 34 | AlertsGenericConditionSpec{ 35 | APMTerms: []AlertConditionTerm{ 36 | { 37 | Duration: "30", 38 | Operator: "above", 39 | Priority: "critical", 40 | Threshold: "0.9", 41 | TimeFunction: "all", 42 | }, 43 | }, 44 | Type: "apm_app_metric", 45 | Name: "K8s generated apm alert condition", 46 | Enabled: true, 47 | ExistingPolicyID: "46286", 48 | APIKey: "111222333", 49 | APIKeySecret: NewRelicAPIKeySecret{}, 50 | Region: "staging", 51 | }, 52 | AlertsAPMSpecificSpec{ 53 | Metric: "apdex", 54 | UserDefined: alerts.ConditionUserDefined{}, 55 | Scope: "", 56 | Entities: []string{"5950260"}, 57 | }, 58 | }, 59 | } 60 | 61 | alertsClient.QueryPolicyStub = func(int, string) (*alerts.AlertsPolicy, error) { 62 | return &alerts.AlertsPolicy{ 63 | ID: "46286", 64 | }, nil 65 | } 66 | }) 67 | 68 | Context("ValidateCreate", func() { 69 | Context("With a valid Apm Condition", func() { 70 | It("Should create the apm condition", func() { 71 | err := r.ValidateCreate() 72 | Expect(err).ToNot(HaveOccurred()) 73 | 74 | }) 75 | }) 76 | 77 | Context("With an invalid Type", func() { 78 | BeforeEach(func() { 79 | r.Spec.Type = "burritos" 80 | }) 81 | 82 | It("Should reject the apm condition creation", func() { 83 | err := r.ValidateCreate() 84 | Expect(err).To(HaveOccurred()) 85 | Expect(err.Error()).To(ContainSubstring("burritos")) 86 | }) 87 | }) 88 | 89 | Context("With an invalid Metric", func() { 90 | BeforeEach(func() { 91 | r.Spec.Type = "moar burritos" 92 | }) 93 | 94 | It("Should reject the apm condition creation", func() { 95 | err := r.ValidateCreate() 96 | Expect(err).To(HaveOccurred()) 97 | Expect(err.Error()).To(ContainSubstring("moar burritos")) 98 | }) 99 | }) 100 | 101 | Context("With an invalid APMTerms", func() { 102 | BeforeEach(func() { 103 | r.Spec.APMTerms[0].TimeFunction = "moar burritos" 104 | r.Spec.APMTerms[0].Priority = "moar tacos" 105 | r.Spec.APMTerms[0].Operator = "moar hamburgers" 106 | 107 | }) 108 | 109 | It("Should reject the apm condition creation", func() { 110 | err := r.ValidateCreate() 111 | Expect(err).To(HaveOccurred()) 112 | Expect(err.Error()).To(ContainSubstring("moar burritos")) 113 | Expect(err.Error()).To(ContainSubstring("moar tacos")) 114 | Expect(err.Error()).To(ContainSubstring("moar hamburgers")) 115 | }) 116 | }) 117 | 118 | Context("With an invalid userDefined type", func() { 119 | BeforeEach(func() { 120 | r.Spec.UserDefined = alerts.ConditionUserDefined{ 121 | Metric: "Custom/foo", 122 | ValueFunction: "invalid type", 123 | } 124 | }) 125 | 126 | It("Should reject the apm condition creation", func() { 127 | err := r.ValidateCreate() 128 | Expect(err).To(HaveOccurred()) 129 | Expect(err.Error()).To(ContainSubstring("invalid type")) 130 | }) 131 | }) 132 | }) 133 | 134 | Context("ValidateUpdate", func() { 135 | Context("When deleting an existing apm Condition with a delete policy", func() { 136 | var update AlertsAPMCondition 137 | 138 | BeforeEach(func() { 139 | currentTime := v1.Time{Time: time.Now()} 140 | //make copy of existing object to update 141 | r.DeepCopyInto(&update) 142 | 143 | update.SetDeletionTimestamp(¤tTime) 144 | alertsClient.GetPolicyStub = func(int) (*alerts.Policy, error) { 145 | return &alerts.Policy{}, errors.New("no alert policy found for id 49092") 146 | } 147 | }) 148 | 149 | It("Should allow the deletion anyway", func() { 150 | err := update.ValidateUpdate(&r) 151 | Expect(err).ToNot(HaveOccurred()) 152 | }) 153 | }) 154 | }) 155 | 156 | }) 157 | -------------------------------------------------------------------------------- /api/v1/alerts_nrqlcondition_types_test.go: -------------------------------------------------------------------------------- 1 | // +build integration 2 | 3 | package v1 4 | 5 | import ( 6 | "fmt" 7 | "reflect" 8 | 9 | "github.com/newrelic/newrelic-client-go/pkg/alerts" 10 | 11 | . "github.com/onsi/ginkgo" 12 | . "github.com/onsi/gomega" 13 | ) 14 | 15 | var _ = Describe("AlertsNrqlConditionSpec", func() { 16 | var condition AlertsNrqlConditionSpec 17 | 18 | BeforeEach(func() { 19 | condition = AlertsNrqlConditionSpec{} 20 | condition.Enabled = true 21 | condition.ExistingPolicyID = "42" 22 | condition.Terms = []AlertsNrqlConditionTerm{ 23 | { 24 | Operator: alerts.AlertsNRQLConditionTermsOperatorTypes.ABOVE, 25 | Priority: alerts.NrqlConditionPriorities.Critical, 26 | Threshold: "5", 27 | ThresholdDuration: 60, 28 | ThresholdOccurrences: alerts.ThresholdOccurrences.AtLeastOnce, 29 | }, 30 | } 31 | condition.Nrql = alerts.NrqlConditionQuery{ 32 | Query: "SELECT 1 FROM MyEvents", 33 | EvaluationOffset: 5, 34 | } 35 | condition.Type = "NRQL" 36 | condition.Name = "NRQL Condition" 37 | condition.RunbookURL = "http://test.com/runbook" 38 | condition.ValueFunction = &alerts.NrqlConditionValueFunctions.SingleValue 39 | condition.ViolationTimeLimit = alerts.NrqlConditionViolationTimeLimits.OneHour 40 | condition.ID = 777 41 | condition.ExpectedGroups = 2 42 | condition.IgnoreOverlap = true 43 | }) 44 | 45 | Describe("ToNrqlConditionInput", func() { 46 | It("converts AlertsNrqlConditionSpec object to NrqlConditionInput object, retaining field values", func() { 47 | conditionInput := condition.ToNrqlConditionInput() 48 | 49 | Expect(fmt.Sprint(reflect.TypeOf(conditionInput))).To(Equal("alerts.NrqlConditionInput")) 50 | 51 | // Expect(conditionInput.Type).To(Equal("NRQL")) 52 | Expect(conditionInput.Name).To(Equal("NRQL Condition")) 53 | Expect(conditionInput.RunbookURL).To(Equal("http://test.com/runbook")) 54 | Expect(string(*conditionInput.ValueFunction)).To(Equal(string(alerts.NrqlConditionValueFunctions.SingleValue))) 55 | //Expect(conditionInput.PolicyID).To(Equal(42)) 56 | //Expect(conditionInput.ID).To(Equal(777)) 57 | //Expect(conditionInput.ViolationCloseTimer).To(Equal(60)) 58 | Expect(conditionInput.ViolationTimeLimit).To(Equal(alerts.NrqlConditionViolationTimeLimits.OneHour)) 59 | //Expect(conditionInput.ExpectedGroups).To(Equal(2)) 60 | //Expect(conditionInput.IgnoreOverlap).To(Equal(true)) 61 | Expect(conditionInput.Enabled).To(Equal(true)) 62 | 63 | apiTerm := conditionInput.Terms[0] 64 | 65 | Expect(fmt.Sprint(reflect.TypeOf(apiTerm))).To(Equal("alerts.NrqlConditionTerm")) 66 | 67 | //Expect(apiTerm.Duration).To(Equal(30)) 68 | Expect(apiTerm.Operator).To(Equal(alerts.AlertsNRQLConditionTermsOperatorTypes.ABOVE)) 69 | Expect(apiTerm.Priority).To(Equal(alerts.NrqlConditionPriorities.Critical)) 70 | expectedThreshold := 5.0 71 | Expect(apiTerm.Threshold).To(Equal(&expectedThreshold)) 72 | Expect(apiTerm.ThresholdDuration).To(Equal(60)) 73 | Expect(apiTerm.ThresholdOccurrences).To(Equal(alerts.ThresholdOccurrences.AtLeastOnce)) 74 | 75 | apiQuery := conditionInput.Nrql 76 | 77 | Expect(fmt.Sprint(reflect.TypeOf(apiQuery))).To(Equal("alerts.NrqlConditionQuery")) 78 | 79 | Expect(apiQuery.Query).To(Equal("SELECT 1 FROM MyEvents")) 80 | //Expect(apiQuery.SinceValue).To(Equal("5")) 81 | }) 82 | }) 83 | }) 84 | -------------------------------------------------------------------------------- /api/v1/alerts_nrqlcondition_webhook.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package v1 17 | 18 | import ( 19 | "context" 20 | "errors" 21 | "strings" 22 | 23 | v1 "k8s.io/api/core/v1" 24 | "k8s.io/apimachinery/pkg/types" 25 | 26 | "k8s.io/apimachinery/pkg/runtime" 27 | ctrl "sigs.k8s.io/controller-runtime" 28 | logf "sigs.k8s.io/controller-runtime/pkg/log" 29 | "sigs.k8s.io/controller-runtime/pkg/webhook" 30 | 31 | "github.com/newrelic/newrelic-kubernetes-operator/interfaces" 32 | ) 33 | 34 | // log is for logging in this package. 35 | var ( 36 | alertsNrqlConditionLog = logf.Log.WithName("alertsnrqlcondition-resource") 37 | ) 38 | 39 | func (r *AlertsNrqlCondition) SetupWebhookWithManager(mgr ctrl.Manager) error { 40 | alertClientFunc = interfaces.InitializeAlertsClient 41 | k8Client = mgr.GetClient() 42 | return ctrl.NewWebhookManagedBy(mgr). 43 | For(r). 44 | Complete() 45 | } 46 | 47 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 48 | 49 | // +kubebuilder:webhook:path=/mutate-nr-k8s-newrelic-com-v1-alertsnrqlcondition,mutating=true,failurePolicy=fail,groups=nr.k8s.newrelic.com,resources=alertsnrqlconditions,verbs=create;update,versions=v1,name=malertsnrqlcondition.kb.io,sideEffects=None 50 | 51 | var _ webhook.Defaulter = &AlertsNrqlCondition{} 52 | 53 | // Default implements webhook.Defaulter so a webhook will be registered for the type 54 | func (r *AlertsNrqlCondition) Default() { 55 | alertsNrqlConditionLog.Info("default", "name", r.Name) 56 | 57 | if r.Status.AppliedSpec == nil { 58 | alertsNrqlConditionLog.Info("Setting null Applied Spec to empty interface") 59 | r.Status.AppliedSpec = &AlertsNrqlConditionSpec{} 60 | } 61 | alertsNrqlConditionLog.Info("r.Status.AppliedSpec after", "r.Status.AppliedSpec", r.Status.AppliedSpec) 62 | } 63 | 64 | // TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. 65 | // +kubebuilder:webhook:verbs=create;update,path=/validate-nr-k8s-newrelic-com-v1-alertsnrqlcondition,mutating=false,failurePolicy=fail,groups=nr.k8s.newrelic.com,resources=alertsnrqlconditions,versions=v1,name=valertsnrqlcondition.kb.io,sideEffects=None 66 | 67 | var _ webhook.Validator = &AlertsNrqlCondition{} 68 | 69 | // ValidateCreate implements webhook.Validator so a webhook will be registered for the type 70 | func (r *AlertsNrqlCondition) ValidateCreate() error { 71 | alertsNrqlConditionLog.Info("validate create", "name", r.Name) 72 | //TODO this should write this value TO a new secret so code path always reads from a secret 73 | err := r.CheckForAPIKeyOrSecret() 74 | if err != nil { 75 | return err 76 | } 77 | 78 | err = r.CheckRequiredFields() 79 | if err != nil { 80 | return err 81 | } 82 | return r.CheckExistingPolicyID() 83 | } 84 | 85 | // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type 86 | func (r *AlertsNrqlCondition) ValidateUpdate(old runtime.Object) error { 87 | alertsNrqlConditionLog.Info("validate update", "name", r.Name) 88 | prevCondition := old.(*AlertsNrqlCondition) 89 | 90 | if (r.Spec.BaselineDirection == nil && prevCondition.Spec.BaselineDirection != nil) || 91 | (r.Spec.BaselineDirection != nil && prevCondition.Spec.BaselineDirection == nil) { 92 | return errors.New("cannot change between condition types, you must delete and create a new alert") 93 | } 94 | 95 | return nil 96 | } 97 | 98 | // ValidateDelete implements webhook.Validator so a webhook will be registered for the type 99 | func (r *AlertsNrqlCondition) ValidateDelete() error { 100 | alertsNrqlConditionLog.Info("validate delete", "name", r.Name) 101 | 102 | // TODO(user): fill in your validation logic upon object deletion. 103 | return nil 104 | } 105 | 106 | func (r *AlertsNrqlCondition) CheckExistingPolicyID() error { 107 | alertsNrqlConditionLog.Info("Checking existing", "policyId", r.Spec.ExistingPolicyID) 108 | var apiKey string 109 | if r.Spec.APIKey == "" { 110 | key := types.NamespacedName{Namespace: r.Spec.APIKeySecret.Namespace, Name: r.Spec.APIKeySecret.Name} 111 | var apiKeySecret v1.Secret 112 | getErr := k8Client.Get(context.Background(), key, &apiKeySecret) 113 | if getErr != nil { 114 | alertsNrqlConditionLog.Error(getErr, "Error getting secret") 115 | return getErr 116 | } 117 | apiKey = string(apiKeySecret.Data[r.Spec.APIKeySecret.KeyName]) 118 | 119 | } else { 120 | apiKey = r.Spec.APIKey 121 | } 122 | 123 | alertsClient, errAlertClient := alertClientFunc(apiKey, r.Spec.Region) 124 | if errAlertClient != nil { 125 | alertsNrqlConditionLog.Error(errAlertClient, "failed to get policy", 126 | "policyId", r.Spec.ExistingPolicyID, 127 | "API Key", interfaces.PartialAPIKey(apiKey), 128 | "region", r.Spec.Region, 129 | ) 130 | return errAlertClient 131 | } 132 | _, errAlertPolicy := alertsClient.QueryPolicy(r.Spec.AccountID, r.Spec.ExistingPolicyID) 133 | if errAlertPolicy != nil { 134 | alertsNrqlConditionLog.Error(errAlertPolicy, "failed to get policy", 135 | "policyId", r.Spec.ExistingPolicyID, 136 | "API Key", interfaces.PartialAPIKey(apiKey), 137 | "region", r.Spec.Region, 138 | ) 139 | return errAlertPolicy 140 | } 141 | return nil 142 | } 143 | 144 | func (r *AlertsNrqlCondition) CheckForAPIKeyOrSecret() error { 145 | if r.Spec.APIKey != "" { 146 | return nil 147 | } 148 | if r.Spec.APIKeySecret != (NewRelicAPIKeySecret{}) { 149 | if r.Spec.APIKeySecret.Name != "" && r.Spec.APIKeySecret.Namespace != "" && r.Spec.APIKeySecret.KeyName != "" { 150 | return nil 151 | } 152 | } 153 | return errors.New("either api_key or api_key_secret must be set") 154 | } 155 | 156 | func (r *AlertsNrqlCondition) CheckRequiredFields() error { 157 | 158 | missingFields := []string{} 159 | if r.Spec.Region == "" { 160 | missingFields = append(missingFields, "region") 161 | } 162 | if r.Spec.ExistingPolicyID == "" { 163 | missingFields = append(missingFields, "existing_policy_id") 164 | } 165 | if len(missingFields) > 0 { 166 | return errors.New(strings.Join(missingFields, " and ") + " must be set") 167 | } 168 | return nil 169 | } 170 | -------------------------------------------------------------------------------- /api/v1/alerts_policy_webhook.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package v1 17 | 18 | import ( 19 | "errors" 20 | "strings" 21 | 22 | "k8s.io/apimachinery/pkg/runtime" 23 | ctrl "sigs.k8s.io/controller-runtime" 24 | logf "sigs.k8s.io/controller-runtime/pkg/log" 25 | "sigs.k8s.io/controller-runtime/pkg/webhook" 26 | 27 | "github.com/newrelic/newrelic-client-go/pkg/alerts" 28 | 29 | customErrors "github.com/newrelic/newrelic-kubernetes-operator/errors" 30 | ) 31 | 32 | // AlertsPolicyLog is for emitting logs in this package. 33 | var AlertsPolicyLog = logf.Log.WithName("alerts-policy-resource") 34 | var defaultAlertsPolicyIncidentPreference = alerts.AlertsIncidentPreferenceTypes.PER_POLICY 35 | 36 | func (r *AlertsPolicy) SetupWebhookWithManager(mgr ctrl.Manager) error { 37 | return ctrl.NewWebhookManagedBy(mgr). 38 | For(r). 39 | Complete() 40 | } 41 | 42 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 43 | 44 | // +kubebuilder:webhook:path=/mutate-nr-k8s-newrelic-com-v1-alertspolicy,mutating=true,failurePolicy=fail,groups=nr.k8s.newrelic.com,resources=alertspolicies,verbs=create;update,versions=v1,name=malertspolicy.kb.io,sideEffects=None 45 | 46 | var _ webhook.Defaulter = &AlertsPolicy{} 47 | 48 | // Default implements webhook.Defaulter so a webhook will be registered for the type 49 | func (r *AlertsPolicy) Default() { 50 | AlertsPolicyLog.Info("default", "name", r.Name) 51 | 52 | if r.Status.AppliedSpec == nil { 53 | AlertsPolicyLog.Info("Setting null Applied Spec to empty interface") 54 | r.Status.AppliedSpec = &AlertsPolicySpec{} 55 | } 56 | 57 | r.DefaultIncidentPreference() 58 | } 59 | 60 | // TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. 61 | // +kubebuilder:webhook:verbs=create;update,path=/validate-nr-k8s-newrelic-com-v1-alertspolicy,mutating=false,failurePolicy=fail,groups=nr.k8s.newrelic.com,resources=alertspolicies,versions=v1,name=valertspolicy.kb.io,sideEffects=None 62 | 63 | var _ webhook.Validator = &AlertsPolicy{} 64 | 65 | // ValidateCreate implements webhook.Validator so a webhook will be registered for the type 66 | func (r *AlertsPolicy) ValidateCreate() error { 67 | AlertsPolicyLog.Info("validate create", "name", r.Name) 68 | 69 | collectedErrors := new(customErrors.ErrorCollector) 70 | err := r.CheckForAPIKeyOrSecret() 71 | 72 | if err != nil { 73 | collectedErrors.Collect(err) 74 | } 75 | 76 | err = r.CheckForDuplicateConditions() 77 | if err != nil { 78 | collectedErrors.Collect(err) 79 | } 80 | 81 | err = r.ValidateIncidentPreference() 82 | if err != nil { 83 | collectedErrors.Collect(err) 84 | } 85 | 86 | if len(*collectedErrors) > 0 { 87 | AlertsPolicyLog.Info("Errors encountered validating policy", "collectedErrors", collectedErrors) 88 | return collectedErrors 89 | } 90 | 91 | return nil 92 | } 93 | 94 | // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type 95 | func (r *AlertsPolicy) ValidateUpdate(old runtime.Object) error { 96 | AlertsPolicyLog.Info("validate update", "name", r.Name) 97 | 98 | collectedErrors := new(customErrors.ErrorCollector) 99 | 100 | err := r.CheckForAPIKeyOrSecret() 101 | if err != nil { 102 | collectedErrors.Collect(err) 103 | } 104 | 105 | err = r.CheckForDuplicateConditions() 106 | if err != nil { 107 | collectedErrors.Collect(err) 108 | } 109 | 110 | err = r.ValidateIncidentPreference() 111 | if err != nil { 112 | collectedErrors.Collect(err) 113 | } 114 | 115 | if len(*collectedErrors) > 0 { 116 | AlertsPolicyLog.Info("Errors encountered validating policy", "collectedErrors", collectedErrors) 117 | return collectedErrors 118 | } 119 | 120 | return nil 121 | } 122 | 123 | // ValidateDelete implements webhook.Validator so a webhook will be registered for the type 124 | func (r *AlertsPolicy) ValidateDelete() error { 125 | AlertsPolicyLog.Info("validate delete", "name", r.Name) 126 | 127 | err := r.CheckForAPIKeyOrSecret() 128 | if err != nil { 129 | return err 130 | } 131 | 132 | return nil 133 | } 134 | 135 | func (r *AlertsPolicy) DefaultIncidentPreference() { 136 | if r.Spec.IncidentPreference == "" { 137 | r.Spec.IncidentPreference = string(defaultAlertsPolicyIncidentPreference) 138 | } 139 | 140 | r.Spec.IncidentPreference = strings.ToUpper(r.Spec.IncidentPreference) 141 | } 142 | 143 | func (r *AlertsPolicy) CheckForDuplicateConditions() error { 144 | var conditionHashMap = make(map[uint32]bool) 145 | 146 | for _, condition := range r.Spec.Conditions { 147 | conditionHashMap[condition.SpecHash()] = true 148 | } 149 | 150 | if len(conditionHashMap) != len(r.Spec.Conditions) { 151 | AlertsPolicyLog.Info("duplicate conditions detected or hash collision", "conditionHash", conditionHashMap) 152 | return errors.New("duplicate conditions detected or hash collision") 153 | } 154 | 155 | return nil 156 | } 157 | 158 | func (r *AlertsPolicy) ValidateIncidentPreference() error { 159 | switch r.Spec.IncidentPreference { 160 | case "PER_POLICY", "PER_CONDITION", "PER_CONDITION_AND_TARGET": 161 | return nil 162 | } 163 | 164 | AlertsPolicyLog.Info("Incident preference must be PER_POLICY, PER_CONDITION, or PER_CONDITION_AND_TARGET", "IncidentPreference value", r.Spec.IncidentPreference) 165 | 166 | return errors.New("incident preference must be PER_POLICY, PER_CONDITION, or PER_CONDITION_AND_TARGET") 167 | } 168 | 169 | func (r *AlertsPolicy) CheckForAPIKeyOrSecret() error { 170 | if r.Spec.APIKey != "" { 171 | return nil 172 | } 173 | 174 | if r.Spec.APIKeySecret != (NewRelicAPIKeySecret{}) { 175 | if r.Spec.APIKeySecret.Name != "" && r.Spec.APIKeySecret.Namespace != "" && r.Spec.APIKeySecret.KeyName != "" { 176 | return nil 177 | } 178 | } 179 | 180 | return errors.New("either api_key or api_key_secret must be set") 181 | } 182 | -------------------------------------------------------------------------------- /api/v1/alertschannel_types.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | v1 "k8s.io/api/core/v1" 8 | "k8s.io/apimachinery/pkg/types" 9 | "sigs.k8s.io/controller-runtime/pkg/client" 10 | 11 | "github.com/newrelic/newrelic-client-go/pkg/alerts" 12 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | ) 14 | 15 | // AlertsChannelSpec defines the desired state of AlertsChannel 16 | type AlertsChannelSpec struct { 17 | ID int `json:"id,omitempty"` 18 | Name string `json:"name"` 19 | APIKey string `json:"api_key,omitempty"` 20 | APIKeySecret NewRelicAPIKeySecret `json:"api_key_secret,omitempty"` 21 | Region string `json:"region,omitempty"` 22 | Type string `json:"type,omitempty"` 23 | Links ChannelLinks `json:"links,omitempty"` 24 | Configuration AlertsChannelConfiguration `json:"configuration,omitempty"` 25 | } 26 | 27 | // ChannelLinks - copy of alerts.ChannelLinks 28 | type ChannelLinks struct { 29 | PolicyIDs []int `json:"policy_ids,omitempty"` 30 | PolicyNames []string `json:"policy_names,omitempty"` 31 | PolicyKubernetesObjects []metav1.ObjectMeta `json:"policy_kubernetes_objects,omitempty"` 32 | } 33 | 34 | // AlertsChannelStatus defines the observed state of AlertsChannel 35 | type AlertsChannelStatus struct { 36 | AppliedSpec *AlertsChannelSpec `json:"applied_spec"` 37 | ChannelID int `json:"channel_id"` 38 | AppliedPolicyIDs []int `json:"appliedPolicyIDs"` 39 | } 40 | 41 | type ChannelHeader struct { 42 | Name string `json:"name,omitempty"` 43 | Value string `json:"value,omitempty"` 44 | Secret string `json:"secret,omitempty"` 45 | Namespace string `json:"namespace,omitempty"` 46 | KeyName string `json:"key_name,omitempty"` 47 | } 48 | 49 | // AlertsChannelConfiguration - copy of alerts.ChannelConfiguration 50 | type AlertsChannelConfiguration struct { 51 | Recipients string `json:"recipients,omitempty"` 52 | IncludeJSONAttachment string `json:"include_json_attachment,omitempty"` 53 | AuthToken string `json:"auth_token,omitempty"` 54 | APIKey string `json:"api_key,omitempty"` 55 | Teams string `json:"teams,omitempty"` 56 | Tags string `json:"tags,omitempty"` 57 | URL string `json:"url,omitempty"` 58 | Channel string `json:"channel,omitempty"` 59 | Key string `json:"key,omitempty"` 60 | RouteKey string `json:"route_key,omitempty"` 61 | ServiceKey string `json:"service_key,omitempty"` 62 | BaseURL string `json:"base_url,omitempty"` 63 | AuthUsername string `json:"auth_username,omitempty"` 64 | AuthPassword string `json:"auth_password,omitempty"` 65 | PayloadType string `json:"payload_type,omitempty"` 66 | Region string `json:"region,omitempty"` 67 | UserID string `json:"user_id,omitempty"` 68 | 69 | Payload map[string]string `json:"payload,omitempty"` 70 | 71 | Headers []ChannelHeader `json:"headers,omitempty"` 72 | } 73 | 74 | // +kubebuilder:object:root=true 75 | // +kubebuilder:printcolumn:name="Created",type="boolean",JSONPath=".status.created" 76 | 77 | // AlertsChannel is the Schema for the AlertsChannel API 78 | type AlertsChannel struct { 79 | metav1.TypeMeta `json:",inline"` 80 | metav1.ObjectMeta `json:"metadata,omitempty"` 81 | 82 | Spec AlertsChannelSpec `json:"spec,omitempty"` 83 | Status AlertsChannelStatus `json:"status,omitempty"` 84 | } 85 | 86 | // +kubebuilder:object:root=true 87 | 88 | // AlertsChannelList contains a list of AlertsChannel 89 | type AlertsChannelList struct { 90 | metav1.TypeMeta `json:",inline"` 91 | metav1.ListMeta `json:"metadata,omitempty"` 92 | Items []AlertsChannel `json:"items"` 93 | } 94 | 95 | func init() { 96 | SchemeBuilder.Register(&AlertsChannel{}, &AlertsChannelList{}) 97 | } 98 | 99 | func getSecret(name types.NamespacedName, key string, k8sClient client.Client) (string, error) { 100 | var apiKeySecret v1.Secret 101 | 102 | err := k8sClient.Get(context.Background(), name, &apiKeySecret) 103 | if err != nil { 104 | return "", err 105 | } 106 | 107 | return string(apiKeySecret.Data[key]), nil 108 | } 109 | 110 | // APIChannel - Converts AlertsChannelSpec object to alerts.Channel 111 | func (in AlertsChannelSpec) APIChannel(k8sClient client.Client) (alerts.Channel, error) { 112 | headers := in.Configuration.Headers 113 | in.Configuration.Headers = nil 114 | jsonString, _ := json.Marshal(in) 115 | 116 | var APIChannel alerts.Channel 117 | err := json.Unmarshal(jsonString, &APIChannel) // nolint 118 | if err != nil { 119 | return alerts.Channel{}, err 120 | } 121 | 122 | APIChannel.Links = alerts.ChannelLinks{} 123 | 124 | // Spec holds headers in a different format, need to convert to API format 125 | if len(headers) > 0 { 126 | APIChannel.Configuration.Headers = map[string]interface{}{} 127 | for _, header := range headers { 128 | if header.Value != "" { 129 | APIChannel.Configuration.Headers[header.Name] = header.Value 130 | } else { 131 | name := types.NamespacedName{ 132 | Namespace: header.Namespace, 133 | Name: header.Secret, 134 | } 135 | value, err := getSecret(name, header.KeyName, k8sClient) 136 | if err != nil { 137 | return alerts.Channel{}, err 138 | } 139 | APIChannel.Configuration.Headers[header.Name] = value 140 | } 141 | } 142 | } 143 | 144 | return APIChannel, nil 145 | } 146 | -------------------------------------------------------------------------------- /api/v1/alertschannel_types_test.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | 8 | "github.com/golang/mock/gomock" 9 | "github.com/newrelic/newrelic-client-go/pkg/alerts" 10 | . "github.com/onsi/ginkgo" 11 | . "github.com/onsi/gomega" 12 | v1 "k8s.io/api/core/v1" 13 | "k8s.io/apimachinery/pkg/types" 14 | 15 | "github.com/newrelic/newrelic-kubernetes-operator/interfaces/interfacesfakes" 16 | ) 17 | 18 | var _ = Describe("AlertsChannelSpec", func() { 19 | Describe("APIChannel", func() { 20 | var client *interfacesfakes.MockClient 21 | 22 | BeforeEach(func() { 23 | ctrl := gomock.NewController(GinkgoT()) 24 | defer ctrl.Finish() 25 | client = interfacesfakes.NewMockClient(ctrl) 26 | }) 27 | 28 | Context("email channel", func() { 29 | var alertsChannelSpec AlertsChannelSpec 30 | BeforeEach(func() { 31 | alertsChannelSpec = AlertsChannelSpec{ 32 | ID: 88, 33 | Name: "my alert channel", 34 | APIKey: "api-key", 35 | Region: "US", 36 | Type: "email", 37 | Links: ChannelLinks{ 38 | PolicyIDs: []int{ 39 | 1, 40 | 2, 41 | }, 42 | }, 43 | Configuration: AlertsChannelConfiguration{ 44 | Recipients: "me@email.com", 45 | }, 46 | } 47 | }) 48 | 49 | It("converts AlertsChannelSpec object to alerts.Channel object from go client, retaining field values", func() { 50 | apiChannel, err := alertsChannelSpec.APIChannel(client) 51 | Expect(err).NotTo(HaveOccurred()) 52 | 53 | Expect(fmt.Sprint(reflect.TypeOf(apiChannel))).To(Equal("alerts.Channel")) 54 | Expect(apiChannel.ID).To(Equal(88)) 55 | Expect(apiChannel.Type).To(Equal(alerts.ChannelTypes.Email)) 56 | Expect(apiChannel.Name).To(Equal("my alert channel")) 57 | apiConfiguration := apiChannel.Configuration 58 | Expect(apiConfiguration.Recipients).To(Equal("me@email.com")) 59 | }) 60 | }) 61 | 62 | Context("webhook channel", func() { 63 | var alertsChannelSpec AlertsChannelSpec 64 | BeforeEach(func() { 65 | alertsChannelSpec = AlertsChannelSpec{ 66 | ID: 88, 67 | Name: "my alert channel", 68 | APIKey: "api-key", 69 | Region: "US", 70 | Type: "webhook", 71 | Links: ChannelLinks{ 72 | PolicyIDs: []int{ 73 | 1, 74 | 2, 75 | }, 76 | }, 77 | Configuration: AlertsChannelConfiguration{ 78 | URL: "https://example.com/", 79 | Headers: []ChannelHeader{ 80 | { 81 | Name: "KEY", 82 | Value: "VALUE", 83 | }, 84 | { 85 | Name: "SECRET", 86 | Secret: "secret", 87 | Namespace: "default", 88 | KeyName: "secret", 89 | }, 90 | }, 91 | }, 92 | } 93 | }) 94 | 95 | It("converts AlertsChannelSpec object to alerts.Channel object from go client, retaining field values", func() { 96 | secret := v1.Secret{ 97 | Data: map[string][]byte{ 98 | "secret": []byte("don't tell anyone"), 99 | }, 100 | } 101 | key := types.NamespacedName{ 102 | Namespace: "default", 103 | Name: "secret", 104 | } 105 | client.EXPECT(). 106 | Get(gomock.Eq(context.Background()), 107 | gomock.Eq(key), 108 | gomock.AssignableToTypeOf(&secret)). 109 | SetArg(2, secret) 110 | 111 | apiChannel, err := alertsChannelSpec.APIChannel(client) 112 | Expect(err).NotTo(HaveOccurred()) 113 | 114 | Expect(fmt.Sprint(reflect.TypeOf(apiChannel))).To(Equal("alerts.Channel")) 115 | Expect(apiChannel.ID).To(Equal(88)) 116 | Expect(apiChannel.Type).To(Equal(alerts.ChannelTypes.Webhook)) 117 | Expect(apiChannel.Name).To(Equal("my alert channel")) 118 | apiConfiguration := apiChannel.Configuration 119 | Expect(apiConfiguration.URL).To(Equal("https://example.com/")) 120 | Expect(apiConfiguration.Headers["KEY"]).To(Equal("VALUE")) 121 | Expect(apiConfiguration.Headers["SECRET"]).To(Equal("don't tell anyone")) 122 | }) 123 | }) 124 | }) 125 | }) 126 | -------------------------------------------------------------------------------- /api/v1/alertschannel_webhook.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package v1 17 | 18 | import ( 19 | "errors" 20 | 21 | "k8s.io/apimachinery/pkg/runtime" 22 | ctrl "sigs.k8s.io/controller-runtime" 23 | logf "sigs.k8s.io/controller-runtime/pkg/log" 24 | "sigs.k8s.io/controller-runtime/pkg/webhook" 25 | 26 | "github.com/newrelic/newrelic-client-go/pkg/alerts" 27 | 28 | "github.com/newrelic/newrelic-kubernetes-operator/interfaces" 29 | ) 30 | 31 | // log is for logging in this package. 32 | var ( 33 | alertschannellog = logf.Log.WithName("alertschannel-resource") 34 | ) 35 | 36 | // SetupWebhookWithManager - instantiates the Webhook 37 | func (r *AlertsChannel) SetupWebhookWithManager(mgr ctrl.Manager) error { 38 | alertClientFunc = interfaces.InitializeAlertsClient 39 | k8Client = mgr.GetClient() 40 | return ctrl.NewWebhookManagedBy(mgr). 41 | For(r). 42 | Complete() 43 | } 44 | 45 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 46 | 47 | // +kubebuilder:webhook:path=/mutate-nr-k8s-newrelic-com-v1-alertschannel,mutating=true,failurePolicy=fail,groups=nr.k8s.newrelic.com,resources=alertschannels,verbs=create;update,versions=v1,name=malertschannel.kb.io,sideEffects=None 48 | 49 | var _ webhook.Defaulter = &AlertsChannel{} 50 | 51 | // Default implements webhook.Defaulter so a webhook will be registered for the type 52 | func (r *AlertsChannel) Default() { 53 | alertschannellog.Info("default", "name", r.Name) 54 | 55 | if r.Status.AppliedSpec == nil { 56 | log.Info("Setting null Applied Spec to empty interface") 57 | r.Status.AppliedSpec = &AlertsChannelSpec{} 58 | } 59 | 60 | if r.Status.AppliedPolicyIDs == nil { 61 | log.Info("Setting null AppliedPolicyIDs to empty interface") 62 | r.Status.AppliedPolicyIDs = []int{} 63 | } 64 | } 65 | 66 | // +kubebuilder:webhook:verbs=create;update,path=/validate-nr-k8s-newrelic-com-v1-alertschannel,mutating=false,failurePolicy=fail,groups=nr.k8s.newrelic.com,resources=alertschannels,versions=v1,name=valertschannel.kb.io,sideEffects=None 67 | 68 | var _ webhook.Validator = &AlertsChannel{} 69 | 70 | // ValidateCreate implements webhook.Validator so a webhook will be registered for the type 71 | func (r *AlertsChannel) ValidateCreate() error { 72 | alertschannellog.Info("validate create", "name", r.Name) 73 | 74 | return r.ValidateAlertsChannel() 75 | } 76 | 77 | // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type 78 | func (r *AlertsChannel) ValidateUpdate(old runtime.Object) error { 79 | alertschannellog.Info("validate update", "name", r) 80 | 81 | return r.ValidateAlertsChannel() 82 | } 83 | 84 | // ValidateDelete implements webhook.Validator so a webhook will be registered for the type 85 | func (r *AlertsChannel) ValidateDelete() error { 86 | alertschannellog.Info("validate delete", "name", r.Name) 87 | 88 | // TODO(user): fill in your validation logic upon object deletion. 89 | return nil 90 | } 91 | 92 | // ValidateAlertsChannel - Validates create/update of AlertsChannel 93 | func (r *AlertsChannel) ValidateAlertsChannel() error { 94 | err := CheckForAPIKeyOrSecret(r.Spec.APIKey, r.Spec.APIKeySecret) 95 | if err != nil { 96 | return err 97 | } 98 | 99 | if !ValidRegion(r.Spec.Region) { 100 | return errors.New("Invalid region set, value was: " + r.Spec.Region) 101 | } 102 | 103 | var invalidAttributes InvalidAttributeSlice 104 | 105 | r.ValidateType() 106 | invalidAttributes = append(invalidAttributes, r.ValidateType()...) 107 | 108 | if len(invalidAttributes) > 0 { 109 | return errors.New("error with invalid attributes: \n" + invalidAttributes.errorString()) 110 | } 111 | 112 | return nil 113 | } 114 | 115 | //ValidateType - Validates the Type attribute 116 | func (r *AlertsChannel) ValidateType() InvalidAttributeSlice { 117 | switch r.Spec.Type { 118 | case string(alerts.ChannelTypes.Email), 119 | string(alerts.ChannelTypes.OpsGenie), 120 | string(alerts.ChannelTypes.PagerDuty), 121 | string(alerts.ChannelTypes.Slack), 122 | string(alerts.ChannelTypes.User), 123 | string(alerts.ChannelTypes.VictorOps), 124 | string(alerts.ChannelTypes.Webhook): 125 | 126 | return []invalidAttribute{} 127 | default: 128 | alertschannellog.Info("Invalid Type attribute", "Type", r.Spec.Type) 129 | 130 | return []invalidAttribute{{attribute: "Type", value: r.Spec.Type}} 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /api/v1/alertschannel_webhook_test.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "github.com/newrelic/newrelic-client-go/pkg/alerts" 5 | . "github.com/onsi/ginkgo" 6 | . "github.com/onsi/gomega" 7 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | 9 | "github.com/newrelic/newrelic-kubernetes-operator/interfaces" 10 | "github.com/newrelic/newrelic-kubernetes-operator/interfaces/interfacesfakes" 11 | ) 12 | 13 | var _ = Describe("AlertsChannel_webhook", func() { 14 | var ( 15 | r AlertsChannel 16 | alertsClient *interfacesfakes.FakeNewRelicAlertsClient 17 | ) 18 | 19 | BeforeEach(func() { 20 | k8Client = testk8sClient 21 | alertsClient = &interfacesfakes.FakeNewRelicAlertsClient{} 22 | fakeAlertFunc := func(string, string) (interfaces.NewRelicAlertsClient, error) { 23 | return alertsClient, nil 24 | } 25 | alertClientFunc = fakeAlertFunc 26 | r = AlertsChannel{ 27 | ObjectMeta: v1.ObjectMeta{ 28 | Name: "test alert channel", 29 | }, 30 | Spec: AlertsChannelSpec{ 31 | ID: 88, 32 | Name: "my alert channel", 33 | APIKey: "api-key", 34 | APIKeySecret: NewRelicAPIKeySecret{}, 35 | Region: "US", 36 | Type: "webhook", 37 | Links: ChannelLinks{ 38 | PolicyIDs: []int{ 39 | 1, 40 | 2, 41 | }, 42 | }, 43 | Configuration: AlertsChannelConfiguration{ 44 | BaseURL: "https://example.com", 45 | Headers: []ChannelHeader{ 46 | { 47 | Name: "KEY", 48 | Value: "VALUE", 49 | }, 50 | }, 51 | }, 52 | }, 53 | } 54 | 55 | alertsClient.GetPolicyStub = func(int) (*alerts.Policy, error) { 56 | return &alerts.Policy{ 57 | ID: 46286, 58 | }, nil 59 | } 60 | }) 61 | 62 | Context("ValidateCreate", func() { 63 | Context("With a valid Alert Channel", func() { 64 | It("Should create the Alert Channel", func() { 65 | err := r.ValidateCreate() 66 | Expect(err).ToNot(HaveOccurred()) 67 | }) 68 | }) 69 | 70 | Context("With an invalid Region", func() { 71 | BeforeEach(func() { 72 | r.Spec.Region = "hamburgers" 73 | }) 74 | 75 | It("Should reject the Alert Channel creation", func() { 76 | err := r.ValidateCreate() 77 | Expect(err).To(HaveOccurred()) 78 | Expect(err.Error()).To(ContainSubstring("hamburgers")) 79 | }) 80 | }) 81 | 82 | Context("With an invalid Type", func() { 83 | BeforeEach(func() { 84 | r.Spec.Type = "burritos" 85 | }) 86 | 87 | It("Should reject the Alert Channel creation", func() { 88 | err := r.ValidateCreate() 89 | Expect(err).To(HaveOccurred()) 90 | Expect(err.Error()).To(ContainSubstring("burritos")) 91 | }) 92 | }) 93 | 94 | Context("With no API Key or secret", func() { 95 | BeforeEach(func() { 96 | r.Spec.APIKey = "" 97 | }) 98 | 99 | It("Should reject the Alert Channel creation", func() { 100 | err := r.ValidateCreate() 101 | Expect(err).To(HaveOccurred()) 102 | Expect(err.Error()).To(ContainSubstring("either api_key or api_key_secret must be set")) 103 | }) 104 | }) 105 | }) 106 | }) 107 | -------------------------------------------------------------------------------- /api/v1/apmalertcondition_types.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/newrelic/newrelic-client-go/pkg/alerts" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | ) 9 | 10 | // ApmAlertConditionSpec defines the desired state of ApmAlertCondition 11 | type ApmAlertConditionSpec struct { 12 | GenericConditionSpec `json:",inline"` 13 | APMSpecificSpec `json:",inline"` 14 | } 15 | 16 | type APMSpecificSpec struct { 17 | Metric string `json:"metric,omitempty"` 18 | UserDefined alerts.ConditionUserDefined `json:"user_defined,omitempty"` 19 | Scope string `json:"condition_scope,omitempty"` 20 | Entities []string `json:"entities,omitempty"` 21 | GCMetric string `json:"gc_metric,omitempty"` 22 | ViolationCloseTimer int `json:"violation_close_timer,omitempty"` 23 | } 24 | 25 | // ApmAlertConditionStatus defines the observed state of ApmAlertCondition 26 | type ApmAlertConditionStatus struct { 27 | AppliedSpec *ApmAlertConditionSpec `json:"applied_spec"` 28 | ConditionID int `json:"condition_id"` 29 | } 30 | 31 | // +kubebuilder:object:root=true 32 | // +kubebuilder:printcolumn:name="Created",type="boolean",JSONPath=".status.created" 33 | 34 | // ApmAlertCondition is the Schema for the apmalertconditions API 35 | type ApmAlertCondition struct { 36 | metav1.TypeMeta `json:",inline"` 37 | metav1.ObjectMeta `json:"metadata,omitempty"` 38 | 39 | Spec ApmAlertConditionSpec `json:"spec,omitempty"` 40 | Status ApmAlertConditionStatus `json:"status,omitempty"` 41 | } 42 | 43 | // +kubebuilder:object:root=true 44 | 45 | // ApmAlertConditionList contains a list of ApmAlertCondition 46 | type ApmAlertConditionList struct { 47 | metav1.TypeMeta `json:",inline"` 48 | metav1.ListMeta `json:"metadata,omitempty"` 49 | Items []ApmAlertCondition `json:"items"` 50 | } 51 | 52 | func init() { 53 | SchemeBuilder.Register(&ApmAlertCondition{}, &ApmAlertConditionList{}) 54 | } 55 | 56 | func (in ApmAlertConditionSpec) APICondition() alerts.Condition { 57 | jsonString, _ := json.Marshal(in) 58 | var APICondition alerts.Condition 59 | json.Unmarshal(jsonString, &APICondition) //nolint 60 | 61 | return APICondition 62 | } 63 | -------------------------------------------------------------------------------- /api/v1/apmalertcondition_types_test.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | 7 | "github.com/newrelic/newrelic-client-go/pkg/alerts" 8 | 9 | . "github.com/onsi/ginkgo" 10 | . "github.com/onsi/gomega" 11 | ) 12 | 13 | var _ = Describe("ApmAlertConditionSpec", func() { 14 | var condition ApmAlertConditionSpec 15 | 16 | BeforeEach(func() { 17 | condition = ApmAlertConditionSpec{ 18 | GenericConditionSpec{ 19 | Terms: []AlertConditionTerm{ 20 | { 21 | Duration: "30", 22 | Operator: "above", 23 | Priority: "critical", 24 | Threshold: "1.5", 25 | TimeFunction: "all", 26 | }, 27 | }, 28 | Type: "apm_app_metric", 29 | Name: "APM Condition", 30 | RunbookURL: "http://test.com/runbook", 31 | PolicyID: 0, 32 | ID: 888, 33 | Enabled: true, 34 | ExistingPolicyID: 42, 35 | }, 36 | APMSpecificSpec{ 37 | Metric: "apdex", 38 | Entities: []string{"333"}, 39 | UserDefined: alerts.ConditionUserDefined{ 40 | Metric: "Custom/foo", 41 | ValueFunction: "average", 42 | }, 43 | Scope: "application", 44 | GCMetric: "", 45 | ViolationCloseTimer: 60, 46 | }, 47 | } 48 | }) 49 | 50 | Describe("APICondition", func() { 51 | It("converts ApmAlertConditionSpec object to Condition object from go client, retaining field values", func() { 52 | apiCondition := condition.APICondition() 53 | 54 | Expect(fmt.Sprint(reflect.TypeOf(apiCondition))).To(Equal("alerts.Condition")) 55 | 56 | Expect(apiCondition.Type).To(Equal(alerts.ConditionTypes.APMApplicationMetric)) 57 | Expect(apiCondition.Name).To(Equal("APM Condition")) 58 | Expect(apiCondition.RunbookURL).To(Equal("http://test.com/runbook")) 59 | Expect(apiCondition.Metric).To(Equal(alerts.MetricTypes.Apdex)) 60 | Expect(apiCondition.Entities[0]).To(Equal("333")) 61 | Expect(apiCondition.Scope).To(Equal("application")) 62 | Expect(apiCondition.GCMetric).To(Equal("")) 63 | 64 | Expect(apiCondition.ID).To(Equal(888)) 65 | Expect(apiCondition.ViolationCloseTimer).To(Equal(60)) 66 | Expect(apiCondition.Enabled).To(Equal(true)) 67 | 68 | apiTerm := apiCondition.Terms[0] 69 | 70 | Expect(fmt.Sprint(reflect.TypeOf(apiTerm))).To(Equal("alerts.ConditionTerm")) 71 | 72 | Expect(apiTerm.Duration).To(Equal(30)) 73 | Expect(apiTerm.Operator).To(Equal(alerts.OperatorTypes.Above)) 74 | Expect(apiTerm.Priority).To(Equal(alerts.PriorityTypes.Critical)) 75 | Expect(apiTerm.Threshold).To(Equal(float64(1.5))) 76 | Expect(apiTerm.TimeFunction).To(Equal(alerts.TimeFunctionTypes.All)) 77 | 78 | userDefinedCondition := apiCondition.UserDefined 79 | Expect(fmt.Sprint(reflect.TypeOf(userDefinedCondition))).To(Equal("alerts.ConditionUserDefined")) 80 | 81 | Expect(userDefinedCondition.Metric).To(Equal("Custom/foo")) 82 | Expect(userDefinedCondition.ValueFunction).To(Equal(alerts.ValueFunctionTypes.Average)) 83 | }) 84 | }) 85 | }) 86 | -------------------------------------------------------------------------------- /api/v1/apmalertcondition_webhook_test.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/newrelic/newrelic-client-go/pkg/alerts" 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/gomega" 10 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | 12 | "github.com/newrelic/newrelic-kubernetes-operator/interfaces" 13 | "github.com/newrelic/newrelic-kubernetes-operator/interfaces/interfacesfakes" 14 | ) 15 | 16 | var _ = Describe("apmAlertCondition_webhook", func() { 17 | var ( 18 | r ApmAlertCondition 19 | alertsClient *interfacesfakes.FakeNewRelicAlertsClient 20 | ) 21 | 22 | BeforeEach(func() { 23 | k8Client = testk8sClient 24 | alertsClient = &interfacesfakes.FakeNewRelicAlertsClient{} 25 | fakeAlertFunc := func(string, string) (interfaces.NewRelicAlertsClient, error) { 26 | return alertsClient, nil 27 | } 28 | alertClientFunc = fakeAlertFunc 29 | r = ApmAlertCondition{ 30 | ObjectMeta: v1.ObjectMeta{ 31 | Name: "test apm condition", 32 | }, 33 | Spec: ApmAlertConditionSpec{ 34 | GenericConditionSpec{ 35 | Terms: []AlertConditionTerm{ 36 | { 37 | Duration: "30", 38 | Operator: "above", 39 | Priority: "critical", 40 | Threshold: "0.9", 41 | TimeFunction: "all", 42 | }, 43 | }, 44 | Type: "apm_app_metric", 45 | Name: "K8s generated apm alert condition", 46 | Enabled: true, 47 | ExistingPolicyID: 46286, 48 | APIKey: "111222333", 49 | APIKeySecret: NewRelicAPIKeySecret{}, 50 | Region: "staging", 51 | }, 52 | APMSpecificSpec{ 53 | Metric: "apdex", 54 | UserDefined: alerts.ConditionUserDefined{}, 55 | Scope: "", 56 | Entities: []string{"5950260"}, 57 | }, 58 | }, 59 | } 60 | 61 | alertsClient.GetPolicyStub = func(int) (*alerts.Policy, error) { 62 | return &alerts.Policy{ 63 | ID: 46286, 64 | }, nil 65 | } 66 | }) 67 | Context("ValidateCreate", func() { 68 | Context("With a valid Apm Condition", func() { 69 | It("Should create the apm condition", func() { 70 | err := r.ValidateCreate() 71 | Expect(err).ToNot(HaveOccurred()) 72 | }) 73 | }) 74 | 75 | Context("With an invalid Type", func() { 76 | BeforeEach(func() { 77 | r.Spec.Type = "burritos" 78 | }) 79 | 80 | It("Should reject the apm condition creation", func() { 81 | err := r.ValidateCreate() 82 | Expect(err).To(HaveOccurred()) 83 | Expect(err.Error()).To(ContainSubstring("burritos")) 84 | }) 85 | }) 86 | 87 | Context("With an invalid Metric", func() { 88 | BeforeEach(func() { 89 | r.Spec.Type = "moar burritos" 90 | }) 91 | 92 | It("Should reject the apm condition creation", func() { 93 | err := r.ValidateCreate() 94 | Expect(err).To(HaveOccurred()) 95 | Expect(err.Error()).To(ContainSubstring("moar burritos")) 96 | }) 97 | }) 98 | 99 | Context("With an invalid Terms", func() { 100 | BeforeEach(func() { 101 | r.Spec.Terms[0].TimeFunction = "moar burritos" 102 | r.Spec.Terms[0].Priority = "moar tacos" 103 | r.Spec.Terms[0].Operator = "moar hamburgers" 104 | }) 105 | 106 | It("Should reject the apm condition creation", func() { 107 | err := r.ValidateCreate() 108 | Expect(err).To(HaveOccurred()) 109 | Expect(err.Error()).To(ContainSubstring("moar burritos")) 110 | Expect(err.Error()).To(ContainSubstring("moar tacos")) 111 | Expect(err.Error()).To(ContainSubstring("moar hamburgers")) 112 | }) 113 | }) 114 | 115 | Context("With an invalid userDefined type", func() { 116 | BeforeEach(func() { 117 | r.Spec.UserDefined = alerts.ConditionUserDefined{ 118 | Metric: "Custom/foo", 119 | ValueFunction: "invalid type", 120 | } 121 | }) 122 | 123 | It("Should reject the apm condition creation", func() { 124 | err := r.ValidateCreate() 125 | Expect(err).To(HaveOccurred()) 126 | Expect(err.Error()).To(ContainSubstring("invalid type")) 127 | }) 128 | }) 129 | 130 | }) 131 | 132 | Context("ValidateUpdate", func() { 133 | Context("When deleting an existing apm Condition with a delete policy", func() { 134 | var update ApmAlertCondition 135 | BeforeEach(func() { 136 | currentTime := v1.Time{Time: time.Now()} 137 | //make copy of existing object to update 138 | r.DeepCopyInto(&update) 139 | 140 | update.SetDeletionTimestamp(¤tTime) 141 | alertsClient.GetPolicyStub = func(int) (*alerts.Policy, error) { 142 | return &alerts.Policy{}, errors.New("no alert policy found for id 49092") 143 | } 144 | }) 145 | 146 | It("Should allow the deletion anyway", func() { 147 | err := update.ValidateUpdate(&r) 148 | Expect(err).ToNot(HaveOccurred()) 149 | }) 150 | }) 151 | }) 152 | }) 153 | -------------------------------------------------------------------------------- /api/v1/common.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "errors" 5 | "hash" 6 | 7 | "github.com/davecgh/go-spew/spew" 8 | "github.com/newrelic/newrelic-client-go/pkg/region" 9 | ) 10 | 11 | // DeepHashObject writes specified object to hash using the spew library 12 | // which follows pointers and prints actual values of the nested objects 13 | // ensuring the hash does not change when a pointer changes. 14 | func DeepHashObject(hasher hash.Hash, objectToWrite interface{}) { 15 | hasher.Reset() 16 | printer := spew.ConfigState{ 17 | Indent: " ", 18 | SortKeys: true, 19 | DisableMethods: true, 20 | SpewKeys: true, 21 | } 22 | printer.Fprintf(hasher, "%#v", objectToWrite) 23 | } 24 | 25 | // AlertConditionTerm represents the terms of a New Relic alert condition. 26 | type AlertConditionTerm struct { 27 | Duration string `json:"duration,omitempty"` 28 | Operator string `json:"operator,omitempty"` 29 | Priority string `json:"priority,omitempty"` 30 | Threshold string `json:"threshold"` 31 | TimeFunction string `json:"time_function,omitempty"` 32 | ViolationCloseTimer int `json:"violation_close_timer,omitempty"` 33 | } 34 | 35 | type NewRelicAPIKeySecret struct { 36 | Name string `json:"name,omitempty"` 37 | Namespace string `json:"namespace,omitempty"` 38 | KeyName string `json:"key_name,omitempty"` 39 | } 40 | 41 | //ValidRegion - returns true if a valid region is passed 42 | func ValidRegion(input string) bool { 43 | _, err := region.Parse(input) 44 | if err != nil { 45 | return false 46 | } else if input == "" { 47 | return false 48 | } 49 | 50 | return true 51 | } 52 | 53 | //CheckForAPIKeyOrSecret - returns error if a API KEY or k8 secret is not passed in 54 | func CheckForAPIKeyOrSecret(apiKey string, secret NewRelicAPIKeySecret) error { 55 | if apiKey != "" { 56 | return nil 57 | } 58 | 59 | if secret != (NewRelicAPIKeySecret{}) { 60 | if secret.Name != "" && secret.Namespace != "" && secret.KeyName != "" { 61 | return nil 62 | } 63 | } 64 | 65 | return errors.New("either api_key or api_key_secret must be set") 66 | } 67 | -------------------------------------------------------------------------------- /api/v1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | // Package v1 contains API Schema definitions for the nralerts v1 API group 17 | // +kubebuilder:object:generate=true 18 | // +groupName=nr.k8s.newrelic.com 19 | package v1 20 | 21 | import ( 22 | "k8s.io/apimachinery/pkg/runtime/schema" 23 | "sigs.k8s.io/controller-runtime/pkg/scheme" 24 | ) 25 | 26 | var ( 27 | // GroupVersion is group version used to register these objects 28 | GroupVersion = schema.GroupVersion{Group: "nr.k8s.newrelic.com", Version: "v1"} 29 | 30 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 31 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 32 | 33 | // AddToScheme adds the types in this group-version to the given scheme. 34 | AddToScheme = SchemeBuilder.AddToScheme 35 | ) 36 | -------------------------------------------------------------------------------- /api/v1/nrqlalertcondition_types.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/newrelic/newrelic-client-go/pkg/alerts" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | ) 9 | 10 | // NrqlAlertConditionSpec defines the desired state of NrqlAlertCondition 11 | type NrqlAlertConditionSpec struct { 12 | GenericConditionSpec `json:",inline"` 13 | NrqlSpecificSpec `json:",inline"` 14 | } 15 | 16 | type GenericConditionSpec struct { 17 | Terms []AlertConditionTerm `json:"terms,omitempty"` 18 | Type string `json:"type,omitempty"` 19 | Name string `json:"name,omitempty"` 20 | RunbookURL string `json:"runbook_url,omitempty"` 21 | PolicyID int `json:"-"` 22 | ID int `json:"id,omitempty"` 23 | Enabled bool `json:"enabled"` 24 | ExistingPolicyID int `json:"existing_policy_id,omitempty"` 25 | APIKey string `json:"api_key,omitempty"` 26 | APIKeySecret NewRelicAPIKeySecret `json:"api_key_secret,omitempty"` 27 | Region string `json:"region,omitempty"` 28 | } 29 | 30 | type NrqlSpecificSpec struct { 31 | Nrql NrqlQuery `json:"nrql,omitempty"` 32 | ValueFunction string `json:"value_function,omitempty"` 33 | ExpectedGroups int `json:"expected_groups,omitempty"` 34 | IgnoreOverlap bool `json:"ignore_overlap,omitempty"` 35 | ViolationCloseTimer int `json:"violation_time_limit_seconds,omitempty"` 36 | } 37 | 38 | // NrqlQuery represents a NRQL query to use with a NRQL alert condition 39 | type NrqlQuery struct { 40 | Query string `json:"query,omitempty"` 41 | SinceValue string `json:"since_value,omitempty"` 42 | } 43 | 44 | // NrqlAlertConditionStatus defines the observed state of NrqlAlertCondition 45 | type NrqlAlertConditionStatus struct { 46 | AppliedSpec *NrqlAlertConditionSpec `json:"applied_spec"` 47 | ConditionID int `json:"condition_id"` 48 | } 49 | 50 | // +kubebuilder:object:root=true 51 | // +kubebuilder:printcolumn:name="Created",type="boolean",JSONPath=".status.created" 52 | 53 | // NrqlAlertCondition is the Schema for the nrqlalertconditions API 54 | type NrqlAlertCondition struct { 55 | metav1.TypeMeta `json:",inline"` 56 | metav1.ObjectMeta `json:"metadata,omitempty"` 57 | 58 | Spec NrqlAlertConditionSpec `json:"spec,omitempty"` 59 | Status NrqlAlertConditionStatus `json:"status,omitempty"` 60 | } 61 | 62 | // +kubebuilder:object:root=true 63 | 64 | // NrqlAlertConditionList contains a list of NrqlAlertCondition 65 | type NrqlAlertConditionList struct { 66 | metav1.TypeMeta `json:",inline"` 67 | metav1.ListMeta `json:"metadata,omitempty"` 68 | Items []NrqlAlertCondition `json:"items"` 69 | } 70 | 71 | func init() { 72 | SchemeBuilder.Register(&NrqlAlertCondition{}, &NrqlAlertConditionList{}) 73 | } 74 | 75 | func (in NrqlAlertConditionSpec) APICondition() alerts.NrqlCondition { 76 | jsonString, _ := json.Marshal(in) 77 | var APICondition alerts.NrqlCondition 78 | json.Unmarshal(jsonString, &APICondition) //nolint 79 | 80 | return APICondition 81 | } 82 | -------------------------------------------------------------------------------- /api/v1/nrqlalertcondition_types_integration_test.go: -------------------------------------------------------------------------------- 1 | // +build integration 2 | 3 | package v1 4 | 5 | import ( 6 | "fmt" 7 | "reflect" 8 | 9 | "github.com/newrelic/newrelic-client-go/pkg/alerts" 10 | 11 | . "github.com/onsi/ginkgo" 12 | . "github.com/onsi/gomega" 13 | ) 14 | 15 | var _ = Describe("NrqlAlertConditionSpec", func() { 16 | var condition NrqlAlertConditionSpec 17 | 18 | BeforeEach(func() { 19 | condition = NrqlAlertConditionSpec{ 20 | GenericConditionSpec{ 21 | Terms: []AlertConditionTerm{ 22 | { 23 | Duration: "30", 24 | Operator: "above", 25 | Priority: "critical", 26 | Threshold: "5", 27 | TimeFunction: "all", 28 | }, 29 | }, 30 | Type: "NRQL", 31 | Name: "NRQL Condition", 32 | RunbookURL: "http://test.com/runbook", 33 | ID: 777, 34 | Enabled: true, 35 | ExistingPolicyID: 42, 36 | }, 37 | NrqlSpecificSpec{ 38 | ViolationCloseTimer: 60, 39 | ExpectedGroups: 2, 40 | IgnoreOverlap: true, 41 | ValueFunction: "max", 42 | Nrql: NrqlQuery{ 43 | Query: "SELECT 1 FROM MyEvents", 44 | SinceValue: "5", 45 | }, 46 | }, 47 | } 48 | }) 49 | 50 | Describe("APICondition", func() { 51 | It("converts NrqlAlertConditionSpec object to NrqlCondition object from go client, retaining field values", func() { 52 | apiCondition := condition.APICondition() 53 | 54 | Expect(fmt.Sprint(reflect.TypeOf(apiCondition))).To(Equal("alerts.NrqlCondition")) 55 | 56 | Expect(apiCondition.Type).To(Equal("NRQL")) 57 | Expect(apiCondition.Name).To(Equal("NRQL Condition")) 58 | Expect(apiCondition.RunbookURL).To(Equal("http://test.com/runbook")) 59 | Expect(apiCondition.ValueFunction).To(Equal(alerts.ValueFunctionTypes.Max)) 60 | Expect(apiCondition.ID).To(Equal(777)) 61 | Expect(apiCondition.ViolationCloseTimer).To(Equal(60)) 62 | Expect(apiCondition.ExpectedGroups).To(Equal(2)) 63 | Expect(apiCondition.IgnoreOverlap).To(Equal(true)) 64 | Expect(apiCondition.Enabled).To(Equal(true)) 65 | 66 | apiTerm := apiCondition.Terms[0] 67 | 68 | Expect(fmt.Sprint(reflect.TypeOf(apiTerm))).To(Equal("alerts.ConditionTerm")) 69 | 70 | Expect(apiTerm.Duration).To(Equal(30)) 71 | Expect(apiTerm.Operator).To(Equal(alerts.OperatorTypes.Above)) 72 | Expect(apiTerm.Priority).To(Equal(alerts.PriorityTypes.Critical)) 73 | Expect(apiTerm.Threshold).To(Equal(float64(5))) 74 | Expect(apiTerm.TimeFunction).To(Equal(alerts.TimeFunctionTypes.All)) 75 | 76 | apiQuery := apiCondition.Nrql 77 | 78 | Expect(fmt.Sprint(reflect.TypeOf(apiQuery))).To(Equal("alerts.NrqlQuery")) 79 | 80 | Expect(apiQuery.Query).To(Equal("SELECT 1 FROM MyEvents")) 81 | Expect(apiQuery.SinceValue).To(Equal("5")) 82 | }) 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /api/v1/nrqlalertcondition_types_test.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | 7 | "github.com/newrelic/newrelic-client-go/pkg/alerts" 8 | 9 | . "github.com/onsi/ginkgo" 10 | . "github.com/onsi/gomega" 11 | ) 12 | 13 | var _ = Describe("NrqlAlertConditionSpec", func() { 14 | var condition NrqlAlertConditionSpec 15 | 16 | BeforeEach(func() { 17 | condition = NrqlAlertConditionSpec{ 18 | GenericConditionSpec{ 19 | Terms: []AlertConditionTerm{ 20 | { 21 | Duration: "30", 22 | Operator: "above", 23 | Priority: "critical", 24 | Threshold: "5", 25 | TimeFunction: "all", 26 | }, 27 | }, 28 | Type: "NRQL", 29 | Name: "NRQL Condition", 30 | RunbookURL: "http://test.com/runbook", 31 | ID: 777, 32 | 33 | Enabled: true, 34 | ExistingPolicyID: 42, 35 | }, 36 | NrqlSpecificSpec{ 37 | ViolationCloseTimer: 60, 38 | ExpectedGroups: 2, 39 | IgnoreOverlap: true, 40 | ValueFunction: "max", 41 | Nrql: NrqlQuery{ 42 | Query: "SELECT 1 FROM MyEvents", 43 | SinceValue: "5", 44 | }, 45 | }, 46 | } 47 | }) 48 | 49 | Describe("APICondition", func() { 50 | It("converts NrqlAlertConditionSpec object to NrqlCondition object from go client, retaining field values", func() { 51 | apiCondition := condition.APICondition() 52 | 53 | Expect(fmt.Sprint(reflect.TypeOf(apiCondition))).To(Equal("alerts.NrqlCondition")) 54 | 55 | Expect(apiCondition.Type).To(Equal("NRQL")) 56 | Expect(apiCondition.Name).To(Equal("NRQL Condition")) 57 | Expect(apiCondition.RunbookURL).To(Equal("http://test.com/runbook")) 58 | Expect(apiCondition.ValueFunction).To(Equal(alerts.ValueFunctionTypes.Max)) 59 | Expect(apiCondition.ID).To(Equal(777)) 60 | Expect(apiCondition.ViolationCloseTimer).To(Equal(60)) 61 | Expect(apiCondition.ExpectedGroups).To(Equal(2)) 62 | Expect(apiCondition.IgnoreOverlap).To(Equal(true)) 63 | Expect(apiCondition.Enabled).To(Equal(true)) 64 | 65 | apiTerm := apiCondition.Terms[0] 66 | 67 | Expect(fmt.Sprint(reflect.TypeOf(apiTerm))).To(Equal("alerts.ConditionTerm")) 68 | 69 | Expect(apiTerm.Duration).To(Equal(30)) 70 | Expect(apiTerm.Operator).To(Equal(alerts.OperatorTypes.Above)) 71 | Expect(apiTerm.Priority).To(Equal(alerts.PriorityTypes.Critical)) 72 | Expect(apiTerm.Threshold).To(Equal(float64(5))) 73 | Expect(apiTerm.TimeFunction).To(Equal(alerts.TimeFunctionTypes.All)) 74 | 75 | apiQuery := apiCondition.Nrql 76 | 77 | Expect(fmt.Sprint(reflect.TypeOf(apiQuery))).To(Equal("alerts.NrqlQuery")) 78 | 79 | Expect(apiQuery.Query).To(Equal("SELECT 1 FROM MyEvents")) 80 | Expect(apiQuery.SinceValue).To(Equal("5")) 81 | }) 82 | }) 83 | }) 84 | -------------------------------------------------------------------------------- /api/v1/nrqlalertcondition_webhook.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package v1 17 | 18 | import ( 19 | "context" 20 | "errors" 21 | "strings" 22 | 23 | v1 "k8s.io/api/core/v1" 24 | "k8s.io/apimachinery/pkg/runtime" 25 | "k8s.io/apimachinery/pkg/types" 26 | ctrl "sigs.k8s.io/controller-runtime" 27 | logf "sigs.k8s.io/controller-runtime/pkg/log" 28 | "sigs.k8s.io/controller-runtime/pkg/webhook" 29 | 30 | "github.com/newrelic/newrelic-kubernetes-operator/interfaces" 31 | ) 32 | 33 | // log is for logging in this package. 34 | var ( 35 | log = logf.Log.WithName("nrqlalertcondition-resource") 36 | ) 37 | 38 | func (r *NrqlAlertCondition) SetupWebhookWithManager(mgr ctrl.Manager) error { 39 | alertClientFunc = interfaces.InitializeAlertsClient 40 | k8Client = mgr.GetClient() 41 | return ctrl.NewWebhookManagedBy(mgr). 42 | For(r). 43 | Complete() 44 | } 45 | 46 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 47 | 48 | // +kubebuilder:webhook:path=/mutate-nr-k8s-newrelic-com-v1-nrqlalertcondition,mutating=true,failurePolicy=fail,groups=nr.k8s.newrelic.com,resources=nrqlalertconditions,verbs=create;update,versions=v1,name=mnrqlalertcondition.kb.io,sideEffects=None 49 | 50 | var _ webhook.Defaulter = &NrqlAlertCondition{} 51 | 52 | // Default implements webhook.Defaulter so a webhook will be registered for the type 53 | func (r *NrqlAlertCondition) Default() { 54 | log.Info("default", "name", r.Name) 55 | 56 | if r.Status.AppliedSpec == nil { 57 | log.Info("Setting null Applied Spec to empty interface") 58 | r.Status.AppliedSpec = &NrqlAlertConditionSpec{} 59 | } 60 | log.Info("r.Status.AppliedSpec after", "r.Status.AppliedSpec", r.Status.AppliedSpec) 61 | } 62 | 63 | // +kubebuilder:webhook:verbs=create;update,path=/validate-nr-k8s-newrelic-com-v1-nrqlalertcondition,mutating=false,failurePolicy=fail,groups=nr.k8s.newrelic.com,resources=nrqlalertconditions,versions=v1,name=vnrqlalertcondition.kb.io,sideEffects=None 64 | 65 | var _ webhook.Validator = &NrqlAlertCondition{} 66 | 67 | // ValidateCreate implements webhook.Validator so a webhook will be registered for the type 68 | func (r *NrqlAlertCondition) ValidateCreate() error { 69 | log.Info("validate create", "name", r.Name) 70 | //TODO this should write this value TO a new secret so code path always reads from a secret 71 | err := r.CheckForAPIKeyOrSecret() 72 | if err != nil { 73 | return err 74 | } 75 | 76 | err = r.CheckRequiredFields() 77 | if err != nil { 78 | return err 79 | } 80 | 81 | return r.CheckExistingPolicyID() 82 | } 83 | 84 | // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type 85 | func (r *NrqlAlertCondition) ValidateUpdate(old runtime.Object) error { 86 | log.Info("validate update", "name", r.Name) 87 | err := r.CheckForAPIKeyOrSecret() 88 | if err != nil { 89 | return err 90 | } 91 | 92 | err = r.CheckRequiredFields() 93 | if err != nil { 94 | return err 95 | } 96 | 97 | return r.CheckExistingPolicyID() 98 | } 99 | 100 | // ValidateDelete implements webhook.Validator so a webhook will be registered for the type 101 | func (r *NrqlAlertCondition) ValidateDelete() error { 102 | log.Info("validate delete", "name", r.Name) 103 | 104 | // TODO(user): fill in your validation logic upon object deletion. 105 | return nil 106 | } 107 | 108 | func (r *NrqlAlertCondition) CheckExistingPolicyID() error { 109 | log.Info("Checking existing", "policyId", r.Spec.ExistingPolicyID) 110 | ctx := context.Background() 111 | var apiKey string 112 | 113 | if r.Spec.APIKey == "" { 114 | key := types.NamespacedName{Namespace: r.Spec.APIKeySecret.Namespace, Name: r.Spec.APIKeySecret.Name} 115 | var apiKeySecret v1.Secret 116 | getErr := k8Client.Get(ctx, key, &apiKeySecret) 117 | if getErr != nil { 118 | log.Error(getErr, "Error getting secret") 119 | return getErr 120 | } 121 | apiKey = string(apiKeySecret.Data[r.Spec.APIKeySecret.KeyName]) 122 | } else { 123 | apiKey = r.Spec.APIKey 124 | } 125 | 126 | alertsClient, errAlertClient := alertClientFunc(apiKey, r.Spec.Region) 127 | if errAlertClient != nil { 128 | log.Error(errAlertClient, "failed to get policy", 129 | "policyId", r.Spec.ExistingPolicyID, 130 | "API Key", interfaces.PartialAPIKey(apiKey), 131 | "region", r.Spec.Region, 132 | ) 133 | return errAlertClient 134 | } 135 | 136 | alertPolicy, errAlertPolicy := alertsClient.GetPolicy(r.Spec.ExistingPolicyID) 137 | if errAlertPolicy != nil { 138 | if r.GetDeletionTimestamp() != nil { 139 | log.Info("Deleting resource", "errAlertPolicy", errAlertPolicy) 140 | if strings.Contains(errAlertPolicy.Error(), "no alert policy found for id") { 141 | log.Info("ExistingAlertPolicy not found but we are deleting the condition so this is ok") 142 | return nil 143 | } 144 | } 145 | log.Error(errAlertPolicy, "failed to get policy", 146 | "policyId", r.Spec.ExistingPolicyID, 147 | "API Key", interfaces.PartialAPIKey(apiKey), 148 | "region", r.Spec.Region, 149 | ) 150 | return errAlertPolicy 151 | } 152 | 153 | if alertPolicy.ID != r.Spec.ExistingPolicyID { 154 | log.Info("Alert policy returned by the API failed to match provided policy ID") 155 | return errors.New("alert policy returned by API did not match") 156 | } 157 | 158 | return nil 159 | } 160 | 161 | func (r *NrqlAlertCondition) CheckForAPIKeyOrSecret() error { 162 | if r.Spec.APIKey != "" { 163 | return nil 164 | } 165 | 166 | if r.Spec.APIKeySecret != (NewRelicAPIKeySecret{}) { 167 | if r.Spec.APIKeySecret.Name != "" && r.Spec.APIKeySecret.Namespace != "" && r.Spec.APIKeySecret.KeyName != "" { 168 | return nil 169 | } 170 | } 171 | 172 | return errors.New("either api_key or api_key_secret must be set") 173 | } 174 | 175 | func (r *NrqlAlertCondition) CheckRequiredFields() error { 176 | missingFields := []string{} 177 | 178 | if r.Spec.Region == "" { 179 | missingFields = append(missingFields, "region") 180 | } 181 | 182 | if r.Spec.ExistingPolicyID == 0 { 183 | missingFields = append(missingFields, "existing_policy_id") 184 | } 185 | 186 | if len(missingFields) > 0 { 187 | return errors.New(strings.Join(missingFields, " and ") + " must be set") 188 | } 189 | 190 | return nil 191 | } 192 | -------------------------------------------------------------------------------- /api/v1/policy_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package v1 17 | 18 | import ( 19 | "encoding/json" 20 | "hash/fnv" 21 | 22 | "github.com/newrelic/newrelic-client-go/pkg/alerts" 23 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | "k8s.io/apimachinery/pkg/types" 25 | ) 26 | 27 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 28 | // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. 29 | 30 | // PolicySpec defines the desired state of Policy 31 | type PolicySpec struct { 32 | IncidentPreference string `json:"incident_preference,omitempty"` 33 | Name string `json:"name"` 34 | APIKey string `json:"api_key,omitempty"` 35 | APIKeySecret NewRelicAPIKeySecret `json:"api_key_secret,omitempty"` 36 | Region string `json:"region"` 37 | Conditions []PolicyCondition `json:"conditions,omitempty"` 38 | } 39 | 40 | //PolicyCondition defined the conditions contained within a a policy 41 | type PolicyCondition struct { 42 | Name string `json:"name"` 43 | Namespace string `json:"namespace"` 44 | Spec ConditionSpec `json:"spec,omitempty"` 45 | } 46 | 47 | //ConditionSpec - Merged superset of Condition types 48 | type ConditionSpec struct { 49 | GenericConditionSpec `json:",inline"` 50 | NrqlSpecificSpec `json:",inline"` 51 | APMSpecificSpec `json:",inline"` 52 | } 53 | 54 | // PolicyStatus defines the observed state of Policy 55 | type PolicyStatus struct { 56 | AppliedSpec *PolicySpec `json:"applied_spec"` 57 | PolicyID int `json:"policy_id"` 58 | } 59 | 60 | // +kubebuilder:object:root=true 61 | 62 | // Policy is the Schema for the policies API 63 | type Policy struct { 64 | metav1.TypeMeta `json:",inline"` 65 | metav1.ObjectMeta `json:"metadata,omitempty"` 66 | 67 | Spec PolicySpec `json:"spec,omitempty"` 68 | Status PolicyStatus `json:"status,omitempty"` 69 | } 70 | 71 | // +kubebuilder:object:root=true 72 | 73 | // PolicyList contains a list of Policy 74 | type PolicyList struct { 75 | metav1.TypeMeta `json:",inline"` 76 | metav1.ListMeta `json:"metadata,omitempty"` 77 | Items []Policy `json:"items"` 78 | } 79 | 80 | func init() { 81 | SchemeBuilder.Register(&Policy{}, &PolicyList{}) 82 | } 83 | 84 | func (in PolicySpec) APIPolicy() alerts.Policy { 85 | jsonString, _ := json.Marshal(in) 86 | var APIPolicy alerts.Policy 87 | json.Unmarshal(jsonString, &APIPolicy) //nolint 88 | 89 | //APICondition.PolicyID = spec.ExistingPolicyId 90 | 91 | return APIPolicy 92 | } 93 | 94 | func (p *PolicyCondition) SpecHash() uint32 { 95 | //remove api keys and condition from object to enable comparison minus inherited fields 96 | strippedPolicy := PolicyCondition{ 97 | Spec: p.Spec, 98 | } 99 | strippedPolicy.Spec.APIKeySecret = NewRelicAPIKeySecret{} 100 | strippedPolicy.Spec.APIKey = "" 101 | strippedPolicy.Spec.Region = "" 102 | strippedPolicy.Spec.ExistingPolicyID = 0 103 | conditionTemplateSpecHasher := fnv.New32a() 104 | DeepHashObject(conditionTemplateSpecHasher, strippedPolicy) 105 | 106 | return conditionTemplateSpecHasher.Sum32() 107 | } 108 | 109 | func (p *PolicyCondition) GetNamespace() types.NamespacedName { 110 | return types.NamespacedName{ 111 | Namespace: p.Namespace, 112 | Name: p.Name, 113 | } 114 | } 115 | 116 | //Equals - comparator function to check for equality 117 | func (in PolicySpec) Equals(policyToCompare PolicySpec) bool { 118 | if in.IncidentPreference != policyToCompare.IncidentPreference { 119 | return false 120 | } 121 | 122 | if in.Name != policyToCompare.Name { 123 | return false 124 | } 125 | 126 | if in.APIKey != policyToCompare.APIKey { 127 | return false 128 | } 129 | 130 | if in.Region != policyToCompare.Region { 131 | return false 132 | } 133 | 134 | if in.APIKeySecret != policyToCompare.APIKeySecret { 135 | return false 136 | } 137 | 138 | if len(in.Conditions) != len(policyToCompare.Conditions) { 139 | return false 140 | } 141 | 142 | checkedHashes := make(map[uint32]bool) 143 | 144 | for _, condition := range in.Conditions { 145 | checkedHashes[condition.SpecHash()] = true 146 | } 147 | 148 | for _, conditionToCompare := range policyToCompare.Conditions { 149 | if _, ok := checkedHashes[conditionToCompare.SpecHash()]; !ok { 150 | return false 151 | } 152 | } 153 | 154 | return true 155 | } 156 | 157 | //GetConditionType - returns the string representative of the Condition type 158 | func GetConditionType(condition PolicyCondition) string { 159 | if condition.Spec.Type == "NRQL" { 160 | return "NrqlAlertCondition" 161 | } 162 | 163 | return "ApmAlertCondition" 164 | } 165 | 166 | func (p *PolicyCondition) GenerateSpecFromNrqlConditionSpec(nrqlConditionSpec NrqlAlertConditionSpec) { 167 | jsonString, _ := json.Marshal(nrqlConditionSpec) 168 | json.Unmarshal(jsonString, &p.Spec) //nolint 169 | } 170 | 171 | func (p *PolicyCondition) GenerateSpecFromApmConditionSpec(apmConditionSpec ApmAlertConditionSpec) { 172 | jsonString, _ := json.Marshal(apmConditionSpec) 173 | json.Unmarshal(jsonString, &p.Spec) //nolint 174 | } 175 | 176 | func (p *PolicyCondition) ReturnNrqlConditionSpec() (nrqlAlertConditionSpec NrqlAlertConditionSpec) { 177 | jsonString, _ := json.Marshal(p.Spec) 178 | json.Unmarshal(jsonString, &nrqlAlertConditionSpec) //nolint 179 | 180 | return 181 | } 182 | 183 | func (p *PolicyCondition) ReturnApmConditionSpec() (apmAlertConditionSpec ApmAlertConditionSpec) { 184 | jsonString, _ := json.Marshal(p.Spec) 185 | json.Unmarshal(jsonString, &apmAlertConditionSpec) //nolint 186 | 187 | return 188 | } 189 | -------------------------------------------------------------------------------- /api/v1/policy_webhook.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package v1 17 | 18 | import ( 19 | "errors" 20 | "strings" 21 | 22 | "k8s.io/apimachinery/pkg/runtime" 23 | ctrl "sigs.k8s.io/controller-runtime" 24 | logf "sigs.k8s.io/controller-runtime/pkg/log" 25 | "sigs.k8s.io/controller-runtime/pkg/webhook" 26 | 27 | customErrors "github.com/newrelic/newrelic-kubernetes-operator/errors" 28 | ) 29 | 30 | // Log is for emitting logs in this package. 31 | var Log = logf.Log.WithName("policy-resource") 32 | var defaultPolicyIncidentPreference = "PER_POLICY" 33 | 34 | func (r *Policy) SetupWebhookWithManager(mgr ctrl.Manager) error { 35 | return ctrl.NewWebhookManagedBy(mgr). 36 | For(r). 37 | Complete() 38 | } 39 | 40 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 41 | 42 | // +kubebuilder:webhook:path=/mutate-nr-k8s-newrelic-com-v1-policy,mutating=true,failurePolicy=fail,groups=nr.k8s.newrelic.com,resources=policies,verbs=create;update,versions=v1,name=mpolicy.kb.io,sideEffects=None 43 | 44 | var _ webhook.Defaulter = &Policy{} 45 | 46 | // Default implements webhook.Defaulter so a webhook will be registered for the type 47 | func (r *Policy) Default() { 48 | Log.Info("default", "name", r.Name) 49 | 50 | if r.Status.AppliedSpec == nil { 51 | log.Info("Setting null Applied Spec to empty interface") 52 | r.Status.AppliedSpec = &PolicySpec{} 53 | } 54 | 55 | r.DefaultIncidentPreference() 56 | } 57 | 58 | // +kubebuilder:webhook:verbs=create;update,path=/validate-nr-k8s-newrelic-com-v1-policy,mutating=false,failurePolicy=fail,groups=nr.k8s.newrelic.com,resources=policies,versions=v1,name=vpolicy.kb.io,sideEffects=None 59 | 60 | var _ webhook.Validator = &Policy{} 61 | 62 | // ValidateCreate implements webhook.Validator so a webhook will be registered for the type 63 | func (r *Policy) ValidateCreate() error { 64 | Log.Info("validate create", "name", r.Name) 65 | 66 | collectedErrors := new(customErrors.ErrorCollector) 67 | 68 | err := r.CheckForAPIKeyOrSecret() 69 | if err != nil { 70 | collectedErrors.Collect(err) 71 | } 72 | 73 | err = r.CheckForDuplicateConditions() 74 | if err != nil { 75 | collectedErrors.Collect(err) 76 | } 77 | 78 | err = r.ValidateIncidentPreference() 79 | if err != nil { 80 | collectedErrors.Collect(err) 81 | } 82 | 83 | if len(*collectedErrors) > 0 { 84 | Log.Info("Errors encountered validating policy", "collectedErrors", collectedErrors) 85 | return collectedErrors 86 | } 87 | 88 | return nil 89 | } 90 | 91 | // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type 92 | func (r *Policy) ValidateUpdate(old runtime.Object) error { 93 | Log.Info("validate update", "name", r.Name) 94 | 95 | collectedErrors := new(customErrors.ErrorCollector) 96 | 97 | err := r.CheckForAPIKeyOrSecret() 98 | if err != nil { 99 | collectedErrors.Collect(err) 100 | } 101 | 102 | err = r.CheckForDuplicateConditions() 103 | if err != nil { 104 | collectedErrors.Collect(err) 105 | } 106 | 107 | err = r.ValidateIncidentPreference() 108 | if err != nil { 109 | collectedErrors.Collect(err) 110 | } 111 | 112 | if len(*collectedErrors) > 0 { 113 | Log.Info("Errors encountered validating policy", "collectedErrors", collectedErrors) 114 | return collectedErrors 115 | } 116 | 117 | return nil 118 | } 119 | 120 | // ValidateDelete implements webhook.Validator so a webhook will be registered for the type 121 | func (r *Policy) ValidateDelete() error { 122 | Log.Info("validate delete", "name", r.Name) 123 | 124 | err := r.CheckForAPIKeyOrSecret() 125 | if err != nil { 126 | return err 127 | } 128 | 129 | return nil 130 | } 131 | 132 | func (r *Policy) DefaultIncidentPreference() { 133 | if r.Spec.IncidentPreference == "" { 134 | r.Spec.IncidentPreference = defaultPolicyIncidentPreference 135 | } 136 | 137 | r.Spec.IncidentPreference = strings.ToUpper(r.Spec.IncidentPreference) 138 | } 139 | 140 | func (r *Policy) CheckForDuplicateConditions() error { 141 | var conditionHashMap = make(map[uint32]bool) 142 | 143 | for _, condition := range r.Spec.Conditions { 144 | conditionHashMap[condition.SpecHash()] = true 145 | } 146 | 147 | if len(conditionHashMap) != len(r.Spec.Conditions) { 148 | log.Info("duplicate conditions detected or hash collision", "conditionHash", conditionHashMap) 149 | 150 | return errors.New("duplicate conditions detected or hash collision") 151 | } 152 | 153 | return nil 154 | } 155 | 156 | func (r *Policy) ValidateIncidentPreference() error { 157 | switch r.Spec.IncidentPreference { 158 | case "PER_POLICY", "PER_CONDITION", "PER_CONDITION_AND_TARGET": 159 | return nil 160 | } 161 | 162 | log.Info("Incident preference must be PER_POLICY, PER_CONDITION, or PER_CONDITION_AND_TARGET", "IncidentPreference value", r.Spec.IncidentPreference) 163 | 164 | return errors.New("incident preference must be PER_POLICY, PER_CONDITION, or PER_CONDITION_AND_TARGET") 165 | } 166 | 167 | func (r *Policy) CheckForAPIKeyOrSecret() error { 168 | if r.Spec.APIKey != "" { 169 | return nil 170 | } 171 | 172 | if r.Spec.APIKeySecret != (NewRelicAPIKeySecret{}) { 173 | if r.Spec.APIKeySecret.Name != "" && r.Spec.APIKeySecret.Namespace != "" && r.Spec.APIKeySecret.KeyName != "" { 174 | return nil 175 | } 176 | } 177 | 178 | return errors.New("either api_key or api_key_secret must be set") 179 | } 180 | -------------------------------------------------------------------------------- /api/v1/suite_test.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "path/filepath" 5 | "testing" 6 | 7 | apierrors "k8s.io/apimachinery/pkg/api/errors" 8 | "k8s.io/client-go/kubernetes/scheme" 9 | "k8s.io/client-go/rest" 10 | "sigs.k8s.io/controller-runtime/pkg/client" 11 | "sigs.k8s.io/controller-runtime/pkg/envtest" 12 | "sigs.k8s.io/controller-runtime/pkg/envtest/printer" 13 | logf "sigs.k8s.io/controller-runtime/pkg/log" 14 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 15 | 16 | . "github.com/onsi/ginkgo" 17 | . "github.com/onsi/gomega" 18 | ) 19 | 20 | var cfg *rest.Config 21 | var testk8sClient client.Client 22 | var testEnv *envtest.Environment 23 | 24 | func TestV1(t *testing.T) { 25 | RegisterFailHandler(Fail) 26 | 27 | RunSpecsWithDefaultAndCustomReporters(t, 28 | "V1 Suite", 29 | []Reporter{printer.NewlineReporter{}}) 30 | } 31 | 32 | var _ = BeforeSuite(func(done Done) { 33 | logf.SetLogger(zap.LoggerTo(GinkgoWriter, true)) 34 | 35 | By("bootstrapping test environment") 36 | testEnv = &envtest.Environment{ 37 | CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, 38 | } 39 | 40 | var err error 41 | cfg, err = testEnv.Start() 42 | Expect(err).ToNot(HaveOccurred()) 43 | Expect(cfg).ToNot(BeNil()) 44 | 45 | err = AddToScheme(scheme.Scheme) 46 | Expect(err).NotTo(HaveOccurred()) 47 | 48 | // +kubebuilder:scaffold:scheme 49 | 50 | testk8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) 51 | Expect(err).ToNot(HaveOccurred()) 52 | Expect(testk8sClient).ToNot(BeNil()) 53 | 54 | close(done) 55 | }, 60) 56 | 57 | var _ = AfterSuite(func() { 58 | By("tearing down the test environment") 59 | err := testEnv.Stop() 60 | Expect(err).ToNot(HaveOccurred()) 61 | }) 62 | 63 | // this is used, but only with -tags=integration 64 | // nolint 65 | func ignoreAlreadyExists(err error) error { 66 | if apierrors.IsAlreadyExists(err) { 67 | return nil 68 | } 69 | 70 | return err 71 | } 72 | -------------------------------------------------------------------------------- /build/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newrelic/newrelic-kubernetes-operator/887746d3ea61631e038b510d6bb3eb711a9cb418/build/.keep -------------------------------------------------------------------------------- /build/compile.mk: -------------------------------------------------------------------------------- 1 | # 2 | # Makefile Fragment for Compiling 3 | # 4 | 5 | GO ?= go 6 | BUILD_DIR ?= ./bin 7 | PROJECT_MODULE ?= $(shell $(GO) list -m) 8 | # $b replaced by the binary name in the compile loop, -s/w remove debug symbols 9 | # TODO: Remove the compile time 'main.NewRelicAPIKey' 10 | LDFLAGS ?= "-s -w -X $(PROJECT_NAME)/internal/info.Name=$(PROJECT_NAME) -X $(PROJECT_MODULE)/internal/info.Version=$(PROJECT_VER)" 11 | SRCDIR ?= . 12 | COMPILE_OS ?= darwin linux windows 13 | BINARYNAME ?= manager 14 | 15 | # Determine commands by looking into cmd/* 16 | COMMANDS ?= $(wildcard ${SRCDIR}/cmd/*) 17 | 18 | compile-clean: 19 | @echo "=== $(PROJECT_NAME) === [ compile-clean ]: removing binaries..." 20 | @rm -rfv $(BUILD_DIR)/* 21 | 22 | compile: compile-only 23 | 24 | compile-all: 25 | @echo "=== $(PROJECT_NAME) === [ compile ]: building commands:" 26 | @mkdir -p $(BUILD_DIR)/$(GOOS) 27 | @for os in $(COMPILE_OS); do \ 28 | echo "=== $(PROJECT_NAME) === [ compile ]: $(BUILD_DIR)/$$os/$(BINARYNAME)"; \ 29 | CGO_ENABLED=0 GOOS=$$os $(GO) build -ldflags=$(LDFLAGS) -o $(BUILD_DIR)/$$os/$(BINARYNAME) . ; \ 30 | done 31 | 32 | compile-only: 33 | @echo "=== $(PROJECT_NAME) === [ compile ]: building commands: $(BINARYNAME)" 34 | @mkdir -p $(BUILD_DIR)/$(GOOS) 35 | @echo "=== $(PROJECT_NAME) === [ compile ]: $(BUILD_DIR)/$(GOOS)/$$BINARYNAME"; 36 | @CGO_ENABLED=0 GOOS=$(GOOS) $(GO) build -ldflags=$(LDFLAGS) -o $(BUILD_DIR)/$(GOOS)/$(BINARYNAME) . ; \ 37 | 38 | # Override GOOS for these specific targets 39 | compile-darwin: GOOS=darwin 40 | compile-darwin: compile-only 41 | 42 | compile-linux: GOOS=linux 43 | compile-linux: compile-only 44 | 45 | compile-windows: GOOS=windows 46 | compile-windows: compile-only 47 | 48 | 49 | .PHONY: clean-compile compile compile-darwin compile-linux compile-only compile-windows 50 | -------------------------------------------------------------------------------- /build/docker.mk: -------------------------------------------------------------------------------- 1 | # 2 | # Makefile fragment for Docker actions 3 | # 4 | DOCKER ?= docker 5 | DOCKER_FILE ?= build/package/Dockerfile 6 | DOCKER_IMAGE ?= newrelic/kubernetes-operator 7 | DOCKER_IMAGE_TAG ?= snapshot 8 | 9 | # Build the docker image 10 | docker-build: compile-linux 11 | @echo "=== $(PROJECT_NAME) === [ docker-build ]: Creating docker image: $(DOCKER_IMAGE):$(DOCKER_IMAGE_TAG) ..." 12 | docker build -f $(DOCKER_FILE) -t $(DOCKER_IMAGE):$(DOCKER_IMAGE_TAG) $(BUILD_DIR)/linux/ 13 | 14 | 15 | docker-login: 16 | @echo "=== $(PROJECT_NAME) === [ docker-login ]: logging into docker hub" 17 | @if [ -z "${DOCKER_USERNAME}" ]; then \ 18 | echo "Failure: DOCKER_USERNAME not set" ; \ 19 | exit 1 ; \ 20 | fi 21 | @if [ -z "${DOCKER_PASSWORD}" ]; then \ 22 | echo "Failure: DOCKER_PASSWORD not set" ; \ 23 | exit 1 ; \ 24 | fi 25 | @echo "=== $(PROJECT_NAME) === [ docker-login ]: username: '$$DOCKER_USERNAME'" 26 | @echo ${DOCKER_PASSWORD} | $(DOCKER) login -u ${DOCKER_USERNAME} --password-stdin 27 | 28 | 29 | # Push the docker image 30 | docker-push: docker-login docker-build 31 | @echo "=== $(PROJECT_NAME) === [ docker-push ]: Pushing docker image: $(DOCKER_IMAGE):$(DOCKER_IMAGE_TAG) ..." 32 | $(DOCKER) push $(DOCKER_IMAGE):$(DOCKER_IMAGE_TAG) 33 | 34 | .PHONY: docker-build docker-login docker-push 35 | -------------------------------------------------------------------------------- /build/document.mk: -------------------------------------------------------------------------------- 1 | # 2 | # Makefile fragment for displaying auto-generated documentation 3 | # 4 | 5 | GOTOOLS += golang.org/x/tools/cmd/godoc \ 6 | github.com/git-chglog/git-chglog/cmd/git-chglog 7 | GODOC ?= godoc 8 | GODOC_HTTP ?= "localhost:6060" 9 | 10 | CHANGELOG_CMD ?= git-chglog 11 | CHANGELOG_FILE ?= CHANGELOG.md 12 | RELEASE_NOTES_FILE ?= relnotes.md 13 | 14 | docs: tools 15 | @echo "=== $(PROJECT_NAME) === [ docs ]: Starting godoc server..." 16 | @echo "=== $(PROJECT_NAME) === [ docs ]:" 17 | @echo "=== $(PROJECT_NAME) === [ docs ]: NOTE: This only works if this codebase is in your GOPATH!" 18 | @echo "=== $(PROJECT_NAME) === [ docs ]: godoc issue: https://github.com/golang/go/issues/26827" 19 | @echo "=== $(PROJECT_NAME) === [ docs ]:" 20 | @echo "=== $(PROJECT_NAME) === [ docs ]: Module Docs: http://$(GODOC_HTTP)/pkg/$(PROJECT_MODULE)" 21 | @$(GODOC) -http=$(GODOC_HTTP) 22 | 23 | changelog: tools 24 | @echo "=== $(PROJECT_NAME) === [ changelog ]: Generating changelog..." 25 | @$(CHANGELOG_CMD) --silent -o $(CHANGELOG_FILE) 26 | 27 | release-notes: tools 28 | @echo "=== $(PROJECT_NAME) === [ release-notes ]: Generating release notes..." 29 | @mkdir -p $(SRCDIR)/tmp 30 | @$(CHANGELOG_CMD) --silent -o $(SRCDIR)/tmp/$(RELEASE_NOTES_FILE) v$(PROJECT_VER_TAGGED) 31 | 32 | .PHONY: docs changelog release-notes 33 | -------------------------------------------------------------------------------- /build/generate.mk: -------------------------------------------------------------------------------- 1 | # 2 | # Makefile Fragment for generating code 3 | # 4 | 5 | CONTROLLER_GEN ?= controller-gen 6 | HEADER_FILE ?= $(SRCDIR)/build/generate/boilerplate.go.txt 7 | 8 | GOTOOLS += sigs.k8s.io/controller-tools/cmd/controller-gen 9 | 10 | 11 | # Generate code 12 | generate: tools manifests 13 | @echo "=== $(PROJECT_NAME) === [ generate ]: Running $(CONTROLLER_GEN)..." 14 | @$(CONTROLLER_GEN) object:headerFile=$(HEADER_FILE) paths="./..." 15 | @cd interfaces/ && $(GO) generate 16 | 17 | -------------------------------------------------------------------------------- /build/generate/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ -------------------------------------------------------------------------------- /build/kube.mk: -------------------------------------------------------------------------------- 1 | # 2 | # Makefile snippet for install/uninstall of the operator 3 | # 4 | 5 | # Image URL to use all building/pushing image targets 6 | DOCKER_IMAGE ?= newrelic/kubernetes-operator:snapshot 7 | # Produce CRDs that work back to Kubernetes 1.11 (no version conversion) 8 | CRD_OPTIONS ?= "crd:trivialVersions=true" 9 | CONFIG_ROOT ?= $(SRCDIR)/config 10 | RBAC_ROLE_NAME ?= manager-role 11 | 12 | # Install CRDs into a cluster 13 | install: manifests 14 | @echo "=== $(PROJECT_NAME) === [ install ]: Applying operator..." 15 | @kustomize build $(CONFIG_ROOT)/crd | kubectl apply -f - 16 | 17 | # Uninstall CRDs from a cluster 18 | uninstall: manifests 19 | @echo "=== $(PROJECT_NAME) === [ uninstall ]: Deleting operator..." 20 | @kustomize build $(CONFIG_ROOT)/crd | kubectl delete -f - 21 | 22 | # Deploy controller in the configured Kubernetes cluster in ~/.kube/config 23 | deploy: manifests docker-build 24 | @echo "=== $(PROJECT_NAME) === [ deploy ]: Deploying operator as docker image ${DOCKER_IMAGE}..." 25 | @cd $(CONFIG_ROOT)/manager && kustomize edit set image controller=${DOCKER_IMAGE} 26 | @kustomize build $(CONFIG_ROOT)/default | kubectl apply -f - 27 | 28 | # Generate manifests e.g. CRD, RBAC etc. 29 | manifests: tools 30 | @echo "=== $(PROJECT_NAME) === [ manifests ]: Generating manifests..." 31 | @$(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=$(RBAC_ROLE_NAME) webhook paths="./..." output:crd:artifacts:config=$(CONFIG_ROOT)/crd/bases 32 | # @$(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=$(RBAC_ROLE_NAME) webhook paths="./..." output:crd:artifacts:config=$(CONFIG_ROOT)/crd/bases output:rbac:artifacts:config=$(CONFIG_ROOT)/rbac output:webhook:artifacts:config=$(CONFIG_ROOT)/webhook 33 | -------------------------------------------------------------------------------- /build/lint.mk: -------------------------------------------------------------------------------- 1 | # 2 | # Makefile fragment for Linting 3 | # 4 | 5 | GO ?= go 6 | MISSPELL ?= misspell 7 | GOFMT ?= gofmt 8 | GOIMPORTS ?= goimports 9 | 10 | COMMIT_LINT_CMD ?= go-gitlint 11 | COMMIT_LINT_REGEX ?= "(bug|chore|docs|feat|fix|refactor|tests?)(\([^\)]+\))?: .*" 12 | COMMIT_LINT_START ?= "2020-06-05" 13 | 14 | GOLINTER = golangci-lint 15 | 16 | EXCLUDEDIR ?= .git 17 | SRCDIR ?= . 18 | GO_PKGS ?= $(shell ${GO} list ./... | grep -v -e "/vendor/" -e "/example") 19 | FILES ?= $(shell find ${SRCDIR} -type f | grep -v -e '.git/' -e '/vendor/') 20 | GO_FILES ?= $(shell find $(SRCDIR) -type f -name "*.go" | grep -v -e ".git/" -e '/vendor/' -e '/example/') 21 | PROJECT_MODULE ?= $(shell $(GO) list -m) 22 | 23 | GO_MOD_OUTDATED ?= go-mod-outdated 24 | 25 | GOTOOLS += github.com/client9/misspell/cmd/misspell \ 26 | github.com/llorllale/go-gitlint/cmd/go-gitlint \ 27 | github.com/psampaz/go-mod-outdated \ 28 | github.com/golangci/golangci-lint/cmd/golangci-lint \ 29 | golang.org/x/tools/cmd/goimports 30 | 31 | 32 | lint: outdated spell-check gofmt golangci lint-commit goimports 33 | lint-fix: spell-check-fix gofmt-fix goimports 34 | 35 | # 36 | # Check spelling on all the files, not just source code 37 | # 38 | spell-check: tools 39 | @echo "=== $(PROJECT_NAME) === [ spell-check ]: Checking for spelling mistakes with $(MISSPELL)..." 40 | @$(MISSPELL) -source text $(FILES) 41 | 42 | spell-check-fix: tools 43 | @echo "=== $(PROJECT_NAME) === [ spell-check-fix ]: Fixing spelling mistakes with $(MISSPELL)..." 44 | @$(MISSPELL) -source text -w $(FILES) 45 | 46 | gofmt: tools 47 | @echo "=== $(PROJECT_NAME) === [ gofmt ]: Checking file format with $(GOFMT)..." 48 | @find . -path "$(EXCLUDEDIR)" -prune -print0 | xargs -0 $(GOFMT) -e -l -s -d ${SRCDIR} 49 | 50 | gofmt-fix: tools 51 | @echo "=== $(PROJECT_NAME) === [ gofmt-fix ]: Fixing file format with $(GOFMT)..." 52 | @find . -path "$(EXCLUDEDIR)" -prune -print0 | xargs -0 $(GOFMT) -e -l -s -w ${SRCDIR} 53 | 54 | goimports: tools 55 | @echo "=== $(PROJECT_NAME) === [ goimports ]: Checking imports with $(GOIMPORTS)..." 56 | @$(GOIMPORTS) -w -local $(PROJECT_MODULE) $(GO_FILES) 57 | 58 | lint-commit: tools 59 | @echo "=== $(PROJECT_NAME) === [ lint-commit ]: Checking that commit messages are properly formatted ($(COMMIT_LINT_CMD))..." 60 | @$(COMMIT_LINT_CMD) --since=$(COMMIT_LINT_START) --subject-minlen=10 --subject-maxlen=120 --subject-regex=$(COMMIT_LINT_REGEX) 61 | 62 | golangci: tools 63 | @echo "=== $(PROJECT_NAME) === [ golangci-lint ]: Linting using $(GOLINTER) ($(COMMIT_LINT_CMD))..." 64 | @$(GOLINTER) run 65 | 66 | outdated: tools 67 | @echo "=== $(PROJECT_NAME) === [ outdated ]: Finding outdated deps with $(GO_MOD_OUTDATED)..." 68 | @$(GO) list -u -m -json all | $(GO_MOD_OUTDATED) -direct -update 69 | 70 | .PHONY: lint spell-check spell-check-fix gofmt gofmt-fix lint-fix lint-commit outdated goimports 71 | -------------------------------------------------------------------------------- /build/package/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use distroless as minimal base image to package the manager binary 2 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 3 | FROM gcr.io/distroless/static:nonroot 4 | WORKDIR / 5 | USER nonroot:nonroot 6 | 7 | # Add the binary 8 | COPY manager /manager 9 | 10 | ENTRYPOINT ["/manager"] 11 | -------------------------------------------------------------------------------- /build/release.mk: -------------------------------------------------------------------------------- 1 | RELEASE_SCRIPT ?= ./scripts/release.sh 2 | 3 | GOTOOLS += github.com/goreleaser/goreleaser 4 | 5 | REL_CMD ?= goreleaser 6 | DIST_DIR ?= ./dist 7 | 8 | # Example usage: make release version=0.11.0 9 | release: build 10 | @echo "=== $(PROJECT_NAME) === [ release ]: Generating release." 11 | $(RELEASE_SCRIPT) $(version) 12 | 13 | release-clean: 14 | @echo "=== $(PROJECT_NAME) === [ release-clean ]: distribution files..." 15 | @rm -rfv $(DIST_DIR) $(SRCDIR)/tmp 16 | 17 | release-publish: clean tools docker-login release-notes 18 | @echo "=== $(PROJECT_NAME) === [ release-publish ]: Publishing release via $(REL_CMD)" 19 | $(REL_CMD) --release-notes=$(SRCDIR)/tmp/$(RELEASE_NOTES_FILE) 20 | 21 | # Local Snapshot 22 | snapshot: release-clean 23 | @echo "=== $(PROJECT_NAME) === [ snapshot ]: Creating release via $(REL_CMD)" 24 | @echo "=== $(PROJECT_NAME) === [ snapshot ]: THIS WILL NOT BE PUBLISHED!" 25 | $(REL_CMD) --skip-publish --snapshot 26 | 27 | 28 | .PHONY: release release-clean release-homebrew release-publish snapshot 29 | -------------------------------------------------------------------------------- /build/test.mk: -------------------------------------------------------------------------------- 1 | # 2 | # Makefile fragment for Testing 3 | # 4 | 5 | GO ?= go 6 | GOLINTER ?= golangci-lint 7 | MISSPELL ?= misspell 8 | GOFMT ?= gofmt 9 | 10 | COVERAGE_DIR ?= ./coverage/ 11 | COVERMODE ?= atomic 12 | SRCDIR ?= . 13 | GO_PKGS ?= $(shell $(GO) list ./... | grep -v -e "/vendor/" -e "/example") 14 | FILES ?= $(shell find $(SRCDIR) -type f | grep -v -e '.git/' -e '/vendor/') 15 | 16 | PROJECT_MODULE ?= $(shell $(GO) list -m) 17 | 18 | LDFLAGS_UNIT ?= '-X $(PROJECT_MODULE)/internal/version.GitTag=$(PROJECT_VER_TAGGED)' 19 | 20 | GOTOOLS += github.com/stretchr/testify/assert 21 | 22 | test: test-only 23 | test-only: test-unit test-integration 24 | 25 | test-unit: 26 | @echo "=== $(PROJECT_NAME) === [ test-unit ]: running unit tests..." 27 | @mkdir -p $(COVERAGE_DIR) 28 | @$(GO) test -v -ldflags=$(LDFLAGS_UNIT) -parallel 4 -tags unit -covermode=$(COVERMODE) -coverprofile $(COVERAGE_DIR)/unit.tmp $(GO_PKGS) 29 | 30 | test-integration: 31 | @echo "=== $(PROJECT_NAME) === [ test-integration ]: running integration tests..." 32 | @mkdir -p $(COVERAGE_DIR) 33 | @$(GO) test -v -parallel 4 -timeout 30m -tags integration -covermode=$(COVERMODE) -coverprofile $(COVERAGE_DIR)/integration.tmp $(GO_PKGS) 34 | 35 | 36 | # 37 | # Coverage 38 | # 39 | cover-clean: 40 | @echo "=== $(PROJECT_NAME) === [ cover-clean ]: removing coverage files..." 41 | @rm -rfv $(COVERAGE_DIR)/* 42 | 43 | cover-report: 44 | @echo "=== $(PROJECT_NAME) === [ cover-report ]: generating coverage results..." 45 | @mkdir -p $(COVERAGE_DIR) 46 | @echo 'mode: $(COVERMODE)' > $(COVERAGE_DIR)/coverage.out 47 | @cat $(COVERAGE_DIR)/*.tmp | grep -v 'mode: $(COVERMODE)' >> $(COVERAGE_DIR)/coverage.out || true 48 | @$(GO) tool cover -html=$(COVERAGE_DIR)/coverage.out -o $(COVERAGE_DIR)/coverage.html 49 | @echo "=== $(PROJECT_NAME) === [ cover-report ]: $(COVERAGE_DIR)coverage.html" 50 | 51 | cover-view: cover-report 52 | @$(GO) tool cover -html=$(COVERAGE_DIR)/coverage.out 53 | 54 | .PHONY: test test-only test-unit test-integration cover-report cover-view 55 | -------------------------------------------------------------------------------- /build/tools.mk: -------------------------------------------------------------------------------- 1 | # 2 | # Makefile fragment for installing deps 3 | # 4 | 5 | GO ?= go 6 | 7 | # These should be mirrored in /tools.go to keep versions consistent 8 | GOTOOLS += github.com/client9/misspell/cmd/misspell 9 | 10 | tools: check-version 11 | @echo "=== $(PROJECT_NAME) === [ tools ]: Installing tools required by the project..." 12 | @$(GO) install $(GOTOOLS) 13 | 14 | tools-update: check-version 15 | @echo "=== $(PROJECT_NAME) === [ tools-update ]: Updating tools required by the project..." 16 | @$(GO) get -u $(GOTOOLS) 17 | 18 | 19 | .PHONY: tools tools-update 20 | -------------------------------------------------------------------------------- /build/util.mk: -------------------------------------------------------------------------------- 1 | # 2 | # Makefile fragment for utility items 3 | # 4 | 5 | NATIVEOS ?= $(shell go version | awk -F '[ /]' '{print $$4}') 6 | NATIVEARCH ?= $(shell go version | awk -F '[ /]' '{print $$5}') 7 | 8 | GIT_HOOKS_PATH ?= .githooks 9 | 10 | git-hooks: 11 | @echo "=== $(PROJECT_NAME) === [ git-hooks ]: Configuring git hooks..." 12 | @git config core.hooksPath $(GIT_HOOKS_PATH) 13 | 14 | check-version: 15 | ifdef GOOS 16 | ifneq "$(GOOS)" "$(NATIVEOS)" 17 | $(error GOOS is not $(NATIVEOS). Cross-compiling is only allowed for 'clean', 'deps-only' and 'compile-only' targets) 18 | endif 19 | else 20 | GOOS = ${NATIVEOS} 21 | endif 22 | ifdef GOARCH 23 | ifneq "$(GOARCH)" "$(NATIVEARCH)" 24 | $(error GOARCH variable is not $(NATIVEARCH). Cross-compiling is only allowed for 'clean', 'deps-only' and 'compile-only' targets) 25 | endif 26 | else 27 | GOARCH = ${NATIVEARCH} 28 | endif 29 | 30 | .PHONY: check-version 31 | -------------------------------------------------------------------------------- /cla.md: -------------------------------------------------------------------------------- 1 | # NEW RELIC, INC. 2 | ## INDIVIDUAL CONTRIBUTOR LICENSE AGREEMENT 3 | Thank you for your interest in contributing to the open source projects of New Relic, Inc. (“New Relic”). In order to clarify the intellectual property license granted with Contributions from any person or entity, New Relic must have a Contributor License Agreement ("Agreement") on file that has been signed by each Contributor, indicating agreement to the license terms below. This Agreement is for your protection as a Contributor as well as the protection of New Relic; it does not change your rights to use your own Contributions for any other purpose. 4 | 5 | You accept and agree to the following terms and conditions for Your present and future Contributions submitted to New Relic. Except for the licenses granted herein to New Relic and recipients of software distributed by New Relic, You reserve all right, title, and interest in and to Your Contributions. 6 | 7 | ## Definitions. 8 | 1. "You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is entering into this Agreement with New Relic. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 9 | 2. "Contribution" shall mean any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to New Relic for inclusion in, or documentation of, any of the products managed or maintained by New Relic (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to New Relic or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, New Relic for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." 10 | 3. Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to New Relic and to recipients of software distributed by New Relic a perpetual, worldwide, non-exclusive, no-charge, royalty-free, transferable, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works. 11 | 4. Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to New Relic and to recipients of software distributed by New Relic a perpetual, worldwide, non-exclusive, no-charge, royalty-free, transferable, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contributions alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that Your Contribution, or the Work to which You have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed. 12 | 5. You represent that You are legally entitled to grant the above licenses. If Your employer(s) has rights to intellectual property that You create that includes Your Contributions, You represent that You have received permission to make Contributions on behalf of that employer, that Your employer has waived such rights for Your Contributions to New Relic, or that Your employer has executed a separate Agreement with New Relic. 13 | 6. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others). You represent that Your Contribution submissions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which You are personally aware and which are associated with any part of Your Contributions. 14 | 7. You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON- INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. 15 | 8. Should You wish to submit work that is not Your original creation, You may submit it to New Relic separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which You are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here]". 16 | 9. You agree to notify New Relic of any facts or circumstances of which You become aware that would make these representations inaccurate in any respect. -------------------------------------------------------------------------------- /config/certmanager/certificate.yaml: -------------------------------------------------------------------------------- 1 | # The following manifests contain a self-signed issuer CR and a certificate CR. 2 | # More document can be found at https://docs.cert-manager.io 3 | # WARNING: Targets CertManager 0.11 check https://docs.cert-manager.io/en/latest/tasks/upgrading/index.html for breaking changes 4 | apiVersion: cert-manager.io/v1alpha2 5 | kind: Issuer 6 | metadata: 7 | name: selfsigned-issuer 8 | namespace: system 9 | spec: 10 | selfSigned: {} 11 | --- 12 | apiVersion: cert-manager.io/v1alpha2 13 | kind: Certificate 14 | metadata: 15 | name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml 16 | namespace: system 17 | spec: 18 | # $(SERVICE_NAME) and $(SERVICE_NAMESPACE) will be substituted by kustomize 19 | dnsNames: 20 | - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc 21 | - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc.cluster.local 22 | issuerRef: 23 | kind: Issuer 24 | name: selfsigned-issuer 25 | secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize 26 | -------------------------------------------------------------------------------- /config/certmanager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - certificate.yaml 3 | 4 | configurations: 5 | - kustomizeconfig.yaml 6 | -------------------------------------------------------------------------------- /config/certmanager/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This configuration is for teaching kustomize how to update name ref and var substitution 2 | nameReference: 3 | - kind: Issuer 4 | group: cert-manager.io 5 | fieldSpecs: 6 | - kind: Certificate 7 | group: cert-manager.io 8 | path: spec/issuerRef/name 9 | 10 | varReference: 11 | - kind: Certificate 12 | group: cert-manager.io 13 | path: spec/commonName 14 | - kind: Certificate 15 | group: cert-manager.io 16 | path: spec/dnsNames 17 | -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # This kustomization.yaml is not intended to be run by itself, 2 | # since it depends on service name and namespace that are out of this kustomize package. 3 | # It should be run by config/default 4 | resources: 5 | - bases/nr.k8s.newrelic.com_nrqlalertconditions.yaml 6 | - bases/nr.k8s.newrelic.com_policies.yaml 7 | - bases/nr.k8s.newrelic.com_apmalertconditions.yaml 8 | - bases/nr.k8s.newrelic.com_alertschannels.yaml 9 | - bases/nr.k8s.newrelic.com_alertsnrqlconditions.yaml 10 | - bases/nr.k8s.newrelic.com_alertspolicies.yaml 11 | - bases/nr.k8s.newrelic.com_alertsapmconditions.yaml 12 | # +kubebuilder:scaffold:crdkustomizeresource 13 | 14 | patchesStrategicMerge: 15 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 16 | # patches here are for enabling the conversion webhook for each CRD 17 | #- patches/webhook_in_nrqlalertconditions.yaml 18 | #- patches/webhook_in_policies.yaml 19 | # +kubebuilder:scaffold:crdkustomizewebhookpatch 20 | 21 | # [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix. 22 | # patches here are for enabling the CA injection for each CRD 23 | #- patches/cainjection_in_nrqlalertconditions.yaml 24 | #- patches/cainjection_in_policies.yaml 25 | # +kubebuilder:scaffold:crdkustomizecainjectionpatch 26 | 27 | # the following config is for teaching kustomize how to do kustomization for CRDs. 28 | configurations: 29 | - kustomizeconfig.yaml 30 | -------------------------------------------------------------------------------- /config/crd/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This file is for teaching kustomize how to substitute name and namespace reference in CRD 2 | nameReference: 3 | - kind: Service 4 | version: v1 5 | fieldSpecs: 6 | - kind: CustomResourceDefinition 7 | group: apiextensions.k8s.io 8 | path: spec/conversion/webhookClientConfig/service/name 9 | 10 | namespace: 11 | - kind: CustomResourceDefinition 12 | group: apiextensions.k8s.io 13 | path: spec/conversion/webhookClientConfig/service/namespace 14 | create: false 15 | 16 | varReference: 17 | - path: metadata/annotations 18 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_alertsapmconditions.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 8 | name: alertsapmconditions.nr.k8s.newrelic.com 9 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_alertschannels.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 8 | name: alertschannels.nr.k8s.newrelic.com 9 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_alertsnrqlconditions.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 8 | name: alertsnrqlconditions.nr.k8s.newrelic.com 9 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_alertspolicies.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 8 | name: alertspolicies.nr.k8s.newrelic.com 9 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_apmalertconditions.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 8 | name: apmalertconditions.nr.k8s.newrelic.com 9 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_newrelicpolicies.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 8 | name: policies.nr.k8s.newrelic.com 9 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_nrqlalertconditions.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 8 | name: nrqlalertconditions.nr.k8s.newrelic.com 9 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_alertsapmconditions.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables conversion webhook for CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | name: alertsapmconditions.nr.k8s.newrelic.com 7 | spec: 8 | conversion: 9 | strategy: Webhook 10 | webhookClientConfig: 11 | # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, 12 | # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) 13 | caBundle: Cg== 14 | service: 15 | namespace: system 16 | name: webhook-service 17 | path: /convert 18 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_alertschannels.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables conversion webhook for CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | name: alertschannels.nr.k8s.newrelic.com 7 | spec: 8 | conversion: 9 | strategy: Webhook 10 | webhookClientConfig: 11 | # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, 12 | # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) 13 | caBundle: Cg== 14 | service: 15 | namespace: system 16 | name: webhook-service 17 | path: /convert 18 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_alertsnrqlconditions.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables conversion webhook for CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | name: alertsnrqlconditions.nr.k8s.newrelic.com 7 | spec: 8 | conversion: 9 | strategy: Webhook 10 | webhookClientConfig: 11 | # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, 12 | # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) 13 | caBundle: Cg== 14 | service: 15 | namespace: system 16 | name: webhook-service 17 | path: /convert 18 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_alertspolicies.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables conversion webhook for CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | name: alertspolicies.nr.k8s.newrelic.com 7 | spec: 8 | conversion: 9 | strategy: Webhook 10 | webhookClientConfig: 11 | # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, 12 | # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) 13 | caBundle: Cg== 14 | service: 15 | namespace: system 16 | name: webhook-service 17 | path: /convert 18 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_apmalertconditions.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables conversion webhook for CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | name: apmalertconditions.nr.k8s.newrelic.com 7 | spec: 8 | conversion: 9 | strategy: Webhook 10 | webhookClientConfig: 11 | # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, 12 | # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) 13 | caBundle: Cg== 14 | service: 15 | namespace: system 16 | name: webhook-service 17 | path: /convert 18 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_newrelicpolicies.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables conversion webhook for CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | name: policies.nr.k8s.newrelic.com 7 | spec: 8 | conversion: 9 | strategy: Webhook 10 | webhookClientConfig: 11 | # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, 12 | # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) 13 | caBundle: Cg== 14 | service: 15 | namespace: system 16 | name: webhook-service 17 | path: /convert 18 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_nrqlalertconditions.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables conversion webhook for CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | name: nrqlalertconditions.nr.k8s.newrelic.com 7 | spec: 8 | conversion: 9 | strategy: Webhook 10 | webhookClientConfig: 11 | # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, 12 | # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) 13 | caBundle: Cg== 14 | service: 15 | namespace: system 16 | name: webhook-service 17 | path: /convert 18 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Adds namespace to all resources. 2 | namespace: newrelic-kubernetes-operator-system 3 | 4 | # Value of this field is prepended to the 5 | # names of all resources, e.g. a deployment named 6 | # "wordpress" becomes "alices-wordpress". 7 | # Note that it should also match with the prefix (text before '-') of the namespace 8 | # field above. 9 | namePrefix: newrelic-kubernetes-operator- 10 | 11 | # Labels to add to all resources and selectors. 12 | #commonLabels: 13 | # someName: someValue 14 | 15 | bases: 16 | - ../crd 17 | - ../rbac 18 | - ../manager 19 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in crd/kustomization.yaml 20 | - ../webhook 21 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. 22 | - ../certmanager 23 | # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. 24 | #- ../prometheus 25 | 26 | patchesStrategicMerge: 27 | # Protect the /metrics endpoint by putting it behind auth. 28 | # Only one of manager_auth_proxy_patch.yaml and 29 | # manager_prometheus_metrics_patch.yaml should be enabled. 30 | - manager_auth_proxy_patch.yaml 31 | # If you want your controller-manager to expose the /metrics 32 | # endpoint w/o any authn/z, uncomment the following line and 33 | # comment manager_auth_proxy_patch.yaml. 34 | # Only one of manager_auth_proxy_patch.yaml and 35 | # manager_prometheus_metrics_patch.yaml should be enabled. 36 | #- manager_prometheus_metrics_patch.yaml 37 | 38 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in crd/kustomization.yaml 39 | - manager_webhook_patch.yaml 40 | 41 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 42 | # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. 43 | # 'CERTMANAGER' needs to be enabled to use ca injection 44 | - webhookcainjection_patch.yaml 45 | 46 | # the following config is for teaching kustomize how to do var substitution 47 | vars: 48 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. 49 | - name: CERTIFICATE_NAMESPACE # namespace of the certificate CR 50 | objref: 51 | kind: Certificate 52 | group: cert-manager.io 53 | version: v1alpha2 54 | name: serving-cert # this name should match the one in certificate.yaml 55 | fieldref: 56 | fieldpath: metadata.namespace 57 | - name: CERTIFICATE_NAME 58 | objref: 59 | kind: Certificate 60 | group: cert-manager.io 61 | version: v1alpha2 62 | name: serving-cert # this name should match the one in certificate.yaml 63 | - name: SERVICE_NAMESPACE # namespace of the service 64 | objref: 65 | kind: Service 66 | version: v1 67 | name: webhook-service 68 | fieldref: 69 | fieldpath: metadata.namespace 70 | - name: SERVICE_NAME 71 | objref: 72 | kind: Service 73 | version: v1 74 | name: webhook-service 75 | -------------------------------------------------------------------------------- /config/default/manager_auth_proxy_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch inject a sidecar container which is a HTTP proxy for the controller manager, 2 | # it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: controller-manager 7 | namespace: system 8 | spec: 9 | template: 10 | spec: 11 | containers: 12 | - name: kube-rbac-proxy 13 | image: gcr.io/kubebuilder/kube-rbac-proxy:v0.4.1 14 | args: 15 | - "--secure-listen-address=0.0.0.0:8443" 16 | - "--upstream=http://127.0.0.1:8080/" 17 | - "--logtostderr=true" 18 | - "--v=10" 19 | ports: 20 | - containerPort: 8443 21 | name: https 22 | - name: manager 23 | args: 24 | - "--metrics-addr=127.0.0.1:8080" 25 | - "--enable-leader-election" 26 | -------------------------------------------------------------------------------- /config/default/manager_webhook_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: manager 11 | ports: 12 | - containerPort: 9443 13 | name: webhook-server 14 | protocol: TCP 15 | volumeMounts: 16 | - mountPath: /tmp/k8s-webhook-server/serving-certs 17 | name: cert 18 | readOnly: true 19 | volumes: 20 | - name: cert 21 | secret: 22 | defaultMode: 420 23 | secretName: webhook-server-cert 24 | -------------------------------------------------------------------------------- /config/default/webhookcainjection_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch add annotation to admission webhook config and 2 | # the variables $(CERTIFICATE_NAMESPACE) and $(CERTIFICATE_NAME) will be substituted by kustomize. 3 | apiVersion: admissionregistration.k8s.io/v1beta1 4 | kind: MutatingWebhookConfiguration 5 | metadata: 6 | name: mutating-webhook-configuration 7 | annotations: 8 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 9 | --- 10 | apiVersion: admissionregistration.k8s.io/v1beta1 11 | kind: ValidatingWebhookConfiguration 12 | metadata: 13 | name: validating-webhook-configuration 14 | annotations: 15 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 16 | -------------------------------------------------------------------------------- /config/development/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - ../default 5 | images: 6 | - name: newrelic/kubernetes-operator 7 | newTag: dev 8 | 9 | -------------------------------------------------------------------------------- /config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manager.yaml 3 | apiVersion: kustomize.config.k8s.io/v1beta1 4 | kind: Kustomization 5 | images: 6 | - name: controller 7 | newName: newrelic/kubernetes-operator 8 | newTag: v0.0.8 9 | 10 | # This enables the New Relic agent for the New Relic Operator 11 | patchesStrategicMerge: 12 | - new_relic_agent_patch.yaml 13 | -------------------------------------------------------------------------------- /config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | name: system 7 | --- 8 | apiVersion: apps/v1 9 | kind: Deployment 10 | metadata: 11 | name: controller-manager 12 | namespace: system 13 | labels: 14 | control-plane: controller-manager 15 | spec: 16 | selector: 17 | matchLabels: 18 | control-plane: controller-manager 19 | replicas: 1 20 | template: 21 | metadata: 22 | labels: 23 | control-plane: controller-manager 24 | spec: 25 | containers: 26 | - command: 27 | - /manager 28 | args: 29 | - --enable-leader-election 30 | image: controller:latest 31 | name: manager 32 | resources: 33 | limits: 34 | cpu: 100m 35 | memory: 100Mi 36 | requests: 37 | cpu: 100m 38 | memory: 50Mi 39 | terminationGracePeriodSeconds: 10 40 | -------------------------------------------------------------------------------- /config/manager/new_relic_agent_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch injects a secret into the container environment for the controller manager 2 | # to enable New Relic agent to send events about the operator behavior itself. 3 | 4 | --- 5 | apiVersion: apps/v1 6 | kind: Deployment 7 | metadata: 8 | name: controller-manager 9 | namespace: system 10 | spec: 11 | selector: 12 | matchLabels: 13 | control-plane: controller-manager 14 | template: 15 | spec: 16 | containers: 17 | - name: manager 18 | env: 19 | - name: NEW_RELIC_LICENSE_KEY 20 | valueFrom: 21 | secretKeyRef: 22 | name: nr-agent 23 | key: license-key 24 | optional: true 25 | - name: NEW_RELIC_APP_NAME 26 | valueFrom: 27 | configMapKeyRef: 28 | name: nr-agent 29 | key: app-name 30 | optional: true 31 | - name: NEW_RELIC_HOST 32 | valueFrom: 33 | configMapKeyRef: 34 | name: nr-agent 35 | key: host 36 | optional: true 37 | - name: NEW_RELIC_LOG 38 | value: stdout 39 | - name: NEW_RELIC_LOG_LEVEL 40 | valueFrom: 41 | configMapKeyRef: 42 | name: nr-agent 43 | key: log-level 44 | optional: true 45 | - name: NEW_RELIC_DISTRIBUTED_TRACING_ENABLED 46 | value: "true" 47 | -------------------------------------------------------------------------------- /config/prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - monitor.yaml 3 | -------------------------------------------------------------------------------- /config/prometheus/monitor.yaml: -------------------------------------------------------------------------------- 1 | 2 | # Prometheus Monitor Service (Metrics) 3 | apiVersion: monitoring.coreos.com/v1 4 | kind: ServiceMonitor 5 | metadata: 6 | labels: 7 | control-plane: controller-manager 8 | name: controller-manager-metrics-monitor 9 | namespace: system 10 | spec: 11 | endpoints: 12 | - path: /metrics 13 | port: https 14 | selector: 15 | control-plane: controller-manager 16 | -------------------------------------------------------------------------------- /config/rbac/alertsapmcondition_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit alertsapmconditions. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: alertsapmcondition-editor-role 6 | rules: 7 | - apiGroups: 8 | - nr.k8s.newrelic.com 9 | resources: 10 | - alertsapmconditions 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - nr.k8s.newrelic.com 21 | resources: 22 | - alertsapmconditions/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /config/rbac/alertsapmcondition_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view alertsapmconditions. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: alertsapmcondition-viewer-role 6 | rules: 7 | - apiGroups: 8 | - nr.k8s.newrelic.com 9 | resources: 10 | - alertsapmconditions 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - nr.k8s.newrelic.com 17 | resources: 18 | - alertsapmconditions/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/rbac/alertschannel_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit alertschannels. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: alertschannel-editor-role 6 | rules: 7 | - apiGroups: 8 | - nr.k8s.newrelic.com 9 | resources: 10 | - alertschannels 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - nr.k8s.newrelic.com 21 | resources: 22 | - alertschannels/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /config/rbac/alertschannel_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view alertschannels. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: alertschannel-viewer-role 6 | rules: 7 | - apiGroups: 8 | - nr.k8s.newrelic.com 9 | resources: 10 | - alertschannels 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - nr.k8s.newrelic.com 17 | resources: 18 | - alertschannels/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/rbac/alertsnrqlcondition_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do edit alertsnrqlconditions. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: alertsnrqlcondition-editor-role 6 | rules: 7 | - apiGroups: 8 | - nr.k8s.newrelic.com 9 | resources: 10 | - alertsnrqlconditions 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - nr.k8s.newrelic.com 21 | resources: 22 | - alertsnrqlconditions/status 23 | verbs: 24 | - get 25 | - patch 26 | - update 27 | -------------------------------------------------------------------------------- /config/rbac/alertsnrqlcondition_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do viewer alertsnrqlconditions. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: alertsnrqlcondition-viewer-role 6 | rules: 7 | - apiGroups: 8 | - nr.k8s.newrelic.com 9 | resources: 10 | - alertsnrqlconditions 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - nr.k8s.newrelic.com 17 | resources: 18 | - alertsnrqlconditions/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/rbac/alertspolicy_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do edit alertspolicies. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: alertspolicy-editor-role 6 | rules: 7 | - apiGroups: 8 | - nr.k8s.newrelic.com 9 | resources: 10 | - alertspolicies 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - nr.k8s.newrelic.com 21 | resources: 22 | - alertspolicies/status 23 | verbs: 24 | - get 25 | - patch 26 | - update 27 | -------------------------------------------------------------------------------- /config/rbac/alertspolicy_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do viewer alertspolicies. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: alertspolicy-viewer-role 6 | rules: 7 | - apiGroups: 8 | - nr.k8s.newrelic.com 9 | resources: 10 | - alertspolicies 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - nr.k8s.newrelic.com 17 | resources: 18 | - alertspolicies/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/rbac/apmalertcondition_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit apmalertconditions. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: apmalertcondition-editor-role 6 | rules: 7 | - apiGroups: 8 | - nr.k8s.newrelic.com 9 | resources: 10 | - apmalertconditions 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - nr.k8s.newrelic.com 21 | resources: 22 | - apmalertconditions/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /config/rbac/apmalertcondition_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view apmalertconditions. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: apmalertcondition-viewer-role 6 | rules: 7 | - apiGroups: 8 | - nr.k8s.newrelic.com 9 | resources: 10 | - apmalertconditions 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - nr.k8s.newrelic.com 17 | resources: 18 | - apmalertconditions/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: proxy-role 5 | rules: 6 | - apiGroups: ["authentication.k8s.io"] 7 | resources: 8 | - tokenreviews 9 | verbs: ["create"] 10 | - apiGroups: ["authorization.k8s.io"] 11 | resources: 12 | - subjectaccessreviews 13 | verbs: ["create"] 14 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: proxy-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: proxy-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: default 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | name: controller-manager-metrics-service 7 | namespace: system 8 | spec: 9 | ports: 10 | - name: https 11 | port: 8443 12 | targetPort: https 13 | selector: 14 | control-plane: controller-manager 15 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - role.yaml 3 | - role_binding.yaml 4 | - leader_election_role.yaml 5 | - leader_election_role_binding.yaml 6 | # Comment the following 3 lines if you want to disable 7 | # the auth proxy (https://github.com/brancz/kube-rbac-proxy) 8 | # which protects your /metrics endpoint. 9 | - auth_proxy_service.yaml 10 | - auth_proxy_role.yaml 11 | - auth_proxy_role_binding.yaml 12 | - secrets_role.yaml 13 | - secrets_role_binding.yaml 14 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: leader-election-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - configmaps 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - create 16 | - update 17 | - patch 18 | - delete 19 | - apiGroups: 20 | - "" 21 | resources: 22 | - configmaps/status 23 | verbs: 24 | - get 25 | - update 26 | - patch 27 | - apiGroups: 28 | - "" 29 | resources: 30 | - events 31 | verbs: 32 | - create 33 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: leader-election-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: Role 8 | name: leader-election-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: default 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/newrelicpolicy_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do edit policies. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: policy-editor-role 6 | rules: 7 | - apiGroups: 8 | - nr.k8s.newrelic.com 9 | resources: 10 | - policies 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - nr.k8s.newrelic.com 21 | resources: 22 | - policies/status 23 | verbs: 24 | - get 25 | - patch 26 | - update 27 | -------------------------------------------------------------------------------- /config/rbac/newrelicpolicy_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do viewer policies. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: policy-viewer-role 6 | rules: 7 | - apiGroups: 8 | - nr.k8s.newrelic.com 9 | resources: 10 | - policies 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - nr.k8s.newrelic.com 17 | resources: 18 | - policies/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/rbac/nrqlalertcondition_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do edit nrqlalertconditions. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: nrqlalertcondition-editor-role 6 | rules: 7 | - apiGroups: 8 | - nr.k8s.newrelic.com 9 | resources: 10 | - nrqlalertconditions 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - nr.k8s.newrelic.com 21 | resources: 22 | - nrqlalertconditions/status 23 | verbs: 24 | - get 25 | - patch 26 | - update 27 | -------------------------------------------------------------------------------- /config/rbac/nrqlalertcondition_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do viewer nrqlalertconditions. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: nrqlalertcondition-viewer-role 6 | rules: 7 | - apiGroups: 8 | - nr.k8s.newrelic.com 9 | resources: 10 | - nrqlalertconditions 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - nr.k8s.newrelic.com 17 | resources: 18 | - nrqlalertconditions/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | kind: ClusterRole 5 | metadata: 6 | creationTimestamp: null 7 | name: manager-role 8 | rules: 9 | - apiGroups: 10 | - nr.k8s.newrelic.com 11 | resources: 12 | - alertsapmconditions 13 | verbs: 14 | - create 15 | - delete 16 | - get 17 | - list 18 | - patch 19 | - update 20 | - watch 21 | - apiGroups: 22 | - nr.k8s.newrelic.com 23 | resources: 24 | - alertsapmconditions/status 25 | verbs: 26 | - get 27 | - patch 28 | - update 29 | - apiGroups: 30 | - nr.k8s.newrelic.com 31 | resources: 32 | - alertschannels 33 | verbs: 34 | - create 35 | - delete 36 | - get 37 | - list 38 | - patch 39 | - update 40 | - watch 41 | - apiGroups: 42 | - nr.k8s.newrelic.com 43 | resources: 44 | - alertschannels/status 45 | verbs: 46 | - get 47 | - patch 48 | - update 49 | - apiGroups: 50 | - nr.k8s.newrelic.com 51 | resources: 52 | - alertsnrqlconditions 53 | verbs: 54 | - create 55 | - delete 56 | - get 57 | - list 58 | - patch 59 | - update 60 | - watch 61 | - apiGroups: 62 | - nr.k8s.newrelic.com 63 | resources: 64 | - alertsnrqlconditions/status 65 | verbs: 66 | - get 67 | - patch 68 | - update 69 | - apiGroups: 70 | - nr.k8s.newrelic.com 71 | resources: 72 | - alertspolicies 73 | verbs: 74 | - create 75 | - delete 76 | - get 77 | - list 78 | - patch 79 | - update 80 | - watch 81 | - apiGroups: 82 | - nr.k8s.newrelic.com 83 | resources: 84 | - alertspolicies/status 85 | verbs: 86 | - get 87 | - patch 88 | - update 89 | - apiGroups: 90 | - nr.k8s.newrelic.com 91 | resources: 92 | - apmalertconditions 93 | verbs: 94 | - create 95 | - delete 96 | - get 97 | - list 98 | - patch 99 | - update 100 | - watch 101 | - apiGroups: 102 | - nr.k8s.newrelic.com 103 | resources: 104 | - apmalertconditions/status 105 | verbs: 106 | - get 107 | - patch 108 | - update 109 | - apiGroups: 110 | - nr.k8s.newrelic.com 111 | resources: 112 | - nrqlalertconditions 113 | verbs: 114 | - create 115 | - delete 116 | - get 117 | - list 118 | - patch 119 | - update 120 | - watch 121 | - apiGroups: 122 | - nr.k8s.newrelic.com 123 | resources: 124 | - nrqlalertconditions/status 125 | verbs: 126 | - get 127 | - patch 128 | - update 129 | - apiGroups: 130 | - nr.k8s.newrelic.com 131 | resources: 132 | - policies 133 | verbs: 134 | - create 135 | - delete 136 | - get 137 | - list 138 | - patch 139 | - update 140 | - watch 141 | - apiGroups: 142 | - nr.k8s.newrelic.com 143 | resources: 144 | - policies/status 145 | verbs: 146 | - get 147 | - patch 148 | - update 149 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: manager-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: manager-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: default 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/secrets_role.yaml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | kind: ClusterRole 5 | metadata: 6 | name: secret-reader 7 | rules: 8 | - apiGroups: 9 | - "" 10 | resources: 11 | - secrets 12 | verbs: 13 | - get 14 | - watch 15 | - list 16 | -------------------------------------------------------------------------------- /config/rbac/secrets_role_binding.yaml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | kind: ClusterRoleBinding 5 | metadata: 6 | name: secrets-rolebinding 7 | roleRef: 8 | apiGroup: rbac.authorization.k8s.io 9 | kind: ClusterRole 10 | name: secret-reader 11 | subjects: 12 | - kind: ServiceAccount 13 | name: default 14 | namespace: newrelic-kubernetes-operator-system 15 | 16 | -------------------------------------------------------------------------------- /config/samples/nr-alerts_v1_nrqlalertcondition.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: nr.k8s.newrelic.com/v1 2 | kind: NrqlAlertCondition 3 | metadata: 4 | name: nrqlalertcondition-sample 5 | spec: 6 | # Add fields here 7 | foo: bar 8 | -------------------------------------------------------------------------------- /config/samples/nr-alerts_v1beta1_nrqlalertcondition.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: nr.k8s.newrelic.com/v1beta1 2 | kind: NrqlAlertCondition 3 | metadata: 4 | name: nrqlalertcondition-sample 5 | spec: 6 | # Add fields here 7 | foo: bar 8 | -------------------------------------------------------------------------------- /config/samples/nr_v1_apmalertcondition.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: nr.k8s.newrelic.com/v1 2 | kind: ApmAlertCondition 3 | metadata: 4 | name: apmalertcondition-sample 5 | spec: 6 | # Add fields here 7 | foo: bar 8 | -------------------------------------------------------------------------------- /config/samples/nr_v1_newrelicpolicy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: nr.k8s.newrelic.com/v1 2 | kind: Policy 3 | metadata: 4 | name: policy-sample 5 | spec: 6 | # Add fields here 7 | foo: bar 8 | -------------------------------------------------------------------------------- /config/webhook/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manifests.yaml 3 | - service.yaml 4 | 5 | configurations: 6 | - kustomizeconfig.yaml 7 | -------------------------------------------------------------------------------- /config/webhook/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # the following config is for teaching kustomize where to look at when substituting vars. 2 | # It requires kustomize v2.1.0 or newer to work properly. 3 | nameReference: 4 | - kind: Service 5 | version: v1 6 | fieldSpecs: 7 | - kind: MutatingWebhookConfiguration 8 | group: admissionregistration.k8s.io 9 | path: webhooks/clientConfig/service/name 10 | - kind: ValidatingWebhookConfiguration 11 | group: admissionregistration.k8s.io 12 | path: webhooks/clientConfig/service/name 13 | 14 | namespace: 15 | - kind: MutatingWebhookConfiguration 16 | group: admissionregistration.k8s.io 17 | path: webhooks/clientConfig/service/namespace 18 | create: true 19 | - kind: ValidatingWebhookConfiguration 20 | group: admissionregistration.k8s.io 21 | path: webhooks/clientConfig/service/namespace 22 | create: true 23 | 24 | varReference: 25 | - path: metadata/annotations 26 | -------------------------------------------------------------------------------- /config/webhook/service.yaml: -------------------------------------------------------------------------------- 1 | 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: webhook-service 6 | namespace: system 7 | spec: 8 | ports: 9 | - port: 443 10 | targetPort: 9443 11 | selector: 12 | control-plane: controller-manager 13 | -------------------------------------------------------------------------------- /controllers/alerts_policy_controller_integration_api_test.go: -------------------------------------------------------------------------------- 1 | // +build integration 2 | 3 | package controllers 4 | 5 | import ( 6 | "testing" 7 | 8 | "k8s.io/apimachinery/pkg/types" 9 | ctrl "sigs.k8s.io/controller-runtime" 10 | logf "sigs.k8s.io/controller-runtime/pkg/log" 11 | 12 | "github.com/stretchr/testify/require" 13 | 14 | "github.com/newrelic/newrelic-kubernetes-operator/interfaces" 15 | "github.com/newrelic/newrelic-kubernetes-operator/internal/testutil" 16 | ) 17 | 18 | func TestIntegrationAlertsPolicyController(t *testing.T) { 19 | t.Parallel() 20 | 21 | // Must come before calling reconciler.Reconcile() 22 | k8sClient := testutil.AlertsPolicyTestSetup(t) 23 | 24 | namespacedName := types.NamespacedName{ 25 | Namespace: "default", 26 | Name: "test-policy", 27 | } 28 | 29 | request := ctrl.Request{ 30 | NamespacedName: namespacedName, 31 | } 32 | 33 | reconciler := &AlertsPolicyReconciler{ 34 | Client: k8sClient, 35 | Log: logf.Log, 36 | AlertClientFunc: interfaces.InitializeAlertsClient, 37 | } 38 | 39 | // call reconcile 40 | _, err := reconciler.Reconcile(request) 41 | require.NoError(t, err) 42 | } 43 | -------------------------------------------------------------------------------- /controllers/policy_controller_integration_api_test.go: -------------------------------------------------------------------------------- 1 | // +build integration 2 | 3 | package controllers 4 | 5 | import ( 6 | "context" 7 | "os" 8 | "path/filepath" 9 | "testing" 10 | 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | "k8s.io/apimachinery/pkg/runtime" 13 | "k8s.io/apimachinery/pkg/types" 14 | "k8s.io/client-go/kubernetes/scheme" 15 | ctrl "sigs.k8s.io/controller-runtime" 16 | "sigs.k8s.io/controller-runtime/pkg/client" 17 | "sigs.k8s.io/controller-runtime/pkg/envtest" 18 | logf "sigs.k8s.io/controller-runtime/pkg/log" 19 | 20 | "github.com/newrelic/newrelic-client-go/newrelic" 21 | "github.com/stretchr/testify/require" 22 | 23 | nralertsv1 "github.com/newrelic/newrelic-kubernetes-operator/api/v1" 24 | nrv1 "github.com/newrelic/newrelic-kubernetes-operator/api/v1" 25 | "github.com/newrelic/newrelic-kubernetes-operator/interfaces" 26 | ) 27 | 28 | func newIntegrationTestClient(t *testing.T) newrelic.NewRelic { 29 | envAPIKey := os.Getenv("NEW_RELIC_API_KEY") 30 | envRegion := os.Getenv("NEW_RELIC_REGION") 31 | 32 | if envAPIKey == "" { 33 | t.Skipf("acceptance testing requires NEW_RELIC_API_KEY") 34 | } 35 | 36 | if envRegion == "" { 37 | envRegion = "us" 38 | } 39 | 40 | client, _ := interfaces.NewClient(envAPIKey, envRegion) 41 | 42 | return *client 43 | } 44 | 45 | func testSetup(t *testing.T, object runtime.Object) client.Client { 46 | ctx := context.Background() 47 | testEnv := &envtest.Environment{ 48 | CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, 49 | } 50 | 51 | // var err error 52 | cfg, err := testEnv.Start() 53 | require.NoError(t, err) 54 | require.NotNil(t, cfg) 55 | 56 | err = nralertsv1.AddToScheme(scheme.Scheme) 57 | require.NoError(t, err) 58 | 59 | // +kubebuilder:scaffold:scheme 60 | 61 | k8sClient, err := client.New(cfg, client.Options{Scheme: scheme.Scheme}) 62 | require.NoError(t, err) 63 | require.NotNil(t, k8sClient) 64 | 65 | err = k8sClient.Create(ctx, object) 66 | require.NoError(t, err) 67 | 68 | return k8sClient 69 | } 70 | 71 | func TestIntegrationPolicyController(t *testing.T) { 72 | t.Parallel() 73 | 74 | envAPIKey := os.Getenv("NEW_RELIC_API_KEY") 75 | envRegion := os.Getenv("NEW_RELIC_REGION") 76 | 77 | if envAPIKey == "" { 78 | t.Skipf("acceptance testing requires NEW_RELIC_API_KEY") 79 | } 80 | 81 | if envRegion == "" { 82 | envRegion = "us" 83 | } 84 | 85 | conditionSpec := &nrv1.ConditionSpec{ 86 | GenericConditionSpec: nrv1.GenericConditionSpec{ 87 | Terms: []nrv1.AlertConditionTerm{ 88 | { 89 | Duration: "30", 90 | Operator: "above", 91 | Priority: "critical", 92 | Threshold: "5", 93 | TimeFunction: "all", 94 | }, 95 | }, 96 | Type: "NRQL", 97 | Name: "NRQL Condition", 98 | RunbookURL: "http://test.com/runbook", 99 | Enabled: true, 100 | }, 101 | NrqlSpecificSpec: nrv1.NrqlSpecificSpec{ 102 | Nrql: nrv1.NrqlQuery{ 103 | Query: "SELECT 1 FROM MyEvents", 104 | SinceValue: "5", 105 | }, 106 | ValueFunction: "max", 107 | ViolationCloseTimer: 60, 108 | ExpectedGroups: 2, 109 | IgnoreOverlap: true, 110 | }, 111 | APMSpecificSpec: nrv1.APMSpecificSpec{}, 112 | } 113 | 114 | policy := &nrv1.Policy{ 115 | ObjectMeta: metav1.ObjectMeta{ 116 | Name: "test-policy", 117 | Namespace: "default", 118 | }, 119 | Spec: nrv1.PolicySpec{ 120 | Name: "test policy", 121 | APIKey: envAPIKey, 122 | IncidentPreference: "PER_POLICY", 123 | Region: envRegion, 124 | Conditions: []nrv1.PolicyCondition{ 125 | { 126 | Spec: *conditionSpec, 127 | }, 128 | }, 129 | }, 130 | Status: nrv1.PolicyStatus{ 131 | AppliedSpec: &nrv1.PolicySpec{}, 132 | PolicyID: 0, 133 | }, 134 | } 135 | 136 | // Must come before calling reconciler.Reconcile() 137 | k8sClient := testSetup(t, policy) 138 | 139 | namespacedName := types.NamespacedName{ 140 | Namespace: "default", 141 | Name: "test-policy", 142 | } 143 | 144 | request := ctrl.Request{ 145 | NamespacedName: namespacedName, 146 | } 147 | 148 | reconciler := &PolicyReconciler{ 149 | Client: k8sClient, 150 | Log: logf.Log, 151 | AlertClientFunc: interfaces.InitializeAlertsClient, 152 | } 153 | 154 | // call reconcile 155 | _, err := reconciler.Reconcile(request) 156 | require.NoError(t, err) 157 | 158 | // Deferred teardown 159 | // defer func() { 160 | // _, err := client.DeletePolicy(policy.ID) 161 | 162 | // if err != nil { 163 | // t.Logf("error cleaning up alert policy %d (%s): %s", policy.ID, policy.Name, err) 164 | // } 165 | // }() 166 | } 167 | 168 | func TestIntegrationAlertsChannelController(t *testing.T) { 169 | t.Parallel() 170 | 171 | envAPIKey := os.Getenv("NEW_RELIC_API_KEY") 172 | envRegion := os.Getenv("NEW_RELIC_REGION") 173 | 174 | if envAPIKey == "" { 175 | t.Skipf("acceptance testing requires NEW_RELIC_API_KEY") 176 | } 177 | 178 | if envRegion == "" { 179 | envRegion = "us" 180 | } 181 | 182 | alertsChannel := &nrv1.AlertsChannel{ 183 | ObjectMeta: metav1.ObjectMeta{ 184 | Name: "myalertschannel", 185 | Namespace: "default", 186 | }, 187 | Spec: nrv1.AlertsChannelSpec{ 188 | Name: "my alert channel", 189 | APIKey: envAPIKey, 190 | Region: envRegion, 191 | Type: "email", 192 | Configuration: nrv1.AlertsChannelConfiguration{ 193 | Recipients: "me@email.com", 194 | }, 195 | }, 196 | 197 | Status: nrv1.AlertsChannelStatus{ 198 | AppliedSpec: &nrv1.AlertsChannelSpec{}, 199 | ChannelID: 0, 200 | AppliedPolicyIDs: []int{}, 201 | }, 202 | } 203 | 204 | // Must come before calling reconciler.Reconcile() 205 | k8sClient := testSetup(t, alertsChannel) 206 | 207 | namespacedName := types.NamespacedName{ 208 | Namespace: "default", 209 | Name: "myalertschannel", 210 | } 211 | 212 | request := ctrl.Request{ 213 | NamespacedName: namespacedName, 214 | } 215 | 216 | reconciler := &AlertsChannelReconciler{ 217 | Client: k8sClient, 218 | Log: logf.Log, 219 | AlertClientFunc: interfaces.InitializeAlertsClient, 220 | } 221 | 222 | // call reconcile 223 | _, err := reconciler.Reconcile(request) 224 | require.NoError(t, err) 225 | 226 | // Deferred teardown 227 | // defer func() { 228 | // _, err := client.DeletePolicy(policy.ID) 229 | 230 | // if err != nil { 231 | // t.Logf("error cleaning up alert policy %d (%s): %s", policy.ID, policy.Name, err) 232 | // } 233 | // }() 234 | } 235 | -------------------------------------------------------------------------------- /controllers/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package controllers 17 | 18 | import ( 19 | "path/filepath" 20 | "testing" 21 | 22 | "github.com/newrelic/newrelic-kubernetes-operator/interfaces/interfacesfakes" 23 | 24 | . "github.com/onsi/ginkgo" 25 | . "github.com/onsi/gomega" 26 | 27 | apierrors "k8s.io/apimachinery/pkg/api/errors" 28 | "k8s.io/client-go/kubernetes/scheme" 29 | "k8s.io/client-go/rest" 30 | "sigs.k8s.io/controller-runtime/pkg/client" 31 | "sigs.k8s.io/controller-runtime/pkg/envtest" 32 | "sigs.k8s.io/controller-runtime/pkg/envtest/printer" 33 | logf "sigs.k8s.io/controller-runtime/pkg/log" 34 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 35 | 36 | nralertsv1 "github.com/newrelic/newrelic-kubernetes-operator/api/v1" 37 | // +kubebuilder:scaffold:imports 38 | ) 39 | 40 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to 41 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 42 | 43 | var cfg *rest.Config 44 | var k8sClient client.Client 45 | var testEnv *envtest.Environment 46 | var alertsClient *interfacesfakes.FakeNewRelicAlertsClient 47 | 48 | func TestAPIs(t *testing.T) { 49 | RegisterFailHandler(Fail) 50 | 51 | RunSpecsWithDefaultAndCustomReporters(t, 52 | "Controller Suite", 53 | []Reporter{printer.NewlineReporter{}}) 54 | } 55 | 56 | var _ = BeforeSuite(func(done Done) { 57 | logf.SetLogger(zap.LoggerTo(GinkgoWriter, true)) 58 | 59 | By("bootstrapping test environment") 60 | testEnv = &envtest.Environment{ 61 | CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, 62 | } 63 | 64 | var err error 65 | cfg, err = testEnv.Start() 66 | Expect(err).ToNot(HaveOccurred()) 67 | Expect(cfg).ToNot(BeNil()) 68 | 69 | err = nralertsv1.AddToScheme(scheme.Scheme) 70 | Expect(err).NotTo(HaveOccurred()) 71 | 72 | // +kubebuilder:scaffold:scheme 73 | 74 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) 75 | Expect(err).ToNot(HaveOccurred()) 76 | Expect(k8sClient).ToNot(BeNil()) 77 | 78 | close(done) 79 | }, 60) 80 | 81 | var _ = AfterSuite(func() { 82 | By("tearing down the test environment") 83 | err := testEnv.Stop() 84 | Expect(err).ToNot(HaveOccurred()) 85 | }) 86 | 87 | func ignoreAlreadyExists(err error) error { 88 | if apierrors.IsAlreadyExists(err) { 89 | return nil 90 | } 91 | 92 | return err 93 | } 94 | -------------------------------------------------------------------------------- /errors/collector.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import "strings" 4 | 5 | type ErrorCollector []error 6 | 7 | func (c *ErrorCollector) Collect(e error) { 8 | if e != nil { 9 | *c = append(*c, e) 10 | } 11 | } 12 | 13 | func (c *ErrorCollector) Error() (errorString string) { 14 | messages := []string{} 15 | for i := range *c { 16 | messages = append(messages, (*c)[i].Error()) 17 | } 18 | return strings.Join(messages, "\n") 19 | } 20 | -------------------------------------------------------------------------------- /examples/development/personal-api-key-patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: nr.k8s.newrelic.com/v1 2 | kind: Policy 3 | metadata: 4 | name: my-policy 5 | spec: 6 | api_key: "intentionally blank" 7 | -------------------------------------------------------------------------------- /examples/development/personalized-policy-name-patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: nr.k8s.newrelic.com/v1 2 | kind: Policy 3 | metadata: 4 | name: my-policy 5 | spec: 6 | name: k8s created policy 7 | -------------------------------------------------------------------------------- /examples/development/use-staging-patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: nr.k8s.newrelic.com/v1 2 | kind: Policy 3 | metadata: 4 | name: my-policy 5 | spec: 6 | region: "Staging" 7 | -------------------------------------------------------------------------------- /examples/example_alerts_channel_email.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: nr.k8s.newrelic.com/v1 2 | kind: AlertsChannel 3 | metadata: 4 | name: my-channel1 5 | spec: 6 | api_key: 7 | # api_key_secret: 8 | # name: nr-api-key 9 | # namespace: default 10 | # key_name: api-key 11 | name: "my alert channel" 12 | region: "US" 13 | type: "email" 14 | links: 15 | # Policy links can be by NR PolicyID, NR PolicyName OR K8s AlertPolicy object reference 16 | policy_ids: 17 | - 1 18 | policy_names: 19 | - "k8s created policy" 20 | policy_kubernetes_objects: 21 | - name: "my-policy" 22 | namespace: "default" 23 | configuration: 24 | recipients: "me@email.com" 25 | -------------------------------------------------------------------------------- /examples/example_alerts_channel_webhook.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: nr.k8s.newrelic.com/v1 2 | kind: AlertsChannel 3 | metadata: 4 | name: my-channel1 5 | spec: 6 | api_key: 7 | # api_key_secret: 8 | # name: nr-api-key 9 | # namespace: default 10 | # key_name: api-key 11 | name: "my alert channel" 12 | region: "US" 13 | type: "webhook" 14 | links: 15 | # Policy links can be by NR PolicyID, NR PolicyName OR K8s AlertPolicy object reference 16 | policy_ids: 17 | - 1 18 | policy_names: 19 | - "k8s created policy" 20 | policy_kubernetes_objects: 21 | - name: "my-policy" 22 | namespace: "default" 23 | configuration: 24 | url: "https://example.com/" 25 | headers: 26 | - name: WEBHOOK_SOURCE 27 | value: newrelic 28 | - name: SECRET_TOKEN 29 | secret: secret 30 | namespace: default 31 | key_name: token 32 | # see https://docs.newrelic.com/docs/alerts-applied-intelligence/new-relic-alerts/alert-notifications/customize-your-webhook-payload 33 | payload: 34 | details: "$EVENT_DETAILS" 35 | current_state: "$EVENT_STATE" 36 | -------------------------------------------------------------------------------- /examples/example_apm_alert_condition.yaml: -------------------------------------------------------------------------------- 1 | # Note: If using a k8s secret, remove `api_key`, uncomment `api_key_secret`, 2 | # add your API key to examples/example_secret.yaml, and run 3 | # `kubectl apply -f examples/example_secret.yaml` 4 | 5 | apiVersion: nr.k8s.newrelic.com/v1 6 | kind: AlertsAPMCondition 7 | metadata: 8 | name: alert-1029267 9 | spec: 10 | account_id: 11 | api_key: 12 | # api_key_secret: 13 | # name: nr-api-key 14 | # namespace: default 15 | # key_name: api-key 16 | type: "apm_app_metric" 17 | enabled: true 18 | metric: "apdex" 19 | condition_scope: application 20 | entities: 21 | - "215037795" 22 | apm_terms: 23 | - threshold: "0.9" 24 | time_function: "all" 25 | duration: "5" 26 | priority: "critical" 27 | operator: "above" 28 | name: "K8s generated apm alert condition" 29 | # Must reference an existing New Relic alert policy from your account 30 | existing_policy_id: "897188" 31 | region: "US" 32 | -------------------------------------------------------------------------------- /examples/example_new_relic_agent_config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: nr-agent 5 | namespace: newrelic-kubernetes-operator-system 6 | type: Opaque 7 | stringData: 8 | license-key: 9 | 10 | --- 11 | apiVersion: v1 12 | kind: ConfigMap 13 | metadata: 14 | name: nr-agent 15 | namespace: newrelic-kubernetes-operator-system 16 | data: 17 | # defaults to New Relic Kubernetes Operator 18 | app-name: 19 | # Defaults to collector.newrelic.com 20 | host: 21 | # Defaults to false 22 | log-level: debug 23 | 24 | -------------------------------------------------------------------------------- /examples/example_nrql_alert_condition.yaml: -------------------------------------------------------------------------------- 1 | # Note: If using a k8s secret, remove `api_key`, uncomment `api_key_secret`, 2 | # add your API key to examples/example_secret.yaml, and run 3 | # `kubectl apply -f examples/example_secret.yaml` 4 | 5 | apiVersion: nr.k8s.newrelic.com/v1 6 | kind: AlertsNrqlCondition 7 | metadata: 8 | name: alert-1029266 9 | spec: 10 | account_id: 11 | api_key: 12 | # api_key_secret: 13 | # name: nr-api-key 14 | # namespace: default 15 | # key_name: api-key 16 | type: "NRQL" 17 | nrql: 18 | query: "SELECT count(*) FROM Transactions" 19 | evaluationOffset: 10 20 | enabled: true 21 | terms: 22 | - threshold: "75.0" 23 | threshold_occurrences: "ALL" 24 | threshold_duration: 60 25 | priority: "CRITICAL" 26 | operator: "ABOVE" 27 | expiration: 28 | expirationDuration: 360 29 | closeViolationsOnExpiration: false 30 | openViolationOnExpiration: true 31 | # signal: 32 | # aggregation_window: 360 33 | # evaluation_offset: 30 34 | # fill_option: NONE 35 | # fill_value: 15 36 | name: "nrql condition (new)" 37 | violationTimeLimit: "ONE_HOUR" 38 | valueFunction: "SINGLE_VALUE" 39 | # Must reference an existing New Relic alert policy from your account 40 | existing_policy_id: "897188" 41 | region: "US" 42 | -------------------------------------------------------------------------------- /examples/example_policy.yaml: -------------------------------------------------------------------------------- 1 | # Note: If using a k8s secret, remove `api_key`, uncomment `api_key_secret`, 2 | # add your API key to examples/example_secret.yaml, and run 3 | # `kubectl apply -f examples/example_secret.yaml` 4 | 5 | apiVersion: nr.k8s.newrelic.com/v1 6 | kind: AlertsPolicy 7 | metadata: 8 | name: my-policy 9 | spec: 10 | account_id: 11 | api_key: 12 | # api_key_secret: 13 | # name: nr-api-key 14 | # namespace: default 15 | # key_name: api-key 16 | name: k8s created policy 17 | incidentPreference: "PER_POLICY" 18 | region: "US" 19 | conditions: 20 | - spec: 21 | type: "NRQL" 22 | nrql: 23 | query: "SELECT count(*) FROM Transactions" 24 | evaluationOffset: 10 25 | enabled: true 26 | terms: 27 | - threshold: "75.0" 28 | threshold_occurrences: "ALL" 29 | threshold_duration: 60 30 | priority: "CRITICAL" 31 | operator: "ABOVE" 32 | name: "nrql condition" 33 | violationTimeLimit: "ONE_HOUR" 34 | valueFunction: "SINGLE_VALUE" 35 | - spec: 36 | type: "apm_app_metric" 37 | enabled: true 38 | metric: "apdex" 39 | condition_scope: application 40 | entities: 41 | - "215037795" 42 | apm_terms: 43 | - threshold: "0.9" 44 | time_function: "all" 45 | duration: "5" 46 | priority: "critical" 47 | operator: "above" 48 | name: "apm condition" 49 | -------------------------------------------------------------------------------- /examples/example_policy_apm.yaml: -------------------------------------------------------------------------------- 1 | # Note: If using a k8s secret, remove `api_key`, uncomment `api_key_secret`, 2 | # add your API key to examples/example_secret.yaml, and run 3 | # `kubectl apply -f examples/example_secret.yaml` 4 | 5 | apiVersion: nr.k8s.newrelic.com/v1 6 | kind: AlertsPolicy 7 | metadata: 8 | name: my-apm-policy 9 | spec: 10 | account_id: 11 | api_key: 12 | # api_key_secret: 13 | # name: nr-api-key 14 | # namespace: default 15 | # key_name: api-key 16 | name: k8s created policy (new) 17 | incidentPreference: "PER_POLICY" 18 | region: "US" 19 | conditions: 20 | - spec: 21 | type: "apm_app_metric" 22 | enabled: true 23 | metric: "apdex" 24 | condition_scope: application 25 | entities: 26 | - "215037795" 27 | apm_terms: 28 | - threshold: "0.9" 29 | time_function: "all" 30 | duration: "5" 31 | priority: "critical" 32 | operator: "above" 33 | name: "K8s generated apm alert condition" 34 | - spec: 35 | type: "apm_app_metric" 36 | enabled: true 37 | metric: "apdex" 38 | condition_scope: application 39 | entities: 40 | - "215037795" 41 | apm_terms: 42 | - threshold: "0.9" 43 | time_function: "all" 44 | duration: "30" 45 | priority: "critical" 46 | operator: "above" 47 | name: "K8s generated apm alert condition 2" -------------------------------------------------------------------------------- /examples/example_secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: nr-api-key 5 | namespace: default 6 | type: Opaque 7 | stringData: 8 | api-key: 9 | --- 10 | apiVersion: v1 11 | kind: Secret 12 | metadata: 13 | name: secret 14 | namespace: default 15 | type: Opaque 16 | stringData: 17 | token: super-secret-example-header-token 18 | -------------------------------------------------------------------------------- /examples/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - example_policy.yaml 5 | patchesStrategicMerge: 6 | - development/personal-api-key-patch.yaml 7 | - development/personalized-policy-name-patch.yaml 8 | - development/use-staging-patch.yaml 9 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/newrelic/newrelic-kubernetes-operator 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/Masterminds/goutils v1.1.1 // indirect 7 | github.com/Masterminds/semver/v3 v3.1.1 // indirect 8 | github.com/davecgh/go-spew v1.1.1 9 | github.com/git-chglog/git-chglog v0.15.0 // indirect 10 | github.com/go-logr/logr v0.1.0 11 | github.com/golang/mock v1.4.3 12 | github.com/goreleaser/goreleaser v0.143.0 13 | github.com/hashicorp/golang-lru v0.5.4 // indirect 14 | github.com/llorllale/go-gitlint v0.0.0-20210608233938-d6303cc52cc5 // indirect 15 | github.com/maxbrunsfeld/counterfeiter/v6 v6.2.3 16 | github.com/newrelic/go-agent/v3 v3.7.0 17 | github.com/newrelic/newrelic-client-go v0.60.0 18 | github.com/onsi/ginkgo v1.13.0 19 | github.com/onsi/gomega v1.10.1 20 | github.com/prometheus/client_golang v1.6.0 // indirect 21 | github.com/prometheus/common v0.10.0 // indirect 22 | github.com/psampaz/go-mod-outdated v0.8.0 // indirect 23 | github.com/stretchr/testify v1.7.0 24 | golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 // indirect 25 | golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305 26 | gomodules.xyz/jsonpatch/v2 v2.1.0 // indirect 27 | k8s.io/api v0.18.4 28 | k8s.io/apiextensions-apiserver v0.18.4 // indirect 29 | k8s.io/apimachinery v0.18.4 30 | k8s.io/client-go v0.18.4 31 | k8s.io/utils v0.0.0-20200601170155-a0dff01d8ea5 // indirect 32 | sigs.k8s.io/controller-runtime v0.6.0 33 | sigs.k8s.io/controller-tools v0.3.0 34 | ) 35 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ -------------------------------------------------------------------------------- /interfaces/new_relic_alert_client.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/newrelic/newrelic-client-go/newrelic" 7 | "github.com/newrelic/newrelic-client-go/pkg/alerts" 8 | "github.com/newrelic/newrelic-client-go/pkg/config" 9 | 10 | "github.com/newrelic/newrelic-kubernetes-operator/internal/info" 11 | ) 12 | 13 | //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 . NewRelicAlertsClient 14 | type NewRelicAlertsClient interface { 15 | CreateNrqlCondition(int, alerts.NrqlCondition) (*alerts.NrqlCondition, error) 16 | UpdateNrqlCondition(alerts.NrqlCondition) (*alerts.NrqlCondition, error) 17 | ListNrqlConditions(int) ([]*alerts.NrqlCondition, error) 18 | DeleteNrqlCondition(int) (*alerts.NrqlCondition, error) 19 | ListConditions(int) ([]*alerts.Condition, error) 20 | CreateCondition(policyID int, condition alerts.Condition) (*alerts.Condition, error) 21 | UpdateCondition(condition alerts.Condition) (*alerts.Condition, error) 22 | DeleteCondition(id int) (*alerts.Condition, error) 23 | GetPolicy(id int) (*alerts.Policy, error) 24 | CreatePolicy(alerts.Policy) (*alerts.Policy, error) 25 | UpdatePolicy(alerts.Policy) (*alerts.Policy, error) 26 | DeletePolicy(int) (*alerts.Policy, error) 27 | ListPolicies(*alerts.ListPoliciesParams) ([]alerts.Policy, error) 28 | CreateChannel(channel alerts.Channel) (*alerts.Channel, error) 29 | DeleteChannel(id int) (*alerts.Channel, error) 30 | ListChannels() ([]*alerts.Channel, error) 31 | UpdatePolicyChannels(policyID int, channelIDs []int) (*alerts.PolicyChannels, error) 32 | DeletePolicyChannel(policyID int, ChannelID int) (*alerts.Channel, error) 33 | 34 | // NerdGraph 35 | CreatePolicyMutation(accountID int, policy alerts.AlertsPolicyInput) (*alerts.AlertsPolicy, error) 36 | UpdatePolicyMutation(accountID int, policyID string, policy alerts.AlertsPolicyUpdateInput) (*alerts.AlertsPolicy, error) 37 | DeletePolicyMutation(accountID int, id string) (*alerts.AlertsPolicy, error) 38 | QueryPolicySearch(accountID int, params alerts.AlertsPoliciesSearchCriteriaInput) ([]*alerts.AlertsPolicy, error) 39 | QueryPolicy(accountID int, id string) (*alerts.AlertsPolicy, error) 40 | 41 | CreateNrqlConditionStaticMutation(accountID int, policyID string, nrqlCondition alerts.NrqlConditionInput) (*alerts.NrqlAlertCondition, error) 42 | UpdateNrqlConditionStaticMutation(accountID int, conditionID string, nrqlCondition alerts.NrqlConditionInput) (*alerts.NrqlAlertCondition, error) 43 | CreateNrqlConditionBaselineMutation(accountID int, policyID string, nrqlCondition alerts.NrqlConditionInput) (*alerts.NrqlAlertCondition, error) 44 | UpdateNrqlConditionBaselineMutation(accountID int, conditionID string, nrqlCondition alerts.NrqlConditionInput) (*alerts.NrqlAlertCondition, error) 45 | DeleteConditionMutation(accountID int, conditionID string) (string, error) 46 | SearchNrqlConditionsQuery(accountID int, searchCriteria alerts.NrqlConditionsSearchCriteria) ([]*alerts.NrqlAlertCondition, error) 47 | GetNrqlConditionQuery(accountID int, conditionID string) (*alerts.NrqlAlertCondition, error) 48 | } 49 | 50 | func NewClient(apiKey string, regionValue string) (*newrelic.NewRelic, error) { 51 | cfg := config.New() 52 | 53 | client, err := newrelic.New( 54 | newrelic.ConfigPersonalAPIKey(apiKey), 55 | newrelic.ConfigLogLevel(cfg.LogLevel), 56 | newrelic.ConfigRegion(regionValue), 57 | newrelic.ConfigUserAgent(info.UserAgent()), 58 | newrelic.ConfigServiceName(info.Name), 59 | ) 60 | 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | return client, nil 66 | } 67 | 68 | func InitializeAlertsClient(apiKey string, regionName string) (NewRelicAlertsClient, error) { 69 | client, err := NewClient(apiKey, regionName) 70 | if err != nil { 71 | return nil, fmt.Errorf("unable to create New Relic client with error: %s", err) 72 | } 73 | 74 | return &client.Alerts, nil 75 | } 76 | 77 | //PartialAPIKey - Returns a partial API key to ensure we don't log the full API Key 78 | func PartialAPIKey(apiKey string) string { 79 | partialKeyLength := min(10, len(apiKey)) 80 | return apiKey[0:partialKeyLength] + "..." 81 | } 82 | 83 | func min(x, y int) int { 84 | if x > y { 85 | return y 86 | } 87 | return x 88 | } 89 | -------------------------------------------------------------------------------- /internal/info/main.go: -------------------------------------------------------------------------------- 1 | package info 2 | 3 | // Version of this library 4 | var ( 5 | Name string = "newrelic-kubernetes-operator" 6 | Version string = "dev" 7 | ) 8 | 9 | const RepoURL = "https://github.com/newrelic/newrelic-kubernetes-operator" 10 | 11 | func UserAgent() string { 12 | return Name + "/" + Version + " (" + RepoURL + ")" 13 | } 14 | -------------------------------------------------------------------------------- /internal/testutil/alerts.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strconv" 8 | "testing" 9 | 10 | "github.com/newrelic/newrelic-client-go/pkg/alerts" 11 | "github.com/newrelic/newrelic-client-go/pkg/testhelpers" 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 | "k8s.io/client-go/kubernetes/scheme" 16 | "sigs.k8s.io/controller-runtime/pkg/client" 17 | "sigs.k8s.io/controller-runtime/pkg/envtest" 18 | 19 | nrv1 "github.com/newrelic/newrelic-kubernetes-operator/api/v1" 20 | ) 21 | 22 | func AlertsPolicyTestSetup(t *testing.T) client.Client { 23 | testEnv := &envtest.Environment{ 24 | CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, 25 | } 26 | 27 | // var err error 28 | cfg, err := testEnv.Start() 29 | require.NoError(t, err) 30 | require.NotNil(t, cfg) 31 | 32 | err = nrv1.AddToScheme(scheme.Scheme) 33 | require.NoError(t, err) 34 | 35 | // +kubebuilder:scaffold:scheme 36 | 37 | k8sClient, err := client.New(cfg, client.Options{Scheme: scheme.Scheme}) 38 | 39 | require.NoError(t, err) 40 | require.NotNil(t, k8sClient) 41 | 42 | return k8sClient 43 | } 44 | 45 | func NewTestAlertsPolicy(t *testing.T) *nrv1.AlertsPolicy { 46 | policyName := fmt.Sprintf("test-policy-%s", testhelpers.RandSeq(5)) 47 | 48 | accountID, err := strconv.Atoi(os.Getenv("NEW_RELIC_ACCOUNT_ID")) 49 | assert.NoError(t, err) 50 | 51 | return &nrv1.AlertsPolicy{ 52 | ObjectMeta: metav1.ObjectMeta{ 53 | Name: policyName, 54 | Namespace: "default", 55 | }, 56 | Spec: nrv1.AlertsPolicySpec{ 57 | APIKey: "112233", 58 | AccountID: accountID, 59 | IncidentPreference: "PER_POLICY", 60 | Name: policyName, 61 | Region: "US", 62 | }, 63 | Status: nrv1.AlertsPolicyStatus{ 64 | AppliedSpec: &nrv1.AlertsPolicySpec{}, 65 | }, 66 | } 67 | 68 | } 69 | 70 | func NewTestAlertsNrqlCondition(t *testing.T) *nrv1.AlertsNrqlCondition { 71 | conditionName := fmt.Sprintf("test-condition-%s", testhelpers.RandSeq(5)) 72 | conditionSpec := NewTestAlertsPolicyConditionSpec(t) 73 | 74 | policyCondition := nrv1.AlertsPolicyCondition{ 75 | Spec: *conditionSpec, 76 | } 77 | 78 | return &nrv1.AlertsNrqlCondition{ 79 | ObjectMeta: metav1.ObjectMeta{ 80 | Name: conditionName, 81 | Namespace: "default", 82 | }, 83 | 84 | Spec: policyCondition.ReturnNrqlConditionSpec(), 85 | Status: nrv1.AlertsNrqlConditionStatus{ 86 | AppliedSpec: &nrv1.AlertsNrqlConditionSpec{}, 87 | }, 88 | } 89 | } 90 | 91 | func NewTestAlertsPolicyConditionSpec(t *testing.T) *nrv1.AlertsPolicyConditionSpec { 92 | accountID, err := strconv.Atoi(os.Getenv("NEW_RELIC_ACCOUNT_ID")) 93 | assert.NoError(t, err) 94 | 95 | return &nrv1.AlertsPolicyConditionSpec{ 96 | AlertsGenericConditionSpec: nrv1.AlertsGenericConditionSpec{ 97 | APIKey: "nraa-key", 98 | AccountID: accountID, 99 | Terms: []nrv1.AlertsNrqlConditionTerm{ 100 | { 101 | Operator: alerts.AlertsNRQLConditionTermsOperatorTypes.ABOVE, 102 | Priority: alerts.NrqlConditionPriorities.Critical, 103 | Threshold: "5", 104 | ThresholdDuration: 60, 105 | ThresholdOccurrences: alerts.ThresholdOccurrences.AtLeastOnce, 106 | }, 107 | }, 108 | Type: "NRQL", 109 | Name: "NRQL Condition", 110 | RunbookURL: "http://test.com/runbook", 111 | ID: 777, 112 | Enabled: true, 113 | ExistingPolicyID: "42", 114 | }, 115 | AlertsNrqlSpecificSpec: nrv1.AlertsNrqlSpecificSpec{ 116 | ViolationTimeLimit: "60", 117 | ExpectedGroups: 2, 118 | IgnoreOverlap: true, 119 | ValueFunction: &alerts.NrqlConditionValueFunctions.SingleValue, 120 | Nrql: alerts.NrqlConditionQuery{ 121 | Query: "SELECT 1 FROM MyEvents", 122 | EvaluationOffset: 5, 123 | }, 124 | }, 125 | AlertsAPMSpecificSpec: nrv1.AlertsAPMSpecificSpec{}, 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package main 17 | 18 | import ( 19 | "flag" 20 | "fmt" 21 | "os" 22 | 23 | "k8s.io/apimachinery/pkg/runtime" 24 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 25 | _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" 26 | ctrl "sigs.k8s.io/controller-runtime" 27 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 28 | 29 | nrv1 "github.com/newrelic/newrelic-kubernetes-operator/api/v1" 30 | "github.com/newrelic/newrelic-kubernetes-operator/internal/info" 31 | // +kubebuilder:scaffold:imports 32 | ) 33 | 34 | var ( 35 | scheme = runtime.NewScheme() 36 | setupLog = ctrl.Log.WithName("setup") 37 | ) 38 | 39 | func init() { 40 | _ = clientgoscheme.AddToScheme(scheme) 41 | 42 | _ = nrv1.AddToScheme(scheme) 43 | // +kubebuilder:scaffold:scheme 44 | } 45 | 46 | func main() { 47 | var metricsAddr string 48 | var enableLeaderElection bool 49 | var showVersion bool 50 | var devMode bool 51 | 52 | flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.") 53 | flag.BoolVar(&enableLeaderElection, "enable-leader-election", false, "Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager.") 54 | flag.BoolVar(&showVersion, "version", false, "Show version information.") 55 | flag.BoolVar(&devMode, "dev-mode", false, "Enable development level logging (stacktraces on warnings, no sampling)") 56 | flag.Parse() 57 | 58 | if showVersion { 59 | fmt.Printf("%s version %s\n", info.Name, info.Version) 60 | os.Exit(0) 61 | } 62 | 63 | logger := zap.New(zap.UseDevMode(devMode)) 64 | ctrl.SetLogger(logger) 65 | 66 | opts := ctrl.Options{ 67 | Scheme: scheme, 68 | MetricsBindAddress: metricsAddr, 69 | LeaderElection: enableLeaderElection, 70 | LeaderElectionID: "newrelic-kubernetes-operator", 71 | Port: 9443, 72 | } 73 | 74 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), opts) 75 | if err != nil { 76 | setupLog.Error(err, "unable to create manager") 77 | os.Exit(1) 78 | } 79 | 80 | // initialize NR go agent 81 | nrApp := InitializeNRAgent() 82 | 83 | //Register Alerts 84 | err = registerAlerts(&mgr, &nrApp) 85 | if err != nil { 86 | setupLog.Error(err, "unable to register alerts") 87 | os.Exit(1) 88 | } 89 | 90 | // This marker is used by kubebuilder and must remain in main.go but generated code should be refactored to another class as appropriate 91 | // This can likely be refactored once https://github.com/kubernetes-sigs/kubebuilder/blob/master/designs/simplified-scaffolding.md is completed 92 | // +kubebuilder:scaffold:builder 93 | 94 | setupLog.Info("starting manager") 95 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 96 | setupLog.Error(err, "problem running manager") 97 | os.Exit(1) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /nrgoagent.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/newrelic/go-agent/v3/newrelic" 7 | ) 8 | 9 | //InitializeNRAgent - Initializes the NR Agent 10 | func InitializeNRAgent() newrelic.Application { 11 | 12 | app, err := newrelic.NewApplication( 13 | //Set a default name which will be overridden by ENV values if provided 14 | newrelic.ConfigAppName("New Relic Kubernetes Operator"), 15 | newrelic.ConfigFromEnvironment(), 16 | ) 17 | 18 | // If an application could not be created then err will reveal why. 19 | if err != nil { 20 | fmt.Println("unable to create New Relic Application", err) 21 | return newrelic.Application{} 22 | } 23 | 24 | return *app 25 | } 26 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | COLOR_RED='\033[0;31m' 4 | COLOR_NONE='\033[0m' 5 | CURRENT_GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD) 6 | 7 | if [ $CURRENT_GIT_BRANCH != 'master' ]; then 8 | printf "\n" 9 | printf "${COLOR_RED} Error: The release.sh script must be run while on the master branch. \n ${COLOR_NONE}" 10 | printf "\n" 11 | 12 | exit 1 13 | fi 14 | 15 | if [ $# -ne 1 ]; then 16 | printf "\n" 17 | printf "${COLOR_RED} Error: Release version argument required. \n\n ${COLOR_NONE}" 18 | printf " Example: \n\n ./tools/release.sh 0.9.0 \n\n" 19 | printf " Example (make): \n\n make release version=0.9.0 \n" 20 | printf "\n" 21 | 22 | exit 1 23 | fi 24 | 25 | RELEASE_VERSION=$1 26 | GIT_USER=$(git config user.email) 27 | 28 | echo "Generating release for v${RELEASE_VERSION} using system user git user ${GIT_USER}" 29 | 30 | git checkout -b release/v${RELEASE_VERSION} 31 | 32 | # Update config/manager/kustomization.yaml to point to the docker tag for this release 33 | cd config/manager && kustomize edit set image controller=newrelic/kubernetes-operator:v${RELEASE_VERSION} && cd ../.. 34 | 35 | # Auto-generate CHANGELOG updates 36 | git-chglog --next-tag v${RELEASE_VERSION} -o CHANGELOG.md 37 | 38 | # Commit updates 39 | git add CHANGELOG.md config/manager/kustomization.yaml 40 | git commit --no-verify -m "chore(release): Updates for v${RELEASE_VERSION}" 41 | git push --no-verify origin release/v${RELEASE_VERSION} 42 | -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | // +build tools 2 | 3 | package tools 4 | 5 | import ( 6 | // project-specific 7 | _ "github.com/maxbrunsfeld/counterfeiter/v6" 8 | 9 | // build/lint.mk 10 | _ "golang.org/x/tools/cmd/goimports" 11 | 12 | // build/generate.mk 13 | _ "sigs.k8s.io/controller-tools/cmd/controller-gen" 14 | 15 | // build/release.mk 16 | _ "github.com/goreleaser/goreleaser" 17 | ) 18 | 19 | // This file imports packages that are used when running go generate, or used 20 | --------------------------------------------------------------------------------