diff --git a/.github/scripts/create-reports.sh b/.github/scripts/create-reports.sh index 5b71a57a0..ca6840273 100755 --- a/.github/scripts/create-reports.sh +++ b/.github/scripts/create-reports.sh @@ -31,7 +31,7 @@ for namespace in $(kubectl get namespaces -o jsonpath='{.items[*].metadata.name} createResourceReport "$logsDir/$namespace" "$namespace" "Daemonsets" false createResourceReport "$logsDir/$namespace" "$namespace" "Statefulsets" false createResourceReport "$logsDir/$namespace" "$namespace" "Jobs" false - createResourceReport "$logsDir/$namespace" "$namespace" "FeatureFlagConfiguration" false - createResourceReport "$logsDir/$namespace" "$namespace" "FlagSourceConfiguration" false + createResourceReport "$logsDir/$namespace" "$namespace" "FeatureFlag" false + createResourceReport "$logsDir/$namespace" "$namespace" "FeatureFlagSource" false done diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml new file mode 100644 index 000000000..811aa95b0 --- /dev/null +++ b/.github/workflows/golangci-lint.yml @@ -0,0 +1,42 @@ +name: Lint checks +on: + push: + branches: + - 'main' + - '[0-9]+.[1-9][0-9]*.x' + pull_request: + branches: + - 'main' + - '[0-9]+.[1-9][0-9]*.x' + paths: + - "**.go" + - "**/go.mod" + - "**/go.sum" + - ".golangi.yml" + - ".github/workflows/golangci-lint.yml" + - "!docs/**" +env: + # renovate: datasource=github-releases depName=golangci/golangci-lint + GOLANGCI_LINT_VERSION: "v1.55.2" + GO_VERSION: "~1.20" +jobs: + golangci-lint: + name: golangci-lint + runs-on: ubuntu-22.04 + strategy: + fail-fast: false + steps: + - name: Check out code + uses: actions/checkout@v4 + + - uses: actions/setup-go@v4 + with: + go-version: ${{ env.GO_VERSION }} + check-latest: true + + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + working-directory: ./ + version: ${{ env.GOLANGCI_LINT_VERSION }} + args: --config ./.golangci.yml -v diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index f81e412b6..eed3e37cd 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -19,32 +19,9 @@ permissions: contents: read jobs: - lint: - runs-on: ubuntu-latest - steps: - - name: Install Go - uses: actions/setup-go@v4 - with: - go-version: ${{ env.DEFAULT_GO_VERSION }} - - name: Checkout repository - uses: actions/checkout@v3 - - name: Setup Environment - run: | - echo "GOPATH=$(go env GOPATH)" >> $GITHUB_ENV - echo "$(go env GOPATH)/bin" >> $GITHUB_PATH - - name: Module cache - uses: actions/cache@v3 - env: - cache-name: go-mod-cache - with: - path: ~/go/pkg/mod - key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/go.sum') }} - - name: Run linter - run: make lint - unit-test: name: Unit Tests - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Install Go uses: actions/setup-go@v4 @@ -78,7 +55,7 @@ jobs: docker-local: permissions: security-events: write # for github/codeql-action/upload-sarif to upload SARIF results - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Checkout uses: actions/checkout@v3 @@ -117,7 +94,7 @@ jobs: path: ${{ github.workspace }}/open-feature-operator-local.tar e2e-test: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 needs: docker-local strategy: matrix: @@ -164,12 +141,12 @@ jobs: IMG=open-feature-operator-local:${{ github.sha }} make deploy-operator IMG=open-feature-operator-local:${{ github.sha }} make e2e-test-kuttl - name: Create reports - if: always() + if: failure() working-directory: ./.github/scripts run: ./create-reports.sh - name: Upload cluster logs - if: always() + if: failure() uses: actions/upload-artifact@v3 with: name: e2e-tests diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml index c7d904fec..1d532ea33 100644 --- a/.github/workflows/pr-lint.yml +++ b/.github/workflows/pr-lint.yml @@ -18,7 +18,7 @@ jobs: pull-requests: write statuses: write # for amannn/action-semantic-pull-request to mark status of analyzed PR name: Validate PR title - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: amannn/action-semantic-pull-request@v5 env: diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 8170f4987..e32cd8aae 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -12,6 +12,10 @@ env: IMAGE_NAME: ${{ github.repository }} GITHUB_PAGES_BRANCH: gh-pages +defaults: + run: + shell: bash + permissions: contents: read @@ -20,7 +24,7 @@ jobs: permissions: contents: write # for google-github-actions/release-please-action to create release commit pull-requests: write # for google-github-actions/release-please-action to create release PR - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 # Release-please creates a PR that tracks all changes steps: @@ -30,6 +34,7 @@ jobs: command: manifest token: ${{secrets.GITHUB_TOKEN}} default-branch: main + outputs: release_created: ${{ steps.release.outputs.release_created }} release_tag_name: ${{ steps.release.outputs.tag_name }} @@ -38,7 +43,7 @@ jobs: permissions: packages: write # to push the container image needs: release-please - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 if: ${{ needs.release-please.outputs.release_created }} steps: - name: Checkout @@ -104,7 +109,7 @@ jobs: permissions: contents: write # for softprops/action-gh-release to create GitHub release needs: release-please - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 if: ${{ needs.release-please.outputs.release_created }} steps: - name: Checkout @@ -139,7 +144,7 @@ jobs: needs: release-please permissions: contents: write - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 if: ${{ needs.release-please.outputs.release_created }} steps: - name: Checkout diff --git a/.github/workflows/validate-crd-docs.yml b/.github/workflows/validate-crd-docs.yml index cc8e340b0..69c9c49b7 100644 --- a/.github/workflows/validate-crd-docs.yml +++ b/.github/workflows/validate-crd-docs.yml @@ -9,7 +9,7 @@ defaults: jobs: check-helm-docs: name: Check crd documentation values - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Check out code uses: actions/checkout@v3 diff --git a/.github/workflows/validate-helm-docs.yml b/.github/workflows/validate-helm-docs.yml index 28ec10258..99464c086 100644 --- a/.github/workflows/validate-helm-docs.yml +++ b/.github/workflows/validate-helm-docs.yml @@ -17,7 +17,7 @@ jobs: uses: actions/checkout@v3 - name: Set up Node - uses: actions/setup-node@v3.7.0 + uses: actions/setup-node@v3.8.1 with: node-version: 16 diff --git a/.gitignore b/.gitignore index cfd1841c7..6b323cd6b 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,5 @@ testbin/* *.swo *~ +go.work +go.work.sum diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 000000000..1b879694c --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,34 @@ +run: + timeout: 5m + go: '1.20' +linters: + enable: + - gofmt # Gofmt checks whether code was gofmt-ed. By default, this tool runs with -s option to check for code simplification + - gci # Gci controls golang package import order and makes it always deterministic. + - errorlint # errorlint can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13. + - containedctx # containedctx is a linter that detects struct contained context.Context field + - dogsled # Checks assignments with too many blank identifiers (e.g. x, , , _, := f()) + - nilnil # Checks that there is no simultaneous return of nil error and an invalid value. + - noctx # noctx finds sending http request without context.Context + - gocyclo # measure cyclomatic complexity + - gocognit # measure cognitive complexity + - funlen # limit function length + - dupl # Detect code duplication + +issues: + exclude-rules: + - linters: + - containedctx + - gocyclo + - gocognit + - funlen + path: _test\.go + +linters-settings: + gocyclo: + min-complexity: 10 + gocognit: + min-complexity: 20 + funlen: + lines: 120 + statements: 120 diff --git a/.release-please-manifest.json b/.release-please-manifest.json index d63ff0171..b7b12ba92 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,4 @@ { - ".": "0.2.36" + ".": "0.3.0", + "apis": "0.2.38" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 79d1307bf..745718ae5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,46 @@ # Changelog +## [0.3.0](https://github.com/open-feature/open-feature-operator/compare/operator-v0.2.36...operator/v0.3.0) (2023-11-29) + + +### โš  BREAKING CHANGES + +* use v1beta1 in operator logic ([#539](https://github.com/open-feature/open-feature-operator/issues/539)) + +### โœจ New Features + +* Introduce v1beta1 API version ([#535](https://github.com/open-feature/open-feature-operator/issues/535)) ([3acd492](https://github.com/open-feature/open-feature-operator/commit/3acd49289a40e8f07fd20aad46185ac42ceb1b7a)) +* prepare apis for v1beta1 controllers onboarding ([#549](https://github.com/open-feature/open-feature-operator/issues/549)) ([e3c8b42](https://github.com/open-feature/open-feature-operator/commit/e3c8b4290be99d78b88ffef686531a38b97e61be)) +* release APIs and Operator independently ([#541](https://github.com/open-feature/open-feature-operator/issues/541)) ([7b1af42](https://github.com/open-feature/open-feature-operator/commit/7b1af42ac41e63ccbb1820b31f579ffea679cff6)) +* restricting sidecar image and tag setup ([#550](https://github.com/open-feature/open-feature-operator/issues/550)) ([233be79](https://github.com/open-feature/open-feature-operator/commit/233be79b56ccca32a1cb041bce53a9848f032a60)) +* update api version to v0.2.38 ([#561](https://github.com/open-feature/open-feature-operator/issues/561)) ([d1f2477](https://github.com/open-feature/open-feature-operator/commit/d1f247727c5b6f4cb5154e94f1090aee0a442346)) +* use v1beta1 in operator logic ([#539](https://github.com/open-feature/open-feature-operator/issues/539)) ([d234410](https://github.com/open-feature/open-feature-operator/commit/d234410a809760ba1c8592f95be56891e0cae855)) + + +### ๐Ÿ› Bug Fixes + +* Revert "chore: release apis 0.2.38" ([#557](https://github.com/open-feature/open-feature-operator/issues/557)) ([ccb8c1d](https://github.com/open-feature/open-feature-operator/commit/ccb8c1d6e12aa36e33239fd96bebbc57fc4ea3bc)) + + +### ๐Ÿงน Chore + +* clean up unused API code after moving to v1beta1 ([#543](https://github.com/open-feature/open-feature-operator/issues/543)) ([1287b07](https://github.com/open-feature/open-feature-operator/commit/1287b0785fd99cb8bfeaf9fe112aa8a0ed6f5cf9)) +* **deps:** update actions/setup-node action to v3.8.1 ([#522](https://github.com/open-feature/open-feature-operator/issues/522)) ([32ddf00](https://github.com/open-feature/open-feature-operator/commit/32ddf002e6c20732d990283946ec124304827bd3)) +* fix file source documentation ([#556](https://github.com/open-feature/open-feature-operator/issues/556)) ([318c52d](https://github.com/open-feature/open-feature-operator/commit/318c52d2ba38dbfee6deb3f06d3392dc14d80a6c)) +* refactor code to decrease complexity ([#554](https://github.com/open-feature/open-feature-operator/issues/554)) ([17a547f](https://github.com/open-feature/open-feature-operator/commit/17a547f88595cb6c177ca93e1a8b4ad49f3c1a5f)) +* release apis 0.2.37 ([#544](https://github.com/open-feature/open-feature-operator/issues/544)) ([854e72d](https://github.com/open-feature/open-feature-operator/commit/854e72d964fce51082220a60fc8a7319676e49c3)) +* release apis 0.2.38 ([#548](https://github.com/open-feature/open-feature-operator/issues/548)) ([c6165d4](https://github.com/open-feature/open-feature-operator/commit/c6165d426b5be2af89e03695d24fe0b802fb1fe2)) +* release apis 0.2.38 ([#558](https://github.com/open-feature/open-feature-operator/issues/558)) ([4ecbc9b](https://github.com/open-feature/open-feature-operator/commit/4ecbc9b8eeac4e1e86c0f4e11ffedf3dbc376f9a)) +* release apis 0.2.38 ([#560](https://github.com/open-feature/open-feature-operator/issues/560)) ([069e275](https://github.com/open-feature/open-feature-operator/commit/069e2754210d1a71bc5b70c0d4e6e193f62a7bcb)) +* revert recent release ([#559](https://github.com/open-feature/open-feature-operator/issues/559)) ([f7c79e4](https://github.com/open-feature/open-feature-operator/commit/f7c79e4c6f5a5dee05d7db1796bfb9891dbd53a0)) +* use apis tag instead of local replace ([#546](https://github.com/open-feature/open-feature-operator/issues/546)) ([1856918](https://github.com/open-feature/open-feature-operator/commit/18569182c1f2eca3e29e9428a64239ac16ea3c08)) +* use github-action for golangci-lint workflow ([#538](https://github.com/open-feature/open-feature-operator/issues/538)) ([a97d336](https://github.com/open-feature/open-feature-operator/commit/a97d336468d5a9b50662f4979784c8388ec10ec1)) + + +### ๐Ÿ“š Documentation + +* use v1beta1 API version ([#553](https://github.com/open-feature/open-feature-operator/issues/553)) ([ccc0471](https://github.com/open-feature/open-feature-operator/commit/ccc0471c15cb42a338cd4c1a69b0b1f7c7828837)) + ## [0.2.36](https://github.com/open-feature/open-feature-operator/compare/v0.2.35...v0.2.36) (2023-08-07) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0e90052d8..7136e757d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,7 +7,7 @@ There are a few things to consider before contributing to open-feature-operator. Firstly, there's [a code of conduct](https://github.com/open-feature/.github/blob/main/CODE_OF_CONDUCT.md). TLDR: be respectful. -Any contributions are expected to include tests. These can be validated with `make test` or the automated github workflow will run them on PR creation. +Any contributions are expected to include tests. These can be validated with `make unit-test` or the automated github workflow will run them on PR creation. The go version in the `go.mod` is the currently supported version of go. @@ -15,8 +15,8 @@ Thanks! Issues and pull requests following these guidelines are welcome. ## Development -### FeatureFlagConfiguration custom resource definition versioning -Custom resource definitions support multiple versions. The kubebuilder framework exposes a system to seamlessly convert between versions (using a "hub and spoke" model) maintaining backwards compatibility. It does this by injecting conversion webhooks that call our defined convert functions. The hub version of the `FeatureFlagConfiguration` custom resource definition (the version to which all other versions are converted) is `v1alpha1`. +### FeatureFlag custom resource definition versioning +Custom resource definitions support multiple versions. The kubebuilder framework exposes a system to seamlessly convert between versions (using a "hub and spoke" model) maintaining backwards compatibility. It does this by injecting conversion webhooks that call our defined convert functions. The hub version of the `FeatureFlag` custom resource definition (the version to which all other versions are converted) is `v1beta1`. Follow [this tutorial](https://book.kubebuilder.io/multiversion-tutorial/conversion-concepts.html) to implement a new version of the custom resource definition. ### Local build diff --git a/Dockerfile b/Dockerfile index 05dc2c319..39902ae41 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,7 @@ COPY main.go main.go COPY apis/ apis/ COPY webhooks/ webhooks/ COPY controllers/ controllers/ -COPY pkg/ pkg/ +COPY common/ common/ ARG TARGETOS ARG TARGETARCH diff --git a/Makefile b/Makefile index e812f9cee..937f3d3f7 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ ARCH?=amd64 IMG?=$(RELEASE_REGISTRY)/$(RELEASE_IMAGE) # customize overlay to be used in the build, DEFAULT or HELM KUSTOMIZE_OVERLAY ?= DEFAULT -CHART_VERSION=v0.2.36# x-release-please-version +CHART_VERSION=v0.3.0# x-release-please-version # ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. ENVTEST_K8S_VERSION = 1.26.1 @@ -63,27 +63,36 @@ vet: ## Run go vet against code. .PHONY: unit-test unit-test: manifests fmt vet generate envtest ## Run tests. - go test ./... -v -short -coverprofile cover.out - -## Requires the operator to be deployed -.PHONY: e2e-test -e2e-test: manifests generate fmt vet - kubectl -n open-feature-operator-system apply -f ./test/e2e/e2e.yml - kubectl wait --for=condition=Available=True deploy --all -n 'open-feature-operator-system' - ./test/e2e/run.sh - -.PHONY: e2e-test-kuttl #these tests should run on a real cluster! + cd apis && go test ./... -v -coverprofile ../cover-apis.out cover-main.out cover-pkg.out + go test ./... -v -coverprofile cover-operator.out + sed -i '/mode: set/d' "cover-operator.out" + sed -i '/mode: set/d' "cover-apis.out" + echo "mode: set" > cover.out + cat cover-operator.out cover-apis.out >> cover.out + rm cover-operator.out cover-apis.out + +## e2e tests require the operator to be deployed in a real cluster +.PHONY: e2e-test-kuttl e2e-test-kuttl: - kubectl kuttl test --start-kind=false ./test/e2e/kuttl --config=./kuttl-test.yaml + kubectl kuttl test --start-kind=false --config=./kuttl-test.yaml -.PHONY: e2e-test-kuttl-local #these tests should run on a real cluster! +.PHONY: e2e-test-kuttl-local e2e-test-kuttl-local: - kubectl kuttl test --start-kind=false ./test/e2e/kuttl/scenarios --config=./kuttl-test-local.yaml + kubectl kuttl test --start-kind=false --config=./kuttl-test-local.yaml + +.PHONY: e2e-test-validate-local +e2e-test-validate-local: + docker build . -t open-feature-operator-local:validate + kind create cluster --config ./test/e2e/kind-cluster.yml --name e2e-tests + kind load docker-image open-feature-operator-local:validate --name e2e-tests + IMG=open-feature-operator-local:validate make deploy-operator + IMG=open-feature-operator-local:validate make e2e-test-kuttl + kind delete cluster --name e2e-tests .PHONY: lint lint: go install -v github.com/golangci/golangci-lint/cmd/golangci-lint@latest - ${GOPATH}/bin/golangci-lint run --deadline=3m --timeout=3m ./... # Run linters + ${GOPATH}/bin/golangci-lint run --deadline=3m --timeout=3m --config=./.golangci.yml -v ./... # Run linters .PHONY: generate-crdocs generate-crdocs: kustomize crdocs @@ -148,7 +157,7 @@ release-manifests: manifests kustomize fi .PHONY: deploy -deploy: generate manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. +deploy: generate kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. cd config/manager && $(KUSTOMIZE) edit set image controller=$(IMG) $(KUSTOMIZE) build config/default | kubectl apply -f - diff --git a/PROJECT b/PROJECT index aaaa4457a..face2ff62 100644 --- a/PROJECT +++ b/PROJECT @@ -43,4 +43,20 @@ resources: kind: FlagSourceConfiguration path: github.com/open-feature/open-feature-operator/apis/core/v1alpha3 version: v1alpha3 +- api: + crdVersion: v1 + namespaced: true + domain: openfeature.dev + group: core + kind: FeatureFlag + path: github.com/open-feature/open-feature-operator/apis/core/v1beta1 + version: v1beta1 +- api: + crdVersion: v1 + namespaced: true + domain: openfeature.dev + group: core + kind: FeatureFlagSource + path: github.com/open-feature/open-feature-operator/apis/core/v1beta1 + version: v1beta1 version: "3" diff --git a/apis/CHANGELOG.md b/apis/CHANGELOG.md new file mode 100644 index 000000000..112c82278 --- /dev/null +++ b/apis/CHANGELOG.md @@ -0,0 +1,39 @@ +# Changelog + +## [0.2.38](https://github.com/open-feature/open-feature-operator/compare/apis/v0.2.37...apis/v0.2.38) (2023-11-29) + + +### โœจ New Features + +* introduce v1beta1/common package ([#547](https://github.com/open-feature/open-feature-operator/issues/547)) ([cdc4af4](https://github.com/open-feature/open-feature-operator/commit/cdc4af495f370da7165fd67ad9ef54ccf74be3bf)) +* prepare apis for v1beta1 controllers onboarding ([#549](https://github.com/open-feature/open-feature-operator/issues/549)) ([e3c8b42](https://github.com/open-feature/open-feature-operator/commit/e3c8b4290be99d78b88ffef686531a38b97e61be)) + + +### ๐Ÿ› Bug Fixes + +* Revert "chore: release apis 0.2.38" ([#557](https://github.com/open-feature/open-feature-operator/issues/557)) ([ccb8c1d](https://github.com/open-feature/open-feature-operator/commit/ccb8c1d6e12aa36e33239fd96bebbc57fc4ea3bc)) + + +### ๐Ÿงน Chore + +* adapt API for sidecar image and tag restriction ([#552](https://github.com/open-feature/open-feature-operator/issues/552)) ([726a7f7](https://github.com/open-feature/open-feature-operator/commit/726a7f7149067d2e2696f746a236151fbb67808c)) +* adapt shortcuts for custom resources ([#551](https://github.com/open-feature/open-feature-operator/issues/551)) ([61c77c0](https://github.com/open-feature/open-feature-operator/commit/61c77c0c137ec624892c9738ee45828a137f6823)) +* clean up unused API code after moving to v1beta1 ([#543](https://github.com/open-feature/open-feature-operator/issues/543)) ([1287b07](https://github.com/open-feature/open-feature-operator/commit/1287b0785fd99cb8bfeaf9fe112aa8a0ed6f5cf9)) +* fix file source documentation ([#556](https://github.com/open-feature/open-feature-operator/issues/556)) ([318c52d](https://github.com/open-feature/open-feature-operator/commit/318c52d2ba38dbfee6deb3f06d3392dc14d80a6c)) +* refactor code to decrease complexity ([#554](https://github.com/open-feature/open-feature-operator/issues/554)) ([17a547f](https://github.com/open-feature/open-feature-operator/commit/17a547f88595cb6c177ca93e1a8b4ad49f3c1a5f)) +* release apis 0.2.38 ([#548](https://github.com/open-feature/open-feature-operator/issues/548)) ([c6165d4](https://github.com/open-feature/open-feature-operator/commit/c6165d426b5be2af89e03695d24fe0b802fb1fe2)) +* release apis 0.2.38 ([#558](https://github.com/open-feature/open-feature-operator/issues/558)) ([4ecbc9b](https://github.com/open-feature/open-feature-operator/commit/4ecbc9b8eeac4e1e86c0f4e11ffedf3dbc376f9a)) +* revert recent release ([#559](https://github.com/open-feature/open-feature-operator/issues/559)) ([f7c79e4](https://github.com/open-feature/open-feature-operator/commit/f7c79e4c6f5a5dee05d7db1796bfb9891dbd53a0)) + +## [0.2.37](https://github.com/open-feature/open-feature-operator/compare/apis-v0.2.36...apis/v0.2.37) (2023-11-15) + + +### โœจ New Features + +* Introduce v1beta1 API version ([#535](https://github.com/open-feature/open-feature-operator/issues/535)) ([3acd492](https://github.com/open-feature/open-feature-operator/commit/3acd49289a40e8f07fd20aad46185ac42ceb1b7a)) +* release APIs and Operator independently ([#541](https://github.com/open-feature/open-feature-operator/issues/541)) ([7b1af42](https://github.com/open-feature/open-feature-operator/commit/7b1af42ac41e63ccbb1820b31f579ffea679cff6)) + + +### ๐Ÿงน Chore + +* use github-action for golangci-lint workflow ([#538](https://github.com/open-feature/open-feature-operator/issues/538)) ([a97d336](https://github.com/open-feature/open-feature-operator/commit/a97d336468d5a9b50662f4979784c8388ec10ec1)) diff --git a/apis/core/v1alpha1/featureflagconfiguration_conversion.go b/apis/core/v1alpha1/featureflagconfiguration_conversion.go deleted file mode 100644 index 1d9e0624a..000000000 --- a/apis/core/v1alpha1/featureflagconfiguration_conversion.go +++ /dev/null @@ -1,20 +0,0 @@ -/* -Copyright 2022. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package v1alpha1 - -// Hub marks this type as a conversion hub. -func (ffc *FeatureFlagConfiguration) Hub() {} diff --git a/apis/core/v1alpha1/featureflagconfiguration_types.go b/apis/core/v1alpha1/featureflagconfiguration_types.go index ead14c34b..34ab029c3 100644 --- a/apis/core/v1alpha1/featureflagconfiguration_types.go +++ b/apis/core/v1alpha1/featureflagconfiguration_types.go @@ -17,19 +17,12 @@ limitations under the License. package v1alpha1 import ( - "github.com/open-feature/open-feature-operator/pkg/utils" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! -// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. - // FeatureFlagConfigurationSpec defines the desired state of FeatureFlagConfiguration type FeatureFlagConfigurationSpec struct { - // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster - // Important: Run "make" to regenerate code after modifying this file - // ServiceProvider [DEPRECATED]: superseded by FlagSourceConfiguration // +optional // +nullable @@ -78,8 +71,6 @@ type FeatureFlagServiceProvider struct { // FeatureFlagConfigurationStatus defines the observed state of FeatureFlagConfiguration type FeatureFlagConfigurationStatus struct { - // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster - // Important: Run "make" to regenerate code after modifying this file } //+kubebuilder:object:root=true @@ -107,33 +98,3 @@ type FeatureFlagConfigurationList struct { func init() { SchemeBuilder.Register(&FeatureFlagConfiguration{}, &FeatureFlagConfigurationList{}) } - -func (ff *FeatureFlagConfiguration) GetReference() metav1.OwnerReference { - return metav1.OwnerReference{ - APIVersion: ff.APIVersion, - Kind: ff.Kind, - Name: ff.Name, - UID: ff.UID, - Controller: utils.TrueVal(), - } -} - -func (ff *FeatureFlagConfiguration) GenerateConfigMap(name string, namespace string, references []metav1.OwnerReference) corev1.ConfigMap { - return corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Annotations: map[string]string{ - "openfeature.dev/featureflagconfiguration": name, - }, - OwnerReferences: references, - }, - Data: map[string]string{ - utils.FeatureFlagConfigurationConfigMapKey(namespace, name): ff.Spec.FeatureFlagSpec, - }, - } -} - -func (p *FeatureFlagServiceProvider) IsSet() bool { - return p != nil && p.Name != "" -} diff --git a/apis/core/v1alpha1/featureflagconfiguration_types_test.go b/apis/core/v1alpha1/featureflagconfiguration_types_test.go deleted file mode 100644 index dd886342c..000000000 --- a/apis/core/v1alpha1/featureflagconfiguration_types_test.go +++ /dev/null @@ -1,80 +0,0 @@ -package v1alpha1 - -import ( - "testing" - - "github.com/open-feature/open-feature-operator/pkg/utils" - "github.com/stretchr/testify/require" - corev1 "k8s.io/api/core/v1" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" -) - -func Test_FeatureFlagConfiguration(t *testing.T) { - ffConfig := FeatureFlagConfiguration{ - ObjectMeta: v1.ObjectMeta{ - Name: "ffconf1", - Namespace: "test", - OwnerReferences: []v1.OwnerReference{ - { - APIVersion: "ver", - Kind: "kind", - Name: "ffconf1", - UID: types.UID("5"), - Controller: utils.TrueVal(), - }, - }, - }, - Spec: FeatureFlagConfigurationSpec{ - FeatureFlagSpec: "spec", - }, - } - - name := "cmname" - namespace := "cmnamespace" - references := []v1.OwnerReference{ - { - APIVersion: "ver", - Kind: "kind", - Name: "ffconf1", - UID: types.UID("5"), - Controller: utils.TrueVal(), - }, - } - - require.Equal(t, v1.OwnerReference{ - APIVersion: ffConfig.APIVersion, - Kind: ffConfig.Kind, - Name: ffConfig.Name, - UID: ffConfig.UID, - Controller: utils.TrueVal(), - }, ffConfig.GetReference()) - - require.Equal(t, corev1.ConfigMap{ - ObjectMeta: v1.ObjectMeta{ - Name: name, - Namespace: namespace, - Annotations: map[string]string{ - "openfeature.dev/featureflagconfiguration": name, - }, - OwnerReferences: references, - }, - Data: map[string]string{ - "cmnamespace_cmname.flagd.json": "spec", - }, - }, ffConfig.GenerateConfigMap(name, namespace, references)) - - require.False(t, ffConfig.Spec.ServiceProvider.IsSet()) - - ffConfig.Spec.ServiceProvider = &FeatureFlagServiceProvider{ - Name: "", - } - - require.False(t, ffConfig.Spec.ServiceProvider.IsSet()) - - ffConfig.Spec.ServiceProvider = &FeatureFlagServiceProvider{ - Name: "prov", - } - - require.True(t, ffConfig.Spec.ServiceProvider.IsSet()) -} diff --git a/apis/core/v1alpha1/featureflagconfiguration_webhook.go b/apis/core/v1alpha1/featureflagconfiguration_webhook.go deleted file mode 100644 index 46e2897c2..000000000 --- a/apis/core/v1alpha1/featureflagconfiguration_webhook.go +++ /dev/null @@ -1,9 +0,0 @@ -package v1alpha1 - -import ctrl "sigs.k8s.io/controller-runtime" - -func (r *FeatureFlagConfiguration) SetupWebhookWithManager(mgr ctrl.Manager) error { - return ctrl.NewWebhookManagedBy(mgr). - For(r). - Complete() -} diff --git a/apis/core/v1alpha1/flagsourceconfiguration_conversion.go b/apis/core/v1alpha1/flagsourceconfiguration_conversion.go deleted file mode 100644 index 883c45ab3..000000000 --- a/apis/core/v1alpha1/flagsourceconfiguration_conversion.go +++ /dev/null @@ -1,20 +0,0 @@ -/* -Copyright 2022. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package v1alpha1 - -// Hub marks this type as a conversion hub. -func (ffc *FlagSourceConfiguration) Hub() {} diff --git a/apis/core/v1alpha1/flagsourceconfiguration_types.go b/apis/core/v1alpha1/flagsourceconfiguration_types.go index bee950c5d..77595816a 100644 --- a/apis/core/v1alpha1/flagsourceconfiguration_types.go +++ b/apis/core/v1alpha1/flagsourceconfiguration_types.go @@ -17,56 +17,14 @@ limitations under the License. package v1alpha1 import ( - "fmt" - "os" - "strconv" - "strings" - - "github.com/open-feature/open-feature-operator/pkg/utils" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type SyncProviderType string -const ( - SidecarEnvVarPrefix string = "SIDECAR_ENV_VAR_PREFIX" - InputConfigurationEnvVarPrefix string = "SIDECAR" - SidecarMetricPortEnvVar string = "METRICS_PORT" - SidecarPortEnvVar string = "PORT" - SidecarSocketPathEnvVar string = "SOCKET_PATH" - SidecarEvaluatorEnvVar string = "EVALUATOR" - SidecarImageEnvVar string = "IMAGE" - SidecarVersionEnvVar string = "TAG" - SidecarProviderArgsEnvVar string = "PROVIDER_ARGS" - SidecarDefaultSyncProviderEnvVar string = "SYNC_PROVIDER" - SidecarLogFormatEnvVar string = "LOG_FORMAT" - SidecarProbesEnabledVar string = "PROBES_ENABLED" - defaultSidecarEnvVarPrefix string = "FLAGD" - DefaultMetricPort int32 = 8014 - defaultPort int32 = 8013 - defaultSocketPath string = "" - defaultEvaluator string = "json" - defaultImage string = "ghcr.io/open-feature/flagd" - // renovate: datasource=github-tags depName=open-feature/flagd/flagd - defaultTag string = "v0.6.3" - defaultLogFormat string = "json" - defaultProbesEnabled bool = true - SyncProviderKubernetes SyncProviderType = "kubernetes" - SyncProviderFilepath SyncProviderType = "filepath" - SyncProviderHttp SyncProviderType = "http" - SyncProviderGrpc SyncProviderType = "grpc" - SyncProviderFlagdProxy SyncProviderType = "flagd-proxy" -) - -// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! -// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. - // FlagSourceConfigurationSpec defines the desired state of FlagSourceConfiguration type FlagSourceConfigurationSpec struct { - // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster - // Important: Run "make" to regenerate code after modifying this file - // MetricsPort defines the port to serve metrics on, defaults to 8014 // +optional MetricsPort int32 `json:"metricsPort"` @@ -169,8 +127,6 @@ type Source struct { // FlagSourceConfigurationStatus defines the observed state of FlagSourceConfiguration type FlagSourceConfigurationStatus struct { - // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster - // Important: Run "make" to regenerate code after modifying this file } // +kubebuilder:resource:shortName="fsc" @@ -196,214 +152,6 @@ type FlagSourceConfigurationList struct { Items []FlagSourceConfiguration `json:"items"` } -func NewFlagSourceConfigurationSpec() (*FlagSourceConfigurationSpec, error) { - fsc := &FlagSourceConfigurationSpec{ - MetricsPort: DefaultMetricPort, - Port: defaultPort, - SocketPath: defaultSocketPath, - Evaluator: defaultEvaluator, - Image: defaultImage, - Tag: defaultTag, - Sources: []Source{}, - EnvVars: []corev1.EnvVar{}, - SyncProviderArgs: []string{}, - DefaultSyncProvider: SyncProviderKubernetes, - EnvVarPrefix: defaultSidecarEnvVarPrefix, - LogFormat: defaultLogFormat, - RolloutOnChange: nil, - DebugLogging: utils.FalseVal(), - OtelCollectorUri: "", - } - - // set default value derived from constant default - probes := defaultProbesEnabled - fsc.ProbesEnabled = &probes - - if metricsPort := os.Getenv(envVarKey(InputConfigurationEnvVarPrefix, SidecarMetricPortEnvVar)); metricsPort != "" { - metricsPortI, err := strconv.Atoi(metricsPort) - if err != nil { - return fsc, fmt.Errorf("unable to parse metrics port value %s to int32: %w", metricsPort, err) - } - fsc.MetricsPort = int32(metricsPortI) - } - - if port := os.Getenv(envVarKey(InputConfigurationEnvVarPrefix, SidecarPortEnvVar)); port != "" { - portI, err := strconv.Atoi(port) - if err != nil { - return fsc, fmt.Errorf("unable to parse sidecar port value %s to int32: %w", port, err) - } - fsc.Port = int32(portI) - } - - if socketPath := os.Getenv(envVarKey(InputConfigurationEnvVarPrefix, SidecarSocketPathEnvVar)); socketPath != "" { - fsc.SocketPath = socketPath - } - - if evaluator := os.Getenv(envVarKey(InputConfigurationEnvVarPrefix, SidecarEvaluatorEnvVar)); evaluator != "" { - fsc.Evaluator = evaluator - } - - if image := os.Getenv(envVarKey(InputConfigurationEnvVarPrefix, SidecarImageEnvVar)); image != "" { - fsc.Image = image - } - - if tag := os.Getenv(envVarKey(InputConfigurationEnvVarPrefix, SidecarVersionEnvVar)); tag != "" { - fsc.Tag = tag - } - - if syncProviderArgs := os.Getenv(envVarKey(InputConfigurationEnvVarPrefix, SidecarProviderArgsEnvVar)); syncProviderArgs != "" { - fsc.SyncProviderArgs = strings.Split(syncProviderArgs, ",") // todo: add documentation for this - } - - if syncProvider := os.Getenv(envVarKey(InputConfigurationEnvVarPrefix, SidecarDefaultSyncProviderEnvVar)); syncProvider != "" { - fsc.DefaultSyncProvider = SyncProviderType(syncProvider) - } - - if logFormat := os.Getenv(envVarKey(InputConfigurationEnvVarPrefix, SidecarLogFormatEnvVar)); logFormat != "" { - fsc.LogFormat = logFormat - } - - if envVarPrefix := os.Getenv(SidecarEnvVarPrefix); envVarPrefix != "" { - fsc.EnvVarPrefix = envVarPrefix - } - - if probesEnabled := os.Getenv(envVarKey(InputConfigurationEnvVarPrefix, SidecarProbesEnabledVar)); probesEnabled != "" { - b, err := strconv.ParseBool(probesEnabled) - if err != nil { - return fsc, fmt.Errorf("unable to parse sidecar probes enabled %s to boolean: %w", probesEnabled, err) - } - fsc.ProbesEnabled = &b - } - - return fsc, nil -} - -func (fc *FlagSourceConfigurationSpec) Merge(new *FlagSourceConfigurationSpec) { - if new == nil { - return - } - if new.MetricsPort != 0 { - fc.MetricsPort = new.MetricsPort - } - if new.Port != 0 { - fc.Port = new.Port - } - if new.SocketPath != "" { - fc.SocketPath = new.SocketPath - } - if new.Evaluator != "" { - fc.Evaluator = new.Evaluator - } - if new.Image != "" { - fc.Image = new.Image - } - if new.Tag != "" { - fc.Tag = new.Tag - } - if len(new.Sources) != 0 { - fc.Sources = append(fc.Sources, new.Sources...) - } - if len(new.EnvVars) != 0 { - fc.EnvVars = append(fc.EnvVars, new.EnvVars...) - } - if new.SyncProviderArgs != nil && len(new.SyncProviderArgs) > 0 { - fc.SyncProviderArgs = append(fc.SyncProviderArgs, new.SyncProviderArgs...) - } - if new.EnvVarPrefix != "" { - fc.EnvVarPrefix = new.EnvVarPrefix - } - if new.DefaultSyncProvider != "" { - fc.DefaultSyncProvider = new.DefaultSyncProvider - } - if new.LogFormat != "" { - fc.LogFormat = new.LogFormat - } - if new.RolloutOnChange != nil { - fc.RolloutOnChange = new.RolloutOnChange - } - if new.ProbesEnabled != nil { - fc.ProbesEnabled = new.ProbesEnabled - } - if new.DebugLogging != nil { - fc.DebugLogging = new.DebugLogging - } - if new.OtelCollectorUri != "" { - fc.OtelCollectorUri = new.OtelCollectorUri - } -} - -func (fc *FlagSourceConfigurationSpec) ToEnvVars() []corev1.EnvVar { - envs := []corev1.EnvVar{} - - for _, envVar := range fc.EnvVars { - envs = append(envs, corev1.EnvVar{ - Name: envVarKey(fc.EnvVarPrefix, envVar.Name), - Value: envVar.Value, - }) - } - - if fc.MetricsPort != DefaultMetricPort { - envs = append(envs, corev1.EnvVar{ - Name: envVarKey(fc.EnvVarPrefix, SidecarMetricPortEnvVar), - Value: fmt.Sprintf("%d", fc.MetricsPort), - }) - } - - if fc.Port != defaultPort { - envs = append(envs, corev1.EnvVar{ - Name: envVarKey(fc.EnvVarPrefix, SidecarPortEnvVar), - Value: fmt.Sprintf("%d", fc.Port), - }) - } - - if fc.Evaluator != defaultEvaluator { - envs = append(envs, corev1.EnvVar{ - Name: envVarKey(fc.EnvVarPrefix, SidecarEvaluatorEnvVar), - Value: fc.Evaluator, - }) - } - - if fc.SocketPath != defaultSocketPath { - envs = append(envs, corev1.EnvVar{ - Name: envVarKey(fc.EnvVarPrefix, SidecarSocketPathEnvVar), - Value: fc.SocketPath, - }) - } - - if fc.LogFormat != defaultLogFormat { - envs = append(envs, corev1.EnvVar{ - Name: envVarKey(fc.EnvVarPrefix, SidecarLogFormatEnvVar), - Value: fc.LogFormat, - }) - } - - return envs -} - -func (s SyncProviderType) IsKubernetes() bool { - return s == SyncProviderKubernetes -} - -func (s SyncProviderType) IsHttp() bool { - return s == SyncProviderHttp -} - -func (s SyncProviderType) IsFilepath() bool { - return s == SyncProviderFilepath -} - -func (s SyncProviderType) IsGrpc() bool { - return s == SyncProviderGrpc -} - -func (s SyncProviderType) IsFlagdProxy() bool { - return s == SyncProviderFlagdProxy -} - -func envVarKey(prefix string, suffix string) string { - return fmt.Sprintf("%s_%s", prefix, suffix) -} - func init() { SchemeBuilder.Register(&FlagSourceConfiguration{}, &FlagSourceConfigurationList{}) } diff --git a/apis/core/v1alpha1/flagsourceconfiguration_types_test.go b/apis/core/v1alpha1/flagsourceconfiguration_types_test.go deleted file mode 100644 index a3d2b9295..000000000 --- a/apis/core/v1alpha1/flagsourceconfiguration_types_test.go +++ /dev/null @@ -1,303 +0,0 @@ -package v1alpha1 - -import ( - "testing" - - "github.com/open-feature/open-feature-operator/pkg/utils" - "github.com/stretchr/testify/require" - v1 "k8s.io/api/core/v1" -) - -func Test_FLagSourceConfiguration_SyncProvider(t *testing.T) { - k := SyncProviderKubernetes - f := SyncProviderFilepath - h := SyncProviderHttp - g := SyncProviderGrpc - - require.True(t, k.IsKubernetes()) - require.True(t, f.IsFilepath()) - require.True(t, h.IsHttp()) - require.True(t, g.IsGrpc()) - - require.False(t, f.IsKubernetes()) - require.False(t, h.IsFilepath()) - require.False(t, k.IsGrpc()) - require.False(t, g.IsHttp()) -} - -func Test_FLagSourceConfiguration_envVarKey(t *testing.T) { - require.Equal(t, "pre_suf", envVarKey("pre", "suf")) -} - -func Test_FLagSourceConfiguration_ToEnvVars(t *testing.T) { - ff := FlagSourceConfiguration{ - Spec: FlagSourceConfigurationSpec{ - EnvVars: []v1.EnvVar{ - { - Name: "env1", - Value: "val1", - }, - { - Name: "env2", - Value: "val2", - }, - }, - EnvVarPrefix: "PRE", - MetricsPort: 22, - Port: 33, - Evaluator: "evaluator", - SocketPath: "socket-path", - LogFormat: "log", - }, - } - expected := []v1.EnvVar{ - { - Name: "PRE_env1", - Value: "val1", - }, - { - Name: "PRE_env2", - Value: "val2", - }, - { - Name: "PRE_METRICS_PORT", - Value: "22", - }, - { - Name: "PRE_PORT", - Value: "33", - }, - { - Name: "PRE_EVALUATOR", - Value: "evaluator", - }, - { - Name: "PRE_SOCKET_PATH", - Value: "socket-path", - }, - { - Name: "PRE_LOG_FORMAT", - Value: "log", - }, - } - require.Equal(t, expected, ff.Spec.ToEnvVars()) -} - -func Test_FLagSourceConfiguration_Merge(t *testing.T) { - ff_old := &FlagSourceConfiguration{ - Spec: FlagSourceConfigurationSpec{ - EnvVars: []v1.EnvVar{ - { - Name: "env1", - Value: "val1", - }, - { - Name: "env2", - Value: "val2", - }, - }, - EnvVarPrefix: "PRE", - MetricsPort: 22, - Port: 33, - Evaluator: "evaluator", - SocketPath: "socket-path", - LogFormat: "log", - Image: "img", - Tag: "tag", - Sources: []Source{ - { - Source: "src1", - Provider: SyncProviderGrpc, - TLS: true, - CertPath: "etc/cert.ca", - ProviderID: "app", - Selector: "source=database", - }, - }, - SyncProviderArgs: []string{"arg1", "arg2"}, - DefaultSyncProvider: SyncProviderKubernetes, - RolloutOnChange: utils.TrueVal(), - ProbesEnabled: utils.TrueVal(), - DebugLogging: utils.TrueVal(), - OtelCollectorUri: "", - }, - } - - ff_old.Spec.Merge(nil) - - require.Equal(t, &FlagSourceConfiguration{ - Spec: FlagSourceConfigurationSpec{ - EnvVars: []v1.EnvVar{ - { - Name: "env1", - Value: "val1", - }, - { - Name: "env2", - Value: "val2", - }, - }, - EnvVarPrefix: "PRE", - MetricsPort: 22, - Port: 33, - Evaluator: "evaluator", - SocketPath: "socket-path", - LogFormat: "log", - Image: "img", - Tag: "tag", - Sources: []Source{ - { - Source: "src1", - Provider: SyncProviderGrpc, - TLS: true, - CertPath: "etc/cert.ca", - ProviderID: "app", - Selector: "source=database", - }, - }, - SyncProviderArgs: []string{"arg1", "arg2"}, - DefaultSyncProvider: SyncProviderKubernetes, - RolloutOnChange: utils.TrueVal(), - ProbesEnabled: utils.TrueVal(), - DebugLogging: utils.TrueVal(), - OtelCollectorUri: "", - }, - }, ff_old) - - ff_new := &FlagSourceConfiguration{ - Spec: FlagSourceConfigurationSpec{ - EnvVars: []v1.EnvVar{ - { - Name: "env3", - Value: "val3", - }, - { - Name: "env4", - Value: "val4", - }, - }, - EnvVarPrefix: "PREFIX", - MetricsPort: 221, - Port: 331, - Evaluator: "evaluator1", - SocketPath: "socket-path1", - LogFormat: "log1", - Image: "img1", - Tag: "tag1", - Sources: []Source{ - { - Source: "src2", - Provider: SyncProviderFilepath, - }, - }, - SyncProviderArgs: []string{"arg3", "arg4"}, - DefaultSyncProvider: SyncProviderFilepath, - RolloutOnChange: utils.FalseVal(), - ProbesEnabled: utils.FalseVal(), - DebugLogging: utils.FalseVal(), - OtelCollectorUri: "", - }, - } - - ff_old.Spec.Merge(&ff_new.Spec) - - require.Equal(t, &FlagSourceConfiguration{ - Spec: FlagSourceConfigurationSpec{ - EnvVars: []v1.EnvVar{ - { - Name: "env1", - Value: "val1", - }, - { - Name: "env2", - Value: "val2", - }, - { - Name: "env3", - Value: "val3", - }, - { - Name: "env4", - Value: "val4", - }, - }, - EnvVarPrefix: "PREFIX", - MetricsPort: 221, - Port: 331, - Evaluator: "evaluator1", - SocketPath: "socket-path1", - LogFormat: "log1", - Image: "img1", - Tag: "tag1", - Sources: []Source{ - { - Source: "src1", - Provider: SyncProviderGrpc, - TLS: true, - CertPath: "etc/cert.ca", - ProviderID: "app", - Selector: "source=database", - }, - { - Source: "src2", - Provider: SyncProviderFilepath, - }, - }, - SyncProviderArgs: []string{"arg1", "arg2", "arg3", "arg4"}, - DefaultSyncProvider: SyncProviderFilepath, - RolloutOnChange: utils.FalseVal(), - ProbesEnabled: utils.FalseVal(), - DebugLogging: utils.FalseVal(), - OtelCollectorUri: "", - }, - }, ff_old) -} - -func Test_FLagSourceConfiguration_NewFlagSourceConfigurationSpec(t *testing.T) { - //happy path - t.Setenv(envVarKey(InputConfigurationEnvVarPrefix, SidecarMetricPortEnvVar), "22") - t.Setenv(envVarKey(InputConfigurationEnvVarPrefix, SidecarPortEnvVar), "33") - t.Setenv(envVarKey(InputConfigurationEnvVarPrefix, SidecarSocketPathEnvVar), "val1") - t.Setenv(envVarKey(InputConfigurationEnvVarPrefix, SidecarEvaluatorEnvVar), "val2") - t.Setenv(envVarKey(InputConfigurationEnvVarPrefix, SidecarImageEnvVar), "val3") - t.Setenv(envVarKey(InputConfigurationEnvVarPrefix, SidecarVersionEnvVar), "val4") - t.Setenv(envVarKey(InputConfigurationEnvVarPrefix, SidecarProviderArgsEnvVar), "val11,val22") - t.Setenv(envVarKey(InputConfigurationEnvVarPrefix, SidecarDefaultSyncProviderEnvVar), "kubernetes") - t.Setenv(envVarKey(InputConfigurationEnvVarPrefix, SidecarLogFormatEnvVar), "val5") - t.Setenv(SidecarEnvVarPrefix, "val6") - t.Setenv(envVarKey(InputConfigurationEnvVarPrefix, SidecarProbesEnabledVar), "true") - - fs, err := NewFlagSourceConfigurationSpec() - - require.Nil(t, err) - require.Equal(t, &FlagSourceConfigurationSpec{ - MetricsPort: 22, - Port: 33, - SocketPath: "val1", - Evaluator: "val2", - Image: "val3", - Tag: "val4", - Sources: []Source{}, - EnvVars: []v1.EnvVar{}, - SyncProviderArgs: []string{"val11", "val22"}, - DefaultSyncProvider: SyncProviderKubernetes, - EnvVarPrefix: "val6", - LogFormat: "val5", - ProbesEnabled: utils.TrueVal(), - DebugLogging: utils.FalseVal(), - OtelCollectorUri: "", - }, fs) - - //error paths - t.Setenv(envVarKey(InputConfigurationEnvVarPrefix, SidecarProbesEnabledVar), "blah") - _, err = NewFlagSourceConfigurationSpec() - require.NotNil(t, err) - - t.Setenv(envVarKey(InputConfigurationEnvVarPrefix, SidecarPortEnvVar), "blah") - _, err = NewFlagSourceConfigurationSpec() - require.NotNil(t, err) - - t.Setenv(envVarKey(InputConfigurationEnvVarPrefix, SidecarMetricPortEnvVar), "blah") - _, err = NewFlagSourceConfigurationSpec() - require.NotNil(t, err) -} diff --git a/apis/core/v1alpha1/flagsourceconfiguration_webhook.go b/apis/core/v1alpha1/flagsourceconfiguration_webhook.go deleted file mode 100644 index e75324e87..000000000 --- a/apis/core/v1alpha1/flagsourceconfiguration_webhook.go +++ /dev/null @@ -1,11 +0,0 @@ -package v1alpha1 - -import ( - ctrl "sigs.k8s.io/controller-runtime" -) - -func (r *FlagSourceConfiguration) SetupWebhookWithManager(mgr ctrl.Manager) error { - return ctrl.NewWebhookManagedBy(mgr). - For(r). - Complete() -} diff --git a/apis/core/v1alpha2/common/common.go b/apis/core/v1alpha2/common/common.go deleted file mode 100644 index 87bbc63c9..000000000 --- a/apis/core/v1alpha2/common/common.go +++ /dev/null @@ -1,6 +0,0 @@ -package common - -import "errors" - -var ErrCannotCastFlagSourceConfiguration = errors.New("cannot cast FlagSourceConfiguration to v1alpha2") -var ErrCannotCastFeatureFlagConfiguration = errors.New("cannot cast FeatureFlagConfiguration to v1alpha2") diff --git a/apis/core/v1alpha2/featureflagconfiguration_conversion.go b/apis/core/v1alpha2/featureflagconfiguration_conversion.go deleted file mode 100644 index a82dabfee..000000000 --- a/apis/core/v1alpha2/featureflagconfiguration_conversion.go +++ /dev/null @@ -1,112 +0,0 @@ -/* -Copyright 2022. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package v1alpha2 - -import ( - "encoding/json" - "fmt" - - "github.com/open-feature/open-feature-operator/apis/core/v1alpha1" - "github.com/open-feature/open-feature-operator/apis/core/v1alpha2/common" - "sigs.k8s.io/controller-runtime/pkg/conversion" -) - -func (src *FeatureFlagConfiguration) ConvertTo(dstRaw conversion.Hub) error { - dst, ok := dstRaw.(*v1alpha1.FeatureFlagConfiguration) - - if !ok { - return fmt.Errorf("type %T %w", dstRaw, common.ErrCannotCastFeatureFlagConfiguration) - } - - // Copy equal stuff to new object - // DO NOT COPY TypeMeta - dst.ObjectMeta = src.ObjectMeta - - if src.Spec.ServiceProvider != nil { - dst.Spec.ServiceProvider = &v1alpha1.FeatureFlagServiceProvider{ - Name: src.Spec.ServiceProvider.Name, - Credentials: src.Spec.ServiceProvider.Credentials, - } - } - - if src.Spec.SyncProvider != nil { - dst.Spec.SyncProvider = &v1alpha1.FeatureFlagSyncProvider{Name: src.Spec.SyncProvider.Name} - if src.Spec.SyncProvider.HttpSyncConfiguration != nil { - dst.Spec.SyncProvider.HttpSyncConfiguration = &v1alpha1.HttpSyncConfiguration{ - Target: src.Spec.SyncProvider.HttpSyncConfiguration.Target, - BearerToken: src.Spec.SyncProvider.HttpSyncConfiguration.BearerToken, - } - } - } - - if src.Spec.FlagDSpec != nil { - dst.Spec.FlagDSpec = &v1alpha1.FlagDSpec{Envs: src.Spec.FlagDSpec.Envs} - } - - featureFlagSpecB, err := json.Marshal(src.Spec.FeatureFlagSpec) - if err != nil { - return fmt.Errorf("featureflagspec: %w", err) - } - - dst.Spec.FeatureFlagSpec = string(featureFlagSpecB) - - return nil -} - -func (dst *FeatureFlagConfiguration) ConvertFrom(srcRaw conversion.Hub) error { - src, ok := srcRaw.(*v1alpha1.FeatureFlagConfiguration) - - if !ok { - return fmt.Errorf("type %T %w", srcRaw, common.ErrCannotCastFeatureFlagConfiguration) - } - - // Copy equal stuff to new object - // DO NOT COPY TypeMeta - dst.ObjectMeta = src.ObjectMeta - - if src.Spec.ServiceProvider != nil { - dst.Spec.ServiceProvider = &FeatureFlagServiceProvider{ - Name: src.Spec.ServiceProvider.Name, - Credentials: src.Spec.ServiceProvider.Credentials, - } - } - - if src.Spec.SyncProvider != nil { - dst.Spec.SyncProvider = &FeatureFlagSyncProvider{ - Name: string(src.Spec.SyncProvider.Name), - } - if src.Spec.SyncProvider.HttpSyncConfiguration != nil { - dst.Spec.SyncProvider.HttpSyncConfiguration = &HttpSyncConfiguration{ - Target: src.Spec.SyncProvider.HttpSyncConfiguration.Target, - BearerToken: src.Spec.SyncProvider.HttpSyncConfiguration.BearerToken, - } - } - } - - if src.Spec.FlagDSpec != nil { - dst.Spec.FlagDSpec = &FlagDSpec{Envs: src.Spec.FlagDSpec.Envs} - } - - var featureFlagSpec FeatureFlagSpec - if err := json.Unmarshal([]byte(src.Spec.FeatureFlagSpec), &featureFlagSpec); err != nil { - return fmt.Errorf("featureflagspec: %w", err) - } - - dst.Spec.FeatureFlagSpec = featureFlagSpec - - return nil -} diff --git a/apis/core/v1alpha2/featureflagconfiguration_conversion_test.go b/apis/core/v1alpha2/featureflagconfiguration_conversion_test.go deleted file mode 100644 index d88ee32ab..000000000 --- a/apis/core/v1alpha2/featureflagconfiguration_conversion_test.go +++ /dev/null @@ -1,318 +0,0 @@ -package v1alpha2 - -import ( - "testing" - - "github.com/open-feature/open-feature-operator/apis/core/v1alpha1" - "github.com/open-feature/open-feature-operator/apis/core/v1alpha2/common" - "github.com/stretchr/testify/require" - corev1 "k8s.io/api/core/v1" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - v2 "sigs.k8s.io/controller-runtime/pkg/webhook/conversion/testdata/api/v2" -) - -func TestFeatureFlagConfiguration_ConvertFrom(t *testing.T) { - tests := []struct { - name string - srcObj *v1alpha1.FeatureFlagConfiguration - wantErr bool - wantObj *FeatureFlagConfiguration - }{ - { - name: "Test that conversion from v1alpha1 to v1alpha2 works", - srcObj: &v1alpha1.FeatureFlagConfiguration{ - TypeMeta: v1.TypeMeta{ - Kind: "FeatureFlagConfiguration", - APIVersion: "core.openfeature.dev/v1alpha1", - }, - ObjectMeta: v1.ObjectMeta{ - Name: "FeatureFlagconfig1", - Namespace: "default", - }, - Spec: v1alpha1.FeatureFlagConfigurationSpec{ - ServiceProvider: &v1alpha1.FeatureFlagServiceProvider{ - Name: "name1", - Credentials: &corev1.ObjectReference{ - Kind: "Pod", - Namespace: "default", - Name: "pod1", - }, - }, - SyncProvider: &v1alpha1.FeatureFlagSyncProvider{ - Name: "syncprovider1", - HttpSyncConfiguration: &v1alpha1.HttpSyncConfiguration{ - Target: "target1", - BearerToken: "token", - }, - }, - FlagDSpec: &v1alpha1.FlagDSpec{ - MetricsPort: 22, - Envs: []corev1.EnvVar{ - { - Name: "var1", - Value: "val1", - }, - { - Name: "var2", - Value: "val2", - }, - }, - }, - FeatureFlagSpec: `{"flags":{"flag1":{"state":"ok","variants":"variant1","defaultVariant":"default"}}}`, - }, - }, - wantErr: false, - wantObj: &FeatureFlagConfiguration{ - ObjectMeta: v1.ObjectMeta{ - Name: "FeatureFlagconfig1", - Namespace: "default", - }, - Spec: FeatureFlagConfigurationSpec{ - ServiceProvider: &FeatureFlagServiceProvider{ - Name: "name1", - Credentials: &corev1.ObjectReference{ - Kind: "Pod", - Namespace: "default", - Name: "pod1", - }, - }, - SyncProvider: &FeatureFlagSyncProvider{ - Name: "syncprovider1", - HttpSyncConfiguration: &HttpSyncConfiguration{ - Target: "target1", - BearerToken: "token", - }, - }, - FlagDSpec: &FlagDSpec{ - Envs: []corev1.EnvVar{ - { - Name: "var1", - Value: "val1", - }, - { - Name: "var2", - Value: "val2", - }, - }, - }, - FeatureFlagSpec: FeatureFlagSpec{ - Flags: map[string]FlagSpec{ - "flag1": { - State: "ok", - DefaultVariant: "default", - Variants: []byte(`"variant1"`), - }, - }, - }, - }, - }, - }, - { - name: "unable to unmarshal featureflagspec json", - srcObj: &v1alpha1.FeatureFlagConfiguration{ - TypeMeta: v1.TypeMeta{ - Kind: "FeatureFlagConfiguration", - APIVersion: "core.openfeature.dev/v1alpha1", - }, - ObjectMeta: v1.ObjectMeta{ - Name: "FeatureFlagconfig1", - Namespace: "default", - }, - Spec: v1alpha1.FeatureFlagConfigurationSpec{ - ServiceProvider: &v1alpha1.FeatureFlagServiceProvider{ - Name: "name1", - Credentials: &corev1.ObjectReference{ - Kind: "Pod", - Namespace: "default", - Name: "pod1", - }, - }, - SyncProvider: &v1alpha1.FeatureFlagSyncProvider{ - Name: "syncprovider1", - HttpSyncConfiguration: &v1alpha1.HttpSyncConfiguration{ - Target: "target1", - BearerToken: "token", - }, - }, - FlagDSpec: &v1alpha1.FlagDSpec{ - MetricsPort: 22, - Envs: []corev1.EnvVar{ - { - Name: "var1", - Value: "val1", - }, - { - Name: "var2", - Value: "val2", - }, - }, - }, - FeatureFlagSpec: `invalid`, - }, - }, - wantErr: true, - wantObj: nil, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - dst := &FeatureFlagConfiguration{ - TypeMeta: v1.TypeMeta{}, - ObjectMeta: v1.ObjectMeta{}, - Spec: FeatureFlagConfigurationSpec{}, - Status: FeatureFlagConfigurationStatus{}, - } - if err := dst.ConvertFrom(tt.srcObj); (err != nil) != tt.wantErr { - t.Errorf("ConvertFrom() error = %v, wantErr %v", err, tt.wantErr) - } - if tt.wantObj != nil { - require.Equal(t, tt.wantObj, dst, "Object was not converted correctly") - } - }) - } -} - -func TestFeatureFlagConfiguration_ConvertTo(t *testing.T) { - tests := []struct { - name string - src *FeatureFlagConfiguration - wantErr bool - wantObj *v1alpha1.FeatureFlagConfiguration - }{ - { - name: "Test that conversion from v1alpha2 to v1alpha1 works", - src: &FeatureFlagConfiguration{ - TypeMeta: v1.TypeMeta{ - Kind: "FeatureFlagConfiguration", - APIVersion: "core.openfeature.dev/v1alpha2", - }, - ObjectMeta: v1.ObjectMeta{ - Name: "FeatureFlagconfig1", - Namespace: "default", - }, - Spec: FeatureFlagConfigurationSpec{ - ServiceProvider: &FeatureFlagServiceProvider{ - Name: "name1", - Credentials: &corev1.ObjectReference{ - Kind: "Pod", - Namespace: "default", - Name: "pod1", - }, - }, - SyncProvider: &FeatureFlagSyncProvider{ - Name: "syncprovider1", - HttpSyncConfiguration: &HttpSyncConfiguration{ - Target: "target1", - BearerToken: "token", - }, - }, - FlagDSpec: &FlagDSpec{ - Envs: []corev1.EnvVar{ - { - Name: "var1", - Value: "val1", - }, - { - Name: "var2", - Value: "val2", - }, - }, - }, - FeatureFlagSpec: FeatureFlagSpec{ - Flags: map[string]FlagSpec{ - "flag1": { - State: "ok", - DefaultVariant: "default", - Variants: []byte(`"variant1"`), - }, - }, - }, - }, - }, - wantErr: false, - wantObj: &v1alpha1.FeatureFlagConfiguration{ - ObjectMeta: v1.ObjectMeta{ - Name: "FeatureFlagconfig1", - Namespace: "default", - }, - Spec: v1alpha1.FeatureFlagConfigurationSpec{ - ServiceProvider: &v1alpha1.FeatureFlagServiceProvider{ - Name: "name1", - Credentials: &corev1.ObjectReference{ - Kind: "Pod", - Namespace: "default", - Name: "pod1", - }, - }, - SyncProvider: &v1alpha1.FeatureFlagSyncProvider{ - Name: "syncprovider1", - HttpSyncConfiguration: &v1alpha1.HttpSyncConfiguration{ - Target: "target1", - BearerToken: "token", - }, - }, - FlagDSpec: &v1alpha1.FlagDSpec{ - Envs: []corev1.EnvVar{ - { - Name: "var1", - Value: "val1", - }, - { - Name: "var2", - Value: "val2", - }, - }, - }, - FeatureFlagSpec: `{"flags":{"flag1":{"state":"ok","variants":"variant1","defaultVariant":"default"}}}`, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - dst := v1alpha1.FeatureFlagConfiguration{ - TypeMeta: v1.TypeMeta{}, - ObjectMeta: v1.ObjectMeta{}, - Spec: v1alpha1.FeatureFlagConfigurationSpec{}, - Status: v1alpha1.FeatureFlagConfigurationStatus{}, - } - if err := tt.src.ConvertTo(&dst); (err != nil) != tt.wantErr { - t.Errorf("ConvertTo() error = %v, wantErr %v", err, tt.wantErr) - } - if tt.wantObj != nil { - require.Equal(t, tt.wantObj, &dst, "Object was not converted correctly") - } - }) - } -} - -func TestFeatureFlagConfiguration_ConvertFrom_Errorcase(t *testing.T) { - // A random different object is used here to simulate a different API version - testObj := v2.ExternalJob{} - - dst := &FeatureFlagConfiguration{ - TypeMeta: v1.TypeMeta{}, - ObjectMeta: v1.ObjectMeta{}, - Spec: FeatureFlagConfigurationSpec{}, - Status: FeatureFlagConfigurationStatus{}, - } - - if err := dst.ConvertFrom(&testObj); err == nil { - t.Errorf("ConvertFrom() error = %v", err) - } else { - require.ErrorIs(t, err, common.ErrCannotCastFeatureFlagConfiguration) - } -} - -func TestFeatureFlagConfiguration_ConvertTo_Errorcase(t *testing.T) { - testObj := FeatureFlagConfiguration{} - - // A random different object is used here to simulate a different API version - dst := v2.ExternalJob{} - - if err := testObj.ConvertTo(&dst); err == nil { - t.Errorf("ConvertTo() error = %v", err) - } else { - require.ErrorIs(t, err, common.ErrCannotCastFeatureFlagConfiguration) - } -} diff --git a/apis/core/v1alpha2/featureflagconfiguration_types.go b/apis/core/v1alpha2/featureflagconfiguration_types.go index ad207d9d9..7320827af 100644 --- a/apis/core/v1alpha2/featureflagconfiguration_types.go +++ b/apis/core/v1alpha2/featureflagconfiguration_types.go @@ -23,14 +23,8 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! -// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. - // FeatureFlagConfigurationSpec defines the desired state of FeatureFlagConfiguration type FeatureFlagConfigurationSpec struct { - // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster - // Important: Run "make" to regenerate code after modifying this file - // ServiceProvider [DEPRECATED]: superseded by FlagSourceConfiguration // +optional // +nullable @@ -109,11 +103,9 @@ type FeatureFlagServiceProvider struct { // FeatureFlagConfigurationStatus defines the observed state of FeatureFlagConfiguration type FeatureFlagConfigurationStatus struct { - // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster - // Important: Run "make" to regenerate code after modifying this file } -//+kubebuilder:resource:shortName="ff" +//+kubebuilder:resource:shortName="ffc" //+kubebuilder:object:root=true //+kubebuilder:subresource:status diff --git a/apis/core/v1alpha2/flagsourceconfiguration_conversion.go b/apis/core/v1alpha2/flagsourceconfiguration_conversion.go deleted file mode 100644 index 256af0cd9..000000000 --- a/apis/core/v1alpha2/flagsourceconfiguration_conversion.go +++ /dev/null @@ -1,81 +0,0 @@ -/* -Copyright 2022. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package v1alpha2 - -import ( - "fmt" - - "github.com/open-feature/open-feature-operator/apis/core/v1alpha1" - "github.com/open-feature/open-feature-operator/apis/core/v1alpha2/common" - "github.com/open-feature/open-feature-operator/pkg/utils" - "sigs.k8s.io/controller-runtime/pkg/conversion" -) - -func (src *FlagSourceConfiguration) ConvertTo(dstRaw conversion.Hub) error { - dst, ok := dstRaw.(*v1alpha1.FlagSourceConfiguration) - - if !ok { - return fmt.Errorf("type %T %w", dstRaw, common.ErrCannotCastFlagSourceConfiguration) - } - - // Copy equal stuff to new object - // DO NOT COPY TypeMeta - dst.ObjectMeta = src.ObjectMeta - - dst.Spec = v1alpha1.FlagSourceConfigurationSpec{ - MetricsPort: src.Spec.MetricsPort, - Port: src.Spec.Port, - SocketPath: src.Spec.SocketPath, - Evaluator: src.Spec.Evaluator, - Image: src.Spec.Image, - Tag: src.Spec.Tag, - Sources: []v1alpha1.Source{}, - SyncProviderArgs: src.Spec.SyncProviderArgs, - LogFormat: src.Spec.LogFormat, - DefaultSyncProvider: v1alpha1.SyncProviderType(src.Spec.DefaultSyncProvider), - ProbesEnabled: src.Spec.ProbesEnabled, - DebugLogging: utils.FalseVal(), - OtelCollectorUri: src.Spec.OtelCollectorUri, - } - return nil -} - -func (dst *FlagSourceConfiguration) ConvertFrom(srcRaw conversion.Hub) error { - src, ok := srcRaw.(*v1alpha1.FlagSourceConfiguration) - - if !ok { - return fmt.Errorf("type %T %w", srcRaw, common.ErrCannotCastFlagSourceConfiguration) - } - - // Copy equal stuff to new object - // DO NOT COPY TypeMeta - dst.ObjectMeta = src.ObjectMeta - - dst.Spec = FlagSourceConfigurationSpec{ - MetricsPort: src.Spec.MetricsPort, - Port: src.Spec.Port, - SocketPath: src.Spec.SocketPath, - Evaluator: src.Spec.Evaluator, - Image: src.Spec.Image, - Tag: src.Spec.Tag, - SyncProviderArgs: src.Spec.SyncProviderArgs, - LogFormat: src.Spec.LogFormat, - DefaultSyncProvider: string(src.Spec.DefaultSyncProvider), - ProbesEnabled: src.Spec.ProbesEnabled, - } - return nil -} diff --git a/apis/core/v1alpha2/flagsourceconfiguration_conversion_test.go b/apis/core/v1alpha2/flagsourceconfiguration_conversion_test.go deleted file mode 100644 index 3d1e449c9..000000000 --- a/apis/core/v1alpha2/flagsourceconfiguration_conversion_test.go +++ /dev/null @@ -1,211 +0,0 @@ -package v1alpha2 - -import ( - "testing" - - "github.com/open-feature/open-feature-operator/apis/core/v1alpha1" - "github.com/open-feature/open-feature-operator/apis/core/v1alpha2/common" - "github.com/open-feature/open-feature-operator/pkg/utils" - "github.com/stretchr/testify/require" - corev1 "k8s.io/api/core/v1" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - v2 "sigs.k8s.io/controller-runtime/pkg/webhook/conversion/testdata/api/v2" -) - -func TestFlagSourceConfiguration_ConvertFrom(t *testing.T) { - tt := true - tests := []struct { - name string - srcObj *v1alpha1.FlagSourceConfiguration - wantErr bool - wantObj *FlagSourceConfiguration - }{ - { - name: "Test that conversion from v1alpha1 to v1alpha2 works", - srcObj: &v1alpha1.FlagSourceConfiguration{ - TypeMeta: v1.TypeMeta{ - Kind: "FlagSourceConfiguration", - APIVersion: "core.openfeature.dev/v1alpha1", - }, - ObjectMeta: v1.ObjectMeta{ - Name: "flagsourceconfig1", - Namespace: "default", - }, - Spec: v1alpha1.FlagSourceConfigurationSpec{ - MetricsPort: 20, - Port: 21, - SocketPath: "path", - Evaluator: "eval", - Image: "img", - Tag: "tag", - Sources: []v1alpha1.Source{ - { - Source: "source", - Provider: "provider", - HttpSyncBearerToken: "token", - TLS: false, - CertPath: "/tmp/path", - ProviderID: "myapp", - Selector: "source=database", - }, - }, - EnvVars: []corev1.EnvVar{ - { - Name: "env1", - Value: "val1", - }, - { - Name: "env2", - Value: "val2", - }, - }, - DefaultSyncProvider: v1alpha1.SyncProviderType("provider"), - LogFormat: "log", - EnvVarPrefix: "pre", - RolloutOnChange: &tt, - SyncProviderArgs: []string{"provider", "arg"}, - DebugLogging: utils.FalseVal(), - OtelCollectorUri: "", - }, - }, - wantErr: false, - wantObj: &FlagSourceConfiguration{ - ObjectMeta: v1.ObjectMeta{ - Name: "flagsourceconfig1", - Namespace: "default", - }, - Spec: FlagSourceConfigurationSpec{ - MetricsPort: 20, - Port: 21, - SocketPath: "path", - Evaluator: "eval", - Image: "img", - Tag: "tag", - SyncProviderArgs: []string{"provider", "arg"}, - LogFormat: "log", - DefaultSyncProvider: "provider", - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - dst := &FlagSourceConfiguration{ - TypeMeta: v1.TypeMeta{}, - ObjectMeta: v1.ObjectMeta{}, - Spec: FlagSourceConfigurationSpec{}, - Status: FlagSourceConfigurationStatus{}, - } - if err := dst.ConvertFrom(tt.srcObj); (err != nil) != tt.wantErr { - t.Errorf("ConvertFrom() error = %v, wantErr %v", err, tt.wantErr) - } - if tt.wantObj != nil { - require.Equal(t, tt.wantObj, dst, "Object was not converted correctly") - } - }) - } -} - -func TestFlagSourceConfiguration_ConvertTo(t *testing.T) { - tests := []struct { - name string - src *FlagSourceConfiguration - wantErr bool - wantObj *v1alpha1.FlagSourceConfiguration - }{ - { - name: "Test that conversion from v1alpha2 to v1alpha1 works", - src: &FlagSourceConfiguration{ - TypeMeta: v1.TypeMeta{ - Kind: "FlagSourceConfiguration", - APIVersion: "core.openfeature.dev/v1alpha2", - }, - ObjectMeta: v1.ObjectMeta{ - Name: "flagsourceconfig1", - Namespace: "default", - }, - Spec: FlagSourceConfigurationSpec{ - MetricsPort: 20, - Port: 21, - SocketPath: "path", - Evaluator: "eval", - Image: "img", - Tag: "tag", - DefaultSyncProvider: "provider", - LogFormat: "log", - SyncProviderArgs: []string{"provider", "arg"}, - }, - }, - wantErr: false, - wantObj: &v1alpha1.FlagSourceConfiguration{ - ObjectMeta: v1.ObjectMeta{ - Name: "flagsourceconfig1", - Namespace: "default", - }, - Spec: v1alpha1.FlagSourceConfigurationSpec{ - MetricsPort: 20, - Port: 21, - SocketPath: "path", - Evaluator: "eval", - Image: "img", - Tag: "tag", - Sources: []v1alpha1.Source{}, - DefaultSyncProvider: v1alpha1.SyncProviderType("provider"), - LogFormat: "log", - EnvVarPrefix: "", - RolloutOnChange: nil, - SyncProviderArgs: []string{"provider", "arg"}, - DebugLogging: utils.FalseVal(), - OtelCollectorUri: "", - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - dst := v1alpha1.FlagSourceConfiguration{ - TypeMeta: v1.TypeMeta{}, - ObjectMeta: v1.ObjectMeta{}, - Spec: v1alpha1.FlagSourceConfigurationSpec{}, - Status: v1alpha1.FlagSourceConfigurationStatus{}, - } - if err := tt.src.ConvertTo(&dst); (err != nil) != tt.wantErr { - t.Errorf("ConvertTo() error = %v, wantErr %v", err, tt.wantErr) - } - if tt.wantObj != nil { - require.Equal(t, tt.wantObj, &dst, "Object was not converted correctly") - } - }) - } -} - -func TestFlagSourceConfiguration_ConvertFrom_Errorcase(t *testing.T) { - // A random different object is used here to simulate a different API version - testObj := v2.ExternalJob{} - - dst := &FlagSourceConfiguration{ - TypeMeta: v1.TypeMeta{}, - ObjectMeta: v1.ObjectMeta{}, - Spec: FlagSourceConfigurationSpec{}, - Status: FlagSourceConfigurationStatus{}, - } - - if err := dst.ConvertFrom(&testObj); err == nil { - t.Errorf("ConvertFrom() error = %v", err) - } else { - require.ErrorIs(t, err, common.ErrCannotCastFlagSourceConfiguration) - } -} - -func TestFlagSourceConfiguration_ConvertTo_Errorcase(t *testing.T) { - testObj := FlagSourceConfiguration{} - - // A random different object is used here to simulate a different API version - dst := v2.ExternalJob{} - - if err := testObj.ConvertTo(&dst); err == nil { - t.Errorf("ConvertTo() error = %v", err) - } else { - require.ErrorIs(t, err, common.ErrCannotCastFlagSourceConfiguration) - } -} diff --git a/apis/core/v1alpha2/flagsourceconfiguration_types.go b/apis/core/v1alpha2/flagsourceconfiguration_types.go index 0913c8317..94bdadd3c 100644 --- a/apis/core/v1alpha2/flagsourceconfiguration_types.go +++ b/apis/core/v1alpha2/flagsourceconfiguration_types.go @@ -20,14 +20,8 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! -// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. - // FlagSourceConfigurationSpec defines the desired state of FlagSourceConfiguration type FlagSourceConfigurationSpec struct { - // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster - // Important: Run "make" to regenerate code after modifying this file - // MetricsPort defines the port to serve metrics on, defaults to 8013 // +optional MetricsPort int32 `json:"metricsPort"` @@ -75,8 +69,6 @@ type FlagSourceConfigurationSpec struct { // FlagSourceConfigurationStatus defines the observed state of FlagSourceConfiguration type FlagSourceConfigurationStatus struct { - // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster - // Important: Run "make" to regenerate code after modifying this file } //+kubebuilder:resource:shortName="fsc" diff --git a/apis/core/v1alpha3/common/common.go b/apis/core/v1alpha3/common/common.go deleted file mode 100644 index f78640a8a..000000000 --- a/apis/core/v1alpha3/common/common.go +++ /dev/null @@ -1,5 +0,0 @@ -package common - -import "errors" - -var ErrCannotCastFlagSourceConfiguration = errors.New("cannot cast FlagSourceConfiguration to v1alpha3") diff --git a/apis/core/v1alpha3/flagsourceconfiguration_conversion.go b/apis/core/v1alpha3/flagsourceconfiguration_conversion.go deleted file mode 100644 index b7f75d662..000000000 --- a/apis/core/v1alpha3/flagsourceconfiguration_conversion.go +++ /dev/null @@ -1,113 +0,0 @@ -/* -Copyright 2022. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package v1alpha3 - -import ( - "fmt" - - "github.com/open-feature/open-feature-operator/apis/core/v1alpha1" - "github.com/open-feature/open-feature-operator/apis/core/v1alpha3/common" - "sigs.k8s.io/controller-runtime/pkg/conversion" -) - -func (src *FlagSourceConfiguration) ConvertTo(dstRaw conversion.Hub) error { - dst, ok := dstRaw.(*v1alpha1.FlagSourceConfiguration) - - if !ok { - return fmt.Errorf("type %T %w", dstRaw, common.ErrCannotCastFlagSourceConfiguration) - } - - // Copy equal stuff to new object - // DO NOT COPY TypeMeta - dst.ObjectMeta = src.ObjectMeta - - sources := []v1alpha1.Source{} - for _, sp := range src.Spec.Sources { - sources = append(sources, v1alpha1.Source{ - Source: sp.Source, - Provider: v1alpha1.SyncProviderType(sp.Provider), - HttpSyncBearerToken: sp.HttpSyncBearerToken, - TLS: sp.TLS, - CertPath: sp.CertPath, - ProviderID: sp.ProviderID, - Selector: sp.Selector, - }) - } - - dst.Spec = v1alpha1.FlagSourceConfigurationSpec{ - MetricsPort: src.Spec.MetricsPort, - Port: src.Spec.Port, - SocketPath: src.Spec.SocketPath, - Evaluator: src.Spec.Evaluator, - Image: src.Spec.Image, - Tag: src.Spec.Tag, - Sources: sources, - EnvVars: src.Spec.EnvVars, - DefaultSyncProvider: v1alpha1.SyncProviderType(src.Spec.DefaultSyncProvider), - LogFormat: src.Spec.LogFormat, - EnvVarPrefix: src.Spec.EnvVarPrefix, - RolloutOnChange: src.Spec.RolloutOnChange, - ProbesEnabled: src.Spec.ProbesEnabled, - DebugLogging: src.Spec.DebugLogging, - OtelCollectorUri: src.Spec.OtelCollectorUri, - } - return nil -} - -func (dst *FlagSourceConfiguration) ConvertFrom(srcRaw conversion.Hub) error { - src, ok := srcRaw.(*v1alpha1.FlagSourceConfiguration) - - if !ok { - return fmt.Errorf("type %T %w", srcRaw, common.ErrCannotCastFlagSourceConfiguration) - } - - // Copy equal stuff to new object - // DO NOT COPY TypeMeta - dst.ObjectMeta = src.ObjectMeta - - sources := []Source{} - for _, sp := range src.Spec.Sources { - sources = append(sources, Source{ - Source: sp.Source, - Provider: SyncProviderType(sp.Provider), - HttpSyncBearerToken: sp.HttpSyncBearerToken, - TLS: sp.TLS, - CertPath: sp.CertPath, - ProviderID: sp.ProviderID, - Selector: sp.Selector, - }) - } - - dst.Spec = FlagSourceConfigurationSpec{ - MetricsPort: src.Spec.MetricsPort, - Port: src.Spec.Port, - SocketPath: src.Spec.SocketPath, - Evaluator: src.Spec.Evaluator, - Image: src.Spec.Image, - Tag: src.Spec.Tag, - Sources: sources, - EnvVars: src.Spec.EnvVars, - DefaultSyncProvider: string(src.Spec.DefaultSyncProvider), - LogFormat: src.Spec.LogFormat, - EnvVarPrefix: src.Spec.EnvVarPrefix, - RolloutOnChange: src.Spec.RolloutOnChange, - ProbesEnabled: src.Spec.ProbesEnabled, - DebugLogging: src.Spec.DebugLogging, - OtelCollectorUri: src.Spec.OtelCollectorUri, - } - return nil -} diff --git a/apis/core/v1alpha3/flagsourceconfiguration_conversion_test.go b/apis/core/v1alpha3/flagsourceconfiguration_conversion_test.go deleted file mode 100644 index 445360098..000000000 --- a/apis/core/v1alpha3/flagsourceconfiguration_conversion_test.go +++ /dev/null @@ -1,276 +0,0 @@ -package v1alpha3 - -import ( - "testing" - - "github.com/open-feature/open-feature-operator/apis/core/v1alpha1" - "github.com/open-feature/open-feature-operator/apis/core/v1alpha3/common" - "github.com/open-feature/open-feature-operator/pkg/utils" - "github.com/stretchr/testify/require" - corev1 "k8s.io/api/core/v1" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - v2 "sigs.k8s.io/controller-runtime/pkg/webhook/conversion/testdata/api/v2" -) - -func TestFlagSourceConfiguration_ConvertFrom(t *testing.T) { - tt := true - tests := []struct { - name string - srcObj *v1alpha1.FlagSourceConfiguration - wantErr bool - wantObj *FlagSourceConfiguration - }{ - { - name: "Test that conversion from v1alpha1 to v1alpha3 works", - srcObj: &v1alpha1.FlagSourceConfiguration{ - TypeMeta: v1.TypeMeta{ - Kind: "FlagSourceConfiguration", - APIVersion: "core.openfeature.dev/v1alpha1", - }, - ObjectMeta: v1.ObjectMeta{ - Name: "flagsourceconfig1", - Namespace: "default", - }, - Spec: v1alpha1.FlagSourceConfigurationSpec{ - MetricsPort: 20, - Port: 21, - SocketPath: "path", - Evaluator: "eval", - Image: "img", - Tag: "tag", - Sources: []v1alpha1.Source{ - { - Source: "source", - Provider: "provider", - HttpSyncBearerToken: "token", - TLS: true, - CertPath: "etc/cert.ca", - ProviderID: "app", - Selector: "source=database", - }, - }, - EnvVars: []corev1.EnvVar{ - { - Name: "env1", - Value: "val1", - }, - { - Name: "env2", - Value: "val2", - }, - }, - DefaultSyncProvider: v1alpha1.SyncProviderType("provider"), - LogFormat: "log", - EnvVarPrefix: "pre", - RolloutOnChange: &tt, - DebugLogging: utils.FalseVal(), - OtelCollectorUri: "", - }, - }, - wantErr: false, - wantObj: &FlagSourceConfiguration{ - ObjectMeta: v1.ObjectMeta{ - Name: "flagsourceconfig1", - Namespace: "default", - }, - Spec: FlagSourceConfigurationSpec{ - MetricsPort: 20, - Port: 21, - SocketPath: "path", - Evaluator: "eval", - Image: "img", - Tag: "tag", - Sources: []Source{ - { - Source: "source", - Provider: "provider", - HttpSyncBearerToken: "token", - TLS: true, - CertPath: "etc/cert.ca", - ProviderID: "app", - Selector: "source=database", - }, - }, - EnvVars: []corev1.EnvVar{ - { - Name: "env1", - Value: "val1", - }, - { - Name: "env2", - Value: "val2", - }, - }, - DefaultSyncProvider: "provider", - LogFormat: "log", - EnvVarPrefix: "pre", - RolloutOnChange: &tt, - DebugLogging: utils.FalseVal(), - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - dst := &FlagSourceConfiguration{ - TypeMeta: v1.TypeMeta{}, - ObjectMeta: v1.ObjectMeta{}, - Spec: FlagSourceConfigurationSpec{}, - Status: FlagSourceConfigurationStatus{}, - } - if err := dst.ConvertFrom(tt.srcObj); (err != nil) != tt.wantErr { - t.Errorf("ConvertFrom() error = %v, wantErr %v", err, tt.wantErr) - } - if tt.wantObj != nil { - require.Equal(t, tt.wantObj, dst, "Object was not converted correctly") - } - }) - } -} - -func TestFlagSourceConfiguration_ConvertTo(t *testing.T) { - tt := true - tests := []struct { - name string - src *FlagSourceConfiguration - wantErr bool - wantObj *v1alpha1.FlagSourceConfiguration - }{ - { - name: "Test that conversion from v1alpha3 to v1alpha1 works", - src: &FlagSourceConfiguration{ - TypeMeta: v1.TypeMeta{ - Kind: "FlagSourceConfiguration", - APIVersion: "core.openfeature.dev/v1alpha3", - }, - ObjectMeta: v1.ObjectMeta{ - Name: "flagsourceconfig1", - Namespace: "default", - }, - Spec: FlagSourceConfigurationSpec{ - MetricsPort: 20, - Port: 21, - SocketPath: "path", - Evaluator: "eval", - Image: "img", - Tag: "tag", - Sources: []Source{ - { - Source: "source", - Provider: "provider", - HttpSyncBearerToken: "token", - TLS: false, - CertPath: "etc/cert.ca", - ProviderID: "app", - Selector: "source=database", - }, - }, - EnvVars: []corev1.EnvVar{ - { - Name: "env1", - Value: "val1", - }, - { - Name: "env2", - Value: "val2", - }, - }, - DefaultSyncProvider: "provider", - LogFormat: "log", - EnvVarPrefix: "pre", - RolloutOnChange: &tt, - DebugLogging: utils.FalseVal(), - }, - }, - wantErr: false, - wantObj: &v1alpha1.FlagSourceConfiguration{ - ObjectMeta: v1.ObjectMeta{ - Name: "flagsourceconfig1", - Namespace: "default", - }, - Spec: v1alpha1.FlagSourceConfigurationSpec{ - MetricsPort: 20, - Port: 21, - SocketPath: "path", - Evaluator: "eval", - Image: "img", - Tag: "tag", - Sources: []v1alpha1.Source{ - { - Source: "source", - Provider: "provider", - HttpSyncBearerToken: "token", - TLS: false, - CertPath: "etc/cert.ca", - ProviderID: "app", - Selector: "source=database", - }, - }, - EnvVars: []corev1.EnvVar{ - { - Name: "env1", - Value: "val1", - }, - { - Name: "env2", - Value: "val2", - }, - }, - DefaultSyncProvider: v1alpha1.SyncProviderType("provider"), - LogFormat: "log", - EnvVarPrefix: "pre", - RolloutOnChange: &tt, - DebugLogging: utils.FalseVal(), - OtelCollectorUri: "", - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - dst := v1alpha1.FlagSourceConfiguration{ - TypeMeta: v1.TypeMeta{}, - ObjectMeta: v1.ObjectMeta{}, - Spec: v1alpha1.FlagSourceConfigurationSpec{}, - Status: v1alpha1.FlagSourceConfigurationStatus{}, - } - if err := tt.src.ConvertTo(&dst); (err != nil) != tt.wantErr { - t.Errorf("ConvertTo() error = %v, wantErr %v", err, tt.wantErr) - } - if tt.wantObj != nil { - require.Equal(t, tt.wantObj, &dst, "Object was not converted correctly") - } - }) - } -} - -func TestFlagSourceConfiguration_ConvertFrom_Errorcase(t *testing.T) { - // A random different object is used here to simulate a different API version - testObj := v2.ExternalJob{} - - dst := &FlagSourceConfiguration{ - TypeMeta: v1.TypeMeta{}, - ObjectMeta: v1.ObjectMeta{}, - Spec: FlagSourceConfigurationSpec{}, - Status: FlagSourceConfigurationStatus{}, - } - - if err := dst.ConvertFrom(&testObj); err == nil { - t.Errorf("ConvertFrom() error = %v", err) - } else { - require.ErrorIs(t, err, common.ErrCannotCastFlagSourceConfiguration) - } -} - -func TestFlagSourceConfiguration_ConvertTo_Errorcase(t *testing.T) { - testObj := FlagSourceConfiguration{} - - // A random different object is used here to simulate a different API version - dst := v2.ExternalJob{} - - if err := testObj.ConvertTo(&dst); err == nil { - t.Errorf("ConvertTo() error = %v", err) - } else { - require.ErrorIs(t, err, common.ErrCannotCastFlagSourceConfiguration) - } -} diff --git a/apis/core/v1alpha3/flagsourceconfiguration_types.go b/apis/core/v1alpha3/flagsourceconfiguration_types.go index 2eea0d9e7..1f03e17ae 100644 --- a/apis/core/v1alpha3/flagsourceconfiguration_types.go +++ b/apis/core/v1alpha3/flagsourceconfiguration_types.go @@ -23,14 +23,8 @@ import ( type SyncProviderType string -// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! -// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. - // FlagSourceConfigurationSpec defines the desired state of FlagSourceConfiguration type FlagSourceConfigurationSpec struct { - // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster - // Important: Run "make" to regenerate code after modifying this file - // MetricsPort defines the port to serve metrics on, defaults to 8014 // +optional MetricsPort int32 `json:"metricsPort"` @@ -133,8 +127,6 @@ type Source struct { // FlagSourceConfigurationStatus defines the observed state of FlagSourceConfiguration type FlagSourceConfigurationStatus struct { - // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster - // Important: Run "make" to regenerate code after modifying this file } //+kubebuilder:resource:shortName="fsc" diff --git a/apis/core/v1beta1/common/common.go b/apis/core/v1beta1/common/common.go new file mode 100644 index 000000000..86cbe422b --- /dev/null +++ b/apis/core/v1beta1/common/common.go @@ -0,0 +1,57 @@ +package common + +import "fmt" + +type SyncProviderType string + +const ( + SyncProviderKubernetes SyncProviderType = "kubernetes" + SyncProviderFilepath SyncProviderType = "file" + SyncProviderHttp SyncProviderType = "http" + SyncProviderGrpc SyncProviderType = "grpc" + SyncProviderFlagdProxy SyncProviderType = "flagd-proxy" +) + +func (s SyncProviderType) IsKubernetes() bool { + return s == SyncProviderKubernetes +} + +func (s SyncProviderType) IsHttp() bool { + return s == SyncProviderHttp +} + +func (s SyncProviderType) IsFilepath() bool { + return s == SyncProviderFilepath +} + +func (s SyncProviderType) IsGrpc() bool { + return s == SyncProviderGrpc +} + +func (s SyncProviderType) IsFlagdProxy() bool { + return s == SyncProviderFlagdProxy +} + +func TrueVal() *bool { + b := true + return &b +} + +func FalseVal() *bool { + b := false + return &b +} + +func EnvVarKey(prefix string, suffix string) string { + return fmt.Sprintf("%s_%s", prefix, suffix) +} + +// unique string used to create unique volume mount and file name +func FeatureFlagConfigurationId(namespace, name string) string { + return EnvVarKey(namespace, name) +} + +// unique key (and filename) for configMap data +func FeatureFlagConfigMapKey(namespace, name string) string { + return fmt.Sprintf("%s.flagd.json", FeatureFlagConfigurationId(namespace, name)) +} diff --git a/apis/core/v1beta1/common/common_test.go b/apis/core/v1beta1/common/common_test.go new file mode 100644 index 000000000..32a69fae1 --- /dev/null +++ b/apis/core/v1beta1/common/common_test.go @@ -0,0 +1,36 @@ +package common + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_FeatureFlagSource_SyncProvider(t *testing.T) { + k := SyncProviderKubernetes + f := SyncProviderFilepath + h := SyncProviderHttp + g := SyncProviderGrpc + + require.True(t, k.IsKubernetes()) + require.True(t, f.IsFilepath()) + require.True(t, h.IsHttp()) + require.True(t, g.IsGrpc()) + + require.False(t, f.IsKubernetes()) + require.False(t, h.IsFilepath()) + require.False(t, k.IsGrpc()) + require.False(t, g.IsHttp()) +} + +func Test_FLagSourceConfiguration_EnvVarKey(t *testing.T) { + require.Equal(t, "pre_suf", EnvVarKey("pre", "suf")) +} + +func Test_FLagSourceConfiguration_FeatureFlagConfigurationId(t *testing.T) { + require.Equal(t, "pre_suf", FeatureFlagConfigurationId("pre", "suf")) +} + +func Test_FLagSourceConfiguration_FeatureFlagConfigMapKey(t *testing.T) { + require.Equal(t, "pre_suf.flagd.json", FeatureFlagConfigMapKey("pre", "suf")) +} diff --git a/apis/core/v1beta1/featureflag_types.go b/apis/core/v1beta1/featureflag_types.go new file mode 100644 index 000000000..4c9736610 --- /dev/null +++ b/apis/core/v1beta1/featureflag_types.go @@ -0,0 +1,117 @@ +/* +Copyright 2022. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +import ( + "encoding/json" + + "github.com/open-feature/open-feature-operator/apis/core/v1beta1/common" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// FeatureFlagSpec defines the desired state of FeatureFlag +type FeatureFlagSpec struct { + // FlagSpec is the structured representation of the feature flag specification + FlagSpec FlagSpec `json:"flagSpec,omitempty"` +} + +type FlagSpec struct { + Flags map[string]Flag `json:"flags"` + // +optional + // +kubebuilder:validation:Schemaless + // +kubebuilder:pruning:PreserveUnknownFields + // +kubebuilder:validation:Type=object + Evaluators json.RawMessage `json:"$evaluators,omitempty"` +} + +type Flag struct { + // +kubebuilder:validation:Enum=ENABLED;DISABLED + State string `json:"state"` + // +kubebuilder:validation:Schemaless + // +kubebuilder:pruning:PreserveUnknownFields + // +kubebuilder:validation:Type=object + Variants json.RawMessage `json:"variants"` + DefaultVariant string `json:"defaultVariant"` + // +optional + // +kubebuilder:validation:Schemaless + // +kubebuilder:pruning:PreserveUnknownFields + // +kubebuilder:validation:Type=object + // Targeting is the json targeting rule + Targeting json.RawMessage `json:"targeting,omitempty"` +} + +// FeatureFlagStatus defines the observed state of FeatureFlag +type FeatureFlagStatus struct { +} + +//+kubebuilder:resource:shortName="ff" +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status +//+kubebuilder:storageversion + +// FeatureFlag is the Schema for the featureflags API +type FeatureFlag struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec FeatureFlagSpec `json:"spec,omitempty"` + Status FeatureFlagStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// FeatureFlagList contains a list of FeatureFlag +type FeatureFlagList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []FeatureFlag `json:"items"` +} + +func init() { + SchemeBuilder.Register(&FeatureFlag{}, &FeatureFlagList{}) +} + +func (ff *FeatureFlag) GetReference() metav1.OwnerReference { + return metav1.OwnerReference{ + APIVersion: ff.APIVersion, + Kind: ff.Kind, + Name: ff.Name, + UID: ff.UID, + Controller: common.TrueVal(), + } +} + +func (ff *FeatureFlag) GenerateConfigMap(name string, namespace string, references []metav1.OwnerReference) (*corev1.ConfigMap, error) { + b, err := json.Marshal(ff.Spec.FlagSpec) + if err != nil { + return nil, err + } + return &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: map[string]string{ + "openfeature.dev/featureflag": name, + }, + OwnerReferences: references, + }, + Data: map[string]string{ + common.FeatureFlagConfigMapKey(namespace, name): string(b), + }, + }, nil +} diff --git a/apis/core/v1beta1/featureflag_types_test.go b/apis/core/v1beta1/featureflag_types_test.go new file mode 100644 index 000000000..d5bcc6514 --- /dev/null +++ b/apis/core/v1beta1/featureflag_types_test.go @@ -0,0 +1,70 @@ +package v1beta1 + +import ( + "testing" + + "github.com/open-feature/open-feature-operator/apis/core/v1beta1/common" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +func Test_FeatureFlag(t *testing.T) { + ff := FeatureFlag{ + ObjectMeta: v1.ObjectMeta{ + Name: "ffconf1", + Namespace: "test", + OwnerReferences: []v1.OwnerReference{ + { + APIVersion: "ver", + Kind: "kind", + Name: "ffconf1", + UID: types.UID("5"), + Controller: common.TrueVal(), + }, + }, + }, + Spec: FeatureFlagSpec{ + FlagSpec: FlagSpec{ + Flags: map[string]Flag{}, + }, + }, + } + + require.Equal(t, v1.OwnerReference{ + APIVersion: ff.APIVersion, + Kind: ff.Kind, + Name: ff.Name, + UID: ff.UID, + Controller: common.TrueVal(), + }, ff.GetReference()) + + name := "cmname" + namespace := "cmnamespace" + references := []v1.OwnerReference{ + { + APIVersion: "ver", + Kind: "kind", + Name: "ffconf1", + UID: types.UID("5"), + Controller: common.TrueVal(), + }, + } + + cm, _ := ff.GenerateConfigMap(name, namespace, references) + + require.Equal(t, corev1.ConfigMap{ + ObjectMeta: v1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: map[string]string{ + "openfeature.dev/featureflag": name, + }, + OwnerReferences: references, + }, + Data: map[string]string{ + "cmnamespace_cmname.flagd.json": "{\"flags\":{}}", + }, + }, *cm) +} diff --git a/apis/core/v1beta1/featureflagsource_types.go b/apis/core/v1beta1/featureflagsource_types.go new file mode 100644 index 000000000..0ff9838b4 --- /dev/null +++ b/apis/core/v1beta1/featureflagsource_types.go @@ -0,0 +1,269 @@ +/* +Copyright 2022. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +import ( + "fmt" + + "github.com/open-feature/open-feature-operator/apis/core/v1beta1/common" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + SidecarEnvVarPrefix string = "SIDECAR_ENV_VAR_PREFIX" + InputConfigurationEnvVarPrefix string = "SIDECAR" + SidecarMetricPortEnvVar string = "MANAGEMENT_PORT" + SidecarPortEnvVar string = "PORT" + SidecarSocketPathEnvVar string = "SOCKET_PATH" + SidecarEvaluatorEnvVar string = "EVALUATOR" + SidecarImageEnvVar string = "IMAGE" + SidecarVersionEnvVar string = "TAG" + SidecarProviderArgsEnvVar string = "PROVIDER_ARGS" + SidecarDefaultSyncProviderEnvVar string = "SYNC_PROVIDER" + SidecarLogFormatEnvVar string = "LOG_FORMAT" + SidecarProbesEnabledVar string = "PROBES_ENABLED" + defaultSidecarEnvVarPrefix string = "FLAGD" + DefaultMetricPort int32 = 8014 + defaultPort int32 = 8013 + defaultSocketPath string = "" + defaultEvaluator string = "json" + defaultLogFormat string = "json" + defaultProbesEnabled bool = true +) + +// FeatureFlagSourceSpec defines the desired state of FeatureFlagSource +type FeatureFlagSourceSpec struct { + // ManagemetPort defines the port to serve management on, defaults to 8014 + // +optional + ManagementPort int32 `json:"managementPort"` + + // Port defines the port to listen on, defaults to 8013 + // +optional + Port int32 `json:"port"` + + // SocketPath defines the unix socket path to listen on + // +optional + SocketPath string `json:"socketPath"` + + // Evaluator sets an evaluator, defaults to 'json' + // +optional + Evaluator string `json:"evaluator"` + + // SyncProviders define the syncProviders and associated configuration to be applied to the sidecar + // +kubebuilder:validation:MinItems=1 + Sources []Source `json:"sources"` + + // EnvVars define the env vars to be applied to the sidecar, any env vars in FeatureFlag CRs + // are added at the lowest index, all values will have the EnvVarPrefix applied, default FLAGD + // +optional + EnvVars []corev1.EnvVar `json:"envVars"` + + // SyncProviderArgs are string arguments passed to all sync providers, defined as key values separated by = + // +optional + SyncProviderArgs []string `json:"syncProviderArgs"` + + // DefaultSyncProvider defines the default sync provider + // +optional + DefaultSyncProvider common.SyncProviderType `json:"defaultSyncProvider"` + + // LogFormat allows for the sidecar log format to be overridden, defaults to 'json' + // +optional + LogFormat string `json:"logFormat"` + + // EnvVarPrefix defines the prefix to be applied to all environment variables applied to the sidecar, default FLAGD + // +optional + EnvVarPrefix string `json:"envVarPrefix"` + + // RolloutOnChange dictates whether annotated deployments will be restarted when configuration changes are + // detected in this CR, defaults to false + // +optional + RolloutOnChange *bool `json:"rolloutOnChange"` + + // ProbesEnabled defines whether to enable liveness and readiness probes of flagd sidecar. Default true (enabled). + // +optional + ProbesEnabled *bool `json:"probesEnabled"` + + // DebugLogging defines whether to enable --debug flag of flagd sidecar. Default false (disabled). + // +optional + DebugLogging *bool `json:"debugLogging"` + + // OtelCollectorUri defines whether to enable --otel-collector-uri flag of flagd sidecar. Default false (disabled). + // +optional + OtelCollectorUri string `json:"otelCollectorUri"` + + // Resources defines flagd sidecar resources. Default to operator sidecar-cpu-* and sidecar-ram-* flags. + // +optional + Resources corev1.ResourceRequirements `json:"resources"` +} + +type Source struct { + // Source is a URI of the flag sources + Source string `json:"source"` + + // Provider type - kubernetes, http(s), grpc(s) or file + // +optional + Provider common.SyncProviderType `json:"provider"` + + // HttpSyncBearerToken is a bearer token. Used by http(s) sync provider only + // +optional + HttpSyncBearerToken string `json:"httpSyncBearerToken"` + + // TLS - Enable/Disable secure TLS connectivity. Currently used only by GRPC sync + // +optional + TLS bool `json:"tls"` + + // CertPath is a path of a certificate to be used by grpc TLS connection + // +optional + CertPath string `json:"certPath"` + + // ProviderID is an identifier to be used in grpc provider + // +optional + ProviderID string `json:"providerID"` + + // Selector is a flag configuration selector used by grpc provider + // +optional + Selector string `json:"selector,omitempty"` +} + +// FeatureFlagSourceStatus defines the observed state of FeatureFlagSource +type FeatureFlagSourceStatus struct { +} + +//+kubebuilder:resource:shortName="ffs" +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status +//+kubebuilder:storageversion + +// FeatureFlagSource is the Schema for the FeatureFlagSources API +type FeatureFlagSource struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec FeatureFlagSourceSpec `json:"spec,omitempty"` + Status FeatureFlagSourceStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// FeatureFlagSourceList contains a list of FeatureFlagSource +type FeatureFlagSourceList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []FeatureFlagSource `json:"items"` +} + +func init() { + SchemeBuilder.Register(&FeatureFlagSource{}, &FeatureFlagSourceList{}) +} + +//nolint:gocyclo +func (fc *FeatureFlagSourceSpec) Merge(new *FeatureFlagSourceSpec) { + if new == nil { + return + } + if new.ManagementPort != 0 { + fc.ManagementPort = new.ManagementPort + } + if new.Port != 0 { + fc.Port = new.Port + } + if new.SocketPath != "" { + fc.SocketPath = new.SocketPath + } + if new.Evaluator != "" { + fc.Evaluator = new.Evaluator + } + if len(new.Sources) != 0 { + fc.Sources = append(fc.Sources, new.Sources...) + } + if len(new.EnvVars) != 0 { + fc.EnvVars = append(fc.EnvVars, new.EnvVars...) + } + if len(new.SyncProviderArgs) != 0 { + fc.SyncProviderArgs = append(fc.SyncProviderArgs, new.SyncProviderArgs...) + } + if new.EnvVarPrefix != "" { + fc.EnvVarPrefix = new.EnvVarPrefix + } + if new.DefaultSyncProvider != "" { + fc.DefaultSyncProvider = new.DefaultSyncProvider + } + if new.LogFormat != "" { + fc.LogFormat = new.LogFormat + } + if new.RolloutOnChange != nil { + fc.RolloutOnChange = new.RolloutOnChange + } + if new.ProbesEnabled != nil { + fc.ProbesEnabled = new.ProbesEnabled + } + if new.DebugLogging != nil { + fc.DebugLogging = new.DebugLogging + } + if new.OtelCollectorUri != "" { + fc.OtelCollectorUri = new.OtelCollectorUri + } +} + +func (fc *FeatureFlagSourceSpec) ToEnvVars() []corev1.EnvVar { + envs := []corev1.EnvVar{} + + for _, envVar := range fc.EnvVars { + envs = append(envs, corev1.EnvVar{ + Name: common.EnvVarKey(fc.EnvVarPrefix, envVar.Name), + Value: envVar.Value, + }) + } + + if fc.ManagementPort != DefaultMetricPort { + envs = append(envs, corev1.EnvVar{ + Name: common.EnvVarKey(fc.EnvVarPrefix, SidecarMetricPortEnvVar), + Value: fmt.Sprintf("%d", fc.ManagementPort), + }) + } + + if fc.Port != defaultPort { + envs = append(envs, corev1.EnvVar{ + Name: common.EnvVarKey(fc.EnvVarPrefix, SidecarPortEnvVar), + Value: fmt.Sprintf("%d", fc.Port), + }) + } + + if fc.Evaluator != defaultEvaluator { + envs = append(envs, corev1.EnvVar{ + Name: common.EnvVarKey(fc.EnvVarPrefix, SidecarEvaluatorEnvVar), + Value: fc.Evaluator, + }) + } + + if fc.SocketPath != defaultSocketPath { + envs = append(envs, corev1.EnvVar{ + Name: common.EnvVarKey(fc.EnvVarPrefix, SidecarSocketPathEnvVar), + Value: fc.SocketPath, + }) + } + + if fc.LogFormat != defaultLogFormat { + envs = append(envs, corev1.EnvVar{ + Name: common.EnvVarKey(fc.EnvVarPrefix, SidecarLogFormatEnvVar), + Value: fc.LogFormat, + }) + } + + return envs +} diff --git a/apis/core/v1beta1/featureflagsource_types_test.go b/apis/core/v1beta1/featureflagsource_types_test.go new file mode 100644 index 000000000..97cde443d --- /dev/null +++ b/apis/core/v1beta1/featureflagsource_types_test.go @@ -0,0 +1,225 @@ +package v1beta1 + +import ( + "testing" + + "github.com/open-feature/open-feature-operator/apis/core/v1beta1/common" + "github.com/stretchr/testify/require" + v1 "k8s.io/api/core/v1" +) + +func Test_FLagSourceConfiguration_Merge(t *testing.T) { + ff_old := &FeatureFlagSource{ + Spec: FeatureFlagSourceSpec{ + EnvVars: []v1.EnvVar{ + { + Name: "env1", + Value: "val1", + }, + { + Name: "env2", + Value: "val2", + }, + }, + EnvVarPrefix: "PRE", + ManagementPort: 22, + Port: 33, + Evaluator: "evaluator", + SocketPath: "socket-path", + LogFormat: "log", + Sources: []Source{ + { + Source: "src1", + Provider: common.SyncProviderGrpc, + TLS: true, + CertPath: "etc/cert.ca", + ProviderID: "app", + Selector: "source=database", + }, + }, + SyncProviderArgs: []string{"arg1", "arg2"}, + DefaultSyncProvider: common.SyncProviderKubernetes, + RolloutOnChange: common.TrueVal(), + ProbesEnabled: common.TrueVal(), + DebugLogging: common.TrueVal(), + OtelCollectorUri: "", + }, + } + + ff_old.Spec.Merge(nil) + + require.Equal(t, &FeatureFlagSource{ + Spec: FeatureFlagSourceSpec{ + EnvVars: []v1.EnvVar{ + { + Name: "env1", + Value: "val1", + }, + { + Name: "env2", + Value: "val2", + }, + }, + EnvVarPrefix: "PRE", + ManagementPort: 22, + Port: 33, + Evaluator: "evaluator", + SocketPath: "socket-path", + LogFormat: "log", + Sources: []Source{ + { + Source: "src1", + Provider: common.SyncProviderGrpc, + TLS: true, + CertPath: "etc/cert.ca", + ProviderID: "app", + Selector: "source=database", + }, + }, + SyncProviderArgs: []string{"arg1", "arg2"}, + DefaultSyncProvider: common.SyncProviderKubernetes, + RolloutOnChange: common.TrueVal(), + ProbesEnabled: common.TrueVal(), + DebugLogging: common.TrueVal(), + OtelCollectorUri: "", + }, + }, ff_old) + + ff_new := &FeatureFlagSource{ + Spec: FeatureFlagSourceSpec{ + EnvVars: []v1.EnvVar{ + { + Name: "env3", + Value: "val3", + }, + { + Name: "env4", + Value: "val4", + }, + }, + EnvVarPrefix: "PREFIX", + ManagementPort: 221, + Port: 331, + Evaluator: "evaluator1", + SocketPath: "socket-path1", + LogFormat: "log1", + Sources: []Source{ + { + Source: "src2", + Provider: common.SyncProviderFilepath, + }, + }, + SyncProviderArgs: []string{"arg3", "arg4"}, + DefaultSyncProvider: common.SyncProviderFilepath, + RolloutOnChange: common.FalseVal(), + ProbesEnabled: common.FalseVal(), + DebugLogging: common.FalseVal(), + OtelCollectorUri: "", + }, + } + + ff_old.Spec.Merge(&ff_new.Spec) + + require.Equal(t, &FeatureFlagSource{ + Spec: FeatureFlagSourceSpec{ + EnvVars: []v1.EnvVar{ + { + Name: "env1", + Value: "val1", + }, + { + Name: "env2", + Value: "val2", + }, + { + Name: "env3", + Value: "val3", + }, + { + Name: "env4", + Value: "val4", + }, + }, + EnvVarPrefix: "PREFIX", + ManagementPort: 221, + Port: 331, + Evaluator: "evaluator1", + SocketPath: "socket-path1", + LogFormat: "log1", + Sources: []Source{ + { + Source: "src1", + Provider: common.SyncProviderGrpc, + TLS: true, + CertPath: "etc/cert.ca", + ProviderID: "app", + Selector: "source=database", + }, + { + Source: "src2", + Provider: common.SyncProviderFilepath, + }, + }, + SyncProviderArgs: []string{"arg1", "arg2", "arg3", "arg4"}, + DefaultSyncProvider: common.SyncProviderFilepath, + RolloutOnChange: common.FalseVal(), + ProbesEnabled: common.FalseVal(), + DebugLogging: common.FalseVal(), + OtelCollectorUri: "", + }, + }, ff_old) +} + +func Test_FLagSourceConfiguration_ToEnvVars(t *testing.T) { + ff := FeatureFlagSource{ + Spec: FeatureFlagSourceSpec{ + EnvVars: []v1.EnvVar{ + { + Name: "env1", + Value: "val1", + }, + { + Name: "env2", + Value: "val2", + }, + }, + EnvVarPrefix: "PRE", + ManagementPort: 22, + Port: 33, + Evaluator: "evaluator", + SocketPath: "socket-path", + LogFormat: "log", + }, + } + expected := []v1.EnvVar{ + { + Name: "PRE_env1", + Value: "val1", + }, + { + Name: "PRE_env2", + Value: "val2", + }, + { + Name: "PRE_MANAGEMENT_PORT", + Value: "22", + }, + { + Name: "PRE_PORT", + Value: "33", + }, + { + Name: "PRE_EVALUATOR", + Value: "evaluator", + }, + { + Name: "PRE_SOCKET_PATH", + Value: "socket-path", + }, + { + Name: "PRE_LOG_FORMAT", + Value: "log", + }, + } + require.Equal(t, expected, ff.Spec.ToEnvVars()) +} diff --git a/apis/core/v1beta1/groupversion_info.go b/apis/core/v1beta1/groupversion_info.go new file mode 100644 index 000000000..29f2d92be --- /dev/null +++ b/apis/core/v1beta1/groupversion_info.go @@ -0,0 +1,36 @@ +/* +Copyright 2022. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1beta1 contains API Schema definitions for the core v1beta1 API group +// +kubebuilder:object:generate=true +// +groupName=core.openfeature.dev +package v1beta1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "core.openfeature.dev", Version: "v1beta1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/apis/core/v1beta1/zz_generated.deepcopy.go b/apis/core/v1beta1/zz_generated.deepcopy.go new file mode 100644 index 000000000..3c111fcc8 --- /dev/null +++ b/apis/core/v1beta1/zz_generated.deepcopy.go @@ -0,0 +1,307 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright 2022. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1beta1 + +import ( + "encoding/json" + "k8s.io/api/core/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FeatureFlag) DeepCopyInto(out *FeatureFlag) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureFlag. +func (in *FeatureFlag) DeepCopy() *FeatureFlag { + if in == nil { + return nil + } + out := new(FeatureFlag) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FeatureFlag) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FeatureFlagList) DeepCopyInto(out *FeatureFlagList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]FeatureFlag, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureFlagList. +func (in *FeatureFlagList) DeepCopy() *FeatureFlagList { + if in == nil { + return nil + } + out := new(FeatureFlagList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FeatureFlagList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FeatureFlagSource) DeepCopyInto(out *FeatureFlagSource) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureFlagSource. +func (in *FeatureFlagSource) DeepCopy() *FeatureFlagSource { + if in == nil { + return nil + } + out := new(FeatureFlagSource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FeatureFlagSource) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FeatureFlagSourceList) DeepCopyInto(out *FeatureFlagSourceList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]FeatureFlagSource, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureFlagSourceList. +func (in *FeatureFlagSourceList) DeepCopy() *FeatureFlagSourceList { + if in == nil { + return nil + } + out := new(FeatureFlagSourceList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FeatureFlagSourceList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FeatureFlagSourceSpec) DeepCopyInto(out *FeatureFlagSourceSpec) { + *out = *in + if in.Sources != nil { + in, out := &in.Sources, &out.Sources + *out = make([]Source, len(*in)) + copy(*out, *in) + } + if in.EnvVars != nil { + in, out := &in.EnvVars, &out.EnvVars + *out = make([]v1.EnvVar, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.SyncProviderArgs != nil { + in, out := &in.SyncProviderArgs, &out.SyncProviderArgs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.RolloutOnChange != nil { + in, out := &in.RolloutOnChange, &out.RolloutOnChange + *out = new(bool) + **out = **in + } + if in.ProbesEnabled != nil { + in, out := &in.ProbesEnabled, &out.ProbesEnabled + *out = new(bool) + **out = **in + } + if in.DebugLogging != nil { + in, out := &in.DebugLogging, &out.DebugLogging + *out = new(bool) + **out = **in + } + in.Resources.DeepCopyInto(&out.Resources) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureFlagSourceSpec. +func (in *FeatureFlagSourceSpec) DeepCopy() *FeatureFlagSourceSpec { + if in == nil { + return nil + } + out := new(FeatureFlagSourceSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FeatureFlagSourceStatus) DeepCopyInto(out *FeatureFlagSourceStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureFlagSourceStatus. +func (in *FeatureFlagSourceStatus) DeepCopy() *FeatureFlagSourceStatus { + if in == nil { + return nil + } + out := new(FeatureFlagSourceStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FeatureFlagSpec) DeepCopyInto(out *FeatureFlagSpec) { + *out = *in + in.FlagSpec.DeepCopyInto(&out.FlagSpec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureFlagSpec. +func (in *FeatureFlagSpec) DeepCopy() *FeatureFlagSpec { + if in == nil { + return nil + } + out := new(FeatureFlagSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FeatureFlagStatus) DeepCopyInto(out *FeatureFlagStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureFlagStatus. +func (in *FeatureFlagStatus) DeepCopy() *FeatureFlagStatus { + if in == nil { + return nil + } + out := new(FeatureFlagStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Flag) DeepCopyInto(out *Flag) { + *out = *in + if in.Variants != nil { + in, out := &in.Variants, &out.Variants + *out = make(json.RawMessage, len(*in)) + copy(*out, *in) + } + if in.Targeting != nil { + in, out := &in.Targeting, &out.Targeting + *out = make(json.RawMessage, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Flag. +func (in *Flag) DeepCopy() *Flag { + if in == nil { + return nil + } + out := new(Flag) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FlagSpec) DeepCopyInto(out *FlagSpec) { + *out = *in + if in.Flags != nil { + in, out := &in.Flags, &out.Flags + *out = make(map[string]Flag, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } + if in.Evaluators != nil { + in, out := &in.Evaluators, &out.Evaluators + *out = make(json.RawMessage, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FlagSpec. +func (in *FlagSpec) DeepCopy() *FlagSpec { + if in == nil { + return nil + } + out := new(FlagSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Source) DeepCopyInto(out *Source) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Source. +func (in *Source) DeepCopy() *Source { + if in == nil { + return nil + } + out := new(Source) + in.DeepCopyInto(out) + return out +} diff --git a/apis/go.mod b/apis/go.mod new file mode 100644 index 000000000..26b4ec15c --- /dev/null +++ b/apis/go.mod @@ -0,0 +1,30 @@ +module github.com/open-feature/open-feature-operator/apis + +go 1.19 + +require ( + github.com/stretchr/testify v1.8.4 + k8s.io/api v0.26.4 + k8s.io/apimachinery v0.26.4 + sigs.k8s.io/controller-runtime v0.14.6 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-logr/logr v1.2.3 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/gofuzz v1.1.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/net v0.7.0 // indirect + golang.org/x/text v0.7.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/klog/v2 v2.80.1 // indirect + k8s.io/utils v0.0.0-20221128185143-99ec85e7a448 // indirect + sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect +) diff --git a/apis/go.sum b/apis/go.sum new file mode 100644 index 000000000..5562ffaa8 --- /dev/null +++ b/apis/go.sum @@ -0,0 +1,88 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/onsi/ginkgo/v2 v2.6.0 h1:9t9b9vRUbFq3C4qKFCGkVuq/fIHji802N1nrtkh1mNc= +github.com/onsi/gomega v1.24.1 h1:KORJXNNTzJXzu4ScJWssJfJMnJ+2QJqhoQSRwNlze9E= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.26.4 h1:qSG2PmtcD23BkYiWfoYAcak870eF/hE7NNYBYavTT94= +k8s.io/api v0.26.4/go.mod h1:WwKEXU3R1rgCZ77AYa7DFksd9/BAIKyOmRlbVxgvjCk= +k8s.io/apimachinery v0.26.4 h1:rZccKdBLg9vP6J09JD+z8Yr99Ce8gk3Lbi9TCx05Jzs= +k8s.io/apimachinery v0.26.4/go.mod h1:ats7nN1LExKHvJ9TmwootT00Yz05MuYqPXEXaVeOy5I= +k8s.io/klog/v2 v2.80.1 h1:atnLQ121W371wYYFawwYx1aEY2eUfs4l3J72wtgAwV4= +k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/utils v0.0.0-20221128185143-99ec85e7a448 h1:KTgPnR10d5zhztWptI952TNtt/4u5h3IzDXkdIMuo2Y= +k8s.io/utils v0.0.0-20221128185143-99ec85e7a448/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.14.6 h1:oxstGVvXGNnMvY7TAESYk+lzr6S3V5VFxQ6d92KcwQA= +sigs.k8s.io/controller-runtime v0.14.6/go.mod h1:WqIdsAY6JBsjfc/CqO0CORmNtoCtE4S6qbPc9s68h+0= +sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k= +sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= diff --git a/chart/open-feature-operator/Chart.yaml b/chart/open-feature-operator/Chart.yaml index 551b5ecff..100c15126 100755 --- a/chart/open-feature-operator/Chart.yaml +++ b/chart/open-feature-operator/Chart.yaml @@ -13,12 +13,12 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: "v0.2.36" # x-release-please-version +version: "v0.3.0" # x-release-please-version # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "v0.2.36" # x-release-please-version +appVersion: "v0.3.0" # x-release-please-version home: https://openfeature.dev icon: https://open-feature.github.io/open-feature-operator/chart/open-feature-operator/openfeature-logo.png diff --git a/chart/open-feature-operator/README.md b/chart/open-feature-operator/README.md index cbd97ec25..6b26df727 100644 --- a/chart/open-feature-operator/README.md +++ b/chart/open-feature-operator/README.md @@ -54,16 +54,16 @@ OpenFeature Operator's CRDs are templated, and can be updated apart from the ope helm template openfeature/open-feature-operator -s templates/{CRD} | kubectl apply -f - ``` -For the `featureflagconfigurations.core.openfeature.dev` CRD: +For the `featureflags.core.openfeature.dev` CRD: ```sh -helm template openfeature/open-feature-operator -s templates/apiextensions.k8s.io_v1_customresourcedefinition_featureflagconfigurations.core.openfeature.dev.yaml | kubectl apply -f - +helm template openfeature/open-feature-operator -s templates/apiextensions.k8s.io_v1_customresourcedefinition_featureflags.core.openfeature.dev.yaml | kubectl apply -f - ``` -For the `flagsourceconfigurations.core.openfeature.dev` CRD: +For the `featureflagsources.core.openfeature.dev` CRD: ```sh -helm template openfeature/open-feature-operator -s templates/apiextensions.k8s.io_v1_customresourcedefinition_flagsourceconfigurations.core.openfeature.dev.yaml | kubectl apply -f - +helm template openfeature/open-feature-operator -s templates/apiextensions.k8s.io_v1_customresourcedefinition_featureflagsources.core.openfeature.dev.yaml | kubectl apply -f - ``` Keep in mind, you can set values as usual during this process: @@ -95,13 +95,13 @@ The command removes all the Kubernetes components associated with the chart and | Name | Description | Value | | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | | `sidecarConfiguration.port` | Sets the value of the `XXX_PORT` environment variable for the injected sidecar. | `8013` | -| `sidecarConfiguration.metricsPort` | Sets the value of the `XXX_METRICS_PORT` environment variable for the injected sidecar. | `8014` | +| `sidecarConfiguration.managementPort` | Sets the value of the `XXX_MANAGEMENT_PORT` environment variable for the injected sidecar. | `8014` | | `sidecarConfiguration.socketPath` | Sets the value of the `XXX_SOCKET_PATH` environment variable for the injected sidecar. | `""` | | `sidecarConfiguration.image.repository` | Sets the image for the injected sidecar. | `ghcr.io/open-feature/flagd` | -| `sidecarConfiguration.image.tag` | Sets the version tag for the injected sidecar. | `v0.6.3` | +| `sidecarConfiguration.image.tag` | Sets the version tag for the injected sidecar. | `v0.7.0` | | `sidecarConfiguration.providerArgs` | Used to append arguments to the sidecar startup command. This value is a comma separated string of key values separated by '=', e.g. `key=value,key2=value2` results in the appending of `--sync-provider-args key=value --sync-provider-args key2=value2`. | `""` | | `sidecarConfiguration.envVarPrefix` | Sets the prefix for all environment variables set in the injected sidecar. | `FLAGD` | -| `sidecarConfiguration.defaultSyncProvider` | Sets the value of the `XXX_SYNC_PROVIDER` environment variable for the injected sidecar container. There are 4 valid sync providers: `kubernetes`, `grpc`, `filepath` and `http`. | `kubernetes` | +| `sidecarConfiguration.defaultSyncProvider` | Sets the value of the `XXX_SYNC_PROVIDER` environment variable for the injected sidecar container. There are 4 valid sync providers: `kubernetes`, `grpc`, `file` and `http`. | `kubernetes` | | `sidecarConfiguration.evaluator` | Sets the value of the `XXX_EVALUATOR` environment variable for the injected sidecar container. | `json` | | `sidecarConfiguration.logFormat` | Sets the value of the `XXX_LOG_FORMAT` environment variable for the injected sidecar container. There are 2 valid log formats: `json` and `console`. | `json` | | `sidecarConfiguration.probesEnabled` | Enable or Disable Liveness and Readiness probes of the flagd sidecar. When enabled, HTTP probes( paths - `/readyz`, `/healthz`) are set with an initial delay of 5 seconds. | `true` | @@ -114,7 +114,7 @@ The command removes all the Kubernetes components associated with the chart and | Name | Description | Value | | ------------------------------------------ | ------------------------------------------------------------------------------- | ---------------------------------- | | `flagdProxyConfiguration.port` | Sets the port to expose the sync API on. | `8015` | -| `flagdProxyConfiguration.metricsPort` | Sets the port to expose the metrics API on. | `8016` | +| `flagdProxyConfiguration.managementPort` | Sets the port to expose the management API on. | `8016` | | `flagdProxyConfiguration.image.repository` | Sets the image for the flagd-proxy deployment. | `ghcr.io/open-feature/flagd-proxy` | | `flagdProxyConfiguration.image.tag` | Sets the tag for the flagd-proxy deployment. | `v0.2.8` | | `flagdProxyConfiguration.debugLogging` | Controls the addition of the `--debug` flag to the container startup arguments. | `false` | diff --git a/chart/open-feature-operator/values.yaml b/chart/open-feature-operator/values.yaml index b414a6a3f..02fd08c0d 100644 --- a/chart/open-feature-operator/values.yaml +++ b/chart/open-feature-operator/values.yaml @@ -7,8 +7,8 @@ defaultNamespace: open-feature-operator-system sidecarConfiguration: ## @param sidecarConfiguration.port Sets the value of the `XXX_PORT` environment variable for the injected sidecar. port: 8013 - ## @param sidecarConfiguration.metricsPort Sets the value of the `XXX_METRICS_PORT` environment variable for the injected sidecar. - metricsPort: 8014 + ## @param sidecarConfiguration.managementPort Sets the value of the `XXX_MANAGEMENT_PORT` environment variable for the injected sidecar. + managementPort: 8014 ## @param sidecarConfiguration.socketPath Sets the value of the `XXX_SOCKET_PATH` environment variable for the injected sidecar. socketPath: "" image: @@ -16,12 +16,12 @@ sidecarConfiguration: ## @param sidecarConfiguration.image.repository Sets the image for the injected sidecar. repository: "ghcr.io/open-feature/flagd" ## @param sidecarConfiguration.image.tag Sets the version tag for the injected sidecar. - tag: v0.6.3 + tag: v0.7.0 ## @param sidecarConfiguration.providerArgs Used to append arguments to the sidecar startup command. This value is a comma separated string of key values separated by '=', e.g. `key=value,key2=value2` results in the appending of `--sync-provider-args key=value --sync-provider-args key2=value2`. providerArgs: "" ## @param sidecarConfiguration.envVarPrefix Sets the prefix for all environment variables set in the injected sidecar. envVarPrefix: "FLAGD" - ## @param sidecarConfiguration.defaultSyncProvider Sets the value of the `XXX_SYNC_PROVIDER` environment variable for the injected sidecar container. There are 4 valid sync providers: `kubernetes`, `grpc`, `filepath` and `http`. + ## @param sidecarConfiguration.defaultSyncProvider Sets the value of the `XXX_SYNC_PROVIDER` environment variable for the injected sidecar container. There are 4 valid sync providers: `kubernetes`, `grpc`, `file` and `http`. defaultSyncProvider: kubernetes ## @param sidecarConfiguration.evaluator Sets the value of the `XXX_EVALUATOR` environment variable for the injected sidecar container. evaluator: json @@ -40,8 +40,8 @@ sidecarConfiguration: flagdProxyConfiguration: ## @param flagdProxyConfiguration.port Sets the port to expose the sync API on. port: 8015 - ## @param flagdProxyConfiguration.metricsPort Sets the port to expose the metrics API on. - metricsPort: 8016 + ## @param flagdProxyConfiguration.managementPort Sets the port to expose the management API on. + managementPort: 8016 image: ## @param flagdProxyConfiguration.image.repository Sets the image for the flagd-proxy deployment. repository: "ghcr.io/open-feature/flagd-proxy" @@ -74,7 +74,7 @@ controllerManager: ## @param controllerManager.manager.image.repository Sets the image for the operator. repository: ghcr.io/open-feature/open-feature-operator ## @param controllerManager.manager.image.tag Sets the version tag for the operator. - tag: v0.2.36 # x-release-please-version + tag: v0.3.0 # x-release-please-version resources: limits: ## @param controllerManager.manager.resources.limits.cpu Sets cpu resource limits for operator. diff --git a/common/common.go b/common/common.go new file mode 100644 index 000000000..42ec9d462 --- /dev/null +++ b/common/common.go @@ -0,0 +1,79 @@ +package common + +import ( + "context" + "errors" + "fmt" + "time" + + api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" + appsV1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + ReconcileErrorInterval = 10 * time.Second + ReconcileSuccessInterval = 120 * time.Second + FinalizerName = "featureflag.core.openfeature.dev/finalizer" + OpenFeatureAnnotationPath = "spec.template.metadata.annotations.openfeature.dev/openfeature.dev" + OpenFeatureAnnotationRoot = "openfeature.dev" + FlagdImagePullPolicy corev1.PullPolicy = "Always" + ClusterRoleBindingName string = "open-feature-operator-flagd-kubernetes-sync" + AllowKubernetesSyncAnnotation = "allowkubernetessync" + OpenFeatureAnnotationPrefix = "openfeature.dev" + PodOpenFeatureAnnotationPath = "metadata.annotations.openfeature.dev" + SourceConfigParam = "--sources" + ProbeReadiness = "/readyz" + ProbeLiveness = "/healthz" + ProbeInitialDelay = 5 + FeatureFlagSourceAnnotation = "featureflagsource" + EnabledAnnotation = "enabled" +) + +var ErrFlagdProxyNotReady = errors.New("flagd-proxy is not ready, deferring pod admission") +var ErrUnrecognizedSyncProvider = errors.New("unrecognized sync provider") + +func FeatureFlagSourceIndex(o client.Object) []string { + deployment, ok := o.(*appsV1.Deployment) + if !ok { + return []string{ + "false", + } + } + + if deployment.Spec.Template.ObjectMeta.Annotations == nil { + return []string{ + "false", + } + } + if _, ok := deployment.Spec.Template.ObjectMeta.Annotations[fmt.Sprintf("openfeature.dev/%s", FeatureFlagSourceAnnotation)]; ok { + return []string{ + "true", + } + } + return []string{ + "false", + } +} + +func FindFlagConfig(ctx context.Context, c client.Client, namespace string, name string) (*api.FeatureFlag, error) { + ffConfig := &api.FeatureFlag{} + if err := c.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, ffConfig); err != nil { + return nil, err + } + return ffConfig, nil +} + +// SharedOwnership returns true if any of the owner references match in the given slices +func SharedOwnership(ownerReferences1, ownerReferences2 []metav1.OwnerReference) bool { + for _, owner1 := range ownerReferences1 { + for _, owner2 := range ownerReferences2 { + if owner1.UID == owner2.UID { + return true + } + } + } + return false +} diff --git a/controllers/common/common_test.go b/common/common_test.go similarity index 56% rename from controllers/common/common_test.go rename to common/common_test.go index 8422141b8..155955fbb 100644 --- a/controllers/common/common_test.go +++ b/common/common_test.go @@ -1,17 +1,21 @@ package common import ( + "context" "fmt" "testing" + api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" "github.com/stretchr/testify/require" appsV1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/scheme" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" ) -func TestFlagSourceConfigurationIndex(t *testing.T) { +func TestFeatureFlagSourceIndex(t *testing.T) { tests := []struct { name string obj client.Object @@ -49,7 +53,7 @@ func TestFlagSourceConfigurationIndex(t *testing.T) { Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{ - fmt.Sprintf("openfeature.dev/%s", FlagSourceConfigurationAnnotation): "true", + fmt.Sprintf("openfeature.dev/%s", FeatureFlagSourceAnnotation): "true", }, }, }, @@ -61,7 +65,7 @@ func TestFlagSourceConfigurationIndex(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - out := FlagSourceConfigurationIndex(tt.obj) + out := FeatureFlagSourceIndex(tt.obj) require.Equal(t, tt.out, out) }) @@ -102,8 +106,60 @@ func TestSharedOwnership(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := SharedOwnership(tt.owner1, tt.owner2); got != tt.want { - t.Errorf("podOwnerIsOwner() = %v, want %v", got, tt.want) + t.Errorf("SharedOwnership() = %v, want %v", got, tt.want) } }) } } + +func TestFindFlagConfig(t *testing.T) { + ff := &api.FeatureFlag{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + } + + tests := []struct { + name string + ns string + obj *api.FeatureFlag + want *api.FeatureFlag + wantErr bool + }{ + { + name: "test", + ns: "default", + obj: ff, + want: ff, + wantErr: false, + }, + { + name: "non-existing", + ns: "default", + obj: ff, + want: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := api.AddToScheme(scheme.Scheme) + require.Nil(t, err) + fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(tt.obj).Build() + + got, err := FindFlagConfig(context.TODO(), fakeClient, tt.ns, tt.name) + + if (err != nil) != tt.wantErr { + t.Errorf("FindFlagConfig() = expected error %t, got %v", tt.wantErr, err) + } + + if !tt.wantErr { + require.Equal(t, tt.want.Name, got.Name) + require.Equal(t, tt.want.Namespace, got.Namespace) + } + + }) + } +} diff --git a/common/flagdinjector/fake/flagdinjector_mock.go b/common/flagdinjector/fake/flagdinjector_mock.go new file mode 100644 index 000000000..150db2954 --- /dev/null +++ b/common/flagdinjector/fake/flagdinjector_mock.go @@ -0,0 +1,65 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: controllers/common/flagd-injector.go + +// Package commonmock is a generated GoMock package. +package commonmock + +import ( + context "context" + gomock "github.com/golang/mock/gomock" + api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" + v1 "k8s.io/api/core/v1" + v10 "k8s.io/apimachinery/pkg/apis/meta/v1" + reflect "reflect" +) + +// MockFlagdContainerInjector is a mock of IFlagdContainerInjector interface. +type MockFlagdContainerInjector struct { + ctrl *gomock.Controller + recorder *MockFlagdContainerInjectorMockRecorder +} + +// MockFlagdContainerInjectorMockRecorder is the mock recorder for MockFlagdContainerInjector. +type MockFlagdContainerInjectorMockRecorder struct { + mock *MockFlagdContainerInjector +} + +// NewMockFlagdContainerInjector creates a new mock instance. +func NewMockFlagdContainerInjector(ctrl *gomock.Controller) *MockFlagdContainerInjector { + mock := &MockFlagdContainerInjector{ctrl: ctrl} + mock.recorder = &MockFlagdContainerInjectorMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockFlagdContainerInjector) EXPECT() *MockFlagdContainerInjectorMockRecorder { + return m.recorder +} + +// EnableClusterRoleBinding mocks base method. +func (m *MockFlagdContainerInjector) EnableClusterRoleBinding(ctx context.Context, namespace, serviceAccountName string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EnableClusterRoleBinding", ctx, namespace, serviceAccountName) + ret0, _ := ret[0].(error) + return ret0 +} + +// EnableClusterRoleBinding indicates an expected call of EnableClusterRoleBinding. +func (mr *MockFlagdContainerInjectorMockRecorder) EnableClusterRoleBinding(ctx, namespace, serviceAccountName interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnableClusterRoleBinding", reflect.TypeOf((*MockFlagdContainerInjector)(nil).EnableClusterRoleBinding), ctx, namespace, serviceAccountName) +} + +// InjectFlagd mocks base method. +func (m *MockFlagdContainerInjector) InjectFlagd(ctx context.Context, objectMeta *v10.ObjectMeta, podSpec *v1.PodSpec, flagSourceConfig *api.FeatureFlagSourceSpec) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InjectFlagd", ctx, objectMeta, podSpec, flagSourceConfig) + ret0, _ := ret[0].(error) + return ret0 +} + +// InjectFlagd indicates an expected call of InjectFlagd. +func (mr *MockFlagdContainerInjectorMockRecorder) InjectFlagd(ctx, objectMeta, podSpec, flagSourceConfig interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InjectFlagd", reflect.TypeOf((*MockFlagdContainerInjector)(nil).InjectFlagd), ctx, objectMeta, podSpec, flagSourceConfig) +} diff --git a/controllers/common/flagd-injector.go b/common/flagdinjector/flagdinjector.go similarity index 65% rename from controllers/common/flagd-injector.go rename to common/flagdinjector/flagdinjector.go index 3ad44679e..c05ef4425 100644 --- a/controllers/common/flagd-injector.go +++ b/common/flagdinjector/flagdinjector.go @@ -1,14 +1,18 @@ -package common +package flagdinjector import ( "context" "encoding/json" "fmt" + "time" + "github.com/go-logr/logr" - "github.com/open-feature/open-feature-operator/apis/core/v1alpha1" - "github.com/open-feature/open-feature-operator/controllers/common/constant" - "github.com/open-feature/open-feature-operator/pkg/types" - "github.com/open-feature/open-feature-operator/pkg/utils" + api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" + apicommon "github.com/open-feature/open-feature-operator/apis/core/v1beta1/common" + "github.com/open-feature/open-feature-operator/common" + "github.com/open-feature/open-feature-operator/common/flagdproxy" + "github.com/open-feature/open-feature-operator/common/types" + "github.com/open-feature/open-feature-operator/common/utils" appsV1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" @@ -16,7 +20,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" "sigs.k8s.io/controller-runtime/pkg/client" - "time" ) const ( @@ -28,7 +31,7 @@ type IFlagdContainerInjector interface { ctx context.Context, objectMeta *metav1.ObjectMeta, podSpec *corev1.PodSpec, - flagSourceConfig *v1alpha1.FlagSourceConfigurationSpec, + flagSourceConfig *api.FeatureFlagSourceSpec, ) error EnableClusterRoleBinding( @@ -41,23 +44,26 @@ type IFlagdContainerInjector interface { type FlagdContainerInjector struct { Client client.Client Logger logr.Logger - FlagdProxyConfig *FlagdProxyConfiguration - FlagDResourceRequirements corev1.ResourceRequirements + FlagdProxyConfig *flagdproxy.FlagdProxyConfiguration + FlagdResourceRequirements corev1.ResourceRequirements + Image string + Tag string } +//nolint:gocyclo func (fi *FlagdContainerInjector) InjectFlagd( ctx context.Context, objectMeta *metav1.ObjectMeta, podSpec *corev1.PodSpec, - flagSourceConfig *v1alpha1.FlagSourceConfigurationSpec, + flagSourceConfig *api.FeatureFlagSourceSpec, ) error { fi.Logger.V(1).Info(fmt.Sprintf("creating flagdContainer for pod %s/%s", objectMeta.Namespace, objectMeta.Name)) flagdContainer := fi.generateBasicFlagdContainer(flagSourceConfig) // Enable probes if flagSourceConfig.ProbesEnabled != nil && *flagSourceConfig.ProbesEnabled { - flagdContainer.LivenessProbe = buildProbe(constant.ProbeLiveness, int(flagSourceConfig.MetricsPort)) - flagdContainer.ReadinessProbe = buildProbe(constant.ProbeReadiness, int(flagSourceConfig.MetricsPort)) + flagdContainer.LivenessProbe = buildProbe(common.ProbeLiveness, int(flagSourceConfig.ManagementPort)) + flagdContainer.ReadinessProbe = buildProbe(common.ProbeReadiness, int(flagSourceConfig.ManagementPort)) } if err := fi.handleSidecarSources(ctx, objectMeta, podSpec, flagSourceConfig, &flagdContainer); err != nil { @@ -71,7 +77,7 @@ func (fi *FlagdContainerInjector) InjectFlagd( } // append sync provider args - if flagSourceConfig.SyncProviderArgs != nil { + if len(flagSourceConfig.SyncProviderArgs) > 0 { for _, v := range flagSourceConfig.SyncProviderArgs { flagdContainer.Args = append( flagdContainer.Args, @@ -121,12 +127,10 @@ func (fi *FlagdContainerInjector) InjectFlagd( // service account under the given namespace (required for kubernetes sync provider) func (fi *FlagdContainerInjector) EnableClusterRoleBinding(ctx context.Context, namespace, serviceAccountName string) error { serviceAccount := client.ObjectKey{ - Name: serviceAccountName, + Name: determineServiceAccountName(serviceAccountName), Namespace: namespace, } - if serviceAccountName == "" { - serviceAccount.Name = "default" - } + // Check if the service account exists fi.Logger.V(1).Info(fmt.Sprintf("Fetching serviceAccount: %s/%s", serviceAccount.Namespace, serviceAccount.Name)) sa := corev1.ServiceAccount{} @@ -134,39 +138,56 @@ func (fi *FlagdContainerInjector) EnableClusterRoleBinding(ctx context.Context, fi.Logger.V(1).Info(fmt.Sprintf("ServiceAccount not found: %s/%s", serviceAccount.Namespace, serviceAccount.Name)) return err } - fi.Logger.V(1).Info(fmt.Sprintf("Fetching clusterrolebinding: %s", constant.ClusterRoleBindingName)) + + fi.Logger.V(1).Info(fmt.Sprintf("Fetching clusterrolebinding: %s", common.ClusterRoleBindingName)) // Fetch service account if it exists crb := rbacv1.ClusterRoleBinding{} - if err := fi.Client.Get(ctx, client.ObjectKey{Name: constant.ClusterRoleBindingName}, &crb); errors.IsNotFound(err) { - fi.Logger.V(1).Info(fmt.Sprintf("ClusterRoleBinding not found: %s", constant.ClusterRoleBindingName)) + if err := fi.Client.Get(ctx, client.ObjectKey{Name: common.ClusterRoleBindingName}, &crb); errors.IsNotFound(err) { + fi.Logger.V(1).Info(fmt.Sprintf("ClusterRoleBinding not found: %s", common.ClusterRoleBindingName)) return err } - found := false + + if !fi.isServiceAccountSet(&crb, serviceAccount) { + return fi.updateServiceAccount(ctx, &crb, serviceAccount) + } + + return nil +} + +func determineServiceAccountName(name string) string { + if name == "" { + return "default" + } + return name +} + +func (fi *FlagdContainerInjector) isServiceAccountSet(crb *rbacv1.ClusterRoleBinding, serviceAccount client.ObjectKey) bool { for _, subject := range crb.Subjects { if subject.Kind == "ServiceAccount" && subject.Name == serviceAccount.Name && subject.Namespace == serviceAccount.Namespace { fi.Logger.V(1).Info(fmt.Sprintf("ClusterRoleBinding already exists for service account: %s/%s", serviceAccount.Namespace, serviceAccount.Name)) - found = true + return true } } - if !found { - fi.Logger.V(1).Info(fmt.Sprintf("Updating ClusterRoleBinding %s for service account: %s/%s", crb.Name, - serviceAccount.Namespace, serviceAccount.Name)) - crb.Subjects = append(crb.Subjects, rbacv1.Subject{ - Kind: "ServiceAccount", - Name: serviceAccount.Name, - Namespace: serviceAccount.Namespace, - }) - if err := fi.Client.Update(ctx, &crb); err != nil { - fi.Logger.V(1).Info(fmt.Sprintf("Failed to update ClusterRoleBinding: %s", err.Error())) - return err - } + return false +} + +func (fi *FlagdContainerInjector) updateServiceAccount(ctx context.Context, crb *rbacv1.ClusterRoleBinding, serviceAccount client.ObjectKey) error { + fi.Logger.V(1).Info(fmt.Sprintf("Updating ClusterRoleBinding %s for service account: %s/%s", crb.Name, + serviceAccount.Namespace, serviceAccount.Name)) + crb.Subjects = append(crb.Subjects, rbacv1.Subject{ + Kind: "ServiceAccount", + Name: serviceAccount.Name, + Namespace: serviceAccount.Namespace, + }) + if err := fi.Client.Update(ctx, crb); err != nil { + fi.Logger.V(1).Info(fmt.Sprintf("Failed to update ClusterRoleBinding: %s", err.Error())) + return err } fi.Logger.V(1).Info(fmt.Sprintf("Updated ClusterRoleBinding: %s", crb.Name)) - return nil } -func (fi *FlagdContainerInjector) handleSidecarSources(ctx context.Context, objectMeta *metav1.ObjectMeta, podSpec *corev1.PodSpec, flagSourceConfig *v1alpha1.FlagSourceConfigurationSpec, sidecar *corev1.Container) error { +func (fi *FlagdContainerInjector) handleSidecarSources(ctx context.Context, objectMeta *metav1.ObjectMeta, podSpec *corev1.PodSpec, flagSourceConfig *api.FeatureFlagSourceSpec, sidecar *corev1.Container) error { sources, err := fi.buildSources(ctx, objectMeta, flagSourceConfig, podSpec, sidecar) if err != nil { return err @@ -179,7 +200,7 @@ func (fi *FlagdContainerInjector) handleSidecarSources(ctx context.Context, obje return nil } -func (fi *FlagdContainerInjector) buildSources(ctx context.Context, objectMeta *metav1.ObjectMeta, flagSourceConfig *v1alpha1.FlagSourceConfigurationSpec, podSpec *corev1.PodSpec, sidecar *corev1.Container) ([]types.SourceConfig, error) { +func (fi *FlagdContainerInjector) buildSources(ctx context.Context, objectMeta *metav1.ObjectMeta, flagSourceConfig *api.FeatureFlagSourceSpec, podSpec *corev1.PodSpec, sidecar *corev1.Container) ([]types.SourceConfig, error) { var sourceCfgCollection []types.SourceConfig for _, source := range flagSourceConfig.Sources { @@ -187,41 +208,41 @@ func (fi *FlagdContainerInjector) buildSources(ctx context.Context, objectMeta * source.Provider = flagSourceConfig.DefaultSyncProvider } - var sourceCfg types.SourceConfig - var err error - - switch { - case source.Provider.IsKubernetes(): - sourceCfg, err = fi.toKubernetesProviderConfig(ctx, objectMeta, podSpec, source) - if err != nil { - return []types.SourceConfig{}, err - } - case source.Provider.IsFilepath(): - sourceCfg, err = fi.toFilepathProviderConfig(ctx, objectMeta, podSpec, sidecar, source) - if err != nil { - return []types.SourceConfig{}, err - } - case source.Provider.IsHttp(): - sourceCfg = fi.toHttpProviderConfig(source) - case source.Provider.IsGrpc(): - sourceCfg = fi.toGrpcProviderConfig(source) - case source.Provider.IsFlagdProxy(): - sourceCfg, err = fi.toFlagdProxyConfig(ctx, objectMeta, source) - if err != nil { - return []types.SourceConfig{}, err - } - default: - return []types.SourceConfig{}, fmt.Errorf("could not add provider %s: %w", source.Provider, constant.ErrUnrecognizedSyncProvider) + sourceCfg, err := fi.newSourceConfig(ctx, source, objectMeta, podSpec, sidecar) + if err != nil { + return []types.SourceConfig{}, err } - sourceCfgCollection = append(sourceCfgCollection, sourceCfg) + sourceCfgCollection = append(sourceCfgCollection, *sourceCfg) } return sourceCfgCollection, nil } -func (fi *FlagdContainerInjector) toFilepathProviderConfig(ctx context.Context, objectMeta *metav1.ObjectMeta, podSpec *corev1.PodSpec, sidecar *corev1.Container, source v1alpha1.Source) (types.SourceConfig, error) { +func (fi *FlagdContainerInjector) newSourceConfig(ctx context.Context, source api.Source, objectMeta *metav1.ObjectMeta, podSpec *corev1.PodSpec, sidecar *corev1.Container) (*types.SourceConfig, error) { + sourceCfg := types.SourceConfig{} + var err error = nil + + switch { + case source.Provider.IsKubernetes(): + sourceCfg, err = fi.toKubernetesProviderConfig(ctx, objectMeta, podSpec, source) + case source.Provider.IsFilepath(): + sourceCfg, err = fi.toFilepathProviderConfig(ctx, objectMeta, podSpec, sidecar, source) + case source.Provider.IsHttp(): + sourceCfg = fi.toHttpProviderConfig(source) + case source.Provider.IsGrpc(): + sourceCfg = fi.toGrpcProviderConfig(source) + case source.Provider.IsFlagdProxy(): + sourceCfg, err = fi.toFlagdProxyConfig(ctx, objectMeta, source) + default: + err = fmt.Errorf("could not add provider %s: %w", source.Provider, common.ErrUnrecognizedSyncProvider) + } + + return &sourceCfg, err +} + +func (fi *FlagdContainerInjector) toFilepathProviderConfig(ctx context.Context, objectMeta *metav1.ObjectMeta, podSpec *corev1.PodSpec, sidecar *corev1.Container, source api.Source) (types.SourceConfig, error) { // create config map ns, n := utils.ParseAnnotation(source.Source, objectMeta.Namespace) cm := corev1.ConfigMap{} @@ -234,7 +255,7 @@ func (fi *FlagdContainerInjector) toFilepathProviderConfig(ctx context.Context, } // Add owner reference of the pod's owner - if !SharedOwnership(objectMeta.OwnerReferences, cm.OwnerReferences) { + if !common.SharedOwnership(objectMeta.OwnerReferences, cm.OwnerReferences) { fi.updateCMOwnerReference(ctx, objectMeta, cm) } @@ -250,7 +271,7 @@ func (fi *FlagdContainerInjector) toFilepathProviderConfig(ctx context.Context, }, }) - mountPath := fmt.Sprintf("%s/%s", rootFileSyncMountPath, utils.FeatureFlagConfigurationId(ns, n)) + mountPath := fmt.Sprintf("%s/%s", rootFileSyncMountPath, utils.FeatureFlagId(ns, n)) sidecar.VolumeMounts = append(sidecar.VolumeMounts, corev1.VolumeMount{ Name: n, // create a directory mount per featureFlag spec @@ -259,8 +280,7 @@ func (fi *FlagdContainerInjector) toFilepathProviderConfig(ctx context.Context, }) return types.SourceConfig{ - URI: fmt.Sprintf("%s/%s", mountPath, utils.FeatureFlagConfigurationConfigMapKey(ns, n)), - // todo - this constant needs to be aligned with flagd. We have a mixed usage of file vs filepath + URI: fmt.Sprintf("%s/%s", mountPath, utils.FeatureFlagConfigMapKey(ns, n)), Provider: "file", }, nil } @@ -278,18 +298,18 @@ func (fi *FlagdContainerInjector) updateCMOwnerReference(ctx context.Context, ob } } -func (fi *FlagdContainerInjector) toHttpProviderConfig(source v1alpha1.Source) types.SourceConfig { +func (fi *FlagdContainerInjector) toHttpProviderConfig(source api.Source) types.SourceConfig { return types.SourceConfig{ URI: source.Source, - Provider: string(v1alpha1.SyncProviderHttp), + Provider: string(apicommon.SyncProviderHttp), BearerToken: source.HttpSyncBearerToken, } } -func (fi *FlagdContainerInjector) toGrpcProviderConfig(source v1alpha1.Source) types.SourceConfig { +func (fi *FlagdContainerInjector) toGrpcProviderConfig(source api.Source) types.SourceConfig { return types.SourceConfig{ URI: source.Source, - Provider: string(v1alpha1.SyncProviderGrpc), + Provider: string(apicommon.SyncProviderGrpc), TLS: source.TLS, CertPath: source.CertPath, ProviderID: source.ProviderID, @@ -297,26 +317,26 @@ func (fi *FlagdContainerInjector) toGrpcProviderConfig(source v1alpha1.Source) t } } -func (fi *FlagdContainerInjector) toFlagdProxyConfig(ctx context.Context, objectMeta *metav1.ObjectMeta, source v1alpha1.Source) (types.SourceConfig, error) { +func (fi *FlagdContainerInjector) toFlagdProxyConfig(ctx context.Context, objectMeta *metav1.ObjectMeta, source api.Source) (types.SourceConfig, error) { // does the proxy exist exists, ready, err := fi.isFlagdProxyReady(ctx) if err != nil { return types.SourceConfig{}, err } if !exists || (exists && !ready) { - return types.SourceConfig{}, constant.ErrFlagdProxyNotReady + return types.SourceConfig{}, common.ErrFlagdProxyNotReady } ns, n := utils.ParseAnnotation(source.Source, objectMeta.Namespace) return types.SourceConfig{ Provider: "grpc", Selector: fmt.Sprintf("core.openfeature.dev/%s/%s", ns, n), - URI: fmt.Sprintf("%s.%s.svc.cluster.local:%d", FlagdProxyServiceName, fi.FlagdProxyConfig.Namespace, fi.FlagdProxyConfig.Port), + URI: fmt.Sprintf("%s.%s.svc.cluster.local:%d", flagdproxy.FlagdProxyServiceName, fi.FlagdProxyConfig.Namespace, fi.FlagdProxyConfig.Port), }, nil } func (fi *FlagdContainerInjector) isFlagdProxyReady(ctx context.Context) (bool, bool, error) { d := appsV1.Deployment{} - err := fi.Client.Get(ctx, client.ObjectKey{Name: FlagdProxyDeploymentName, Namespace: fi.FlagdProxyConfig.Namespace}, &d) + err := fi.Client.Get(ctx, client.ObjectKey{Name: flagdproxy.FlagdProxyDeploymentName, Namespace: fi.FlagdProxyConfig.Namespace}, &d) if err != nil { if errors.IsNotFound(err) { // does not exist, is not ready, no error @@ -331,7 +351,7 @@ func (fi *FlagdContainerInjector) isFlagdProxyReady(ctx context.Context) (bool, return true, false, fmt.Errorf( "flagd-proxy not ready after 3 minutes, was created at %s: %w", d.CreationTimestamp.Time.String(), - constant.ErrFlagdProxyNotReady, + common.ErrFlagdProxyNotReady, ) } return true, false, nil @@ -340,12 +360,12 @@ func (fi *FlagdContainerInjector) isFlagdProxyReady(ctx context.Context) (bool, return true, true, nil } -func (fi *FlagdContainerInjector) toKubernetesProviderConfig(ctx context.Context, objectMeta *metav1.ObjectMeta, podSpec *corev1.PodSpec, source v1alpha1.Source) (types.SourceConfig, error) { +func (fi *FlagdContainerInjector) toKubernetesProviderConfig(ctx context.Context, objectMeta *metav1.ObjectMeta, podSpec *corev1.PodSpec, source api.Source) (types.SourceConfig, error) { ns, n := utils.ParseAnnotation(source.Source, objectMeta.Namespace) - // ensure that the FeatureFlagConfiguration exists - if _, err := FindFlagConfig(ctx, fi.Client, ns, n); err != nil { - return types.SourceConfig{}, fmt.Errorf("could not retrieve feature flag configuration %s/%s: %w", ns, n, err) + // ensure that the FeatureFlag exists + if _, err := common.FindFlagConfig(ctx, fi.Client, ns, n); err != nil { + return types.SourceConfig{}, fmt.Errorf("could not retrieve featureflag %s/%s: %w", ns, n, err) } // add permissions to pod @@ -357,33 +377,33 @@ func (fi *FlagdContainerInjector) toKubernetesProviderConfig(ctx context.Context if objectMeta.Annotations == nil { objectMeta.Annotations = map[string]string{} } - objectMeta.Annotations[fmt.Sprintf("%s/%s", constant.OpenFeatureAnnotationPrefix, constant.AllowKubernetesSyncAnnotation)] = "true" + objectMeta.Annotations[fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationPrefix, common.AllowKubernetesSyncAnnotation)] = "true" // build K8s config return types.SourceConfig{ URI: fmt.Sprintf("%s/%s", ns, n), - Provider: string(v1alpha1.SyncProviderKubernetes), + Provider: string(apicommon.SyncProviderKubernetes), }, nil } -func (fi *FlagdContainerInjector) generateBasicFlagdContainer(flagSourceConfig *v1alpha1.FlagSourceConfigurationSpec) corev1.Container { +func (fi *FlagdContainerInjector) generateBasicFlagdContainer(flagSourceConfig *api.FeatureFlagSourceSpec) corev1.Container { return corev1.Container{ Name: "flagd", - Image: fmt.Sprintf("%s:%s", flagSourceConfig.Image, flagSourceConfig.Tag), + Image: fmt.Sprintf("%s:%s", fi.Image, fi.Tag), Args: []string{ "start", }, - ImagePullPolicy: constant.FlagDImagePullPolicy, + ImagePullPolicy: common.FlagdImagePullPolicy, VolumeMounts: []corev1.VolumeMount{}, Env: []corev1.EnvVar{}, Ports: []corev1.ContainerPort{ { Name: "metrics", - ContainerPort: flagSourceConfig.MetricsPort, + ContainerPort: flagSourceConfig.ManagementPort, }, }, SecurityContext: getSecurityContext(), - Resources: fi.FlagDResourceRequirements, + Resources: fi.FlagdResourceRequirements, } } @@ -394,16 +414,19 @@ func (fi *FlagdContainerInjector) createConfigMap(ctx context.Context, namespace references = append(references, ownerReferences[0]) references[0].Controller = utils.FalseVal() } - ff, err := FindFlagConfig(ctx, fi.Client, namespace, name) + ff, err := common.FindFlagConfig(ctx, fi.Client, namespace, name) if err != nil { - return fmt.Errorf("could not retrieve feature flag configuration %s/%s: %w", namespace, name, err) + return fmt.Errorf("could not retrieve featureflag %s/%s: %w", namespace, name, err) } references = append(references, ff.GetReference()) - cm := ff.GenerateConfigMap(name, namespace, references) + cm, err := ff.GenerateConfigMap(name, namespace, references) + if err != nil { + return fmt.Errorf("could generate configmap for featureflag %s/%s: %w", namespace, name, err) + } - return fi.Client.Create(ctx, &cm) + return fi.Client.Create(ctx, cm) } func addFlagdContainer(spec *corev1.PodSpec, flagdContainer corev1.Container) { @@ -426,7 +449,7 @@ func appendSources(sources []types.SourceConfig, sidecar *corev1.Container) erro return err } - sidecar.Args = append(sidecar.Args, constant.SourceConfigParam, string(bytes)) + sidecar.Args = append(sidecar.Args, common.SourceConfigParam, string(bytes)) return nil } @@ -468,6 +491,6 @@ func buildProbe(path string, port int) *corev1.Probe { ProbeHandler: corev1.ProbeHandler{ HTTPGet: httpGetAction, }, - InitialDelaySeconds: constant.ProbeInitialDelay, + InitialDelaySeconds: common.ProbeInitialDelay, } } diff --git a/controllers/common/flagd-injector_test.go b/common/flagdinjector/flagdinjector_test.go similarity index 83% rename from controllers/common/flagd-injector_test.go rename to common/flagdinjector/flagdinjector_test.go index c988b18d0..e3752c46a 100644 --- a/controllers/common/flagd-injector_test.go +++ b/common/flagdinjector/flagdinjector_test.go @@ -1,12 +1,17 @@ -package common +package flagdinjector import ( "context" "errors" + "reflect" + "testing" + "github.com/go-logr/logr/testr" - "github.com/open-feature/open-feature-operator/apis/core/v1alpha1" - "github.com/open-feature/open-feature-operator/controllers/common/constant" - "github.com/open-feature/open-feature-operator/pkg/utils" + api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" + apicommon "github.com/open-feature/open-feature-operator/apis/core/v1beta1/common" + "github.com/open-feature/open-feature-operator/common" + "github.com/open-feature/open-feature-operator/common/flagdproxy" + "github.com/open-feature/open-feature-operator/common/utils" "github.com/stretchr/testify/require" appsV1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -17,10 +22,13 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/client-go/kubernetes/scheme" - "reflect" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" - "testing" +) + +const ( + testTag = "0.5.0" + testImage = "flagd" ) func TestFlagdContainerInjector_InjectDefaultSyncProvider(t *testing.T) { @@ -31,7 +39,9 @@ func TestFlagdContainerInjector_InjectDefaultSyncProvider(t *testing.T) { Client: fakeClient, Logger: testr.New(t), FlagdProxyConfig: getProxyConfig(), - FlagDResourceRequirements: getResourceRequirements(), + FlagdResourceRequirements: getResourceRequirements(), + Image: testImage, + Tag: testTag, } deployment := appsV1.Deployment{ @@ -44,9 +54,9 @@ func TestFlagdContainerInjector_InjectDefaultSyncProvider(t *testing.T) { flagSourceConfig := getFlagSourceConfigSpec() - flagSourceConfig.DefaultSyncProvider = v1alpha1.SyncProviderGrpc + flagSourceConfig.DefaultSyncProvider = apicommon.SyncProviderGrpc - flagSourceConfig.Sources = []v1alpha1.Source{{}} + flagSourceConfig.Sources = []api.Source{{}} err := fi.InjectFlagd(context.Background(), &deployment.ObjectMeta, &deployment.Spec.Template.Spec, flagSourceConfig) require.Nil(t, err) @@ -68,7 +78,9 @@ func TestFlagdContainerInjector_InjectDefaultSyncProvider_WithDebugLogging(t *te Client: fakeClient, Logger: testr.New(t), FlagdProxyConfig: getProxyConfig(), - FlagDResourceRequirements: getResourceRequirements(), + FlagdResourceRequirements: getResourceRequirements(), + Image: testImage, + Tag: testTag, } deployment := appsV1.Deployment{ @@ -81,11 +93,11 @@ func TestFlagdContainerInjector_InjectDefaultSyncProvider_WithDebugLogging(t *te flagSourceConfig := getFlagSourceConfigSpec() - flagSourceConfig.DefaultSyncProvider = v1alpha1.SyncProviderGrpc + flagSourceConfig.DefaultSyncProvider = apicommon.SyncProviderGrpc flagSourceConfig.DebugLogging = utils.TrueVal() - flagSourceConfig.Sources = []v1alpha1.Source{{}} + flagSourceConfig.Sources = []api.Source{{}} err := fi.InjectFlagd(context.Background(), &deployment.ObjectMeta, &deployment.Spec.Template.Spec, flagSourceConfig) require.Nil(t, err) @@ -107,7 +119,9 @@ func TestFlagdContainerInjector_InjectDefaultSyncProvider_WithOtelCollectorUri(t Client: fakeClient, Logger: testr.New(t), FlagdProxyConfig: getProxyConfig(), - FlagDResourceRequirements: getResourceRequirements(), + FlagdResourceRequirements: getResourceRequirements(), + Image: testImage, + Tag: testTag, } deployment := appsV1.Deployment{ @@ -120,11 +134,11 @@ func TestFlagdContainerInjector_InjectDefaultSyncProvider_WithOtelCollectorUri(t flagSourceConfig := getFlagSourceConfigSpec() - flagSourceConfig.DefaultSyncProvider = v1alpha1.SyncProviderGrpc + flagSourceConfig.DefaultSyncProvider = apicommon.SyncProviderGrpc flagSourceConfig.OtelCollectorUri = "localhost:4317" - flagSourceConfig.Sources = []v1alpha1.Source{{}} + flagSourceConfig.Sources = []api.Source{{}} err := fi.InjectFlagd(context.Background(), &deployment.ObjectMeta, &deployment.Spec.Template.Spec, flagSourceConfig) require.Nil(t, err) @@ -146,7 +160,9 @@ func TestFlagdContainerInjector_InjectDefaultSyncProvider_WithResources(t *testi Client: fakeClient, Logger: testr.New(t), FlagdProxyConfig: getProxyConfig(), - FlagDResourceRequirements: getResourceRequirements(), + FlagdResourceRequirements: getResourceRequirements(), + Image: testImage, + Tag: testTag, } deployment := appsV1.Deployment{ @@ -159,7 +175,7 @@ func TestFlagdContainerInjector_InjectDefaultSyncProvider_WithResources(t *testi flagSourceConfig := getFlagSourceConfigSpec() - flagSourceConfig.DefaultSyncProvider = v1alpha1.SyncProviderGrpc + flagSourceConfig.DefaultSyncProvider = apicommon.SyncProviderGrpc flagSourceConfig.Resources = corev1.ResourceRequirements{ Limits: map[corev1.ResourceName]resource.Quantity{ @@ -172,7 +188,7 @@ func TestFlagdContainerInjector_InjectDefaultSyncProvider_WithResources(t *testi }, } - flagSourceConfig.Sources = []v1alpha1.Source{{}} + flagSourceConfig.Sources = []api.Source{{}} err := fi.InjectFlagd(context.Background(), &deployment.ObjectMeta, &deployment.Spec.Template.Spec, flagSourceConfig) require.Nil(t, err) @@ -195,7 +211,9 @@ func TestFlagdContainerInjector_InjectDefaultSyncProvider_WithSyncProviderArgs(t Client: fakeClient, Logger: testr.New(t), FlagdProxyConfig: getProxyConfig(), - FlagDResourceRequirements: getResourceRequirements(), + FlagdResourceRequirements: getResourceRequirements(), + Image: testImage, + Tag: testTag, } deployment := appsV1.Deployment{ @@ -210,9 +228,9 @@ func TestFlagdContainerInjector_InjectDefaultSyncProvider_WithSyncProviderArgs(t flagSourceConfig.SyncProviderArgs = []string{"arg-1", "arg-2"} - flagSourceConfig.DefaultSyncProvider = v1alpha1.SyncProviderGrpc + flagSourceConfig.DefaultSyncProvider = apicommon.SyncProviderGrpc - flagSourceConfig.Sources = []v1alpha1.Source{{}} + flagSourceConfig.Sources = []api.Source{{}} err := fi.InjectFlagd(context.Background(), &deployment.ObjectMeta, &deployment.Spec.Template.Spec, flagSourceConfig) require.Nil(t, err) @@ -234,7 +252,9 @@ func TestFlagdContainerInjector_InjectFlagdKubernetesSource(t *testing.T) { Client: fakeClient, Logger: testr.New(t), FlagdProxyConfig: getProxyConfig(), - FlagDResourceRequirements: getResourceRequirements(), + FlagdResourceRequirements: getResourceRequirements(), + Image: testImage, + Tag: testTag, } deployment := appsV1.Deployment{ @@ -246,10 +266,10 @@ func TestFlagdContainerInjector_InjectFlagdKubernetesSource(t *testing.T) { flagSourceConfig := getFlagSourceConfigSpec() - flagSourceConfig.Sources = []v1alpha1.Source{ + flagSourceConfig.Sources = []api.Source{ { Source: "my-namespace/server-side", - Provider: v1alpha1.SyncProviderKubernetes, + Provider: apicommon.SyncProviderKubernetes, }, } @@ -265,7 +285,7 @@ func TestFlagdContainerInjector_InjectFlagdKubernetesSource(t *testing.T) { // verify the update of the ClusterRoleBinding cbr := &rbacv1.ClusterRoleBinding{} - err = fakeClient.Get(context.Background(), client.ObjectKey{Name: constant.ClusterRoleBindingName}, cbr) + err = fakeClient.Get(context.Background(), client.ObjectKey{Name: common.ClusterRoleBindingName}, cbr) require.Nil(t, err) @@ -285,7 +305,9 @@ func TestFlagdContainerInjector_InjectFlagdFilePathSource(t *testing.T) { Client: fakeClient, Logger: testr.New(t), FlagdProxyConfig: getProxyConfig(), - FlagDResourceRequirements: getResourceRequirements(), + FlagdResourceRequirements: getResourceRequirements(), + Image: testImage, + Tag: testTag, } deployment := appsV1.Deployment{ @@ -298,10 +320,10 @@ func TestFlagdContainerInjector_InjectFlagdFilePathSource(t *testing.T) { flagSourceConfig := getFlagSourceConfigSpec() - flagSourceConfig.Sources = []v1alpha1.Source{ + flagSourceConfig.Sources = []api.Source{ { Source: "my-namespace/server-side", - Provider: v1alpha1.SyncProviderFilepath, + Provider: apicommon.SyncProviderFilepath, }, } @@ -361,7 +383,9 @@ func TestFlagdContainerInjector_InjectFlagdFilePathSource_UpdateReferencedConfig Client: fakeClient, Logger: testr.New(t), FlagdProxyConfig: getProxyConfig(), - FlagDResourceRequirements: getResourceRequirements(), + FlagdResourceRequirements: getResourceRequirements(), + Image: testImage, + Tag: testTag, } ownerRef := metav1.OwnerReference{ @@ -382,10 +406,10 @@ func TestFlagdContainerInjector_InjectFlagdFilePathSource_UpdateReferencedConfig flagSourceConfig := getFlagSourceConfigSpec() - flagSourceConfig.Sources = []v1alpha1.Source{ + flagSourceConfig.Sources = []api.Source{ { Source: "my-namespace/server-side", - Provider: v1alpha1.SyncProviderFilepath, + Provider: apicommon.SyncProviderFilepath, }, } @@ -440,7 +464,9 @@ func TestFlagdContainerInjector_InjectHttpSource(t *testing.T) { Client: fakeClient, Logger: testr.New(t), FlagdProxyConfig: getProxyConfig(), - FlagDResourceRequirements: getResourceRequirements(), + FlagdResourceRequirements: getResourceRequirements(), + Image: testImage, + Tag: testTag, } deployment := appsV1.Deployment{ @@ -453,11 +479,11 @@ func TestFlagdContainerInjector_InjectHttpSource(t *testing.T) { flagSourceConfig := getFlagSourceConfigSpec() - flagSourceConfig.Sources = []v1alpha1.Source{ + flagSourceConfig.Sources = []api.Source{ { Source: "http://localhost:8013", HttpSyncBearerToken: "my-token", - Provider: v1alpha1.SyncProviderHttp, + Provider: apicommon.SyncProviderHttp, }, } @@ -482,7 +508,9 @@ func TestFlagdContainerInjector_InjectGrpcSource(t *testing.T) { Client: fakeClient, Logger: testr.New(t), FlagdProxyConfig: getProxyConfig(), - FlagDResourceRequirements: getResourceRequirements(), + FlagdResourceRequirements: getResourceRequirements(), + Image: testImage, + Tag: testTag, } deployment := appsV1.Deployment{ @@ -495,10 +523,10 @@ func TestFlagdContainerInjector_InjectGrpcSource(t *testing.T) { flagSourceConfig := getFlagSourceConfigSpec() - flagSourceConfig.Sources = []v1alpha1.Source{ + flagSourceConfig.Sources = []api.Source{ { Source: "grpc://localhost:8013", - Provider: v1alpha1.SyncProviderGrpc, + Provider: apicommon.SyncProviderGrpc, TLS: true, CertPath: "cert-path", ProviderID: "provider-id", @@ -527,7 +555,9 @@ func TestFlagdContainerInjector_InjectProxySource_ProxyNotAvailable(t *testing.T Client: fakeClient, Logger: testr.New(t), FlagdProxyConfig: getProxyConfig(), - FlagDResourceRequirements: getResourceRequirements(), + FlagdResourceRequirements: getResourceRequirements(), + Image: testImage, + Tag: testTag, } deployment := appsV1.Deployment{ @@ -540,9 +570,9 @@ func TestFlagdContainerInjector_InjectProxySource_ProxyNotAvailable(t *testing.T flagSourceConfig := getFlagSourceConfigSpec() - flagSourceConfig.Sources = []v1alpha1.Source{ + flagSourceConfig.Sources = []api.Source{ { - Provider: v1alpha1.SyncProviderFlagdProxy, + Provider: apicommon.SyncProviderFlagdProxy, }, } @@ -550,7 +580,7 @@ func TestFlagdContainerInjector_InjectProxySource_ProxyNotAvailable(t *testing.T // expect an error here because we do not have a flagd proxy in our cluster require.NotNil(t, err) - require.ErrorIs(t, err, constant.ErrFlagdProxyNotReady) + require.ErrorIs(t, err, common.ErrFlagdProxyNotReady) } func TestFlagdContainerInjector_InjectProxySource_ProxyNotReady(t *testing.T) { @@ -558,7 +588,7 @@ func TestFlagdContainerInjector_InjectProxySource_ProxyNotReady(t *testing.T) { namespace, fakeClient := initContainerInjectionTestEnv() flagdProxyDeployment := &appsV1.Deployment{ - ObjectMeta: metav1.ObjectMeta{Name: FlagdProxyDeploymentName, Namespace: namespace}, + ObjectMeta: metav1.ObjectMeta{Name: flagdproxy.FlagdProxyDeploymentName, Namespace: namespace}, } err := fakeClient.Create(context.Background(), flagdProxyDeployment) @@ -568,7 +598,9 @@ func TestFlagdContainerInjector_InjectProxySource_ProxyNotReady(t *testing.T) { Client: fakeClient, Logger: testr.New(t), FlagdProxyConfig: getProxyConfig(), - FlagDResourceRequirements: getResourceRequirements(), + FlagdResourceRequirements: getResourceRequirements(), + Image: testImage, + Tag: testTag, } deployment := appsV1.Deployment{ @@ -581,15 +613,15 @@ func TestFlagdContainerInjector_InjectProxySource_ProxyNotReady(t *testing.T) { flagSourceConfig := getFlagSourceConfigSpec() - flagSourceConfig.Sources = []v1alpha1.Source{ + flagSourceConfig.Sources = []api.Source{ { - Provider: v1alpha1.SyncProviderFlagdProxy, + Provider: apicommon.SyncProviderFlagdProxy, }, } err = fi.InjectFlagd(context.Background(), &deployment.ObjectMeta, &deployment.Spec.Template.Spec, flagSourceConfig) require.NotNil(t, err) - require.ErrorIs(t, err, constant.ErrFlagdProxyNotReady) + require.ErrorIs(t, err, common.ErrFlagdProxyNotReady) } func TestFlagdContainerInjector_InjectProxySource_ProxyIsReady(t *testing.T) { @@ -597,7 +629,7 @@ func TestFlagdContainerInjector_InjectProxySource_ProxyIsReady(t *testing.T) { namespace, fakeClient := initContainerInjectionTestEnv() flagdProxyDeployment := &appsV1.Deployment{ - ObjectMeta: metav1.ObjectMeta{Name: FlagdProxyDeploymentName, Namespace: namespace}, + ObjectMeta: metav1.ObjectMeta{Name: flagdproxy.FlagdProxyDeploymentName, Namespace: namespace}, } err := fakeClient.Create(context.Background(), flagdProxyDeployment) @@ -612,7 +644,9 @@ func TestFlagdContainerInjector_InjectProxySource_ProxyIsReady(t *testing.T) { Client: fakeClient, Logger: testr.New(t), FlagdProxyConfig: getProxyConfig(), - FlagDResourceRequirements: getResourceRequirements(), + FlagdResourceRequirements: getResourceRequirements(), + Image: testImage, + Tag: testTag, } deployment := appsV1.Deployment{ @@ -625,9 +659,9 @@ func TestFlagdContainerInjector_InjectProxySource_ProxyIsReady(t *testing.T) { flagSourceConfig := getFlagSourceConfigSpec() - flagSourceConfig.Sources = []v1alpha1.Source{ + flagSourceConfig.Sources = []api.Source{ { - Provider: v1alpha1.SyncProviderFlagdProxy, + Provider: apicommon.SyncProviderFlagdProxy, }, } @@ -651,7 +685,9 @@ func TestFlagdContainerInjector_Inject_FlagdContainerAlreadyPresent(t *testing.T Client: fakeClient, Logger: testr.New(t), FlagdProxyConfig: getProxyConfig(), - FlagDResourceRequirements: getResourceRequirements(), + FlagdResourceRequirements: getResourceRequirements(), + Image: testImage, + Tag: testTag, } deployment := appsV1.Deployment{ @@ -693,7 +729,9 @@ func TestFlagdContainerInjector_InjectUnknownSyncProvider(t *testing.T) { Client: fakeClient, Logger: testr.New(t), FlagdProxyConfig: getProxyConfig(), - FlagDResourceRequirements: getResourceRequirements(), + FlagdResourceRequirements: getResourceRequirements(), + Image: testImage, + Tag: testTag, } deployment := appsV1.Deployment{ @@ -706,7 +744,7 @@ func TestFlagdContainerInjector_InjectUnknownSyncProvider(t *testing.T) { flagSourceConfig := getFlagSourceConfigSpec() - flagSourceConfig.Sources = []v1alpha1.Source{ + flagSourceConfig.Sources = []api.Source{ { Provider: "unknown", }, @@ -715,12 +753,12 @@ func TestFlagdContainerInjector_InjectUnknownSyncProvider(t *testing.T) { err := fi.InjectFlagd(context.Background(), &deployment.ObjectMeta, &deployment.Spec.Template.Spec, flagSourceConfig) require.NotNil(t, err) - require.ErrorIs(t, err, constant.ErrUnrecognizedSyncProvider) + require.ErrorIs(t, err, common.ErrUnrecognizedSyncProvider) } func TestFlagdContainerInjector_createConfigMap(t *testing.T) { - _ = v1alpha1.AddToScheme(scheme.Scheme) + _ = api.AddToScheme(scheme.Scheme) fakeClientBuilder := fake.NewClientBuilder(). WithScheme(scheme.Scheme) @@ -735,7 +773,7 @@ func TestFlagdContainerInjector_createConfigMap(t *testing.T) { wantErr error }{ { - name: "feature flag config not found", + name: "featureflag not found", flagdInjector: &FlagdContainerInjector{ Client: fakeClientBuilder.Build(), Logger: testr.New(t), @@ -743,12 +781,12 @@ func TestFlagdContainerInjector_createConfigMap(t *testing.T) { namespace: "myns", confname: "mypod", ownerRefs: []metav1.OwnerReference{{}}, - wantErr: errors.New("could not retrieve feature flag configuration myns/mypod: featureflagconfigurations.core.openfeature.dev \"mypod\" not found"), + wantErr: errors.New("could not retrieve featureflag myns/mypod: featureflags.core.openfeature.dev \"mypod\" not found"), }, { - name: "feature flag config found, config map created", + name: "featureflag found, config map created", flagdInjector: &FlagdContainerInjector{ - Client: fakeClientBuilder.WithObjects(&v1alpha1.FeatureFlagConfiguration{ + Client: fakeClientBuilder.WithObjects(&api.FeatureFlag{ ObjectMeta: metav1.ObjectMeta{ Name: "myconf", Namespace: "myns", @@ -775,7 +813,7 @@ func TestFlagdContainerInjector_createConfigMap(t *testing.T) { require.Nil(t, err) require.Equal(t, map[string]string{ - "openfeature.dev/featureflagconfiguration": tt.confname, + "openfeature.dev/featureflag": tt.confname, }, ffConfig.Annotations) require.EqualValues(t, utils.FalseVal(), ffConfig.OwnerReferences[0].Controller) @@ -794,7 +832,7 @@ func TestFlagdContainerInjector_createConfigMap(t *testing.T) { func initContainerInjectionTestEnv() (string, client.WithWatch) { namespace := "my-namespace" - _ = v1alpha1.AddToScheme(scheme.Scheme) + _ = api.AddToScheme(scheme.Scheme) serviceAccount := &v1.ServiceAccount{ ObjectMeta: metav1.ObjectMeta{ @@ -805,16 +843,16 @@ func initContainerInjectionTestEnv() (string, client.WithWatch) { cbr := &rbacv1.ClusterRoleBinding{ ObjectMeta: metav1.ObjectMeta{ - Name: constant.ClusterRoleBindingName, + Name: common.ClusterRoleBindingName, }, } - ffConfig := &v1alpha1.FeatureFlagConfiguration{ + ffConfig := &api.FeatureFlag{ ObjectMeta: metav1.ObjectMeta{ Name: "server-side", Namespace: namespace, }, - Spec: v1alpha1.FeatureFlagConfigurationSpec{}, + Spec: api.FeatureFlagSpec{}, } fakeClientBuilder := fake.NewClientBuilder(). @@ -824,14 +862,12 @@ func initContainerInjectionTestEnv() (string, client.WithWatch) { return namespace, fakeClient } -func getFlagSourceConfigSpec() *v1alpha1.FlagSourceConfigurationSpec { +func getFlagSourceConfigSpec() *api.FeatureFlagSourceSpec { probesEnabled := true - return &v1alpha1.FlagSourceConfigurationSpec{ - MetricsPort: 8014, - Port: 8013, - Image: "flagd", - Tag: "0.5.0", + return &api.FeatureFlagSourceSpec{ + ManagementPort: 8014, + Port: 8013, EnvVars: []v1.EnvVar{ { Name: "my-env-var", @@ -943,14 +979,14 @@ func intPtr(i int64) *int64 { return &i } -func getProxyConfig() *FlagdProxyConfiguration { - return &FlagdProxyConfiguration{ - Port: 8013, - MetricsPort: 8014, - DebugLogging: false, - Image: "flagd", - Tag: "0.5.0", - Namespace: "my-namespace", +func getProxyConfig() *flagdproxy.FlagdProxyConfiguration { + return &flagdproxy.FlagdProxyConfiguration{ + Port: 8013, + ManagementPort: 8014, + DebugLogging: false, + Image: testImage, + Tag: testTag, + Namespace: "my-namespace", } } @@ -1000,62 +1036,26 @@ func Test_getSecurityContext(t *testing.T) { } func TestFlagdContainerInjector_EnableClusterRoleBinding_AddDefaultServiceAccountName(t *testing.T) { - - namespace, fakeClient := initEnableClusterroleBindingTestEnv() - - serviceAccount := &v1.ServiceAccount{ - ObjectMeta: metav1.ObjectMeta{ - Name: "default", - Namespace: namespace, - }, - } - - crb := &rbacv1.ClusterRoleBinding{ - ObjectMeta: metav1.ObjectMeta{ - Name: constant.ClusterRoleBindingName, - }, - } - - err := fakeClient.Create(context.Background(), serviceAccount) - require.Nil(t, err) - - err = fakeClient.Create(context.Background(), crb) - require.Nil(t, err) - - fi := &FlagdContainerInjector{ - Client: fakeClient, - Logger: testr.New(t), - FlagdProxyConfig: getProxyConfig(), - FlagDResourceRequirements: getResourceRequirements(), - } - - err = fi.EnableClusterRoleBinding(context.Background(), namespace, "") - require.Nil(t, err) - - updatedCrb := &rbacv1.ClusterRoleBinding{} - err = fakeClient.Get(context.Background(), client.ObjectKey{Name: crb.Name}, updatedCrb) - - require.Nil(t, err) - - require.Len(t, updatedCrb.Subjects, 1) - require.Equal(t, "default", updatedCrb.Subjects[0].Name) - require.Equal(t, namespace, updatedCrb.Subjects[0].Namespace) + enableClusterRoleBindingTest(t, "default", "") } func TestFlagdContainerInjector_EnableClusterRoleBinding_ServiceAccountName(t *testing.T) { + enableClusterRoleBindingTest(t, "my-serviceaccount", "my-serviceaccount") +} +func enableClusterRoleBindingTest(t *testing.T, name string, input string) { namespace, fakeClient := initEnableClusterroleBindingTestEnv() serviceAccount := &v1.ServiceAccount{ ObjectMeta: metav1.ObjectMeta{ - Name: "my-serviceaccount", + Name: name, Namespace: namespace, }, } crb := &rbacv1.ClusterRoleBinding{ ObjectMeta: metav1.ObjectMeta{ - Name: constant.ClusterRoleBindingName, + Name: common.ClusterRoleBindingName, }, } @@ -1069,10 +1069,12 @@ func TestFlagdContainerInjector_EnableClusterRoleBinding_ServiceAccountName(t *t Client: fakeClient, Logger: testr.New(t), FlagdProxyConfig: getProxyConfig(), - FlagDResourceRequirements: getResourceRequirements(), + FlagdResourceRequirements: getResourceRequirements(), + Image: testImage, + Tag: testTag, } - err = fi.EnableClusterRoleBinding(context.Background(), namespace, "my-serviceaccount") + err = fi.EnableClusterRoleBinding(context.Background(), namespace, input) require.Nil(t, err) updatedCrb := &rbacv1.ClusterRoleBinding{} @@ -1081,7 +1083,7 @@ func TestFlagdContainerInjector_EnableClusterRoleBinding_ServiceAccountName(t *t require.Nil(t, err) require.Len(t, updatedCrb.Subjects, 1) - require.Equal(t, "my-serviceaccount", updatedCrb.Subjects[0].Name) + require.Equal(t, name, updatedCrb.Subjects[0].Name) require.Equal(t, namespace, updatedCrb.Subjects[0].Namespace) } @@ -1098,7 +1100,7 @@ func TestFlagdContainerInjector_EnableClusterRoleBinding_ServiceAccountAlreadyIn crb := &rbacv1.ClusterRoleBinding{ ObjectMeta: metav1.ObjectMeta{ - Name: constant.ClusterRoleBindingName, + Name: common.ClusterRoleBindingName, }, Subjects: []rbacv1.Subject{ { @@ -1119,7 +1121,9 @@ func TestFlagdContainerInjector_EnableClusterRoleBinding_ServiceAccountAlreadyIn Client: fakeClient, Logger: testr.New(t), FlagdProxyConfig: getProxyConfig(), - FlagDResourceRequirements: getResourceRequirements(), + FlagdResourceRequirements: getResourceRequirements(), + Image: testImage, + Tag: testTag, } err = fi.EnableClusterRoleBinding(context.Background(), namespace, "my-serviceaccount") @@ -1153,7 +1157,9 @@ func TestFlagdContainerInjector_EnableClusterRoleBinding_ClusterRoleBindingNotFo Client: fakeClient, Logger: testr.New(t), FlagdProxyConfig: getProxyConfig(), - FlagDResourceRequirements: getResourceRequirements(), + FlagdResourceRequirements: getResourceRequirements(), + Image: testImage, + Tag: testTag, } err = fi.EnableClusterRoleBinding(context.Background(), namespace, "my-serviceaccount") @@ -1168,7 +1174,7 @@ func TestFlagdContainerInjector_EnableClusterRoleBinding_ServiceAccountNotFound( Client: fakeClient, Logger: testr.New(t), FlagdProxyConfig: getProxyConfig(), - FlagDResourceRequirements: getResourceRequirements(), + FlagdResourceRequirements: getResourceRequirements(), } err := fi.EnableClusterRoleBinding(context.Background(), namespace, "my-serviceaccount") @@ -1178,7 +1184,7 @@ func TestFlagdContainerInjector_EnableClusterRoleBinding_ServiceAccountNotFound( func initEnableClusterroleBindingTestEnv() (string, client.WithWatch) { namespace := "my-namespace" - _ = v1alpha1.AddToScheme(scheme.Scheme) + _ = api.AddToScheme(scheme.Scheme) fakeClientBuilder := fake.NewClientBuilder(). WithScheme(scheme.Scheme) diff --git a/controllers/common/flagd-proxy.go b/common/flagdproxy/flagdproxy.go similarity index 72% rename from controllers/common/flagd-proxy.go rename to common/flagdproxy/flagdproxy.go index 43475cf42..fbb809935 100644 --- a/controllers/common/flagd-proxy.go +++ b/common/flagdproxy/flagdproxy.go @@ -1,13 +1,11 @@ -package common +package flagdproxy import ( "context" "fmt" - "os" "github.com/go-logr/logr" - corev1alpha1 "github.com/open-feature/open-feature-operator/apis/core/v1alpha1" - "github.com/open-feature/open-feature-operator/pkg/utils" + "github.com/open-feature/open-feature-operator/common/types" appsV1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" @@ -21,21 +19,7 @@ const ( FlagdProxyDeploymentName = "flagd-proxy" FlagdProxyServiceAccountName = "open-feature-operator-flagd-proxy" FlagdProxyServiceName = "flagd-proxy-svc" - // renovate: datasource=github-tags depName=open-feature/flagd/flagd-proxy - DefaultFlagdProxyTag = "v0.2.8" - DefaultFlagdProxyImage = "ghcr.io/open-feature/flagd-proxy" - DefaultFlagdProxyPort = 8015 - DefaultFlagdProxyMetricsPort = 8016 - DefaultFlagdProxyDebugLogging = false - DefaultFlagdProxyNamespace = "open-feature-operator-system" - - envVarPodNamespace = "POD_NAMESPACE" - envVarProxyImage = "FLAGD_PROXY_IMAGE" - envVarProxyTag = "FLAGD_PROXY_TAG" - envVarProxyPort = "FLAGD_PROXY_PORT" - envVarProxyMetricsPort = "FLAGD_PROXY_METRICS_PORT" - envVarProxyDebugLogging = "FLAGD_PROXY_DEBUG_LOGGING" - operatorDeploymentName = "open-feature-operator-controller-manager" + operatorDeploymentName = "open-feature-operator-controller-manager" ) type FlagdProxyHandler struct { @@ -46,7 +30,7 @@ type FlagdProxyHandler struct { type FlagdProxyConfiguration struct { Port int - MetricsPort int + ManagementPort int DebugLogging bool Image string Tag string @@ -54,44 +38,16 @@ type FlagdProxyConfiguration struct { OperatorDeploymentName string } -func NewFlagdProxyConfiguration() (*FlagdProxyConfiguration, error) { - config := &FlagdProxyConfiguration{ - Image: DefaultFlagdProxyImage, - Tag: DefaultFlagdProxyTag, - Namespace: DefaultFlagdProxyNamespace, +func NewFlagdProxyConfiguration(env types.EnvConfig) *FlagdProxyConfiguration { + return &FlagdProxyConfiguration{ + Image: env.FlagdProxyImage, + Tag: env.FlagdProxyTag, + Namespace: env.PodNamespace, OperatorDeploymentName: operatorDeploymentName, + Port: env.FlagdProxyPort, + ManagementPort: env.FlagdProxyManagementPort, + DebugLogging: env.FlagdProxyDebugLogging, } - ns, ok := os.LookupEnv(envVarPodNamespace) - if ok { - config.Namespace = ns - } - kpi, ok := os.LookupEnv(envVarProxyImage) - if ok { - config.Image = kpi - } - kpt, ok := os.LookupEnv(envVarProxyTag) - if ok { - config.Tag = kpt - } - port, err := utils.GetIntEnvVar(envVarProxyPort, DefaultFlagdProxyPort) - if err != nil { - return config, err - } - config.Port = port - - metricsPort, err := utils.GetIntEnvVar(envVarProxyMetricsPort, DefaultFlagdProxyMetricsPort) - if err != nil { - return config, err - } - config.MetricsPort = metricsPort - - kpDebugLogging, err := utils.GetBoolEnvVar(envVarProxyDebugLogging, DefaultFlagdProxyDebugLogging) - if err != nil { - return config, err - } - config.DebugLogging = kpDebugLogging - - return config, nil } func NewFlagdProxyHandler(config *FlagdProxyConfiguration, client client.Client, logger logr.Logger) *FlagdProxyHandler { @@ -106,7 +62,7 @@ func (f *FlagdProxyHandler) Config() *FlagdProxyConfiguration { return f.config } -func (f *FlagdProxyHandler) HandleFlagdProxy(ctx context.Context, flagSourceConfiguration *corev1alpha1.FlagSourceConfiguration) error { +func (f *FlagdProxyHandler) HandleFlagdProxy(ctx context.Context) error { exists, err := f.doesFlagdProxyExist(ctx) if err != nil { return err @@ -165,7 +121,7 @@ func (f *FlagdProxyHandler) newFlagdProxyManifest(ownerReferences []metav1.Owner args := []string{ "start", "--metrics-port", - fmt.Sprintf("%d", f.config.MetricsPort), + fmt.Sprintf("%d", f.config.ManagementPort), } if f.config.DebugLogging { args = append(args, "--debug") @@ -210,7 +166,7 @@ func (f *FlagdProxyHandler) newFlagdProxyManifest(ownerReferences []metav1.Owner }, { Name: "metrics-port", - ContainerPort: int32(f.config.MetricsPort), + ContainerPort: int32(f.config.ManagementPort), }, }, Args: args, diff --git a/common/flagdproxy/flagdproxy_test.go b/common/flagdproxy/flagdproxy_test.go new file mode 100644 index 000000000..ae1d6a88d --- /dev/null +++ b/common/flagdproxy/flagdproxy_test.go @@ -0,0 +1,308 @@ +package flagdproxy + +import ( + "context" + "fmt" + "testing" + + "github.com/go-logr/logr/testr" + "github.com/open-feature/open-feature-operator/common/types" + "github.com/stretchr/testify/require" + appsV1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + v12 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestNewFlagdProxyConfiguration(t *testing.T) { + kpConfig := NewFlagdProxyConfiguration(types.EnvConfig{ + FlagdProxyPort: 8015, + FlagdProxyManagementPort: 8016, + }) + + require.NotNil(t, kpConfig) + require.Equal(t, &FlagdProxyConfiguration{ + Port: 8015, + ManagementPort: 8016, + DebugLogging: false, + OperatorDeploymentName: operatorDeploymentName, + }, kpConfig) +} + +func TestNewFlagdProxyConfiguration_OverrideEnvVars(t *testing.T) { + env := types.EnvConfig{ + FlagdProxyImage: "my-image", + FlagdProxyTag: "my-tag", + PodNamespace: "my-namespace", + FlagdProxyPort: 8080, + FlagdProxyManagementPort: 8081, + FlagdProxyDebugLogging: true, + } + + kpConfig := NewFlagdProxyConfiguration(env) + + require.NotNil(t, kpConfig) + require.Equal(t, &FlagdProxyConfiguration{ + Port: 8080, + ManagementPort: 8081, + DebugLogging: true, + Image: "my-image", + Tag: "my-tag", + Namespace: "my-namespace", + OperatorDeploymentName: operatorDeploymentName, + }, kpConfig) +} + +func TestNewFlagdProxyHandler(t *testing.T) { + kpConfig := NewFlagdProxyConfiguration(types.EnvConfig{}) + + require.NotNil(t, kpConfig) + + fakeClient := fake.NewClientBuilder().Build() + + ph := NewFlagdProxyHandler(kpConfig, fakeClient, testr.New(t)) + + require.NotNil(t, ph) + + require.Equal(t, kpConfig, ph.Config()) +} + +func TestDoesFlagdProxyExist(t *testing.T) { + env := types.EnvConfig{ + PodNamespace: "ns", + } + + deployment := &v1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns", + Name: FlagdProxyDeploymentName, + }, + Spec: v1.DeploymentSpec{ + Template: v12.PodTemplateSpec{ + Spec: v12.PodSpec{ + Containers: []v12.Container{ + { + Name: "my-container", + }, + }, + }, + }, + }, + } + + kpConfig := NewFlagdProxyConfiguration(env) + + require.NotNil(t, kpConfig) + + fakeClient := fake.NewClientBuilder().WithObjects().Build() + + ph := NewFlagdProxyHandler(kpConfig, fakeClient, testr.New(t)) + + require.NotNil(t, ph) + + res, err := ph.doesFlagdProxyExist(context.TODO()) + require.Nil(t, err) + require.False(t, res) + + err = fakeClient.Create(context.TODO(), deployment) + require.Nil(t, err) + + res, err = ph.doesFlagdProxyExist(context.TODO()) + require.Nil(t, err) + require.True(t, res) +} + +func TestFlagdProxyHandler_HandleFlagdProxy_ProxyExists(t *testing.T) { + env := types.EnvConfig{ + PodNamespace: "ns", + } + kpConfig := NewFlagdProxyConfiguration(env) + + require.NotNil(t, kpConfig) + + fakeClient := fake.NewClientBuilder().WithObjects(&v1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: kpConfig.Namespace, + Name: FlagdProxyDeploymentName, + }, + Spec: v1.DeploymentSpec{ + Template: v12.PodTemplateSpec{ + Spec: v12.PodSpec{ + Containers: []v12.Container{ + { + Name: "my-container", + }, + }, + }, + }, + }, + }).Build() + + ph := NewFlagdProxyHandler(kpConfig, fakeClient, testr.New(t)) + + require.NotNil(t, ph) + + err := ph.HandleFlagdProxy(context.Background()) + + require.Nil(t, err) + + deployment := &v1.Deployment{} + err = fakeClient.Get(context.Background(), client.ObjectKey{ + Namespace: env.PodNamespace, + Name: FlagdProxyDeploymentName, + }, deployment) + + require.Nil(t, err) + require.NotNil(t, deployment) + + // verify that the existing deployment has not been changed + require.Equal(t, "my-container", deployment.Spec.Template.Spec.Containers[0].Name) +} + +func TestFlagdProxyHandler_HandleFlagdProxy_CreateProxy(t *testing.T) { + env := types.EnvConfig{ + PodNamespace: "ns", + FlagdProxyImage: "image", + FlagdProxyTag: "tag", + FlagdProxyPort: 88, + FlagdProxyManagementPort: 90, + FlagdProxyDebugLogging: true, + } + kpConfig := NewFlagdProxyConfiguration(env) + + require.NotNil(t, kpConfig) + + fakeClient := fake.NewClientBuilder().Build() + + ph := NewFlagdProxyHandler(kpConfig, fakeClient, testr.New(t)) + + require.NotNil(t, ph) + + // proxy does not exist + deployment := &v1.Deployment{} + err := fakeClient.Get(context.Background(), client.ObjectKey{ + Namespace: env.PodNamespace, + Name: FlagdProxyDeploymentName, + }, deployment) + + require.NotNil(t, err) + + err = ph.HandleFlagdProxy(context.Background()) + + require.Nil(t, err) + + // proxy should exist + deployment = &v1.Deployment{} + err = fakeClient.Get(context.Background(), client.ObjectKey{ + Namespace: env.PodNamespace, + Name: FlagdProxyDeploymentName, + }, deployment) + + require.Nil(t, err) + require.NotNil(t, deployment) + + replicas := int32(1) + args := []string{ + "start", + "--metrics-port", + fmt.Sprintf("%d", 90), + "--debug", + } + + expectedDeployment := &appsV1.Deployment{ + TypeMeta: metav1.TypeMeta{ + Kind: "Deployment", + APIVersion: "apps/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: FlagdProxyDeploymentName, + Namespace: "ns", + Labels: map[string]string{ + "app": FlagdProxyDeploymentName, + "app.kubernetes.io/managed-by": ManagedByAnnotationValue, + "app.kubernetes.io/version": "tag", + }, + ResourceVersion: "1", + }, + Spec: appsV1.DeploymentSpec{ + Replicas: &replicas, + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": FlagdProxyDeploymentName, + }, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "app": FlagdProxyDeploymentName, + "app.kubernetes.io/name": FlagdProxyDeploymentName, + "app.kubernetes.io/managed-by": ManagedByAnnotationValue, + "app.kubernetes.io/version": "tag", + }, + }, + Spec: corev1.PodSpec{ + ServiceAccountName: FlagdProxyServiceAccountName, + Containers: []corev1.Container{ + { + Image: "image:tag", + Name: FlagdProxyDeploymentName, + Ports: []corev1.ContainerPort{ + { + Name: "port", + ContainerPort: int32(88), + }, + { + Name: "metrics-port", + ContainerPort: int32(90), + }, + }, + Args: args, + }, + }, + }, + }, + }, + } + + require.Equal(t, expectedDeployment, deployment) + + service := &corev1.Service{} + err = fakeClient.Get(context.Background(), client.ObjectKey{ + Namespace: env.PodNamespace, + Name: FlagdProxyServiceName, + }, service) + + require.Nil(t, err) + require.NotNil(t, service) + + expectedService := &corev1.Service{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Service", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: FlagdProxyServiceName, + Namespace: "ns", + ResourceVersion: "1", + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{ + "app.kubernetes.io/name": FlagdProxyDeploymentName, + "app.kubernetes.io/managed-by": ManagedByAnnotationValue, + }, + Ports: []corev1.ServicePort{ + { + Name: "flagd-proxy", + Port: int32(88), + TargetPort: intstr.FromInt(88), + }, + }, + }, + } + + require.Equal(t, expectedService, service) +} diff --git a/common/types/envconfig.go b/common/types/envconfig.go new file mode 100644 index 000000000..4cd2495af --- /dev/null +++ b/common/types/envconfig.go @@ -0,0 +1,24 @@ +package types + +type EnvConfig struct { + PodNamespace string `envconfig:"POD_NAMESPACE" default:"open-feature-operator-system"` + FlagdProxyImage string `envconfig:"FLAGD_PROXY_IMAGE" default:"ghcr.io/open-feature/flagd-proxy"` + // renovate: datasource=github-tags depName=open-feature/flagd/flagd-proxy + FlagdProxyTag string `envconfig:"FLAGD_PROXY_TAG" default:"v0.3.0"` + FlagdProxyPort int `envconfig:"FLAGD_PROXY_PORT" default:"8015"` + FlagdProxyManagementPort int `envconfig:"FLAGD_PROXY_MANAGEMENT_PORT" default:"8016"` + FlagdProxyDebugLogging bool `envconfig:"FLAGD_PROXY_DEBUG_LOGGING" default:"false"` + + SidecarEnvVarPrefix string `envconfig:"SIDECAR_ENV_VAR_PREFIX" default:"FLAGD"` + SidecarManagementPort int `envconfig:"SIDECAR_MANAGEMENT_PORT" default:"8014"` + SidecarPort int `envconfig:"SIDECAR_PORT" default:"8013"` + SidecarImage string `envconfig:"SIDECAR_IMAGE" default:"ghcr.io/open-feature/flagd"` + // renovate: datasource=github-tags depName=open-feature/flagd/flagd + SidecarTag string `envconfig:"SIDECAR_TAG" default:"v0.7.0"` + SidecarSocketPath string `envconfig:"SIDECAR_SOCKET_PATH" default:""` + SidecarEvaluator string `envconfig:"SIDECAR_EVALUATOR" default:"json"` + SidecarProviderArgs string `envconfig:"SIDECAR_PROVIDER_ARGS" default:""` + SidecarSyncProvider string `envconfig:"SIDECAR_SYNC_PROVIDER" default:"kubernetes"` + SidecarLogFormat string `envconfig:"SIDECAR_LOG_FORMAT" default:"json"` + SidecarProbesEnabled bool `envconfig:"SIDECAR_PROBES_ENABLED" default:"true"` +} diff --git a/pkg/types/source-config.go b/common/types/sourceconfig.go similarity index 100% rename from pkg/types/source-config.go rename to common/types/sourceconfig.go diff --git a/common/utils/utils.go b/common/utils/utils.go new file mode 100644 index 000000000..52515c4b4 --- /dev/null +++ b/common/utils/utils.go @@ -0,0 +1,43 @@ +package utils + +import ( + "fmt" + "strings" +) + +func TrueVal() *bool { + b := true + return &b +} + +func FalseVal() *bool { + b := false + return &b +} + +func ContainsString(slice []string, s string) bool { + for _, item := range slice { + if item == s { + return true + } + } + return false +} + +func ParseAnnotation(s string, defaultNs string) (string, string) { + ss := strings.Split(s, "/") + if len(ss) == 2 { + return ss[0], ss[1] + } + return defaultNs, s +} + +// unique string used to create unique volume mount and file name +func FeatureFlagId(namespace, name string) string { + return fmt.Sprintf("%s_%s", namespace, name) +} + +// unique key (and filename) for configMap data +func FeatureFlagConfigMapKey(namespace, name string) string { + return fmt.Sprintf("%s.flagd.json", FeatureFlagId(namespace, name)) +} diff --git a/pkg/utils/utils_test.go b/common/utils/utils_test.go similarity index 51% rename from pkg/utils/utils_test.go rename to common/utils/utils_test.go index 7e7b6d170..5f7517321 100644 --- a/pkg/utils/utils_test.go +++ b/common/utils/utils_test.go @@ -6,12 +6,12 @@ import ( "github.com/stretchr/testify/require" ) -func Test_FeatureFlagConfigurationId(t *testing.T) { - require.Equal(t, "namespace_name", FeatureFlagConfigurationId("namespace", "name")) +func Test_FeatureFlagId(t *testing.T) { + require.Equal(t, "namespace_name", FeatureFlagId("namespace", "name")) } -func Test_FeatureFlagConfigurationConfigMapKey(t *testing.T) { - require.Equal(t, "namespace_name.flagd.json", FeatureFlagConfigurationConfigMapKey("namespace", "name")) +func Test_FeatureFlagConfigMapKey(t *testing.T) { + require.Equal(t, "namespace_name.flagd.json", FeatureFlagConfigMapKey("namespace", "name")) } func Test_FalseVal(t *testing.T) { @@ -29,3 +29,13 @@ func Test_ContainsString(t *testing.T) { require.True(t, ContainsString(slice, "str1")) require.False(t, ContainsString(slice, "some")) } + +func Test_ParseAnnotations(t *testing.T) { + s1, s2 := ParseAnnotation("some/anno", "default") + require.Equal(t, "some", s1) + require.Equal(t, "anno", s2) + + s1, s2 = ParseAnnotation("anno", "default") + require.Equal(t, "default", s1) + require.Equal(t, "anno", s2) +} diff --git a/config/crd/bases/core.openfeature.dev_featureflagconfigurations.yaml b/config/crd/bases/core.openfeature.dev_featureflagconfigurations.yaml index dfb0ed59d..025396e86 100644 --- a/config/crd/bases/core.openfeature.dev_featureflagconfigurations.yaml +++ b/config/crd/bases/core.openfeature.dev_featureflagconfigurations.yaml @@ -13,7 +13,7 @@ spec: listKind: FeatureFlagConfigurationList plural: featureflagconfigurations shortNames: - - ff + - ffc singular: featureflagconfiguration scope: Namespaced versions: @@ -442,6 +442,54 @@ spec: type: object type: array type: object + resources: + description: Resources defines flagd sidecar resources. Default to + operator sidecar-cpu-* and sidecar-ram-* flags. + properties: + claims: + description: "Claims lists the names of resources, defined in + spec.resourceClaims, that are used by this container. \n This + is an alpha field and requires enabling the DynamicResourceAllocation + feature gate. \n This field is immutable. It can only be set + for containers." + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: Name must match the name of one entry in pod.spec.resourceClaims + of the Pod where this field is used. It makes that resource + available inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Limits describes the maximum amount of compute resources + allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Requests describes the minimum amount of compute + resources required. If Requests is omitted for a container, + it defaults to Limits if that is explicitly specified, otherwise + to an implementation-defined value. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + type: object serviceProvider: description: 'ServiceProvider [DEPRECATED]: superseded by FlagSourceConfiguration' nullable: true diff --git a/config/crd/bases/core.openfeature.dev_featureflags.yaml b/config/crd/bases/core.openfeature.dev_featureflags.yaml new file mode 100644 index 000000000..2c7ce7a37 --- /dev/null +++ b/config/crd/bases/core.openfeature.dev_featureflags.yaml @@ -0,0 +1,81 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.10.0 + creationTimestamp: null + name: featureflags.core.openfeature.dev +spec: + group: core.openfeature.dev + names: + kind: FeatureFlag + listKind: FeatureFlagList + plural: featureflags + shortNames: + - ff + singular: featureflag + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + description: FeatureFlag is the Schema for the featureflags API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: FeatureFlagSpec defines the desired state of FeatureFlag + properties: + flagSpec: + description: FlagSpec is the structured representation of the feature + flag specification + properties: + $evaluators: + type: object + x-kubernetes-preserve-unknown-fields: true + flags: + additionalProperties: + properties: + defaultVariant: + type: string + state: + enum: + - ENABLED + - DISABLED + type: string + targeting: + description: Targeting is the json targeting rule + type: object + x-kubernetes-preserve-unknown-fields: true + variants: + type: object + x-kubernetes-preserve-unknown-fields: true + required: + - defaultVariant + - state + - variants + type: object + type: object + required: + - flags + type: object + type: object + status: + description: FeatureFlagStatus defines the observed state of FeatureFlag + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/core.openfeature.dev_featureflagsources.yaml b/config/crd/bases/core.openfeature.dev_featureflagsources.yaml new file mode 100644 index 000000000..ede27d3b0 --- /dev/null +++ b/config/crd/bases/core.openfeature.dev_featureflagsources.yaml @@ -0,0 +1,295 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.10.0 + creationTimestamp: null + name: featureflagsources.core.openfeature.dev +spec: + group: core.openfeature.dev + names: + kind: FeatureFlagSource + listKind: FeatureFlagSourceList + plural: featureflagsources + shortNames: + - ffs + singular: featureflagsource + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + description: FeatureFlagSource is the Schema for the FeatureFlagSources API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: FeatureFlagSourceSpec defines the desired state of FeatureFlagSource + properties: + debugLogging: + description: DebugLogging defines whether to enable --debug flag of + flagd sidecar. Default false (disabled). + type: boolean + defaultSyncProvider: + description: DefaultSyncProvider defines the default sync provider + type: string + envVarPrefix: + description: EnvVarPrefix defines the prefix to be applied to all + environment variables applied to the sidecar, default FLAGD + type: string + envVars: + description: EnvVars define the env vars to be applied to the sidecar, + any env vars in FeatureFlag CRs are added at the lowest index, all + values will have the EnvVarPrefix applied, default FLAGD + items: + description: EnvVar represents an environment variable present in + a Container. + properties: + name: + description: Name of the environment variable. Must be a C_IDENTIFIER. + type: string + value: + description: 'Variable references $(VAR_NAME) are expanded using + the previously defined environment variables in the container + and any service environment variables. If a variable cannot + be resolved, the reference in the input string will be unchanged. + Double $$ are reduced to a single $, which allows for escaping + the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" will produce the + string literal "$(VAR_NAME)". Escaped references will never + be expanded, regardless of whether the variable exists or + not. Defaults to "".' + type: string + valueFrom: + description: Source for the environment variable's value. Cannot + be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + optional: + description: Specify whether the ConfigMap or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports metadata.name, + metadata.namespace, `metadata.labels['''']`, `metadata.annotations['''']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, + status.podIP, status.podIPs.' + properties: + apiVersion: + description: Version of the schema the FieldPath is + written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the specified + API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: 'Selects a resource of the container: only + resources limits and requests (limits.cpu, limits.memory, + limits.ephemeral-storage, requests.cpu, requests.memory + and requests.ephemeral-storage) are currently supported.' + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the exposed + resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + evaluator: + description: Evaluator sets an evaluator, defaults to 'json' + type: string + logFormat: + description: LogFormat allows for the sidecar log format to be overridden, + defaults to 'json' + type: string + managementPort: + description: ManagemetPort defines the port to serve management on, + defaults to 8014 + format: int32 + type: integer + otelCollectorUri: + description: OtelCollectorUri defines whether to enable --otel-collector-uri + flag of flagd sidecar. Default false (disabled). + type: string + port: + description: Port defines the port to listen on, defaults to 8013 + format: int32 + type: integer + probesEnabled: + description: ProbesEnabled defines whether to enable liveness and + readiness probes of flagd sidecar. Default true (enabled). + type: boolean + resources: + description: Resources defines flagd sidecar resources. Default to + operator sidecar-cpu-* and sidecar-ram-* flags. + properties: + claims: + description: "Claims lists the names of resources, defined in + spec.resourceClaims, that are used by this container. \n This + is an alpha field and requires enabling the DynamicResourceAllocation + feature gate. \n This field is immutable. It can only be set + for containers." + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: Name must match the name of one entry in pod.spec.resourceClaims + of the Pod where this field is used. It makes that resource + available inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Limits describes the maximum amount of compute resources + allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Requests describes the minimum amount of compute + resources required. If Requests is omitted for a container, + it defaults to Limits if that is explicitly specified, otherwise + to an implementation-defined value. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + type: object + rolloutOnChange: + description: RolloutOnChange dictates whether annotated deployments + will be restarted when configuration changes are detected in this + CR, defaults to false + type: boolean + socketPath: + description: SocketPath defines the unix socket path to listen on + type: string + sources: + description: SyncProviders define the syncProviders and associated + configuration to be applied to the sidecar + items: + properties: + certPath: + description: CertPath is a path of a certificate to be used + by grpc TLS connection + type: string + httpSyncBearerToken: + description: HttpSyncBearerToken is a bearer token. Used by + http(s) sync provider only + type: string + provider: + description: Provider type - kubernetes, http(s), grpc(s) or + file + type: string + providerID: + description: ProviderID is an identifier to be used in grpc + provider + type: string + selector: + description: Selector is a flag configuration selector used + by grpc provider + type: string + source: + description: Source is a URI of the flag sources + type: string + tls: + description: TLS - Enable/Disable secure TLS connectivity. Currently + used only by GRPC sync + type: boolean + required: + - source + type: object + minItems: 1 + type: array + syncProviderArgs: + description: SyncProviderArgs are string arguments passed to all sync + providers, defined as key values separated by = + items: + type: string + type: array + required: + - sources + type: object + status: + description: FeatureFlagSourceStatus defines the observed state of FeatureFlagSource + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/core.openfeature.dev_flagsourceconfigurations.yaml b/config/crd/bases/core.openfeature.dev_flagsourceconfigurations.yaml index da40f1f67..8a419a104 100644 --- a/config/crd/bases/core.openfeature.dev_flagsourceconfigurations.yaml +++ b/config/crd/bases/core.openfeature.dev_flagsourceconfigurations.yaml @@ -191,7 +191,7 @@ spec: type: boolean resources: description: Resources defines flagd sidecar resources. Default to - operator sidecar-cpu-limit and sidecar-ram-limit flags. + operator sidecar-cpu-* and sidecar-ram-* flags. properties: claims: description: "Claims lists the names of resources, defined in @@ -551,6 +551,54 @@ spec: description: ProbesEnabled defines whether to enable liveness and readiness probes of flagd sidecar. Default true (enabled). type: boolean + resources: + description: Resources defines flagd sidecar resources. Default to + operator sidecar-cpu-* and sidecar-ram-* flags. + properties: + claims: + description: "Claims lists the names of resources, defined in + spec.resourceClaims, that are used by this container. \n This + is an alpha field and requires enabling the DynamicResourceAllocation + feature gate. \n This field is immutable. It can only be set + for containers." + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: Name must match the name of one entry in pod.spec.resourceClaims + of the Pod where this field is used. It makes that resource + available inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Limits describes the maximum amount of compute resources + allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Requests describes the minimum amount of compute + resources required. If Requests is omitted for a container, + it defaults to Limits if that is explicitly specified, otherwise + to an implementation-defined value. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + type: object rolloutOnChange: description: RolloutOnChange dictates whether annotated deployments will be restarted when configuration changes are detected in this diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 267d52874..b7902f7e0 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -2,21 +2,21 @@ # since it depends on service name and namespace that are out of this kustomize package. # It should be run by config/default resources: -- bases/core.openfeature.dev_featureflagconfigurations.yaml -- bases/core.openfeature.dev_flagsourceconfigurations.yaml +- bases/core.openfeature.dev_featureflags.yaml +- bases/core.openfeature.dev_featureflagsources.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. # patches here are for enabling the conversion webhook for each CRD -- patches/webhook_in_featureflagconfigurations.yaml -- patches/webhook_in_flagsourceconfigurations.yaml +#- patches/webhook_in_featureflags.yaml +#- patches/webhook_in_featureflagsources.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. # patches here are for enabling the CA injection for each CRD -- patches/cainjection_in_featureflagconfigurations.yaml -- patches/cainjection_in_flagsourceconfigurations.yaml +#- patches/cainjection_in_featureflags.yaml +#- patches/cainjection_in_featureflagsources.yaml #+kubebuilder:scaffold:crdkustomizecainjectionpatch # the following config is for teaching kustomize how to do kustomization for CRDs. diff --git a/config/crd/patches/cainjection_in_core_flagsourceconfigurations.yaml b/config/crd/patches/cainjection_in_core_featureflags.yaml similarity index 82% rename from config/crd/patches/cainjection_in_core_flagsourceconfigurations.yaml rename to config/crd/patches/cainjection_in_core_featureflags.yaml index 11e3fce17..67581e1e6 100644 --- a/config/crd/patches/cainjection_in_core_flagsourceconfigurations.yaml +++ b/config/crd/patches/cainjection_in_core_featureflags.yaml @@ -4,4 +4,4 @@ kind: CustomResourceDefinition metadata: annotations: cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) - name: flagsourceconfigurations.core.openfeature.dev + name: featureflags.core.openfeature.dev diff --git a/config/crd/patches/cainjection_in_flagsourceconfigurations.yaml b/config/crd/patches/cainjection_in_core_featureflagsources.yaml similarity index 82% rename from config/crd/patches/cainjection_in_flagsourceconfigurations.yaml rename to config/crd/patches/cainjection_in_core_featureflagsources.yaml index 11e3fce17..d23936843 100644 --- a/config/crd/patches/cainjection_in_flagsourceconfigurations.yaml +++ b/config/crd/patches/cainjection_in_core_featureflagsources.yaml @@ -4,4 +4,4 @@ kind: CustomResourceDefinition metadata: annotations: cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) - name: flagsourceconfigurations.core.openfeature.dev + name: featureflagsources.core.openfeature.dev diff --git a/config/crd/patches/cainjection_in_featureflagconfigurations.yaml b/config/crd/patches/cainjection_in_featureflagconfigurations.yaml deleted file mode 100644 index 74c4735c4..000000000 --- a/config/crd/patches/cainjection_in_featureflagconfigurations.yaml +++ /dev/null @@ -1,7 +0,0 @@ -# The following patch adds a directive for certmanager to inject CA into the CRD -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) - name: featureflagconfigurations.core.openfeature.dev diff --git a/config/crd/patches/webhook_in_core_flagsourceconfigurations.yaml b/config/crd/patches/webhook_in_core_flagsourceconfigurations.yaml deleted file mode 100644 index 2ae67ba6a..000000000 --- a/config/crd/patches/webhook_in_core_flagsourceconfigurations.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# The following patch enables a conversion webhook for the CRD -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: flagsourceconfigurations.core.openfeature.dev -spec: - conversion: - strategy: Webhook - webhook: - clientConfig: - service: - namespace: system - name: webhook-service - path: /convert - conversionReviewVersions: - - v1 diff --git a/config/crd/patches/webhook_in_featureflagconfigurations.yaml b/config/crd/patches/webhook_in_featureflagconfigurations.yaml deleted file mode 100644 index 0983a2ebb..000000000 --- a/config/crd/patches/webhook_in_featureflagconfigurations.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# The following patch enables a conversion webhook for the CRD -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: featureflagconfigurations.core.openfeature.dev -spec: - conversion: - strategy: Webhook - webhook: - clientConfig: - service: - namespace: system - name: webhook-service - path: /convert - conversionReviewVersions: - - v1 diff --git a/config/crd/patches/webhook_in_flagsourceconfigurations.yaml b/config/crd/patches/webhook_in_flagsourceconfigurations.yaml deleted file mode 100644 index 2ae67ba6a..000000000 --- a/config/crd/patches/webhook_in_flagsourceconfigurations.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# The following patch enables a conversion webhook for the CRD -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: flagsourceconfigurations.core.openfeature.dev -spec: - conversion: - strategy: Webhook - webhook: - clientConfig: - service: - namespace: system - name: webhook-service - path: /convert - conversionReviewVersions: - - v1 diff --git a/config/default/webhookcainjection_patch.yaml b/config/default/webhookcainjection_patch.yaml index 02ab515d4..28b1826d4 100644 --- a/config/default/webhookcainjection_patch.yaml +++ b/config/default/webhookcainjection_patch.yaml @@ -6,10 +6,3 @@ metadata: name: mutating-webhook-configuration annotations: cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) ---- -apiVersion: admissionregistration.k8s.io/v1 -kind: ValidatingWebhookConfiguration -metadata: - name: validating-webhook-configuration - annotations: - cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) diff --git a/config/overlays/helm/manager.yaml b/config/overlays/helm/manager.yaml index 5b7098fe5..94818e3b1 100644 --- a/config/overlays/helm/manager.yaml +++ b/config/overlays/helm/manager.yaml @@ -18,8 +18,8 @@ spec: cpu: "{{ .Values.controllerManager.manager.resources.requests.cpu }}" memory: "{{ .Values.controllerManager.manager.resources.requests.memory }}" env: - - name: SIDECAR_METRICS_PORT - value: "{{ .Values.sidecarConfiguration.metricsPort }}" + - name: SIDECAR_MANAGEMENT_PORT + value: "{{ .Values.sidecarConfiguration.managementPort }}" - name: SIDECAR_PORT value: "{{ .Values.sidecarConfiguration.port }}" - name: SIDECAR_SOCKET_PATH @@ -46,8 +46,8 @@ spec: value: "{{ .Values.flagdProxyConfiguration.image.tag }}" - name: FLAGD_PROXY_PORT value: "{{ .Values.flagdProxyConfiguration.port }}" - - name: FLAGD_PROXY_METRICS_PORT - value: "{{ .Values.flagdProxyConfiguration.metricsPort }}" + - name: FLAGD_PROXY_MANAGEMENT_PORT + value: "{{ .Values.flagdProxyConfiguration.managementPort }}" - name: FLAGD_PROXY_DEBUG_LOGGING value: "{{ .Values.flagdProxyConfiguration.debugLogging }}" - name: kube-rbac-proxy diff --git a/config/rbac/core_featureflag_editor_role.yaml b/config/rbac/core_featureflag_editor_role.yaml new file mode 100644 index 000000000..61631ccf7 --- /dev/null +++ b/config/rbac/core_featureflag_editor_role.yaml @@ -0,0 +1,24 @@ +# permissions for end users to edit featureflags. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: featureflag-editor-role +rules: +- apiGroups: + - core.openfeature.dev + resources: + - featureflags + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - core.openfeature.dev + resources: + - featureflags/status + verbs: + - get diff --git a/config/rbac/core_featureflag_viewer_role.yaml b/config/rbac/core_featureflag_viewer_role.yaml new file mode 100644 index 000000000..4a3d9d414 --- /dev/null +++ b/config/rbac/core_featureflag_viewer_role.yaml @@ -0,0 +1,20 @@ +# permissions for end users to view featureflags. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: featureflag-viewer-role +rules: +- apiGroups: + - core.openfeature.dev + resources: + - featureflags + verbs: + - get + - list + - watch +- apiGroups: + - core.openfeature.dev + resources: + - featureflags/status + verbs: + - get diff --git a/config/rbac/core_featureflagsource_editor_role.yaml b/config/rbac/core_featureflagsource_editor_role.yaml new file mode 100644 index 000000000..0dd935a85 --- /dev/null +++ b/config/rbac/core_featureflagsource_editor_role.yaml @@ -0,0 +1,24 @@ +# permissions for end users to edit featureflagsources. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: featureflagsource-editor-role +rules: +- apiGroups: + - core.openfeature.dev + resources: + - featureflagsources + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - core.openfeature.dev + resources: + - featureflagsources/status + verbs: + - get diff --git a/config/rbac/core_featureflagsource_viewer_role.yaml b/config/rbac/core_featureflagsource_viewer_role.yaml new file mode 100644 index 000000000..fd08e073c --- /dev/null +++ b/config/rbac/core_featureflagsource_viewer_role.yaml @@ -0,0 +1,20 @@ +# permissions for end users to view featureflagsources. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: featureflagsource-viewer-role +rules: +- apiGroups: + - core.openfeature.dev + resources: + - featureflagsources + verbs: + - get + - list + - watch +- apiGroups: + - core.openfeature.dev + resources: + - featureflagsources/status + verbs: + - get diff --git a/config/rbac/core_flagsourceconfiguration_editor_role.yaml b/config/rbac/core_flagsourceconfiguration_editor_role.yaml deleted file mode 100644 index 16e463740..000000000 --- a/config/rbac/core_flagsourceconfiguration_editor_role.yaml +++ /dev/null @@ -1,31 +0,0 @@ -# permissions for end users to edit flagsourceconfigurations. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - labels: - app.kubernetes.io/name: clusterrole - app.kubernetes.io/instance: flagsourceconfiguration-editor-role - app.kubernetes.io/component: rbac - app.kubernetes.io/created-by: open-feature-operator - app.kubernetes.io/part-of: open-feature-operator - app.kubernetes.io/managed-by: kustomize - name: flagsourceconfiguration-editor-role -rules: -- apiGroups: - - core.openfeature.dev - resources: - - flagsourceconfigurations - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - core.openfeature.dev - resources: - - flagsourceconfigurations/status - verbs: - - get diff --git a/config/rbac/core_flagsourceconfiguration_viewer_role.yaml b/config/rbac/core_flagsourceconfiguration_viewer_role.yaml deleted file mode 100644 index 1193eed64..000000000 --- a/config/rbac/core_flagsourceconfiguration_viewer_role.yaml +++ /dev/null @@ -1,27 +0,0 @@ -# permissions for end users to view flagsourceconfigurations. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - labels: - app.kubernetes.io/name: clusterrole - app.kubernetes.io/instance: flagsourceconfiguration-viewer-role - app.kubernetes.io/component: rbac - app.kubernetes.io/created-by: open-feature-operator - app.kubernetes.io/part-of: open-feature-operator - app.kubernetes.io/managed-by: kustomize - name: flagsourceconfiguration-viewer-role -rules: -- apiGroups: - - core.openfeature.dev - resources: - - flagsourceconfigurations - verbs: - - get - - list - - watch -- apiGroups: - - core.openfeature.dev - resources: - - flagsourceconfigurations/status - verbs: - - get diff --git a/config/rbac/featureflagconfiguration_editor_role.yaml b/config/rbac/featureflagconfiguration_editor_role.yaml deleted file mode 100644 index ecc6f1600..000000000 --- a/config/rbac/featureflagconfiguration_editor_role.yaml +++ /dev/null @@ -1,24 +0,0 @@ -# permissions for end users to edit featureflagconfigurations. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: featureflagconfiguration-editor-role -rules: -- apiGroups: - - config.openfeature.dev - resources: - - featureflagconfigurations - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - config.openfeature.dev - resources: - - featureflagconfigurations/status - verbs: - - get diff --git a/config/rbac/featureflagconfiguration_viewer_role.yaml b/config/rbac/featureflagconfiguration_viewer_role.yaml deleted file mode 100644 index 1a2c23ba3..000000000 --- a/config/rbac/featureflagconfiguration_viewer_role.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# permissions for end users to view featureflagconfigurations. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: featureflagconfiguration-viewer-role -rules: -- apiGroups: - - config.openfeature.dev - resources: - - featureflagconfigurations - verbs: - - get - - list - - watch -- apiGroups: - - config.openfeature.dev - resources: - - featureflagconfigurations/status - verbs: - - get diff --git a/config/rbac/flagd_kubernetes_sync_clusterrole.yaml b/config/rbac/flagd_kubernetes_sync_clusterrole.yaml index b6849439e..65e20af9b 100644 --- a/config/rbac/flagd_kubernetes_sync_clusterrole.yaml +++ b/config/rbac/flagd_kubernetes_sync_clusterrole.yaml @@ -5,5 +5,5 @@ metadata: name: flagd-kubernetes-sync rules: - apiGroups: ["core.openfeature.dev"] - resources: ["flagsourceconfigurations", "featureflagconfigurations"] + resources: ["featureflagsources", "featureflags"] verbs: ["get", "watch", "list"] diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index eca339434..0fb890069 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -60,7 +60,7 @@ rules: - apiGroups: - core.openfeature.dev resources: - - featureflagconfigurations + - featureflagsources verbs: - create - delete @@ -72,39 +72,13 @@ rules: - apiGroups: - core.openfeature.dev resources: - - featureflagconfigurations/finalizers + - featureflagsources/finalizers verbs: - update - apiGroups: - core.openfeature.dev resources: - - featureflagconfigurations/status - verbs: - - get - - patch - - update -- apiGroups: - - core.openfeature.dev - resources: - - flagsourceconfigurations - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - core.openfeature.dev - resources: - - flagsourceconfigurations/finalizers - verbs: - - update -- apiGroups: - - core.openfeature.dev - resources: - - flagsourceconfigurations/status + - featureflagsources/status verbs: - get - patch diff --git a/config/samples/core_v1beta1_featureflag.yaml b/config/samples/core_v1beta1_featureflag.yaml new file mode 100644 index 000000000..35d1cc9a9 --- /dev/null +++ b/config/samples/core_v1beta1_featureflag.yaml @@ -0,0 +1,13 @@ +apiVersion: core.openfeature.dev/v1beta1 +kind: FeatureFlag +metadata: + name: featureflag-sample +spec: + flagSpec: + flags: + "simple-flag": + state: "ENABLED" + variants: + "on": true + "off": false + defaultVariant: "on" diff --git a/config/samples/core_v1beta1_featureflagsource.yaml b/config/samples/core_v1beta1_featureflagsource.yaml new file mode 100644 index 000000000..c1651528a --- /dev/null +++ b/config/samples/core_v1beta1_featureflagsource.yaml @@ -0,0 +1,13 @@ +apiVersion: core.openfeature.dev/v1beta1 +kind: FeatureFlagSource +metadata: + name: featureflagsource-sample +spec: + managementPort: 8080 + evaluator: json + defaultSyncProvider: file + tag: latest + sources: + - source: end-to-end-test + provider: file + probesEnabled: true diff --git a/config/samples/crds/custom_provider.yaml b/config/samples/crds/custom_provider.yaml deleted file mode 100644 index 79dcad67e..000000000 --- a/config/samples/crds/custom_provider.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: core.openfeature.dev/v1alpha1 -kind: FeatureFlagConfiguration -metadata: - name: featureflagconfiguration-sample -spec: - flagDSpec: - envs: - - name: FOO - value: BAR - serviceProvider: - name: "flagd" - credentials: - name: "sample-provider-secret" - namespace: "default" - featureFlagSpec: | - {} diff --git a/config/samples/crds/featureflagconfiguration.yaml b/config/samples/crds/featureflagconfiguration.yaml deleted file mode 100644 index 427e4c5f7..000000000 --- a/config/samples/crds/featureflagconfiguration.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: core.openfeature.dev/v1alpha1 -kind: FeatureFlagConfiguration -metadata: - name: featureflagconfiguration-sample -spec: - flagDSpec: - envs: - - name: FOO - value: BAR - featureFlagSpec: | - {} \ No newline at end of file diff --git a/config/samples/deployment.yaml b/config/samples/deployment.yaml index 95eb3dadf..218b7b5ba 100644 --- a/config/samples/deployment.yaml +++ b/config/samples/deployment.yaml @@ -13,7 +13,7 @@ spec: app: nginx annotations: openfeature.dev/enabled: "true" - openfeature.dev/featureflagconfiguration: "featureflagconfiguration-sample" + openfeature.dev/featureflagsource: "configuration-sample" spec: containers: - name: nginx @@ -36,7 +36,7 @@ spec: app: nginx annotations: openfeature.dev/enabled: "true" - openfeature.dev/featureflagconfiguration: "featureflagconfiguration-sample" + openfeature.dev/featureflagsource: "configuration-sample" spec: containers: - name: nginx diff --git a/config/samples/end-to-end.yaml b/config/samples/end-to-end.yaml index 4403a55a9..27485bf81 100644 --- a/config/samples/end-to-end.yaml +++ b/config/samples/end-to-end.yaml @@ -4,48 +4,22 @@ kind: Namespace metadata: name: open-feature-demo --- -apiVersion: core.openfeature.dev/v1alpha2 -kind: FeatureFlagConfiguration +apiVersion: core.openfeature.dev/v1beta1 +kind: FeatureFlag metadata: - name: end-to-end - namespace: open-feature-demo + name: featureflag-sample spec: - featureFlagSpec: + flagSpec: flags: - new-welcome-message: - state: ENABLED + "simple-flag": + state: "ENABLED" variants: "on": true "off": false defaultVariant: "on" - hex-color: - state: ENABLED - variants: - red: CC0000 - green: 00CC00 - blue: 0000CC - yellow: yellow - defaultVariant: blue - fib-algo: - state: ENABLED - variants: - recursive: recursive - memo: memo - loop: loop - binet: binet - defaultVariant: recursive - "targeting": { - "if": [ - { - "in": [ "@faas.com", { - "var": [ "email" ] - } ] - }, "binet", null - ] - } --- -apiVersion: core.openfeature.dev/v1alpha2 -kind: FlagSourceConfiguration +apiVersion: core.openfeature.dev/v1beta1 +kind: FeatureFlagSource metadata: name: end-to-end namespace: open-feature-demo @@ -73,7 +47,7 @@ spec: app: open-feature-demo annotations: openfeature.dev/enabled: "true" - openfeature.dev/flagsourceconfiguration: "end-to-end" + openfeature.dev/featureflagsource: "end-to-end" spec: serviceAccountName: open-feature-demo-sa containers: diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index 4c44bd898..e03b39199 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -25,30 +25,3 @@ webhooks: resources: - pods sideEffects: NoneOnDryRun ---- -apiVersion: admissionregistration.k8s.io/v1 -kind: ValidatingWebhookConfiguration -metadata: - creationTimestamp: null - name: validating-webhook-configuration -webhooks: -- admissionReviewVersions: - - v1 - clientConfig: - service: - name: webhook-service - namespace: system - path: /validate-v1alpha1-featureflagconfiguration - failurePolicy: Fail - name: validate.featureflagconfiguration.openfeature.dev - rules: - - apiGroups: - - core.openfeature.dev - apiVersions: - - v1alpha1 - operations: - - CREATE - - UPDATE - resources: - - featureflagconfigurations - sideEffects: None diff --git a/controllers/common/common.go b/controllers/common/common.go deleted file mode 100644 index c8bc91af3..000000000 --- a/controllers/common/common.go +++ /dev/null @@ -1,64 +0,0 @@ -package common - -import ( - "context" - "fmt" - "github.com/open-feature/open-feature-operator/apis/core/v1alpha1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "time" - - appsV1 "k8s.io/api/apps/v1" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -const ( - ReconcileErrorInterval = 10 * time.Second - ReconcileSuccessInterval = 120 * time.Second - FinalizerName = "featureflagconfiguration.core.openfeature.dev/finalizer" - OpenFeatureAnnotationPath = "spec.template.metadata.annotations.openfeature.dev/openfeature.dev" - FlagSourceConfigurationAnnotation = "flagsourceconfiguration" - OpenFeatureAnnotationRoot = "openfeature.dev" -) - -func FlagSourceConfigurationIndex(o client.Object) []string { - deployment, ok := o.(*appsV1.Deployment) - if !ok { - return []string{ - "false", - } - } - - if deployment.Spec.Template.ObjectMeta.Annotations == nil { - return []string{ - "false", - } - } - if _, ok := deployment.Spec.Template.ObjectMeta.Annotations[fmt.Sprintf("openfeature.dev/%s", FlagSourceConfigurationAnnotation)]; ok { - return []string{ - "true", - } - } - return []string{ - "false", - } -} - -func FindFlagConfig(ctx context.Context, c client.Client, namespace string, name string) (*v1alpha1.FeatureFlagConfiguration, error) { - ffConfig := &v1alpha1.FeatureFlagConfiguration{} - if err := c.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, ffConfig); err != nil { - return nil, err - } - return ffConfig, nil -} - -// SharedOwnership returns true if any of the owner references match in the given slices -func SharedOwnership(ownerReferences1, ownerReferences2 []metav1.OwnerReference) bool { - for _, owner1 := range ownerReferences1 { - for _, owner2 := range ownerReferences2 { - if owner1.UID == owner2.UID { - return true - } - } - } - return false -} diff --git a/controllers/common/constant/configuration.go b/controllers/common/constant/configuration.go deleted file mode 100644 index 3201eeb83..000000000 --- a/controllers/common/constant/configuration.go +++ /dev/null @@ -1,14 +0,0 @@ -package constant - -import corev1 "k8s.io/api/core/v1" - -const ( - FlagDImagePullPolicy corev1.PullPolicy = "Always" - ClusterRoleBindingName string = "open-feature-operator-flagd-kubernetes-sync" - AllowKubernetesSyncAnnotation = "allowkubernetessync" - OpenFeatureAnnotationPrefix = "openfeature.dev" - SourceConfigParam = "--sources" - ProbeReadiness = "/readyz" - ProbeLiveness = "/healthz" - ProbeInitialDelay = 5 -) diff --git a/controllers/common/constant/errors.go b/controllers/common/constant/errors.go deleted file mode 100644 index 2975b6a42..000000000 --- a/controllers/common/constant/errors.go +++ /dev/null @@ -1,6 +0,0 @@ -package constant - -import "errors" - -var ErrFlagdProxyNotReady = errors.New("flagd-proxy is not ready, deferring pod admission") -var ErrUnrecognizedSyncProvider = errors.New("unrecognized sync provider") diff --git a/controllers/common/flagd-proxy_test.go b/controllers/common/flagd-proxy_test.go deleted file mode 100644 index ccfb041a0..000000000 --- a/controllers/common/flagd-proxy_test.go +++ /dev/null @@ -1,141 +0,0 @@ -package common - -import ( - "context" - "github.com/go-logr/logr/testr" - "github.com/stretchr/testify/require" - v1 "k8s.io/api/apps/v1" - v12 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/client/fake" - "testing" -) - -func TestNewFlagdProxyConfiguration(t *testing.T) { - kpConfig, err := NewFlagdProxyConfiguration() - - require.Nil(t, err) - require.NotNil(t, kpConfig) - require.Equal(t, &FlagdProxyConfiguration{ - Port: 8015, - MetricsPort: 8016, - DebugLogging: false, - Image: DefaultFlagdProxyImage, - Tag: DefaultFlagdProxyTag, - Namespace: DefaultFlagdProxyNamespace, - OperatorDeploymentName: operatorDeploymentName, - }, kpConfig) -} - -func TestNewFlagdProxyConfiguration_OverrideEnvVars(t *testing.T) { - - t.Setenv(envVarProxyImage, "my-image") - t.Setenv(envVarProxyTag, "my-tag") - t.Setenv(envVarPodNamespace, "my-namespace") - t.Setenv(envVarProxyPort, "8080") - t.Setenv(envVarProxyMetricsPort, "8081") - t.Setenv(envVarProxyDebugLogging, "true") - - kpConfig, err := NewFlagdProxyConfiguration() - - require.Nil(t, err) - require.NotNil(t, kpConfig) - require.Equal(t, &FlagdProxyConfiguration{ - Port: 8080, - MetricsPort: 8081, - DebugLogging: true, - Image: "my-image", - Tag: "my-tag", - Namespace: "my-namespace", - OperatorDeploymentName: operatorDeploymentName, - }, kpConfig) -} - -func TestNewFlagdProxyHandler(t *testing.T) { - kpConfig, err := NewFlagdProxyConfiguration() - - require.Nil(t, err) - require.NotNil(t, kpConfig) - - fakeClient := fake.NewClientBuilder().Build() - - ph := NewFlagdProxyHandler(kpConfig, fakeClient, testr.New(t)) - - require.NotNil(t, ph) - - require.Equal(t, kpConfig, ph.Config()) -} - -func TestFlagdProxyHandler_HandleFlagdProxy_ProxyExists(t *testing.T) { - kpConfig, err := NewFlagdProxyConfiguration() - - require.Nil(t, err) - require.NotNil(t, kpConfig) - - fakeClient := fake.NewClientBuilder().WithObjects(&v1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: kpConfig.Namespace, - Name: FlagdProxyDeploymentName, - }, - Spec: v1.DeploymentSpec{ - Template: v12.PodTemplateSpec{ - Spec: v12.PodSpec{ - Containers: []v12.Container{ - { - Name: "my-container", - }, - }, - }, - }, - }, - }).Build() - - ph := NewFlagdProxyHandler(kpConfig, fakeClient, testr.New(t)) - - require.NotNil(t, ph) - - err = ph.HandleFlagdProxy(context.Background(), nil) - - require.Nil(t, err) - - deployment := &v1.Deployment{} - err = fakeClient.Get(context.Background(), client.ObjectKey{ - Namespace: DefaultFlagdProxyNamespace, - Name: FlagdProxyDeploymentName, - }, deployment) - - require.Nil(t, err) - require.NotNil(t, deployment) - - // verify that the existing deployment has not been changed - require.Equal(t, "my-container", deployment.Spec.Template.Spec.Containers[0].Name) -} - -func TestFlagdProxyHandler_HandleFlagdProxy_CreateProxy(t *testing.T) { - kpConfig, err := NewFlagdProxyConfiguration() - - require.Nil(t, err) - require.NotNil(t, kpConfig) - - fakeClient := fake.NewClientBuilder().Build() - - ph := NewFlagdProxyHandler(kpConfig, fakeClient, testr.New(t)) - - require.NotNil(t, ph) - - err = ph.HandleFlagdProxy(context.Background(), nil) - - require.Nil(t, err) - - deployment := &v1.Deployment{} - err = fakeClient.Get(context.Background(), client.ObjectKey{ - Namespace: DefaultFlagdProxyNamespace, - Name: FlagdProxyDeploymentName, - }, deployment) - - require.Nil(t, err) - require.NotNil(t, deployment) - - require.Equal(t, FlagdProxyDeploymentName, deployment.Spec.Template.Spec.Containers[0].Name) -} diff --git a/controllers/common/mock/flagd-injector.go b/controllers/common/mock/flagd-injector.go deleted file mode 100644 index 237eb8d5f..000000000 --- a/controllers/common/mock/flagd-injector.go +++ /dev/null @@ -1,66 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: controllers/common/flagd-injector.go - -// Package commonmock is a generated GoMock package. -package commonmock - -import ( - context "context" - reflect "reflect" - - gomock "github.com/golang/mock/gomock" - v1alpha1 "github.com/open-feature/open-feature-operator/apis/core/v1alpha1" - v1 "k8s.io/api/core/v1" - v10 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// MockIFlagdContainerInjector is a mock of IFlagdContainerInjector interface. -type MockIFlagdContainerInjector struct { - ctrl *gomock.Controller - recorder *MockIFlagdContainerInjectorMockRecorder -} - -// MockIFlagdContainerInjectorMockRecorder is the mock recorder for MockIFlagdContainerInjector. -type MockIFlagdContainerInjectorMockRecorder struct { - mock *MockIFlagdContainerInjector -} - -// NewMockIFlagdContainerInjector creates a new mock instance. -func NewMockIFlagdContainerInjector(ctrl *gomock.Controller) *MockIFlagdContainerInjector { - mock := &MockIFlagdContainerInjector{ctrl: ctrl} - mock.recorder = &MockIFlagdContainerInjectorMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockIFlagdContainerInjector) EXPECT() *MockIFlagdContainerInjectorMockRecorder { - return m.recorder -} - -// EnableClusterRoleBinding mocks base method. -func (m *MockIFlagdContainerInjector) EnableClusterRoleBinding(ctx context.Context, namespace, serviceAccountName string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "EnableClusterRoleBinding", ctx, namespace, serviceAccountName) - ret0, _ := ret[0].(error) - return ret0 -} - -// EnableClusterRoleBinding indicates an expected call of EnableClusterRoleBinding. -func (mr *MockIFlagdContainerInjectorMockRecorder) EnableClusterRoleBinding(ctx, namespace, serviceAccountName interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnableClusterRoleBinding", reflect.TypeOf((*MockIFlagdContainerInjector)(nil).EnableClusterRoleBinding), ctx, namespace, serviceAccountName) -} - -// InjectFlagd mocks base method. -func (m *MockIFlagdContainerInjector) InjectFlagd(ctx context.Context, objectMeta *v10.ObjectMeta, podSpec *v1.PodSpec, flagSourceConfig *v1alpha1.FlagSourceConfigurationSpec) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "InjectFlagd", ctx, objectMeta, podSpec, flagSourceConfig) - ret0, _ := ret[0].(error) - return ret0 -} - -// InjectFlagd indicates an expected call of InjectFlagd. -func (mr *MockIFlagdContainerInjectorMockRecorder) InjectFlagd(ctx, objectMeta, podSpec, flagSourceConfig interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InjectFlagd", reflect.TypeOf((*MockIFlagdContainerInjector)(nil).InjectFlagd), ctx, objectMeta, podSpec, flagSourceConfig) -} diff --git a/controllers/core/featureflagconfiguration/controller.go b/controllers/core/featureflagconfiguration/controller.go deleted file mode 100644 index 42a23be94..000000000 --- a/controllers/core/featureflagconfiguration/controller.go +++ /dev/null @@ -1,188 +0,0 @@ -/* -Copyright 2022. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package featureflagconfiguration - -import ( - "context" - - "github.com/go-logr/logr" - "github.com/open-feature/open-feature-operator/controllers/common" - "github.com/open-feature/open-feature-operator/pkg/utils" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" - "sigs.k8s.io/controller-runtime/pkg/builder" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - "sigs.k8s.io/controller-runtime/pkg/predicate" - - "k8s.io/apimachinery/pkg/runtime" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - - corev1alpha1 "github.com/open-feature/open-feature-operator/apis/core/v1alpha1" -) - -// FeatureFlagConfigurationReconciler reconciles a FeatureFlagConfiguration object -type FeatureFlagConfigurationReconciler struct { - client.Client - - // Scheme contains the scheme of this controller - Scheme *runtime.Scheme - // ReqLogger contains the Logger of this controller - Log logr.Logger -} - -//+kubebuilder:rbac:groups=core.openfeature.dev,resources=featureflagconfigurations,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=core.openfeature.dev,resources=featureflagconfigurations/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=core.openfeature.dev,resources=featureflagconfigurations/finalizers,verbs=update - -// Reconcile is part of the main kubernetes reconciliation loop which aims to -// move the current state of the cluster closer to the desired state. -// TODO(user): Modify the Reconcile function to compare the state specified by -// the FeatureFlagConfiguration object against the actual cluster state, and then -// perform operations to make the cluster state reflect the state specified by -// the user. -// -// For more details, check Reconcile and its Result here: -// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.11.0/pkg/reconcile - -const CrdName = "FeatureFlagConfiguration" - -func (r *FeatureFlagConfigurationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - r.Log.Info("Reconciling" + CrdName) - - ffconf := &corev1alpha1.FeatureFlagConfiguration{} - if err := r.Client.Get(ctx, req.NamespacedName, ffconf); err != nil { - if errors.IsNotFound(err) { - // taking down all associated K8s resources is handled by K8s - r.Log.Info(CrdName + " resource not found. Ignoring since object must be deleted") - return r.finishReconcile(nil, false) - } - r.Log.Error(err, "Failed to get the "+CrdName) - return r.finishReconcile(err, false) - } - - if ffconf.ObjectMeta.DeletionTimestamp.IsZero() { - // The object is not being deleted, so if it does not have our finalizer, - // then lets add the finalizer and update the object. This is equivalent - // registering our finalizer. - if !utils.ContainsString(ffconf.GetFinalizers(), common.FinalizerName) { - controllerutil.AddFinalizer(ffconf, common.FinalizerName) - if err := r.Update(ctx, ffconf); err != nil { - return r.finishReconcile(err, false) - } - } - } else { - // The object is being deleted - if utils.ContainsString(ffconf.GetFinalizers(), common.FinalizerName) { - controllerutil.RemoveFinalizer(ffconf, common.FinalizerName) - if err := r.Update(ctx, ffconf); err != nil { - return ctrl.Result{}, err - } - } - // Stop reconciliation as the item is being deleted - return r.finishReconcile(nil, false) - } - - // Check the provider on the FeatureFlagConfiguration - if !ffconf.Spec.ServiceProvider.IsSet() { - r.Log.Info("No service provider specified for FeatureFlagConfiguration, using FlagD") - ffconf.Spec.ServiceProvider = &corev1alpha1.FeatureFlagServiceProvider{ - Name: "flagd", - } - if err := r.Update(ctx, ffconf); err != nil { - r.Log.Error(err, "Failed to update FeatureFlagConfiguration service provider") - return r.finishReconcile(err, false) - } - } - - // Get list of configmaps - configMapList := &corev1.ConfigMapList{} - var ffConfigMapList []corev1.ConfigMap - if err := r.List(ctx, configMapList); err != nil { - return r.finishReconcile(err, false) - } - - // Get list of configmaps with annotation - for _, cm := range configMapList.Items { - val, ok := cm.GetAnnotations()["openfeature.dev/featureflagconfiguration"] - if ok && val == ffconf.Name { - ffConfigMapList = append(ffConfigMapList, cm) - } - } - - for _, cm := range ffConfigMapList { - // Append OwnerReference if not set - if !r.featureFlagResourceIsOwner(ffconf, cm) { - r.Log.Info("Setting owner reference for " + cm.Name) - cm.OwnerReferences = append(cm.OwnerReferences, ffconf.GetReference()) - err := r.Client.Update(ctx, &cm) - if err != nil { - return r.finishReconcile(err, true) - } - } else if len(cm.OwnerReferences) == 1 { - // Delete ConfigMap if the Controller is the only reference - r.Log.Info("Deleting configmap " + cm.Name) - err := r.Client.Delete(ctx, &cm) - return r.finishReconcile(err, true) - } - // Update ConfigMap Spec - r.Log.Info("Updating ConfigMap Spec " + cm.Name) - cm.Data = map[string]string{ - utils.FeatureFlagConfigurationConfigMapKey(cm.Namespace, cm.Name): ffconf.Spec.FeatureFlagSpec, - } - err := r.Client.Update(ctx, &cm) - if err != nil { - return r.finishReconcile(err, true) - } - } - - return r.finishReconcile(nil, false) -} - -func (r *FeatureFlagConfigurationReconciler) finishReconcile(err error, requeueImmediate bool) (ctrl.Result, error) { - if err != nil { - interval := common.ReconcileErrorInterval - if requeueImmediate { - interval = 0 - } - r.Log.Error(err, "Finished Reconciling "+CrdName+" with error: %w") - return ctrl.Result{Requeue: true, RequeueAfter: interval}, err - } - interval := common.ReconcileSuccessInterval - if requeueImmediate { - interval = 0 - } - r.Log.Info("Finished Reconciling " + CrdName) - return ctrl.Result{Requeue: true, RequeueAfter: interval}, nil -} - -func (r *FeatureFlagConfigurationReconciler) featureFlagResourceIsOwner(ff *corev1alpha1.FeatureFlagConfiguration, cm corev1.ConfigMap) bool { - for _, cmOwner := range cm.OwnerReferences { - if cmOwner.UID == ff.GetReference().UID { - return true - } - } - return false -} - -// SetupWithManager sets up the controller with the Manager. -func (r *FeatureFlagConfigurationReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&corev1alpha1.FeatureFlagConfiguration{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). - Owns(&corev1.ConfigMap{}). - Complete(r) -} diff --git a/controllers/core/featureflagconfiguration/controller_test.go b/controllers/core/featureflagconfiguration/controller_test.go deleted file mode 100644 index f26d7d0f0..000000000 --- a/controllers/core/featureflagconfiguration/controller_test.go +++ /dev/null @@ -1,175 +0,0 @@ -package featureflagconfiguration - -import ( - "context" - "testing" - - "github.com/open-feature/open-feature-operator/apis/core/v1alpha1" - "github.com/open-feature/open-feature-operator/pkg/utils" - "github.com/stretchr/testify/require" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/kubernetes/scheme" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client/fake" -) - -func TestFlagSourceConfigurationReconciler_Reconcile(t *testing.T) { - const ( - testNamespace = "test-namespace" - ffConfigName = "test-config" - cmName = "test-cm" - ) - - tests := []struct { - name string - ffConfig *v1alpha1.FeatureFlagConfiguration - cm *corev1.ConfigMap - wantProvider string - wantCM *corev1.ConfigMap - cmDeleted bool - }{ - { - name: "no provider set + no owner set -> ffconfig and cm will be updated", - cm: &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: cmName, - Namespace: testNamespace, - Annotations: map[string]string{ - "openfeature.dev/featureflagconfiguration": ffConfigName, - }, - }, - }, - ffConfig: createTestFFConfig(ffConfigName, testNamespace, cmName, ""), - wantProvider: "flagd", - wantCM: &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: cmName, - Namespace: testNamespace, - Annotations: map[string]string{ - "openfeature.dev/featureflagconfiguration": ffConfigName, - }, - OwnerReferences: []metav1.OwnerReference{ - { - APIVersion: "core.openfeature.dev/v1alpha1", - Kind: "FeatureFlagConfiguration", - Name: ffConfigName, - }, - }, - }, - Data: map[string]string{ - utils.FeatureFlagConfigurationConfigMapKey(testNamespace, cmName): "spec", - }, - }, - cmDeleted: false, - }, - { - name: "one owner ref set -> cm will be deleted", - cm: &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: cmName, - Namespace: testNamespace, - Annotations: map[string]string{ - "openfeature.dev/featureflagconfiguration": ffConfigName, - }, - }, - }, - ffConfig: createTestFFConfig(ffConfigName, testNamespace, cmName, ""), - wantProvider: "flagd", - wantCM: &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: cmName, - Namespace: testNamespace, - Annotations: map[string]string{ - "openfeature.dev/featureflagconfiguration": ffConfigName, - }, - }, - }, - cmDeleted: true, - }, - } - - err := v1alpha1.AddToScheme(scheme.Scheme) - require.Nil(t, err) - req := ctrl.Request{ - NamespacedName: types.NamespacedName{ - Namespace: testNamespace, - Name: ffConfigName, - }, - } - - ctx := context.TODO() - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // set up k8s fake client - fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(tt.ffConfig, tt.cm).Build() - - r := &FeatureFlagConfigurationReconciler{ - Client: fakeClient, - Log: ctrl.Log.WithName("featureflagconfiguration-controller"), - Scheme: fakeClient.Scheme(), - } - - if tt.cmDeleted { - // if configMap should be deleted, we need to set ffConfig as the only OwnerRef before executing the Reconcile function - ffConfig := &v1alpha1.FeatureFlagConfiguration{} - err = fakeClient.Get(ctx, types.NamespacedName{Name: ffConfigName, Namespace: testNamespace}, ffConfig) - require.Nil(t, err) - - cm := &corev1.ConfigMap{} - err = fakeClient.Get(ctx, types.NamespacedName{Name: cmName, Namespace: testNamespace}, cm) - require.Nil(t, err) - - cm.OwnerReferences = append(cm.OwnerReferences, ffConfig.GetReference()) - err := r.Client.Update(ctx, cm) - require.Nil(t, err) - } - - // reconcile - _, err = r.Reconcile(ctx, req) - require.Nil(t, err) - - ffConfig := &v1alpha1.FeatureFlagConfiguration{} - err = fakeClient.Get(ctx, types.NamespacedName{Name: ffConfigName, Namespace: testNamespace}, ffConfig) - require.Nil(t, err) - - // check that the provider name is set correctly - require.Equal(t, tt.wantProvider, ffConfig.Spec.ServiceProvider.Name) - - cm := &corev1.ConfigMap{} - err = fakeClient.Get(ctx, types.NamespacedName{Name: cmName, Namespace: testNamespace}, cm) - - if !tt.cmDeleted { - // if configMap should not be deleted, check the correct values - require.Nil(t, err) - require.Equal(t, tt.wantCM.Data, cm.Data) - require.Len(t, cm.OwnerReferences, len(tt.wantCM.OwnerReferences)) - require.Equal(t, tt.wantCM.OwnerReferences[0].APIVersion, cm.OwnerReferences[0].APIVersion) - require.Equal(t, tt.wantCM.OwnerReferences[0].Name, cm.OwnerReferences[0].Name) - require.Equal(t, tt.wantCM.OwnerReferences[0].Kind, cm.OwnerReferences[0].Kind) - } else { - // if configMap should be deleted, we expect error - require.NotNil(t, err) - } - }) - } -} - -func createTestFFConfig(ffConfigName string, testNamespace string, cmName string, provider string) *v1alpha1.FeatureFlagConfiguration { - fsConfig := &v1alpha1.FeatureFlagConfiguration{ - ObjectMeta: metav1.ObjectMeta{ - Name: ffConfigName, - Namespace: testNamespace, - }, - Spec: v1alpha1.FeatureFlagConfigurationSpec{ - ServiceProvider: &v1alpha1.FeatureFlagServiceProvider{ - Name: provider, - }, - FeatureFlagSpec: "spec", - }, - } - - return fsConfig -} diff --git a/controllers/core/flagsourceconfiguration/controller.go b/controllers/core/featureflagsource/controller.go similarity index 66% rename from controllers/core/flagsourceconfiguration/controller.go rename to controllers/core/featureflagsource/controller.go index fa0f9c6aa..a8e2b8d37 100644 --- a/controllers/core/flagsourceconfiguration/controller.go +++ b/controllers/core/featureflagsource/controller.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package flagsourceconfiguration +package featureflagsource import ( "context" @@ -22,7 +22,10 @@ import ( "strings" "time" - "github.com/open-feature/open-feature-operator/controllers/common" + "github.com/go-logr/logr" + api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" + "github.com/open-feature/open-feature-operator/common" + "github.com/open-feature/open-feature-operator/common/flagdproxy" appsV1 "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" @@ -30,36 +33,36 @@ import ( "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/predicate" - - "github.com/go-logr/logr" - corev1alpha1 "github.com/open-feature/open-feature-operator/apis/core/v1alpha1" ) -// FlagSourceConfigurationReconciler reconciles a FlagSourceConfiguration object -type FlagSourceConfigurationReconciler struct { +// FeatureFlagSourceReconciler reconciles a FeatureFlagSource object +type FeatureFlagSourceReconciler struct { client.Client Scheme *runtime.Scheme // ReqLogger contains the Logger of this controller Log logr.Logger - FlagdProxy *common.FlagdProxyHandler + FlagdProxy *flagdproxy.FlagdProxyHandler } -//+kubebuilder:rbac:groups=core.openfeature.dev,resources=flagsourceconfigurations,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=core.openfeature.dev,resources=flagsourceconfigurations/status,verbs=get;update;patch +// renovate: datasource=github-tags depName=open-feature/flagd/flagd-proxy +const flagdProxyTag = "v0.3.0" + +//+kubebuilder:rbac:groups=core.openfeature.dev,resources=featureflagsources,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=core.openfeature.dev,resources=featureflagsources/status,verbs=get;update;patch //+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups="",resources=services,verbs=get;list;create -//+kubebuilder:rbac:groups=core.openfeature.dev,resources=flagsourceconfigurations/finalizers,verbs=update +//+kubebuilder:rbac:groups=core.openfeature.dev,resources=featureflagsources/finalizers,verbs=update // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.13.0/pkg/reconcile -func (r *FlagSourceConfigurationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - r.Log.Info("Searching for FlagSourceConfiguration") +func (r *FeatureFlagSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + r.Log.Info("Searching for FeatureFlagSource") - // Fetch the FlagSourceConfiguration from the cache - fsConfig := &corev1alpha1.FlagSourceConfiguration{} + // Fetch the FeatureFlagSource from the cache + fsConfig := &api.FeatureFlagSource{} if err := r.Client.Get(ctx, req.NamespacedName, fsConfig); err != nil { if errors.IsNotFound(err) { // taking down all associated K8s resources is handled by K8s @@ -72,8 +75,8 @@ func (r *FlagSourceConfigurationReconciler) Reconcile(ctx context.Context, req c for _, source := range fsConfig.Spec.Sources { if source.Provider.IsFlagdProxy() { - r.Log.Info(fmt.Sprintf("flagsourceconfiguration %s uses flagd-proxy, checking deployment", req.NamespacedName)) - if err := r.FlagdProxy.HandleFlagdProxy(ctx, fsConfig); err != nil { + r.Log.Info(fmt.Sprintf("featureflagsource %s uses flagd-proxy, checking deployment", req.NamespacedName)) + if err := r.FlagdProxy.HandleFlagdProxy(ctx); err != nil { r.Log.Error(err, "error handling the flagd-proxy deployment") } break @@ -84,22 +87,28 @@ func (r *FlagSourceConfigurationReconciler) Reconcile(ctx context.Context, req c return r.finishReconcile(nil, false) } + err := r.handleDeploymentUpdate(ctx, fsConfig) + + return r.finishReconcile(err, false) +} + +func (r *FeatureFlagSourceReconciler) handleDeploymentUpdate(ctx context.Context, fsConfig *api.FeatureFlagSource) error { // Object has been updated, so, we can restart any deployments that are using this annotation // => we know there has been an update because we are using the GenerationChangedPredicate filter // and our resource exists within the cluster deployList := &appsV1.DeploymentList{} if err := r.Client.List(ctx, deployList, client.MatchingFields{ - fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationPath, common.FlagSourceConfigurationAnnotation): "true", + fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationPath, common.FeatureFlagSourceAnnotation): "true", }); err != nil { - r.Log.Error(err, fmt.Sprintf("Failed to get the pods with annotation %s/%s", common.OpenFeatureAnnotationPath, common.FlagSourceConfigurationAnnotation)) - return r.finishReconcile(err, false) + r.Log.Error(err, fmt.Sprintf("Failed to get the pods with annotation %s/%s", common.OpenFeatureAnnotationPath, common.FeatureFlagSourceAnnotation)) + return err } - // Loop through all deployments containing the openfeature.dev/flagsourceconfiguration annotation + // Loop through all deployments containing the openfeature.dev/featureflagsource annotation // and trigger a restart for any which have our resource listed as a configuration for _, deployment := range deployList.Items { annotations := deployment.Spec.Template.Annotations - annotation, ok := annotations[fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationRoot, common.FlagSourceConfigurationAnnotation)] + annotation, ok := annotations[fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationRoot, common.FeatureFlagSourceAnnotation)] if !ok { continue } @@ -113,10 +122,10 @@ func (r *FlagSourceConfigurationReconciler) Reconcile(ctx context.Context, req c } } - return r.finishReconcile(nil, false) + return nil } -func (r *FlagSourceConfigurationReconciler) isUsingConfiguration(namespace string, name string, deploymentNamespace string, annotation string) bool { +func (r *FeatureFlagSourceReconciler) isUsingConfiguration(namespace string, name string, deploymentNamespace string, annotation string) bool { s := strings.Split(annotation, ",") // parse annotation list for _, target := range s { ss := strings.Split(strings.TrimSpace(target), "/") @@ -130,23 +139,23 @@ func (r *FlagSourceConfigurationReconciler) isUsingConfiguration(namespace strin return false } -func (r *FlagSourceConfigurationReconciler) finishReconcile(err error, requeueImmediate bool) (ctrl.Result, error) { +func (r *FeatureFlagSourceReconciler) finishReconcile(err error, requeueImmediate bool) (ctrl.Result, error) { if err != nil { interval := common.ReconcileErrorInterval if requeueImmediate { interval = 0 } - r.Log.Error(err, "Finished Reconciling FlagSourceConfiguration with error: %w") + r.Log.Error(err, "Finished Reconciling FeatureFlagSource with error: %w") return ctrl.Result{Requeue: true, RequeueAfter: interval}, err } - r.Log.Info("Finished Reconciling FlagSourceConfiguration") + r.Log.Info("Finished Reconciling FeatureFlagSource") return ctrl.Result{Requeue: false}, nil } // SetupWithManager sets up the controller with the Manager. -func (r *FlagSourceConfigurationReconciler) SetupWithManager(mgr ctrl.Manager) error { +func (r *FeatureFlagSourceReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). - For(&corev1alpha1.FlagSourceConfiguration{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). + For(&api.FeatureFlagSource{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). // we are only interested in update events for this reconciliation loop WithEventFilter(predicate.GenerationChangedPredicate{}). Complete(r) diff --git a/controllers/core/flagsourceconfiguration/controller_test.go b/controllers/core/featureflagsource/controller_test.go similarity index 74% rename from controllers/core/flagsourceconfiguration/controller_test.go rename to controllers/core/featureflagsource/controller_test.go index 39a905e03..baba1f4c4 100644 --- a/controllers/core/flagsourceconfiguration/controller_test.go +++ b/controllers/core/featureflagsource/controller_test.go @@ -1,4 +1,4 @@ -package flagsourceconfiguration +package featureflagsource import ( "context" @@ -6,8 +6,11 @@ import ( "testing" "time" - "github.com/open-feature/open-feature-operator/apis/core/v1alpha1" - "github.com/open-feature/open-feature-operator/controllers/common" + api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" + apicommon "github.com/open-feature/open-feature-operator/apis/core/v1beta1/common" + "github.com/open-feature/open-feature-operator/common" + "github.com/open-feature/open-feature-operator/common/flagdproxy" + commontypes "github.com/open-feature/open-feature-operator/common/types" "github.com/stretchr/testify/require" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -19,7 +22,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/fake" ) -func TestFlagSourceConfigurationReconciler_Reconcile(t *testing.T) { +func TestFeatureFlagSourceReconciler_Reconcile(t *testing.T) { const ( testNamespace = "test-namespace" fsConfigName = "test-config" @@ -28,7 +31,7 @@ func TestFlagSourceConfigurationReconciler_Reconcile(t *testing.T) { tests := []struct { name string - fsConfig *v1alpha1.FlagSourceConfiguration + fsConfig *api.FeatureFlagSource deployment *appsv1.Deployment restartedAtValueBeforeReconcile string restartedAtValueAfterReconcile string @@ -36,28 +39,28 @@ func TestFlagSourceConfigurationReconciler_Reconcile(t *testing.T) { }{ { name: "deployment gets restarted with rollout", - fsConfig: createTestFSConfig(fsConfigName, testNamespace, deploymentName, true, v1alpha1.SyncProviderHttp), + fsConfig: createTestFSConfig(fsConfigName, testNamespace, true, apicommon.SyncProviderHttp), deployment: createTestDeployment(fsConfigName, testNamespace, deploymentName), restartedAtValueBeforeReconcile: "", restartedAtValueAfterReconcile: time.Now().Format(time.RFC3339), }, { name: "deployment without rollout", - fsConfig: createTestFSConfig(fsConfigName, testNamespace, deploymentName, false, v1alpha1.SyncProviderHttp), + fsConfig: createTestFSConfig(fsConfigName, testNamespace, false, apicommon.SyncProviderHttp), deployment: createTestDeployment(fsConfigName, testNamespace, deploymentName), restartedAtValueBeforeReconcile: "", restartedAtValueAfterReconcile: "", }, { name: "no deployment", - fsConfig: createTestFSConfig(fsConfigName, testNamespace, deploymentName, true, v1alpha1.SyncProviderHttp), + fsConfig: createTestFSConfig(fsConfigName, testNamespace, true, apicommon.SyncProviderHttp), deployment: nil, restartedAtValueBeforeReconcile: "", restartedAtValueAfterReconcile: "", }, { name: "no deployment, kube proxy deployment", - fsConfig: createTestFSConfig(fsConfigName, testNamespace, deploymentName, true, v1alpha1.SyncProviderFlagdProxy), + fsConfig: createTestFSConfig(fsConfigName, testNamespace, true, apicommon.SyncProviderFlagdProxy), deployment: nil, restartedAtValueBeforeReconcile: "", restartedAtValueAfterReconcile: "", @@ -65,7 +68,7 @@ func TestFlagSourceConfigurationReconciler_Reconcile(t *testing.T) { }, } - err := v1alpha1.AddToScheme(scheme.Scheme) + err := api.AddToScheme(scheme.Scheme) require.Nil(t, err) req := ctrl.Request{ @@ -82,23 +85,25 @@ func TestFlagSourceConfigurationReconciler_Reconcile(t *testing.T) { // setting up fake k8s client var fakeClient client.Client if tt.deployment != nil { - fakeClient = fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(tt.fsConfig, tt.deployment).WithIndex(&appsv1.Deployment{}, fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationPath, common.FlagSourceConfigurationAnnotation), common.FlagSourceConfigurationIndex).Build() + fakeClient = fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(tt.fsConfig, tt.deployment).WithIndex(&appsv1.Deployment{}, fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationPath, common.FeatureFlagSourceAnnotation), common.FeatureFlagSourceIndex).Build() } else { - fakeClient = fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(tt.fsConfig).WithIndex(&appsv1.Deployment{}, fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationPath, common.FlagSourceConfigurationAnnotation), common.FlagSourceConfigurationIndex).Build() + fakeClient = fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(tt.fsConfig).WithIndex(&appsv1.Deployment{}, fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationPath, common.FeatureFlagSourceAnnotation), common.FeatureFlagSourceIndex).Build() } - kpConfig, err := common.NewFlagdProxyConfiguration() - require.Nil(t, err) + kpConfig := flagdproxy.NewFlagdProxyConfiguration(commontypes.EnvConfig{ + FlagdProxyImage: "ghcr.io/open-feature/flagd-proxy", + FlagdProxyTag: flagdProxyTag, + }) kpConfig.Namespace = testNamespace - kph := common.NewFlagdProxyHandler( + kph := flagdproxy.NewFlagdProxyHandler( kpConfig, fakeClient, - ctrl.Log.WithName("flagsourceconfiguration-FlagdProxyhandler"), + ctrl.Log.WithName("featureflagsource-FlagdProxyhandler"), ) - r := &FlagSourceConfigurationReconciler{ + r := &FeatureFlagSourceReconciler{ Client: fakeClient, - Log: ctrl.Log.WithName("flagsourceconfiguration-controller"), + Log: ctrl.Log.WithName("featureflagsource-controller"), Scheme: fakeClient.Scheme(), FlagdProxy: kph, } @@ -129,14 +134,14 @@ func TestFlagSourceConfigurationReconciler_Reconcile(t *testing.T) { // check that a deployment exists in the default namespace with the correct image and tag // ensure that the associated service has also been deployed deployment := &appsv1.Deployment{} - err = fakeClient.Get(ctx, types.NamespacedName{Name: common.FlagdProxyDeploymentName, Namespace: testNamespace}, deployment) + err = fakeClient.Get(ctx, types.NamespacedName{Name: flagdproxy.FlagdProxyDeploymentName, Namespace: testNamespace}, deployment) require.Nil(t, err) require.Equal(t, len(deployment.Spec.Template.Spec.Containers), 1) require.Equal(t, len(deployment.Spec.Template.Spec.Containers[0].Ports), 2) - require.Equal(t, deployment.Spec.Template.Spec.Containers[0].Image, fmt.Sprintf("%s:%s", common.DefaultFlagdProxyImage, common.DefaultFlagdProxyTag)) + require.Equal(t, deployment.Spec.Template.Spec.Containers[0].Image, "ghcr.io/open-feature/flagd-proxy:"+flagdProxyTag) service := &corev1.Service{} - err = fakeClient.Get(ctx, types.NamespacedName{Name: common.FlagdProxyServiceName, Namespace: testNamespace}, service) + err = fakeClient.Get(ctx, types.NamespacedName{Name: flagdproxy.FlagdProxyServiceName, Namespace: testNamespace}, service) require.Nil(t, err) require.Equal(t, len(service.Spec.Ports), 1) require.Equal(t, service.Spec.Ports[0].TargetPort.IntVal, deployment.Spec.Template.Spec.Containers[0].Ports[0].ContainerPort) @@ -155,8 +160,8 @@ func createTestDeployment(fsConfigName string, testNamespace string, deploymentN Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{ - fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationPath, common.FlagSourceConfigurationAnnotation): "true", - fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationRoot, common.FlagSourceConfigurationAnnotation): fmt.Sprintf("%s/%s", testNamespace, fsConfigName), + fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationPath, common.FeatureFlagSourceAnnotation): "true", + fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationRoot, common.FeatureFlagSourceAnnotation): fmt.Sprintf("%s/%s", testNamespace, fsConfigName), }, Labels: map[string]string{ "app": "test", @@ -186,15 +191,14 @@ func createTestDeployment(fsConfigName string, testNamespace string, deploymentN return deployment } -func createTestFSConfig(fsConfigName string, testNamespace string, deploymentName string, rollout bool, provider v1alpha1.SyncProviderType) *v1alpha1.FlagSourceConfiguration { - fsConfig := &v1alpha1.FlagSourceConfiguration{ +func createTestFSConfig(fsConfigName string, testNamespace string, rollout bool, provider apicommon.SyncProviderType) *api.FeatureFlagSource { + fsConfig := &api.FeatureFlagSource{ ObjectMeta: metav1.ObjectMeta{ Name: fsConfigName, Namespace: testNamespace, }, - Spec: v1alpha1.FlagSourceConfigurationSpec{ - Image: deploymentName, - Sources: []v1alpha1.Source{ + Spec: api.FeatureFlagSourceSpec{ + Sources: []api.Source{ { Source: "my-source", Provider: provider, diff --git a/docs/README.md b/docs/README.md index 409cf0e34..62863f952 100644 --- a/docs/README.md +++ b/docs/README.md @@ -12,16 +12,16 @@ Follow the detailed installation guide to deploy open feature operator to your l ## Configuration -Configuration of the deployed sidecars is handled through the `FlagSourceConfiguration` custom resources defined at`openfeature.dev/flagsourceconfiguration` annotation of the deployed `PodSpec`. +Configuration of the deployed sidecars is handled through the `FeatureFlagSource` custom resources referenced via `openfeature.dev/featureflagsource` annotations of the deployed `PodSpec`. The relationship between the deployment and custom resources is highlighted in the diagram below, ```mermaid flowchart TD - A[Pod]-->|Annotation: openfeature.dev/flagsourceconfiguration| B[FlagSourceConfiguration CR] - B--> |Flag source| C[FeatureFlagConfiguration CR] + A[Pod]-->|Annotation: openfeature.dev/featureflagsource| B[FeatureFlagSource CR] + B--> |Flag source| C[FeatureFlag CR] B--> |Flag source| D[HTTP sync] - B--> |Flag source| E[Filepath sync] + B--> |Flag source| E[File sync] B--> |Flag source| F[GRPC sync] B--> |Flag source| G[flagd-proxy] ``` @@ -29,8 +29,8 @@ flowchart TD To configure and understand more, - Deployment configurations: [Annotations](./annotations.md) -- Define flag sources for the deployment: [FlagSourceConfiguration](./flag_source_configuration.md) -- Define feature flags as custom resource: [FeatureFlagConfigurations](./feature_flag_configuration.md) +- Define flag sources for the deployment: [FeatureFlagSource](./feature_flag_source.md) +- Define feature flags as custom resource: [FeatureFlags](./feature_flag.md) ## Other Resources - [Permissions](./permissions.md) diff --git a/docs/annotations.md b/docs/annotations.md index d20b1fc26..e6224235c 100644 --- a/docs/annotations.md +++ b/docs/annotations.md @@ -13,9 +13,9 @@ Example: openfeature.dev/enabled: "true" ``` -### `openfeature.dev/flagsourceconfiguration` +### `openfeature.dev/featureflagsource` -This annotation specifies the names of the `FlagSourceConfigurations` used to configure the injected flagd sidecar. +This annotation specifies the names of the `FeatureFlagSources` used to configure the injected flagd sidecar. The annotation value is a comma separated list of values following one of 2 patterns: {NAME} or {NAMESPACE}/{NAME}. If no namespace is provided, it is assumed that the custom resource is within the **same namespace** as the annotated pod. @@ -28,7 +28,7 @@ Example: metadata: annotations: openfeature.dev/enabled: "true" - openfeature.dev/flagsourceconfiguration: "config-A, config-B" + openfeature.dev/featureflagsource: "config-A, config-B" ``` ### `openfeature.dev/allowkubernetessync` @@ -40,28 +40,3 @@ When the OFO manager pod is started, all `Service Accounts` of any `Pods` with t ## Deprecated annotations Given below are references to **deprecated** annotations used by previous versions of the operator. - -### `openfeature.dev/featureflagconfiguration` -*This annotation is DEPRECATED in favour of the `openfeature.dev/flagsourceconfiguration` annotation and should no longer be used.* - -This annotation specifies the names of the FeatureFlagConfigurations used to configure the injected flagd sidecar. -The annotation value is a comma separated list of values following one of 2 patterns: {NAME} or {NAMESPACE}/{NAME}. -If no namespace is provided it is assumed that the CR is within the same namespace as the deployed pod. -Example: -```yaml - metadata: - annotations: - openfeature.dev/enabled: "true" - openfeature.dev/featureflagconfiguration: "demo, test/demo-2" -``` - -### `openfeature.dev` -*This annotation is DEPRECATED in favour of the `openfeature.dev/enabled` annotation and should no longer be used.* - -When a value of `"enabled"` is provided, the operator will inject a flagd sidecar into the annotated pods. -Example: -```yaml - metadata: - annotations: - openfeature.dev: "enabled" -``` diff --git a/docs/concepts.md b/docs/concepts.md index a6ebb3217..08f310623 100644 --- a/docs/concepts.md +++ b/docs/concepts.md @@ -13,11 +13,10 @@ The high level architecture of the operator is as follows: ## Modes of flag syncs -- Kubernetes: sync configuration which configures injected flagd sidecar instances to monitor the Kubernetes API - for changes in flag configuration custom resources (`FeatureFlagConfiguration`). -- filepath: sync configuration which creates and mounts ConfigMap files from flag configuration custom resources - (`FeatureFlagConfiguration`) and configures injected flagd sidecar instances to monitor them. -- grpc: sync configuration which listen for flagd compatible grpc stream +- Kubernetes: sync configuration that configures injected flagd sidecar instances to monitor the Kubernetes API + for changes in flag definition custom resources (`FeatureFlag`). +- file: sync configuration that creates and mounts ConfigMap files from flag configuration custom resources (`FeatureFlag`) and configures injected flagd sidecar instances to source them. +- grpc: sync configuration that listens for flagd compatible grpc stream - http: sync configuration which watch and periodically poll flagd compatible http endpoint - [flagd-proxy](./flagd_proxy.md) @@ -38,7 +37,7 @@ able to fetch Feature Flag information. For further information on how to avoid **When deploying an application via GitOps, we recommend using the `flagd-proxy` mode, which doesn't suffer from the shortcomings above.** -The `"filepath"` provider requires no such communication, but relies on the fact that [Kubernetes automatically updates mounted ConfigMaps](https://kubernetes.io/docs/concepts/configuration/configmap/#mounted-configmaps-are-updated-automatically). +The `"file"` provider requires no such communication, but relies on the fact that [Kubernetes automatically updates mounted ConfigMaps](https://kubernetes.io/docs/concepts/configuration/configmap/#mounted-configmaps-are-updated-automatically). The disadvantage of this approach is that flag configuration updates may take as long as two minutes to propagate, depending on cluster configuration: > "the total delay from the moment when the ConfigMap is updated to the moment when new keys are projected to the Pod can be as long as the kubelet sync period + cache propagation delay" diff --git a/docs/crds.md b/docs/crds.md index bc8ebbe4e..949e50f2a 100644 --- a/docs/crds.md +++ b/docs/crds.md @@ -2,30 +2,28 @@ Packages: -- [core.openfeature.dev/v1alpha1](#coreopenfeaturedevv1alpha1) -- [core.openfeature.dev/v1alpha2](#coreopenfeaturedevv1alpha2) -- [core.openfeature.dev/v1alpha3](#coreopenfeaturedevv1alpha3) +- [core.openfeature.dev/v1beta1](#coreopenfeaturedevv1beta1) -# core.openfeature.dev/v1alpha1 +# core.openfeature.dev/v1beta1 Resource Types: -- [FeatureFlagConfiguration](#featureflagconfiguration) +- [FeatureFlag](#featureflag) -- [FlagSourceConfiguration](#flagsourceconfiguration) +- [FeatureFlagSource](#featureflagsource) -## FeatureFlagConfiguration -[โ†ฉ Parent](#coreopenfeaturedevv1alpha1 ) +## FeatureFlag +[โ†ฉ Parent](#coreopenfeaturedevv1beta1 ) -FeatureFlagConfiguration is the Schema for the featureflagconfigurations API +FeatureFlag is the Schema for the featureflags API @@ -39,13 +37,13 @@ FeatureFlagConfiguration is the Schema for the featureflagconfigurations API - + - + @@ -54,29 +52,29 @@ FeatureFlagConfiguration is the Schema for the featureflagconfigurations API - +
apiVersion stringcore.openfeature.dev/v1alpha1core.openfeature.dev/v1beta1 true
kind stringFeatureFlagConfigurationFeatureFlag true
Refer to the Kubernetes API documentation for the fields of the `metadata` field. true
specspec object - FeatureFlagConfigurationSpec defines the desired state of FeatureFlagConfiguration
+ FeatureFlagSpec defines the desired state of FeatureFlag
false
status object - FeatureFlagConfigurationStatus defines the observed state of FeatureFlagConfiguration
+ FeatureFlagStatus defines the observed state of FeatureFlag
false
-### FeatureFlagConfiguration.spec -[โ†ฉ Parent](#featureflagconfiguration) +### FeatureFlag.spec +[โ†ฉ Parent](#featureflag) -FeatureFlagConfigurationSpec defines the desired state of FeatureFlagConfiguration +FeatureFlagSpec defines the desired state of FeatureFlag @@ -88,1657 +86,22 @@ FeatureFlagConfigurationSpec defines the desired state of FeatureFlagConfigurati - - - - - - - - - - - - - - - - - - - - -
featureFlagSpecstring - FeatureFlagSpec is the json representation of the feature flag
-
false
flagDSpecobject - FlagDSpec [DEPRECATED]: superseded by FlagSourceConfiguration
-
false
serviceProviderobject - ServiceProvider [DEPRECATED]: superseded by FlagSourceConfiguration
-
false
syncProviderobject - SyncProvider [DEPRECATED]: superseded by FlagSourceConfiguration
-
false
- - -### FeatureFlagConfiguration.spec.flagDSpec -[โ†ฉ Parent](#featureflagconfigurationspec) - - - -FlagDSpec [DEPRECATED]: superseded by FlagSourceConfiguration - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
envs[]object -
-
false
metricsPortinteger -
-
- Format: int32
-
false
- - -### FeatureFlagConfiguration.spec.flagDSpec.envs[index] -[โ†ฉ Parent](#featureflagconfigurationspecflagdspec) - - - -EnvVar represents an environment variable present in a Container. - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
namestring - Name of the environment variable. Must be a C_IDENTIFIER.
-
true
valuestring - Variable references $(VAR_NAME) are expanded using the previously defined environment variables in the container and any service environment variables. If a variable cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". Escaped references will never be expanded, regardless of whether the variable exists or not. Defaults to "".
-
false
valueFromobject - Source for the environment variable's value. Cannot be used if value is not empty.
-
false
- - -### FeatureFlagConfiguration.spec.flagDSpec.envs[index].valueFrom -[โ†ฉ Parent](#featureflagconfigurationspecflagdspecenvsindex) - - - -Source for the environment variable's value. Cannot be used if value is not empty. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
configMapKeyRefobject - Selects a key of a ConfigMap.
-
false
fieldRefobject - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`, spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs.
-
false
resourceFieldRefobject - Selects a resource of the container: only resources limits and requests (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported.
-
false
secretKeyRefobject - Selects a key of a secret in the pod's namespace
-
false
- - -### FeatureFlagConfiguration.spec.flagDSpec.envs[index].valueFrom.configMapKeyRef -[โ†ฉ Parent](#featureflagconfigurationspecflagdspecenvsindexvaluefrom) - - - -Selects a key of a ConfigMap. - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
keystring - The key to select.
-
true
namestring - Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add other useful fields. apiVersion, kind, uid?
-
false
optionalboolean - Specify whether the ConfigMap or its key must be defined
-
false
- - -### FeatureFlagConfiguration.spec.flagDSpec.envs[index].valueFrom.fieldRef -[โ†ฉ Parent](#featureflagconfigurationspecflagdspecenvsindexvaluefrom) - - - -Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`, spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
fieldPathstring - Path of the field to select in the specified API version.
-
true
apiVersionstring - Version of the schema the FieldPath is written in terms of, defaults to "v1".
-
false
- - -### FeatureFlagConfiguration.spec.flagDSpec.envs[index].valueFrom.resourceFieldRef -[โ†ฉ Parent](#featureflagconfigurationspecflagdspecenvsindexvaluefrom) - - - -Selects a resource of the container: only resources limits and requests (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
resourcestring - Required: resource to select
-
true
containerNamestring - Container name: required for volumes, optional for env vars
-
false
divisorint or string - Specifies the output format of the exposed resources, defaults to "1"
-
false
- - -### FeatureFlagConfiguration.spec.flagDSpec.envs[index].valueFrom.secretKeyRef -[โ†ฉ Parent](#featureflagconfigurationspecflagdspecenvsindexvaluefrom) - - - -Selects a key of a secret in the pod's namespace - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
keystring - The key of the secret to select from. Must be a valid secret key.
-
true
namestring - Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add other useful fields. apiVersion, kind, uid?
-
false
optionalboolean - Specify whether the Secret or its key must be defined
-
false
- - -### FeatureFlagConfiguration.spec.serviceProvider -[โ†ฉ Parent](#featureflagconfigurationspec) - - - -ServiceProvider [DEPRECATED]: superseded by FlagSourceConfiguration - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
nameenum -
-
- Enum: flagd
-
true
credentialsobject - ObjectReference contains enough information to let you inspect or modify the referred object. --- New uses of this type are discouraged because of difficulty describing its usage when embedded in APIs. 1. Ignored fields. It includes many fields which are not generally honored. For instance, ResourceVersion and FieldPath are both very rarely valid in actual usage. 2. Invalid usage help. It is impossible to add specific help for individual usage. In most embedded usages, there are particular restrictions like, "must refer only to types A and B" or "UID not honored" or "name must be restricted". Those cannot be well described when embedded. 3. Inconsistent validation. Because the usages are different, the validation rules are different by usage, which makes it hard for users to predict what will happen. 4. The fields are both imprecise and overly precise. Kind is not a precise mapping to a URL. This can produce ambiguity during interpretation and require a REST mapping. In most cases, the dependency is on the group,resource tuple and the version of the actual struct is irrelevant. 5. We cannot easily change it. Because this type is embedded in many locations, updates to this type will affect numerous schemas. Don't make new APIs embed an underspecified API type they do not control. - Instead of using this type, create a locally provided and used type that is well-focused on your reference. For example, ServiceReferences for admission registration: https://github.com/kubernetes/api/blob/release-1.17/admissionregistration/v1/types.go#L533 .
-
false
- - -### FeatureFlagConfiguration.spec.serviceProvider.credentials -[โ†ฉ Parent](#featureflagconfigurationspecserviceprovider) - - - -ObjectReference contains enough information to let you inspect or modify the referred object. --- New uses of this type are discouraged because of difficulty describing its usage when embedded in APIs. 1. Ignored fields. It includes many fields which are not generally honored. For instance, ResourceVersion and FieldPath are both very rarely valid in actual usage. 2. Invalid usage help. It is impossible to add specific help for individual usage. In most embedded usages, there are particular restrictions like, "must refer only to types A and B" or "UID not honored" or "name must be restricted". Those cannot be well described when embedded. 3. Inconsistent validation. Because the usages are different, the validation rules are different by usage, which makes it hard for users to predict what will happen. 4. The fields are both imprecise and overly precise. Kind is not a precise mapping to a URL. This can produce ambiguity during interpretation and require a REST mapping. In most cases, the dependency is on the group,resource tuple and the version of the actual struct is irrelevant. 5. We cannot easily change it. Because this type is embedded in many locations, updates to this type will affect numerous schemas. Don't make new APIs embed an underspecified API type they do not control. - Instead of using this type, create a locally provided and used type that is well-focused on your reference. For example, ServiceReferences for admission registration: https://github.com/kubernetes/api/blob/release-1.17/admissionregistration/v1/types.go#L533 . - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
apiVersionstring - API version of the referent.
-
false
fieldPathstring - If referring to a piece of an object instead of an entire object, this string should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. For example, if the object reference is to a container within a pod, this would take on a value like: "spec.containers{name}" (where "name" refers to the name of the container that triggered the event) or if no container name is specified "spec.containers[2]" (container with index 2 in this pod). This syntax is chosen only to have some well-defined way of referencing a part of an object. TODO: this design is not final and this field is subject to change in the future.
-
false
kindstring - Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
-
false
namestring - Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
-
false
namespacestring - Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/
-
false
resourceVersionstring - Specific resourceVersion to which this reference is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency
-
false
uidstring - UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids
-
false
- - -### FeatureFlagConfiguration.spec.syncProvider -[โ†ฉ Parent](#featureflagconfigurationspec) - - - -SyncProvider [DEPRECATED]: superseded by FlagSourceConfiguration - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
namestring -
-
true
httpSyncConfigurationobject - HttpSyncConfiguration defines the desired configuration for a http sync
-
false
- - -### FeatureFlagConfiguration.spec.syncProvider.httpSyncConfiguration -[โ†ฉ Parent](#featureflagconfigurationspecsyncprovider) - - - -HttpSyncConfiguration defines the desired configuration for a http sync - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
targetstring - Target is the target url for flagd to poll
-
true
bearerTokenstring -
-
false
- -## FlagSourceConfiguration -[โ†ฉ Parent](#coreopenfeaturedevv1alpha1 ) - - - - - - -FlagSourceConfiguration is the Schema for the FlagSourceConfigurations API - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
apiVersionstringcore.openfeature.dev/v1alpha1true
kindstringFlagSourceConfigurationtrue
metadataobjectRefer to the Kubernetes API documentation for the fields of the `metadata` field.true
specobject - FlagSourceConfigurationSpec defines the desired state of FlagSourceConfiguration
-
false
statusobject - FlagSourceConfigurationStatus defines the observed state of FlagSourceConfiguration
-
false
- - -### FlagSourceConfiguration.spec -[โ†ฉ Parent](#flagsourceconfiguration) - - - -FlagSourceConfigurationSpec defines the desired state of FlagSourceConfiguration - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
sources[]object - Sources defines the syncProviders and associated configuration to be applied to the sidecar
-
true
debugLoggingboolean - DebugLogging defines whether to enable --debug flag of flagd sidecar. Default false (disabled).
-
false
defaultSyncProviderstring - DefaultSyncProvider defines the default sync provider
-
false
envVarPrefixstring - EnvVarPrefix defines the prefix to be applied to all environment variables applied to the sidecar, default FLAGD
-
false
envVars[]object - EnvVars define the env vars to be applied to the sidecar, any env vars in FeatureFlagConfiguration CRs are added at the lowest index, all values will have the EnvVarPrefix applied
-
false
evaluatorstring - Evaluator sets an evaluator, defaults to 'json'
-
false
imagestring - Image allows for the sidecar image to be overridden, defaults to 'ghcr.io/open-feature/flagd'
-
false
logFormatstring - LogFormat allows for the sidecar log format to be overridden, defaults to 'json'
-
false
metricsPortinteger - MetricsPort defines the port to serve metrics on, defaults to 8014
-
- Format: int32
-
false
otelCollectorUristring - OtelCollectorUri defines whether to enable --otel-collector-uri flag of flagd sidecar. Default false (disabled).
-
false
portinteger - Port defines the port to listen on, defaults to 8013
-
- Format: int32
-
false
probesEnabledboolean - ProbesEnabled defines whether to enable liveness and readiness probes of flagd sidecar. Default true (enabled).
-
false
resourcesobject - Resources defines flagd sidecar resources. Default to operator sidecar-cpu-limit and sidecar-ram-limit flags.
-
false
rolloutOnChangeboolean - RolloutOnChange dictates whether annotated deployments will be restarted when configuration changes are detected in this CR, defaults to false
-
false
socketPathstring - SocketPath defines the unix socket path to listen on
-
false
syncProviderArgs[]string - SyncProviderArgs are string arguments passed to all sync providers, defined as key values separated by =
-
false
tagstring - Tag to be appended to the sidecar image, defaults to 'main'
-
false
- - -### FlagSourceConfiguration.spec.sources[index] -[โ†ฉ Parent](#flagsourceconfigurationspec) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
sourcestring - Source is a URI of the flag sources
-
true
certPathstring - CertPath is a path of a certificate to be used by grpc TLS connection
-
false
httpSyncBearerTokenstring - HttpSyncBearerToken is a bearer token. Used by http(s) sync provider only
-
false
providerstring - Provider type - kubernetes, http, grpc or filepath
-
false
providerIDstring - ProviderID is an identifier to be used in grpc provider
-
false
selectorstring - Selector is a flag configuration selector used by grpc provider
-
false
tlsboolean - TLS - Enable/Disable secure TLS connectivity. Currently used only by GRPC sync
-
false
- - -### FlagSourceConfiguration.spec.envVars[index] -[โ†ฉ Parent](#flagsourceconfigurationspec) - - - -EnvVar represents an environment variable present in a Container. - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
namestring - Name of the environment variable. Must be a C_IDENTIFIER.
-
true
valuestring - Variable references $(VAR_NAME) are expanded using the previously defined environment variables in the container and any service environment variables. If a variable cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". Escaped references will never be expanded, regardless of whether the variable exists or not. Defaults to "".
-
false
valueFromobject - Source for the environment variable's value. Cannot be used if value is not empty.
-
false
- - -### FlagSourceConfiguration.spec.envVars[index].valueFrom -[โ†ฉ Parent](#flagsourceconfigurationspecenvvarsindex) - - - -Source for the environment variable's value. Cannot be used if value is not empty. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
configMapKeyRefobject - Selects a key of a ConfigMap.
-
false
fieldRefobject - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`, spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs.
-
false
resourceFieldRefobject - Selects a resource of the container: only resources limits and requests (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported.
-
false
secretKeyRefobject - Selects a key of a secret in the pod's namespace
-
false
- - -### FlagSourceConfiguration.spec.envVars[index].valueFrom.configMapKeyRef -[โ†ฉ Parent](#flagsourceconfigurationspecenvvarsindexvaluefrom) - - - -Selects a key of a ConfigMap. - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
keystring - The key to select.
-
true
namestring - Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add other useful fields. apiVersion, kind, uid?
-
false
optionalboolean - Specify whether the ConfigMap or its key must be defined
-
false
- - -### FlagSourceConfiguration.spec.envVars[index].valueFrom.fieldRef -[โ†ฉ Parent](#flagsourceconfigurationspecenvvarsindexvaluefrom) - - - -Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`, spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
fieldPathstring - Path of the field to select in the specified API version.
-
true
apiVersionstring - Version of the schema the FieldPath is written in terms of, defaults to "v1".
-
false
- - -### FlagSourceConfiguration.spec.envVars[index].valueFrom.resourceFieldRef -[โ†ฉ Parent](#flagsourceconfigurationspecenvvarsindexvaluefrom) - - - -Selects a resource of the container: only resources limits and requests (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
resourcestring - Required: resource to select
-
true
containerNamestring - Container name: required for volumes, optional for env vars
-
false
divisorint or string - Specifies the output format of the exposed resources, defaults to "1"
-
false
- - -### FlagSourceConfiguration.spec.envVars[index].valueFrom.secretKeyRef -[โ†ฉ Parent](#flagsourceconfigurationspecenvvarsindexvaluefrom) - - - -Selects a key of a secret in the pod's namespace - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
keystring - The key of the secret to select from. Must be a valid secret key.
-
true
namestring - Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add other useful fields. apiVersion, kind, uid?
-
false
optionalboolean - Specify whether the Secret or its key must be defined
-
false
- - -### FlagSourceConfiguration.spec.resources -[โ†ฉ Parent](#flagsourceconfigurationspec) - - - -Resources defines flagd sidecar resources. Default to operator sidecar-cpu-limit and sidecar-ram-limit flags. - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
claims[]object - Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container. - This is an alpha field and requires enabling the DynamicResourceAllocation feature gate. - This field is immutable. It can only be set for containers.
-
false
limitsmap[string]int or string - Limits describes the maximum amount of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
-
false
requestsmap[string]int or string - Requests describes the minimum amount of compute resources required. If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, otherwise to an implementation-defined value. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
-
false
- - -### FlagSourceConfiguration.spec.resources.claims[index] -[โ†ฉ Parent](#flagsourceconfigurationspecresources) - - - -ResourceClaim references one entry in PodSpec.ResourceClaims. - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
namestring - Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. It makes that resource available inside a container.
-
true
- -# core.openfeature.dev/v1alpha2 - -Resource Types: - -- [FeatureFlagConfiguration](#featureflagconfiguration) - -- [FlagSourceConfiguration](#flagsourceconfiguration) - - - - -## FeatureFlagConfiguration -[โ†ฉ Parent](#coreopenfeaturedevv1alpha2 ) - - - - - - -FeatureFlagConfiguration is the Schema for the featureflagconfigurations API - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
apiVersionstringcore.openfeature.dev/v1alpha2true
kindstringFeatureFlagConfigurationtrue
metadataobjectRefer to the Kubernetes API documentation for the fields of the `metadata` field.true
specobject - FeatureFlagConfigurationSpec defines the desired state of FeatureFlagConfiguration
-
false
statusobject - FeatureFlagConfigurationStatus defines the observed state of FeatureFlagConfiguration
-
false
- - -### FeatureFlagConfiguration.spec -[โ†ฉ Parent](#featureflagconfiguration-1) - - - -FeatureFlagConfigurationSpec defines the desired state of FeatureFlagConfiguration - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
featureFlagSpecobject - FeatureFlagSpec is the structured representation of the feature flag specification
-
false
flagDSpecobject - FlagDSpec [DEPRECATED]: superseded by FlagSourceConfiguration
-
false
serviceProviderobject - ServiceProvider [DEPRECATED]: superseded by FlagSourceConfiguration
-
false
syncProviderobject - SyncProvider [DEPRECATED]: superseded by FlagSourceConfiguration
-
false
- - -### FeatureFlagConfiguration.spec.featureFlagSpec -[โ†ฉ Parent](#featureflagconfigurationspec-1) - - - -FeatureFlagSpec is the structured representation of the feature flag specification - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
flagsmap[string]object -
-
true
$evaluatorsobject -
-
false
- - -### FeatureFlagConfiguration.spec.featureFlagSpec.flags[key] -[โ†ฉ Parent](#featureflagconfigurationspecfeatureflagspec) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
defaultVariantstring -
-
true
stateenum -
-
- Enum: ENABLED, DISABLED
-
true
variantsobject -
-
true
targetingobject - Targeting is the json targeting rule
-
false
- - -### FeatureFlagConfiguration.spec.flagDSpec -[โ†ฉ Parent](#featureflagconfigurationspec-1) - - - -FlagDSpec [DEPRECATED]: superseded by FlagSourceConfiguration - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
envs[]object -
-
false
- - -### FeatureFlagConfiguration.spec.flagDSpec.envs[index] -[โ†ฉ Parent](#featureflagconfigurationspecflagdspec-1) - - - -EnvVar represents an environment variable present in a Container. - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
namestring - Name of the environment variable. Must be a C_IDENTIFIER.
-
true
valuestring - Variable references $(VAR_NAME) are expanded using the previously defined environment variables in the container and any service environment variables. If a variable cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". Escaped references will never be expanded, regardless of whether the variable exists or not. Defaults to "".
-
false
valueFromobject - Source for the environment variable's value. Cannot be used if value is not empty.
-
false
- - -### FeatureFlagConfiguration.spec.flagDSpec.envs[index].valueFrom -[โ†ฉ Parent](#featureflagconfigurationspecflagdspecenvsindex-1) - - - -Source for the environment variable's value. Cannot be used if value is not empty. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
configMapKeyRefobject - Selects a key of a ConfigMap.
-
false
fieldRefobject - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`, spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs.
-
false
resourceFieldRefobject - Selects a resource of the container: only resources limits and requests (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported.
-
false
secretKeyRefobject - Selects a key of a secret in the pod's namespace
-
false
- - -### FeatureFlagConfiguration.spec.flagDSpec.envs[index].valueFrom.configMapKeyRef -[โ†ฉ Parent](#featureflagconfigurationspecflagdspecenvsindexvaluefrom-1) - - - -Selects a key of a ConfigMap. - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
keystring - The key to select.
-
true
namestring - Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add other useful fields. apiVersion, kind, uid?
-
false
optionalboolean - Specify whether the ConfigMap or its key must be defined
-
false
- - -### FeatureFlagConfiguration.spec.flagDSpec.envs[index].valueFrom.fieldRef -[โ†ฉ Parent](#featureflagconfigurationspecflagdspecenvsindexvaluefrom-1) - - - -Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`, spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
fieldPathstring - Path of the field to select in the specified API version.
-
true
apiVersionstring - Version of the schema the FieldPath is written in terms of, defaults to "v1".
-
false
- - -### FeatureFlagConfiguration.spec.flagDSpec.envs[index].valueFrom.resourceFieldRef -[โ†ฉ Parent](#featureflagconfigurationspecflagdspecenvsindexvaluefrom-1) - - - -Selects a resource of the container: only resources limits and requests (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
resourcestring - Required: resource to select
-
true
containerNamestring - Container name: required for volumes, optional for env vars
-
false
divisorint or string - Specifies the output format of the exposed resources, defaults to "1"
-
false
- - -### FeatureFlagConfiguration.spec.flagDSpec.envs[index].valueFrom.secretKeyRef -[โ†ฉ Parent](#featureflagconfigurationspecflagdspecenvsindexvaluefrom-1) - - - -Selects a key of a secret in the pod's namespace - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
keystring - The key of the secret to select from. Must be a valid secret key.
-
true
namestring - Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add other useful fields. apiVersion, kind, uid?
-
false
optionalboolean - Specify whether the Secret or its key must be defined
-
false
- - -### FeatureFlagConfiguration.spec.serviceProvider -[โ†ฉ Parent](#featureflagconfigurationspec-1) - - - -ServiceProvider [DEPRECATED]: superseded by FlagSourceConfiguration - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
nameenum -
-
- Enum: flagd
-
true
credentialsobject - ObjectReference contains enough information to let you inspect or modify the referred object. --- New uses of this type are discouraged because of difficulty describing its usage when embedded in APIs. 1. Ignored fields. It includes many fields which are not generally honored. For instance, ResourceVersion and FieldPath are both very rarely valid in actual usage. 2. Invalid usage help. It is impossible to add specific help for individual usage. In most embedded usages, there are particular restrictions like, "must refer only to types A and B" or "UID not honored" or "name must be restricted". Those cannot be well described when embedded. 3. Inconsistent validation. Because the usages are different, the validation rules are different by usage, which makes it hard for users to predict what will happen. 4. The fields are both imprecise and overly precise. Kind is not a precise mapping to a URL. This can produce ambiguity during interpretation and require a REST mapping. In most cases, the dependency is on the group,resource tuple and the version of the actual struct is irrelevant. 5. We cannot easily change it. Because this type is embedded in many locations, updates to this type will affect numerous schemas. Don't make new APIs embed an underspecified API type they do not control. - Instead of using this type, create a locally provided and used type that is well-focused on your reference. For example, ServiceReferences for admission registration: https://github.com/kubernetes/api/blob/release-1.17/admissionregistration/v1/types.go#L533 .
-
false
- - -### FeatureFlagConfiguration.spec.serviceProvider.credentials -[โ†ฉ Parent](#featureflagconfigurationspecserviceprovider-1) - - - -ObjectReference contains enough information to let you inspect or modify the referred object. --- New uses of this type are discouraged because of difficulty describing its usage when embedded in APIs. 1. Ignored fields. It includes many fields which are not generally honored. For instance, ResourceVersion and FieldPath are both very rarely valid in actual usage. 2. Invalid usage help. It is impossible to add specific help for individual usage. In most embedded usages, there are particular restrictions like, "must refer only to types A and B" or "UID not honored" or "name must be restricted". Those cannot be well described when embedded. 3. Inconsistent validation. Because the usages are different, the validation rules are different by usage, which makes it hard for users to predict what will happen. 4. The fields are both imprecise and overly precise. Kind is not a precise mapping to a URL. This can produce ambiguity during interpretation and require a REST mapping. In most cases, the dependency is on the group,resource tuple and the version of the actual struct is irrelevant. 5. We cannot easily change it. Because this type is embedded in many locations, updates to this type will affect numerous schemas. Don't make new APIs embed an underspecified API type they do not control. - Instead of using this type, create a locally provided and used type that is well-focused on your reference. For example, ServiceReferences for admission registration: https://github.com/kubernetes/api/blob/release-1.17/admissionregistration/v1/types.go#L533 . - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +
NameTypeDescriptionRequired
apiVersionstring - API version of the referent.
-
false
fieldPathstring - If referring to a piece of an object instead of an entire object, this string should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. For example, if the object reference is to a container within a pod, this would take on a value like: "spec.containers{name}" (where "name" refers to the name of the container that triggered the event) or if no container name is specified "spec.containers[2]" (container with index 2 in this pod). This syntax is chosen only to have some well-defined way of referencing a part of an object. TODO: this design is not final and this field is subject to change in the future.
-
false
kindstring - Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
-
false
namestring - Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
-
false
namespacestring - Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/
-
false
resourceVersionstring - Specific resourceVersion to which this reference is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency
-
false
uidstringflagSpecobject - UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids
+ FlagSpec is the structured representation of the feature flag specification
false
-### FeatureFlagConfiguration.spec.syncProvider -[โ†ฉ Parent](#featureflagconfigurationspec-1) +### FeatureFlag.spec.flagSpec +[โ†ฉ Parent](#featureflagspec) -SyncProvider [DEPRECATED]: superseded by FlagSourceConfiguration +FlagSpec is the structured representation of the feature flag specification @@ -1750,49 +113,15 @@ SyncProvider [DEPRECATED]: superseded by FlagSourceConfiguration - - + + - + - - - -
namestringflagsmap[string]object
true
httpSyncConfiguration$evaluators object - HttpSyncConfiguration defines the desired configuration for a http sync
-
false
- - -### FeatureFlagConfiguration.spec.syncProvider.httpSyncConfiguration -[โ†ฉ Parent](#featureflagconfigurationspecsyncprovider-1) - - - -HttpSyncConfiguration defines the desired configuration for a http sync - - - - - - - - - - - - - - - - - - @@ -1800,66 +129,13 @@ HttpSyncConfiguration defines the desired configuration for a http sync
NameTypeDescriptionRequired
targetstring - Target is the target url for flagd to poll
-
true
bearerTokenstring
-## FlagSourceConfiguration -[โ†ฉ Parent](#coreopenfeaturedevv1alpha2 ) - - - - - - -FlagSourceConfiguration is the Schema for the FlagSourceConfigurations API - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
apiVersionstringcore.openfeature.dev/v1alpha2true
kindstringFlagSourceConfigurationtrue
metadataobjectRefer to the Kubernetes API documentation for the fields of the `metadata` field.true
specobject - FlagSourceConfigurationSpec defines the desired state of FlagSourceConfiguration
-
false
statusobject - FlagSourceConfigurationStatus defines the observed state of FlagSourceConfiguration
-
false
+### FeatureFlag.spec.flagSpec.flags[key] +[โ†ฉ Parent](#featureflagspecflagspec) -### FlagSourceConfiguration.spec -[โ†ฉ Parent](#flagsourceconfiguration-1) -FlagSourceConfigurationSpec defines the desired state of FlagSourceConfiguration @@ -1871,107 +147,47 @@ FlagSourceConfigurationSpec defines the desired state of FlagSourceConfiguration - - - - - - - - - - - - - - - - + - - - - - - - - - - - + - - + + - - - - - - - - - - - + - - + + - + - - + +
defaultSyncProviderstring - DefaultSyncProvider defines the default sync provider
-
false
evaluatorstring - Evaluator sets an evaluator, defaults to 'json'
-
false
imagestring - Image allows for the sidecar image to be overridden, defaults to 'ghcr.io/open-feature/flagd'
-
false
logFormatdefaultVariant string - LogFormat allows for the sidecar log format to be overridden, defaults to 'json'
-
false
metricsPortinteger - MetricsPort defines the port to serve metrics on, defaults to 8013

- Format: int32
-
false
otelCollectorUristring - OtelCollectorUri defines whether to enable --otel-collector-uri flag of flagd sidecar. Default false (disabled).
falsetrue
portintegerstateenum - Port defines the port to listen on, defaults to 8014

- Format: int32
-
false
probesEnabledboolean - ProbesEnabled defines whether to enable liveness and readiness probes of flagd sidecar. Default true (enabled).
-
false
socketPathstring - SocketPath defines the unix socket path to listen on
+
+ Enum: ENABLED, DISABLED
falsetrue
syncProviderArgs[]stringvariantsobject - SyncProviderArgs are string arguments passed to all sync providers, defined as key values separated by =
+
falsetrue
tagstringtargetingobject - Tag to be appended to the sidecar image, defaults to 'main'
+ Targeting is the json targeting rule
false
-# core.openfeature.dev/v1alpha3 - -Resource Types: - -- [FlagSourceConfiguration](#flagsourceconfiguration) - - - +## FeatureFlagSource +[โ†ฉ Parent](#coreopenfeaturedevv1beta1 ) -## FlagSourceConfiguration -[โ†ฉ Parent](#coreopenfeaturedevv1alpha3 ) - -FlagSourceConfiguration is the Schema for the FlagSourceConfigurations API +FeatureFlagSource is the Schema for the FeatureFlagSources API @@ -1985,13 +201,13 @@ FlagSourceConfiguration is the Schema for the FlagSourceConfigurations API - + - + @@ -2000,29 +216,29 @@ FlagSourceConfiguration is the Schema for the FlagSourceConfigurations API - +
apiVersion stringcore.openfeature.dev/v1alpha3core.openfeature.dev/v1beta1 true
kind stringFlagSourceConfigurationFeatureFlagSource true
Refer to the Kubernetes API documentation for the fields of the `metadata` field. true
specspec object - FlagSourceConfigurationSpec defines the desired state of FlagSourceConfiguration
+ FeatureFlagSourceSpec defines the desired state of FeatureFlagSource
false
status object - FlagSourceConfigurationStatus defines the observed state of FlagSourceConfiguration
+ FeatureFlagSourceStatus defines the observed state of FeatureFlagSource
false
-### FlagSourceConfiguration.spec -[โ†ฉ Parent](#flagsourceconfiguration-1) +### FeatureFlagSource.spec +[โ†ฉ Parent](#featureflagsource) -FlagSourceConfigurationSpec defines the desired state of FlagSourceConfiguration +FeatureFlagSourceSpec defines the desired state of FeatureFlagSource @@ -2034,7 +250,7 @@ FlagSourceConfigurationSpec defines the desired state of FlagSourceConfiguration - + - + @@ -2075,13 +291,6 @@ FlagSourceConfigurationSpec defines the desired state of FlagSourceConfiguration Evaluator sets an evaluator, defaults to 'json'
- - - - - @@ -2090,10 +299,10 @@ FlagSourceConfigurationSpec defines the desired state of FlagSourceConfiguration - + @@ -2121,6 +330,13 @@ FlagSourceConfigurationSpec defines the desired state of FlagSourceConfiguration ProbesEnabled defines whether to enable liveness and readiness probes of flagd sidecar. Default true (enabled).
+ + + + + @@ -2142,19 +358,12 @@ FlagSourceConfigurationSpec defines the desired state of FlagSourceConfiguration SyncProviderArgs are string arguments passed to all sync providers, defined as key values separated by =
- - - - -
sourcessources []object SyncProviders define the syncProviders and associated configuration to be applied to the sidecar
@@ -2062,10 +278,10 @@ FlagSourceConfigurationSpec defines the desired state of FlagSourceConfiguration
false
envVarsenvVars []object - EnvVars define the env vars to be applied to the sidecar, any env vars in FeatureFlagConfiguration CRs are added at the lowest index, all values will have the EnvVarPrefix applied, default FLAGD
+ EnvVars define the env vars to be applied to the sidecar, any env vars in FeatureFlag CRs are added at the lowest index, all values will have the EnvVarPrefix applied, default FLAGD
false
false
imagestring - Image allows for the sidecar image to be overridden, defaults to 'ghcr.io/open-feature/flagd'
-
false
logFormat string false
metricsPortmanagementPort integer - MetricsPort defines the port to serve metrics on, defaults to 8014
+ ManagemetPort defines the port to serve management on, defaults to 8014

Format: int32
false
resourcesobject + Resources defines flagd sidecar resources. Default to operator sidecar-cpu-* and sidecar-ram-* flags.
+
false
rolloutOnChange boolean false
tagstring - Tag to be appended to the sidecar image, defaults to 'main'
-
false
-### FlagSourceConfiguration.spec.sources[index] -[โ†ฉ Parent](#flagsourceconfigurationspec-1) +### FeatureFlagSource.spec.sources[index] +[โ†ฉ Parent](#featureflagsourcespec) @@ -2194,7 +403,7 @@ FlagSourceConfigurationSpec defines the desired state of FlagSourceConfiguration provider string - Provider type - kubernetes, http(s), grpc(s) or filepath
+ Provider type - kubernetes, http(s), grpc(s) or file
false @@ -2222,8 +431,8 @@ FlagSourceConfigurationSpec defines the desired state of FlagSourceConfiguration -### FlagSourceConfiguration.spec.envVars[index] -[โ†ฉ Parent](#flagsourceconfigurationspec-1) +### FeatureFlagSource.spec.envVars[index] +[โ†ฉ Parent](#featureflagsourcespec) @@ -2253,7 +462,7 @@ EnvVar represents an environment variable present in a Container. false - valueFrom + valueFrom object Source for the environment variable's value. Cannot be used if value is not empty.
@@ -2263,8 +472,8 @@ EnvVar represents an environment variable present in a Container. -### FlagSourceConfiguration.spec.envVars[index].valueFrom -[โ†ฉ Parent](#flagsourceconfigurationspecenvvarsindex-1) +### FeatureFlagSource.spec.envVars[index].valueFrom +[โ†ฉ Parent](#featureflagsourcespecenvvarsindex) @@ -2280,28 +489,28 @@ Source for the environment variable's value. Cannot be used if value is not empt - configMapKeyRef + configMapKeyRef object Selects a key of a ConfigMap.
false - fieldRef + fieldRef object Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`, spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs.
false - resourceFieldRef + resourceFieldRef object Selects a resource of the container: only resources limits and requests (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported.
false - secretKeyRef + secretKeyRef object Selects a key of a secret in the pod's namespace
@@ -2311,8 +520,8 @@ Source for the environment variable's value. Cannot be used if value is not empt -### FlagSourceConfiguration.spec.envVars[index].valueFrom.configMapKeyRef -[โ†ฉ Parent](#flagsourceconfigurationspecenvvarsindexvaluefrom-1) +### FeatureFlagSource.spec.envVars[index].valueFrom.configMapKeyRef +[โ†ฉ Parent](#featureflagsourcespecenvvarsindexvaluefrom) @@ -2352,8 +561,8 @@ Selects a key of a ConfigMap. -### FlagSourceConfiguration.spec.envVars[index].valueFrom.fieldRef -[โ†ฉ Parent](#flagsourceconfigurationspecenvvarsindexvaluefrom-1) +### FeatureFlagSource.spec.envVars[index].valueFrom.fieldRef +[โ†ฉ Parent](#featureflagsourcespecenvvarsindexvaluefrom) @@ -2386,8 +595,8 @@ Selects a field of the pod: supports metadata.name, metadata.namespace, `metadat -### FlagSourceConfiguration.spec.envVars[index].valueFrom.resourceFieldRef -[โ†ฉ Parent](#flagsourceconfigurationspecenvvarsindexvaluefrom-1) +### FeatureFlagSource.spec.envVars[index].valueFrom.resourceFieldRef +[โ†ฉ Parent](#featureflagsourcespecenvvarsindexvaluefrom) @@ -2427,8 +636,8 @@ Selects a resource of the container: only resources limits and requests (limits. -### FlagSourceConfiguration.spec.envVars[index].valueFrom.secretKeyRef -[โ†ฉ Parent](#flagsourceconfigurationspecenvvarsindexvaluefrom-1) +### FeatureFlagSource.spec.envVars[index].valueFrom.secretKeyRef +[โ†ฉ Parent](#featureflagsourcespecenvvarsindexvaluefrom) @@ -2465,4 +674,74 @@ Selects a key of a secret in the pod's namespace false + + + +### FeatureFlagSource.spec.resources +[โ†ฉ Parent](#featureflagsourcespec) + + + +Resources defines flagd sidecar resources. Default to operator sidecar-cpu-* and sidecar-ram-* flags. + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
claims[]object + Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container. + This is an alpha field and requires enabling the DynamicResourceAllocation feature gate. + This field is immutable. It can only be set for containers.
+
false
limitsmap[string]int or string + Limits describes the maximum amount of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
+
false
requestsmap[string]int or string + Requests describes the minimum amount of compute resources required. If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, otherwise to an implementation-defined value. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
+
false
+ + +### FeatureFlagSource.spec.resources.claims[index] +[โ†ฉ Parent](#featureflagsourcespecresources) + + + +ResourceClaim references one entry in PodSpec.ResourceClaims. + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
namestring + Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. It makes that resource available inside a container.
+
true
\ No newline at end of file diff --git a/docs/development_notes.md b/docs/development_notes.md index 01974aa83..77f2957bc 100644 --- a/docs/development_notes.md +++ b/docs/development_notes.md @@ -13,7 +13,7 @@ Run `make test` to run the test suite. The controller integration tests use [env This provides means of asserting that the Kubernetes components reach the desired state without the overhead of using an actual cluster, keeping test runtime and resource consumption down. -An e2e test suite can also be found in the [`/test/e2e`](../test/e2e/DEVELOPER.md) directory. These tests are run as part of the `pr-lint` github action, they work by deploying an nginx reverse proxy and asserting that curls to the proxy elicit expected behaviour from the flagd sidecar created by open-feature-operator. +An e2e test suite can also be found in the [`/test/e2e`](../test/e2e/README.md) directory. These tests are run as part of the `pr-checks` github action, they work by deploying an nginx reverse proxy and asserting that curls to the proxy elicit expected behaviour from the flagd sidecar created by open-feature-operator. ## Releases diff --git a/docs/feature_flag_configuration.md b/docs/feature_flag.md similarity index 59% rename from docs/feature_flag_configuration.md rename to docs/feature_flag.md index 958db0d35..780703514 100644 --- a/docs/feature_flag_configuration.md +++ b/docs/feature_flag.md @@ -1,14 +1,14 @@ -# Feature Flag Configuration +# Feature Flag -The `FeatureFlagConfiguration` version `v1alpha2` CRD defines a CR with the following example structure: +The `FeatureFlag` version `v1beta1` CRD defines a CR with the following example structure: ```yaml -apiVersion: core.openfeature.dev/v1alpha2 -kind: FeatureFlagConfiguration +apiVersion: core.openfeature.dev/v1beta1 +kind: FeatureFlag metadata: - name: featureflagconfiguration-sample + name: featureflag-sample spec: - featureFlagSpec: + flagSpec: flags: foo: state: "ENABLED" @@ -22,7 +22,7 @@ In the example above, we have defined a `String` type feature flag named `foo` a It has variants of `bar` and `baz`, referring to respected values of `BAR` and `BAZ`. The default variant is set to`bar`. -## featureFlagSpec +## flagSpec -The `featureFlagSpec` is an object representing the flag configurations themselves. +The `flagSpec` is an object representing the flag definitions themselves. The documentation for this object can be found [here](https://github.com/open-feature/flagd/blob/main/docs/configuration/flag_configuration.md). diff --git a/docs/flag_source_configuration.md b/docs/feature_flag_source.md similarity index 61% rename from docs/flag_source_configuration.md rename to docs/feature_flag_source.md index a09f81ba7..2cfd0a5cb 100644 --- a/docs/flag_source_configuration.md +++ b/docs/feature_flag_source.md @@ -1,19 +1,19 @@ # Flag Source configuration -The injected sidecar is configured using the `FlagSourceConfiguration` custom resource definition. -The `openfeature.dev/flagsourceconfiguration` annotation is used to assign Pods with their respective`FlagSourceConfiguration` custom resources. +The injected sidecar is configured using the `FeatureFlagSource` custom resource definition. +The `openfeature.dev/featureflagsource` annotation is used to assign Pods with their respective `FeatureFlagSource` custom resources. -A minimal example of a `FlagSourceConfiguration` is given below, +A minimal example of a `FeatureFlagSource` is given below, ```yaml -apiVersion: core.openfeature.dev/v1alpha3 -kind: FlagSourceConfiguration +apiVersion: core.openfeature.dev/v1beta1 +kind: FeatureFlagSource metadata: - name: flag-source-configuration + name: feature-flag-source spec: sources: # flag sources for the injected flagd - - source: flags/sample-flags # FlagSourceConfiguration - namespace/name - provider: kubernetes # kubernetes flag source backed by FlagSourceConfiguration custom resource + - source: flags/sample-flags # FeatureFlag - namespace/name + provider: kubernetes # kubernetes flag source backed by FeatureFlag custom resource port: 8080 # port of the flagd sidecar ``` @@ -21,37 +21,40 @@ spec: This section explains how to configure feature flag sources to injected flag sidecar. -`FlagSourceConfiguration` support multiple flag sources. Sources are configured as a list and given below are supported sources and their configurations, +`FeatureFlagSource` support multiple flag sources. Sources are configured as a list. +Supported sources and their configurations are listed below. -### kubernetes aka `FeatureFlagConfiguration` +### kubernetes aka `FeatureFlag` -This is `FeatureFlagConfiguration` custom resource backed flagd feature flag definition. -Read more on the custom resource at the dedicated documentation of [FeatureFlagConfiguration](./feature_flag_configuration.md) +This is `FeatureFlag` custom resource backed flagd feature flag definition. +Read more about the custom resource at the dedicated documentation of [FeatureFlag](./feature_flag.md) -To refer this custom resource in `FlagSourceConfiguration`, provider type `kubernetes` is used as below example, +The following example of a `FeatureFlagSource` uses `kubernetes` as the `provider` type: ```yaml sources: - - source: flags/sample-flags # FeatureFlagConfiguration - namespace/custom_resource_name - provider: kubernetes # kubernetes flag source backed by FeatureFlagConfiguration custom resource + - source: flags/sample-flags # FeatureFlag - namespace/custom_resource_name + provider: kubernetes # kubernetes flag source backed by FeatureFlag custom resource ``` ### flagd-proxy -`flagd-proxy` is an alternative to direct resource access on `FeatureFlagConfiguration` custom resources. +`flagd-proxy` is an alternative to direct resource access on `FeatureFlag` custom resources. This source type is useful when there is a need for restricting workload permissions and/or to reduce k8s API load. Read more about proxy approach to access kubernetes resources: [flagd-proxy](./flagd_proxy.md) -### filepath +### file -Injected sidecar can use volume mounted files as flag sources. -For this, provider type `filepath` is used as below example, +In this mode, `FeatureFlag` custom resources are volume mounted to the injected flagd sidecar. +flagd then source flag configurations from this volume. + +For example, given `FeatureFlag` exist at `flags/sample-flags`, this source configuration look like below, ```yaml sources: - - source: /etc/flagd/config.json - provider: filepath + - source: flags/sample-flags + provider: file ``` ### http @@ -81,7 +84,7 @@ sources: ## Sidecar configurations -`FlagSourceConfiguration` further allows to provide configurations to the injected flagd sidecar. +`FeatureFlagSource` provides configurations to the injected flagd sidecar. Table given below is non-exhaustive list of overriding options, | Configuration | Explanation | Default | @@ -105,24 +108,26 @@ If no namespace is provided, it is assumed that the CR is within the same namesp namespace: test-ns annotations: openfeature.dev/enabled: "true" - openfeature.dev/flagsourceconfiguration: "config-A, test-ns-2/config-B" + openfeature.dev/featureflagsource: "config-A, test-ns-2/config-B" ``` In this example, 2 CRs are being used to configure the injected container (by default the operator uses the `flagd:main` image), `config-A` (which is assumed to be in the namespace `test-ns`) and `config-B` from the `test-ns-2` namespace, with `config-B` taking precedence in the configuration merge. -The `FlagSourceConfiguration` version `v1alpha3` CRD defines a CR with the following example structure, the documentation for this CRD can be found [here](crds.md#flagsourceconfiguration): +The `FeatureFlagSource` version `v1beta1` CRD defines a CR with the following example structure. +The documentation for this CRD can be found +[here](crds.md#featureflagsource): ```yaml -apiVersion: core.openfeature.dev/v1alpha3 -kind: FlagSourceConfiguration +apiVersion: core.openfeature.dev/v1beta1 +kind: FeatureFlagSource metadata: - name: flag-source-sample + name: feature-flag-source-sample spec: metricsPort: 8080 port: 80 evaluator: json image: my-custom-sidecar-image - defaultSyncProvider: filepath + defaultSyncProvider: file tag: main sources: - source: namespace/name @@ -145,16 +150,16 @@ spec: memory: 256Mi ``` -The relevant `FlagSourceConfigurations` are passed to the operator by setting the `openfeature.dev/flagsourceconfiguration` annotation, and is responsible for providing the full configuration of the injected sidecar. +The relevant `FeatureFlagSources` are passed to the operator by setting the `openfeature.dev/featureflagsource` annotation, which provides the full configuration of the injected sidecar. ## Configuration Merging -When multiple `FlagSourceConfigurations` are provided, the configurations are merged. The last `CR` takes precedence over the first, with any configuration from the deprecated `FlagDSpec` field of the `FeatureFlagConfiguration` CRD taking the lowest priority. +When multiple `FeatureFlagSources` are provided, the configurations are merged. The last `CR` takes precedence over the first. ```mermaid flowchart LR - FlagSourceConfiguration-values -->|highest priority| environment-variables -->|lowest priority| defaults + FeatureFlagSource-values -->|highest priority| environment-variables -->|lowest priority| defaults ``` @@ -163,12 +168,12 @@ An example of this behavior: metadata: annotations: openfeature.dev/enabled: "true" - openfeature.dev/flagsourceconfiguration:"config-A, config-B" + openfeature.dev/featureflagsource:"config-A, config-B" ``` Config-A: ``` -apiVersion: core.openfeature.dev/v1alpha2 -kind: FlagSourceConfiguration +apiVersion: core.openfeature.dev/v1beta1 +kind: FeatureFlagSource metadata: name: config-A spec: @@ -177,8 +182,8 @@ spec: ``` Config-B: ``` -apiVersion: core.openfeature.dev/v1alpha2 -kind: FlagSourceConfiguration +apiVersion: core.openfeature.dev/v1beta1 +kind: FeatureFlagSource metadata: name: config-B spec: diff --git a/docs/flagd_proxy.md b/docs/flagd_proxy.md index b7a85bc54..2bca89005 100644 --- a/docs/flagd_proxy.md +++ b/docs/flagd_proxy.md @@ -2,13 +2,13 @@ > The flagd kube proxy is currently in an experimental state -The `flagd-proxy` is a pub/sub for mechanism watching configuration changes in `FeatureFlagConfiguration` CRs. +The `flagd-proxy` is a pub/sub for mechanism watching configuration changes in `FeatureFlag` CRs. This source type avoids the need for additional cluster wide permissions in the workload pod, and reduces load on the k8s API. -In order for a pod to have the required permissions to watch a `FeatureFlagConfiguration` CR in the default implementation, it must have its service account appended to the `flagd-kubernetes-sync` role binding, the details for this role can be found [here](./permissions.md). +In order for a pod to have the required permissions to watch a `FeatureFlag` CR in the default implementation, it must have its service account appended to the `flagd-kubernetes-sync` role binding, the details for this role can be found [here](./permissions.md). In some use cases this may not be favorable, in these scenarios the alternative `flagd-proxy` implementation may be used. The `flagd-proxy` bypasses the widespread permissions issue by acting as the single source of truth for subscribed flagd instances, broadcasting configuration changes to all subscribed pods via gRPC streams. -For each requested `FeatureFlagConfiguration` a new ISync implementation is started, and closed once there are no longer any listeners. +For each requested `FeatureFlag` a new ISync implementation is started, and closed once there are no longer any listeners. This results in only one set of resources requiring the `flagd-kubernetes-sync` permissions, tightening the restrictions on all other pods. ## Architecture @@ -19,15 +19,15 @@ The diagram below describes the high level architecture and implementation of th

-The `flagd-proxy` is only deployed once the reconcile loop for a `FlagSourceConfiguration` is run with a CR containing the provider `"flagd-proxy"` in its source array. +The `flagd-proxy` is only deployed once the reconcile loop for a `FeatureFlagSource` is run with a CR containing the provider `"flagd-proxy"` in its source array. ## Implementation Update the end-to-end test in `/config/samples/end-to-end.yaml` to use the `"flagd-proxy"` provider, the source should be a `namespace/name`. ```diff -apiVersion: core.openfeature.dev/v1alpha2 -kind: FlagSourceConfiguration +apiVersion: core.openfeature.dev/v1beta1 +kind: FeatureFlagSource metadata: name: end-to-end namespace: open-feature-demo diff --git a/docs/installation.md b/docs/installation.md index 259585359..344a64cb0 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -37,16 +37,16 @@ OpenFeature Operator's CRDs are templated, and can be updated apart from the ope helm template openfeature/open-feature-operator -s templates/{CRD} | kubectl apply -f - ``` -For the `featureflagconfigurations.core.openfeature.dev` CRD: +For the `featureflags.core.openfeature.dev` CRD: ```sh -helm template openfeature/open-feature-operator -s templates/apiextensions.k8s.io_v1_customresourcedefinition_featureflagconfigurations.core.openfeature.dev.yaml | kubectl apply -f - +helm template openfeature/open-feature-operator -s templates/apiextensions.k8s.io_v1_customresourcedefinition_featureflags.core.openfeature.dev.yaml | kubectl apply -f - ``` -For the `flagsourceconfigurations.core.openfeature.dev` CRD: +For the `featureflagsources.core.openfeature.dev` CRD: ```sh -helm template openfeature/open-feature-operator -s templates/apiextensions.k8s.io_v1_customresourcedefinition_flagsourceconfigurations.core.openfeature.dev.yaml | kubectl apply -f - +helm template openfeature/open-feature-operator -s templates/apiextensions.k8s.io_v1_customresourcedefinition_featureflagsources.core.openfeature.dev.yaml | kubectl apply -f - ``` Keep in mind, you can set values as usual during this process: @@ -65,19 +65,19 @@ Apply the release yaml directly via kubectl ```sh kubectl create namespace open-feature-operator-system && -kubectl apply -f https://github.com/open-feature/open-feature-operator/releases/download/v0.2.36/release.yaml +kubectl apply -f https://github.com/open-feature/open-feature-operator/releases/download/v0.3.0/release.yaml ``` ### Uninstall ```sh -kubectl delete -f https://github.com/open-feature/open-feature-operator/releases/download/v0.2.36/release.yaml && +kubectl delete -f https://github.com/open-feature/open-feature-operator/releases/download/v0.3.0/release.yaml && kubectl delete namespace open-feature-operator-system ``` ## Release contents -- `FeatureFlagConfiguration` `CustomResourceDefinition` (custom type that holds the configured state of feature flags). +- `FeatureFlag` `CustomResourceDefinition` (custom type that holds the configured state of feature flags). - Standard kubernetes primitives (e.g. namespace, accounts, roles, bindings, configmaps). - Operator controller manager service. - Operator webhook service. diff --git a/docs/permissions.md b/docs/permissions.md index 99d14bb96..d20273c53 100644 --- a/docs/permissions.md +++ b/docs/permissions.md @@ -19,16 +19,18 @@ The definition of this role can be found [here](../config/rbac//leader_election_ ### Manager Role -The `manager-role` applies the rules described below, its definition can be found [here](../config/rbac/role.yaml). It provides the operator with sufficient permissions over the `core.openfeature.dev` resources, and the required permissions for injecting the `flagd` sidecar into appropriate pods. The `ConfigMap` permissions are needed to allow the mounting of `FeatureFlagConfiguration` resources for filepath syncs. +The `manager-role` applies the rules described below, its definition can be found [here](../config/rbac/role.yaml). +It provides the operator with sufficient permissions over the `core.openfeature.dev` resources, and the required permissions for injecting the `flagd` sidecar into appropriate pods. +The `ConfigMap` permissions are needed to allow the mounting of `FeatureFlag` resources for file syncs. | API Group | Resource | Verbs | |-----------------------------|---------------------------------------|-------------------------------------------------| | - | `ConfigMap` | create, delete, get, list, patch, update, watch | | - | `Pod` | create, delete, get, list, patch, update, watch | | - | `ServiceAccount` | get, list, watch | -| `core.openfeature.dev` | `FeatureFlagConfiguration` | create, delete, get, list, patch, update, watch | -| `core.openfeature.dev` | `FeatureFlagConfiguration Finalizers` | update | -| `core.openfeature.dev` | `FeatureFlagConfiguration Status` | get, patch, update | +| `core.openfeature.dev` | `FeatureFlag` | create, delete, get, list, patch, update, watch | +| `core.openfeature.dev` | `FeatureFlag Finalizers` | update | +| `core.openfeature.dev` | `FeatureFlag Status` | get, patch, update | | `rbac.authorization.k8s.io` | `ClusterRoleBinding` | get, list, update, watch | ### Proxy Role @@ -48,8 +50,8 @@ During startup the operator will backfill permissions to the `flagd-kubernetes-s | API Group | Resource | Verbs | |------------------------|----------------------------|------------------| -| `core.openfeature.dev` | `FlagSourceConfiguration` | get, watch, list | -| `core.openfeature.dev` | `FeatureFlagConfiguration` | get, watch, list | +| `core.openfeature.dev` | `FeatureFlagSource` | get, watch, list | +| `core.openfeature.dev` | `FeatureFlag` | get, watch, list | -When a `Pod` has the `core.openfeature.dev/enabled` annotation value set to `"true"`, its `Service Account` is added as a subject for this role's `Role Binding`, granting it all required permissions for watching its associated `FeatureFlagConfigurations`. As a result `flagd` can provide real time events describing flag configuration changes. +When a `Pod` has the `core.openfeature.dev/enabled` annotation value set to `"true"`, its `Service Account` is added as a subject for this role's `Role Binding`, granting it all required permissions for watching its associated `FeatureFlags`. As a result `flagd` can provide real time events describing flag definition changes. diff --git a/docs/quick_start.md b/docs/quick_start.md index cdc2ffbc9..76e0580fd 100644 --- a/docs/quick_start.md +++ b/docs/quick_start.md @@ -40,7 +40,7 @@ helm upgrade --install openfeature openfeature/open-feature-operator ```sh kubectl create namespace open-feature-operator-system && -kubectl apply -f https://github.com/open-feature/open-feature-operator/releases/download/v0.2.36/release.yaml +kubectl apply -f https://github.com/open-feature/open-feature-operator/releases/download/v0.3.0/release.yaml ``` @@ -57,12 +57,12 @@ kubectl create ns flags #### 5. Install feature flags definition -This is added as a custom resource of kind `FeatureFlagConfiguration` in `flags` namespace +This is added as a custom resource of kind `FeatureFlag` in `flags` namespace ```sh kubectl apply -n flags -f - < cpuLimitResource.Value() || - ramRequestResource.Value() > ramLimitResource.Value() { - setupLog.Error(err, "sidecar resource request is higher than the resource maximum") os.Exit(1) } @@ -188,14 +137,14 @@ func main() { if err := mgr.GetFieldIndexer().IndexField( context.Background(), &corev1.Pod{}, - fmt.Sprintf("%s/%s", webhooks.OpenFeatureAnnotationPath, webhooks.AllowKubernetesSyncAnnotation), + fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationPath, common.AllowKubernetesSyncAnnotation), webhooks.OpenFeatureEnabledAnnotationIndex, ); err != nil { setupLog.Error( err, "unable to create indexer", "webhook", - fmt.Sprintf("%s/%s", webhooks.OpenFeatureAnnotationPath, webhooks.AllowKubernetesSyncAnnotation), + fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationPath, common.AllowKubernetesSyncAnnotation), ) os.Exit(1) } @@ -203,55 +152,32 @@ func main() { if err := mgr.GetFieldIndexer().IndexField( context.Background(), &appsV1.Deployment{}, - fmt.Sprintf("%s/%s", controllercommon.OpenFeatureAnnotationPath, controllercommon.FlagSourceConfigurationAnnotation), - controllercommon.FlagSourceConfigurationIndex, + fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationPath, common.FeatureFlagSourceAnnotation), + common.FeatureFlagSourceIndex, ); err != nil { setupLog.Error( err, "unable to create indexer", "webhook", - fmt.Sprintf("%s/%s", webhooks.OpenFeatureAnnotationPath, webhooks.FlagSourceConfigurationAnnotation), + fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationPath, common.FeatureFlagSourceAnnotation), ) os.Exit(1) } - if err = (&featureflagconfiguration.FeatureFlagConfigurationReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - Log: ctrl.Log.WithName("FeatureFlagConfiguration Controller"), - }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "FeatureFlagConfiguration") - os.Exit(1) - } - - if err := (&corev1alpha1.FeatureFlagConfiguration{}).SetupWebhookWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create webhook", "webhook", "FeatureFlagConfiguration") - os.Exit(1) - } - cnfg, err := controllercommon.NewFlagdProxyConfiguration() - if err != nil { - setupLog.Error(err, "unable to create kube proxy handler configuration", "controller", "FlagSourceConfiguration") - os.Exit(1) - } - kph := controllercommon.NewFlagdProxyHandler( - cnfg, + kph := flagdproxy.NewFlagdProxyHandler( + flagdproxy.NewFlagdProxyConfiguration(env), mgr.GetClient(), - ctrl.Log.WithName("FlagSourceConfiguration FlagdProxyHandler"), + ctrl.Log.WithName("FeatureFlagSource FlagdProxyHandler"), ) - flagSourceController := &flagsourceconfiguration.FlagSourceConfigurationReconciler{ + flagSourceController := &featureflagsource.FeatureFlagSourceReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), - Log: ctrl.Log.WithName("FlagSourceConfiguration Controller"), + Log: ctrl.Log.WithName("FeatureFlagSource Controller"), FlagdProxy: kph, } if err = flagSourceController.SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "FlagSourceConfiguration") - os.Exit(1) - } - - if err := (&corev1alpha1.FlagSourceConfiguration{}).SetupWebhookWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create webhook", "webhook", "FlagSourceConfiguration") + setupLog.Error(err, "unable to create controller", "controller", "FeatureFlagSource") os.Exit(1) } @@ -261,27 +187,17 @@ func main() { Client: mgr.GetClient(), Log: ctrl.Log.WithName("mutating-pod-webhook"), FlagdProxyConfig: kph.Config(), - FlagdInjector: &controllercommon.FlagdContainerInjector{ - Client: mgr.GetClient(), - Logger: ctrl.Log.WithName("flagd-container injector"), - FlagdProxyConfig: kph.Config(), - FlagDResourceRequirements: corev1.ResourceRequirements{ - Limits: map[corev1.ResourceName]resource.Quantity{ - corev1.ResourceCPU: cpuLimitResource, - corev1.ResourceMemory: ramLimitResource, - }, - Requests: map[corev1.ResourceName]resource.Quantity{ - corev1.ResourceCPU: cpuRequestResource, - corev1.ResourceMemory: ramRequestResource, - }, - }, + Env: env, + FlagdInjector: &flagdinjector.FlagdContainerInjector{ + Client: mgr.GetClient(), + Logger: ctrl.Log.WithName("flagd-container injector"), + FlagdProxyConfig: kph.Config(), + FlagdResourceRequirements: *resources, + Image: env.SidecarImage, + Tag: env.SidecarTag, }, } hookServer.Register("/mutate-v1-pod", &webhook.Admission{Handler: podMutator}) - hookServer.Register("/validate-v1alpha1-featureflagconfiguration", &webhook.Admission{Handler: &webhooks.FeatureFlagConfigurationValidator{ - Client: mgr.GetClient(), - Log: ctrl.Log.WithName("validating-featureflagconfiguration-webhook"), - }}) if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { setupLog.Error(err, "unable to set up health check") @@ -314,3 +230,46 @@ func main() { os.Exit(1) } } + +func processResources() (*corev1.ResourceRequirements, error) { + cpuLimitResource, err := resource.ParseQuantity(sidecarCpuLimit) + if err != nil { + setupLog.Error(err, "parse sidecar cpu limit", sidecarCpuLimitFlagName, sidecarCpuLimit) + return nil, err + } + + ramLimitResource, err := resource.ParseQuantity(sidecarRamLimit) + if err != nil { + setupLog.Error(err, "parse sidecar ram limit", sidecarRamLimitFlagName, sidecarRamLimit) + return nil, err + } + + cpuRequestResource, err := resource.ParseQuantity(sidecarCpuRequest) + if err != nil { + setupLog.Error(err, "parse sidecar cpu request", sidecarCpuRequestFlagName, sidecarCpuRequest) + return nil, err + } + + ramRequestResource, err := resource.ParseQuantity(sidecarRamRequest) + if err != nil { + setupLog.Error(err, "parse sidecar ram request", sidecarRamRequestFlagName, sidecarRamRequest) + return nil, err + } + + if cpuRequestResource.Value() > cpuLimitResource.Value() || + ramRequestResource.Value() > ramLimitResource.Value() { + setupLog.Error(err, "sidecar resource request is higher than the resource maximum") + return nil, err + } + + return &corev1.ResourceRequirements{ + Limits: map[corev1.ResourceName]resource.Quantity{ + corev1.ResourceCPU: cpuLimitResource, + corev1.ResourceMemory: ramLimitResource, + }, + Requests: map[corev1.ResourceName]resource.Quantity{ + corev1.ResourceCPU: cpuRequestResource, + corev1.ResourceMemory: ramRequestResource, + }, + }, nil +} diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go deleted file mode 100644 index a8c6b1070..000000000 --- a/pkg/utils/utils.go +++ /dev/null @@ -1,69 +0,0 @@ -package utils - -import ( - "fmt" - "os" - "strconv" - "strings" -) - -func TrueVal() *bool { - b := true - return &b -} - -func FalseVal() *bool { - b := false - return &b -} - -func ContainsString(slice []string, s string) bool { - for _, item := range slice { - if item == s { - return true - } - } - return false -} - -func ParseAnnotation(s string, defaultNs string) (string, string) { - ss := strings.Split(s, "/") - if len(ss) == 2 { - return ss[0], ss[1] - } - return defaultNs, s -} - -func GetIntEnvVar(key string, defaultVal int) (int, error) { - val, ok := os.LookupEnv(key) - if !ok { - return defaultVal, nil - } - valInt, err := strconv.Atoi(val) - if err != nil { - return 0, fmt.Errorf("could not parse %s env var to int: %w", key, err) - } - return valInt, nil -} - -func GetBoolEnvVar(key string, defaultVal bool) (bool, error) { - val, ok := os.LookupEnv(key) - if !ok { - return defaultVal, nil - } - valBool, err := strconv.ParseBool(val) - if err != nil { - return false, fmt.Errorf("could not parse %s env var to bool: %w", key, err) - } - return valBool, nil -} - -// unique string used to create unique volume mount and file name -func FeatureFlagConfigurationId(namespace, name string) string { - return fmt.Sprintf("%s_%s", namespace, name) -} - -// unique key (and filename) for configMap data -func FeatureFlagConfigurationConfigMapKey(namespace, name string) string { - return fmt.Sprintf("%s.flagd.json", FeatureFlagConfigurationId(namespace, name)) -} diff --git a/release-please-config.json b/release-please-config.json index bf3ccaeb8..8f99af073 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -1,11 +1,28 @@ { - "bootstrap-sha": "7d099c7b72f9a7430581696218458eaee31fb0df", + "separate-pull-requests": true, + "pull-request-title-pattern": "chore: release${component} ${version}", + "last-release-sha": "e5445ae5f41c29a5a62503438d86dcddf2c59617", + "tag-separator": "/", "packages": { + "apis": { + "release-type": "go", + "package-name": "apis", + "draft": false, + "prerelease": false, + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": true, + "extra-files": [] + }, ".": { "release-type": "go", + "package-name": "operator", + "draft": false, "prerelease": false, "bump-minor-pre-major": true, "bump-patch-for-minor-pre-major": true, + "exclude-paths": [ + "apis" + ], "extra-files": [ "README.md", "chart/open-feature-operator/Chart.yaml", @@ -14,62 +31,62 @@ "Makefile", "docs/quick_start.md", "docs/installation.md" - ], - "changelog-sections": [ - { - "type": "feat", - "section": "โœจ New Features" - }, - { - "type": "fix", - "section": "๐Ÿ› Bug Fixes" - }, - { - "type": "chore", - "section": "๐Ÿงน Chore" - }, - { - "type": "docs", - "section": "๐Ÿ“š Documentation" - }, - { - "type": "perf", - "section": "๐Ÿš€ Performance" - }, - { - "type": "build", - "hidden": true, - "section": "๐Ÿ› ๏ธ Build" - }, - { - "type": "deps", - "section": "๐Ÿ“ฆ Dependencies" - }, - { - "type": "ci", - "hidden": true, - "section": "๐Ÿšฆ CI" - }, - { - "type": "refactor", - "section": "๐Ÿ”„ Refactoring" - }, - { - "type": "revert", - "section": "๐Ÿ”™ Reverts" - }, - { - "type": "style", - "hidden": true, - "section": "๐ŸŽจ Styling" - }, - { - "type": "test", - "hidden": true, - "section": "๐Ÿงช Tests" - } ] } }, + "changelog-sections": [ + { + "type": "feat", + "section": "โœจ New Features" + }, + { + "type": "fix", + "section": "๐Ÿ› Bug Fixes" + }, + { + "type": "chore", + "section": "๐Ÿงน Chore" + }, + { + "type": "docs", + "section": "๐Ÿ“š Documentation" + }, + { + "type": "perf", + "section": "๐Ÿš€ Performance" + }, + { + "type": "build", + "hidden": true, + "section": "๐Ÿ› ๏ธ Build" + }, + { + "type": "deps", + "section": "๐Ÿ“ฆ Dependencies" + }, + { + "type": "ci", + "hidden": true, + "section": "๐Ÿšฆ CI" + }, + { + "type": "refactor", + "section": "๐Ÿ”„ Refactoring" + }, + { + "type": "revert", + "section": "๐Ÿ”™ Reverts" + }, + { + "type": "style", + "hidden": true, + "section": "๐ŸŽจ Styling" + }, + { + "type": "test", + "hidden": true, + "section": "๐Ÿงช Tests" + } + ], "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json" } diff --git a/test/e2e/DEVELOPER.md b/test/e2e/DEVELOPER.md deleted file mode 100644 index 0cbc835bc..000000000 --- a/test/e2e/DEVELOPER.md +++ /dev/null @@ -1,21 +0,0 @@ -# E2E Testing - -This suite tests the end-to-end deployment of open-feature-operator by deploying an nginx reverse proxy and asserting that curls to the proxy elicit expected behaviour from the flagd sidecar created by open-feature-operator. - -## Running on a Kind cluster - -```shell -kind create cluster --config ./test/e2e/kind-cluster.yml -IMG=ghcr.io/open-feature/open-feature-operator:main make deploy-operator -IMG=ghcr.io/open-feature/open-feature-operator:main make e2e-test -``` - -## Running on a Kind cluster using a locally built image - -```shell -kind create cluster --config ./test/e2e/kind-cluster.yml -kind load docker-image local-image-tag:latest -IMG=local-image-tag:latest make deploy-operator -IMG=local-image-tag:latest make e2e-test -``` - diff --git a/test/e2e/README.md b/test/e2e/README.md new file mode 100644 index 000000000..545826141 --- /dev/null +++ b/test/e2e/README.md @@ -0,0 +1,41 @@ +# E2E Testing + +This suite tests the end-to-end operation of the open-feature-operator. + +Tests are written with [kuttl](https://kuttl.dev/) and assertions are executed from a curl enabled Job. +Ngnix reverse proxy is used as the workload where flagd get injected using OFO annotations. + +## Running and validating locally + +It is recommended to run and validate e2e test locally before opening a pull request. + +To run locally (commands are executed from the project root level), + +1. Build the operator locally - `docker build . -t open-feature-operator-local:validate` +2. Create a kind cluster - `kind create cluster --config ./test/e2e/kind-cluster.yml --name e2e-tests` +3. Load locally build operator image - `kind load docker-image open-feature-operator-local:validate --name e2e-tests` +4. Deploy Operator to kind cluster - `IMG=open-feature-operator-local:validate make deploy-operator` +5. Execute kuttl tests - `IMG=open-feature-operator-local:validate make e2e-test-kuttl` + +Alternatively, you can use `e2e-test-validate-local` Makefile rule to execute all above and cleanup the kind cluster, + +> make e2e-test-validate-local + +After the test run, make sure test status by validating kuttl output, + +```text +--- PASS: kuttl (48.71s) + --- PASS: kuttl/harness (0.00s) + --- PASS: kuttl/harness/assets (0.01s) + --- PASS: kuttl/harness/flagd-disabled (12.58s) + --- PASS: kuttl/harness/inject-flagd (26.41s) + --- PASS: kuttl/harness/fsconfig-file-sync (31.73s) + --- PASS: kuttl/harness/fsconfig-k8s-sync (31.74s) + --- PASS: kuttl/harness/fsconfig-flagd-proxy-sync (48.49s) +``` + +### Running individual tests + +You can use kuttl command options to execute individual tests. Consider the example command below, + +>$ kubectl kuttl test --start-kind=false ./test/e2e/kuttl --config=kuttl-test.yaml --test=flagd-disabled \ No newline at end of file diff --git a/test/e2e/e2e.yml b/test/e2e/e2e.yml deleted file mode 100644 index cc844d61b..000000000 --- a/test/e2e/e2e.yml +++ /dev/null @@ -1,137 +0,0 @@ -# This configuration deploys the means to test the mutating injection webhook by proxying through an nginx server -# to assert that flagd is reachable ---- -apiVersion: core.openfeature.dev/v1alpha1 -kind: FeatureFlagConfiguration -metadata: - name: end-to-end-test-default -spec: - featureFlagSpec: | - { - "flags": { - "simple-flag": { - "state": "ENABLED", - "variants": { - "on": true, - "off": false - }, - "defaultVariant": "on" - } - } - } ---- -apiVersion: core.openfeature.dev/v1alpha2 -kind: FeatureFlagConfiguration -metadata: - name: end-to-end-test-filepath -spec: - syncProvider: - name: filepath - featureFlagSpec: - flags: - simple-flag-filepath: - state: ENABLED - variants: - "on": true - "off": false - defaultVariant: "on" ---- -apiVersion: core.openfeature.dev/v1alpha2 -kind: FeatureFlagConfiguration -metadata: - name: end-to-end-test-filepath2 -spec: - syncProvider: - name: filepath - featureFlagSpec: - flags: - simple-flag-filepath2: - state: ENABLED - variants: - "on": true - "off": false - defaultVariant: "on" ---- -apiVersion: core.openfeature.dev/v1alpha3 -kind: FlagSourceConfiguration -metadata: - name: end-to-end-test-fs-config -spec: - sources: - - source: end-to-end-test-filepath2 - provider: kubernetes ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: open-feature-e2e-test-sa -automountServiceAccountToken: true ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: open-feature-e2e-nginx-conf -data: - nginx.conf: | - events {} - http { - server { - location / { - proxy_pass http://127.0.0.1:8013; - } - } - } ---- -# Deployment of nginx using our custom resource -apiVersion: apps/v1 -kind: Deployment -metadata: - name: open-feature-e2e-test-deployment - labels: - app: open-feature-e2e-test -spec: - replicas: 1 - selector: - matchLabels: - app: open-feature-e2e-test - template: - metadata: - labels: - app: open-feature-e2e-test - annotations: - openfeature.dev/enabled: "true" - openfeature.dev/featureflagconfiguration: "end-to-end-test-default,end-to-end-test-filepath" - openfeature.dev/flagsourceconfiguration: "end-to-end-test-fs-config" - spec: - serviceAccountName: open-feature-e2e-test-sa - volumes: - - name: open-feature-e2e-nginx-conf - configMap: - name: open-feature-e2e-nginx-conf - items: - - key: nginx.conf - path: nginx.conf - containers: - - name: open-feature-e2e-test - image: nginx:stable-alpine - ports: - - containerPort: 80 - volumeMounts: - - name: open-feature-e2e-nginx-conf - mountPath: /etc/nginx - readOnly: true ---- -# Service exposed using NodePort -apiVersion: v1 -kind: Service -metadata: - name: open-feature-e2e-test-service -spec: - type: NodePort - selector: - app: open-feature-e2e-test - ports: - - protocol: TCP - port: 30000 - targetPort: 80 - nodePort: 30000 \ No newline at end of file diff --git a/test/e2e/flag-evaluation.sh b/test/e2e/flag-evaluation.sh deleted file mode 100755 index a58f7f484..000000000 --- a/test/e2e/flag-evaluation.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash - -FLAG_KEY="$1" -EXPECTED_RESPONSE_CONTAIN="$2" - -# attempt up to 5 times -MAX_ATTEMPTS=5 -# retry every x seconds -RETRY_INTERVAL=1 -if [[ "$3" =~ ^[0-9]+$ ]] - then - MAX_ATTEMPTS=$3 -fi -if [[ "$4" =~ ^[0-9]+$ ]] - then - RETRY_INTERVAL=$4 -fi - - -for (( ATTEMPT_COUNTER=0; ATTEMPT_COUNTER<${MAX_ATTEMPTS}; ATTEMPT_COUNTER++ )) -do - - RESPONSE=$(curl -s -X POST "localhost:30000/schema.v1.Service/ResolveBoolean" -d "{\"flagKey\":\"$FLAG_KEY\",\"context\":{}}" -H "Content-Type: application/json") - RESPONSE="${RESPONSE//[[:space:]]/}" # strip whitespace from response - - if [[ "$RESPONSE" == *"$EXPECTED_RESPONSE_CONTAIN"* ]] - then - exit 0 - fi - - echo "Expected response for flag $FLAG_KEY to contain: EXPECTED_RESPONSE_CONTAIN" - echo "Got response for flag $FLAG_KEY: $RESPONSE" - echo "Retrying in ${RETRY_INTERVAL} seconds" - sleep "${RETRY_INTERVAL}" -done -echo "Max attempts reached" -exit 1 \ No newline at end of file diff --git a/test/e2e/kuttl/assets/manifests.yaml b/test/e2e/kuttl/assets/manifests.yaml index 661e9009f..ecc006c2a 100644 --- a/test/e2e/kuttl/assets/manifests.yaml +++ b/test/e2e/kuttl/assets/manifests.yaml @@ -1,21 +1,16 @@ -apiVersion: core.openfeature.dev/v1alpha1 -kind: FeatureFlagConfiguration +apiVersion: core.openfeature.dev/v1beta1 +kind: FeatureFlag metadata: name: end-to-end-test spec: - featureFlagSpec: | - { - "flags": { - "simple-flag": { - "state": "ENABLED", - "variants": { - "on": true, - "off": false - }, - "defaultVariant": "on" - } - } - } + flagSpec: + flags: + "simple-flag": + state: "ENABLED" + variants: + "on": true + "off": false + defaultVariant: "on" --- apiVersion: v1 kind: ConfigMap @@ -32,6 +27,12 @@ data: } } --- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: open-feature-e2e-test-sa +automountServiceAccountToken: true +--- # Deployment of nginx using our custom resource apiVersion: apps/v1 kind: Deployment @@ -50,8 +51,9 @@ spec: app: open-feature-e2e-test annotations: openfeature.dev/enabled: "true" - openfeature.dev/flagsourceconfiguration: "source-configuration" + openfeature.dev/featureflagsource: "source-configuration" spec: + serviceAccountName: open-feature-e2e-test-sa volumes: - name: open-feature-e2e-nginx-conf configMap: diff --git a/test/e2e/kuttl/flagd-disabled/00-assert.yaml b/test/e2e/kuttl/flagd-disabled/00-assert.yaml index 0ce96ba41..7e70f3316 100644 --- a/test/e2e/kuttl/flagd-disabled/00-assert.yaml +++ b/test/e2e/kuttl/flagd-disabled/00-assert.yaml @@ -2,3 +2,5 @@ apiVersion: kuttl.dev/v1beta1 kind: TestAssert commands: - command: kubectl wait --for=condition=complete job flagd-query-test -n $NAMESPACE +collectors: + - command: kubectl logs -l job-name=flagd-query-test -n $NAMESPACE diff --git a/test/e2e/kuttl/flagd-disabled/00-install.yaml b/test/e2e/kuttl/flagd-disabled/00-install.yaml index b9d4648b6..db284067c 100644 --- a/test/e2e/kuttl/flagd-disabled/00-install.yaml +++ b/test/e2e/kuttl/flagd-disabled/00-install.yaml @@ -1,24 +1,19 @@ # This configuration deploys the means to test the mutating injection webhook by proxying through an nginx server # to assert that flagd is reachable --- -apiVersion: core.openfeature.dev/v1alpha1 -kind: FeatureFlagConfiguration +apiVersion: core.openfeature.dev/v1beta1 +kind: FeatureFlag metadata: name: end-to-end-test spec: - featureFlagSpec: | - { - "flags": { - "simple-flag": { - "state": "ENABLED", - "variants": { - "on": true, - "off": false - }, - "defaultVariant": "on" - } - } - } + flagSpec: + flags: + "simple-flag": + state: "ENABLED" + variants: + "on": true + "off": false + defaultVariant: "on" --- apiVersion: v1 kind: ServiceAccount diff --git a/test/e2e/kuttl/fsconfig-file-sync/00-assert.yaml b/test/e2e/kuttl/fsconfig-file-sync/00-assert.yaml index 0ce96ba41..7e70f3316 100644 --- a/test/e2e/kuttl/fsconfig-file-sync/00-assert.yaml +++ b/test/e2e/kuttl/fsconfig-file-sync/00-assert.yaml @@ -2,3 +2,5 @@ apiVersion: kuttl.dev/v1beta1 kind: TestAssert commands: - command: kubectl wait --for=condition=complete job flagd-query-test -n $NAMESPACE +collectors: + - command: kubectl logs -l job-name=flagd-query-test -n $NAMESPACE diff --git a/test/e2e/kuttl/fsconfig-file-sync/00-install.yaml b/test/e2e/kuttl/fsconfig-file-sync/00-install.yaml index 5f732b2a7..f2fd90f13 100644 --- a/test/e2e/kuttl/fsconfig-file-sync/00-install.yaml +++ b/test/e2e/kuttl/fsconfig-file-sync/00-install.yaml @@ -1,15 +1,12 @@ --- -apiVersion: core.openfeature.dev/v1alpha3 -kind: FlagSourceConfiguration +apiVersion: core.openfeature.dev/v1beta1 +kind: FeatureFlagSource metadata: name: source-configuration spec: - metricsPort: 8080 evaluator: json - defaultSyncProvider: filepath - # renovate: datasource=github-tags depName=open-feature/flagd/flagd - tag: v0.6.3 + defaultSyncProvider: file sources: - source: end-to-end-test - provider: filepath + provider: file probesEnabled: true diff --git a/test/e2e/kuttl/fsconfig-file-sync/01-assert.yaml b/test/e2e/kuttl/fsconfig-file-sync/01-assert.yaml index 39ee90dfa..747459483 100644 --- a/test/e2e/kuttl/fsconfig-file-sync/01-assert.yaml +++ b/test/e2e/kuttl/fsconfig-file-sync/01-assert.yaml @@ -3,7 +3,7 @@ kind: Pod metadata: annotations: openfeature.dev/enabled: "true" - openfeature.dev/flagsourceconfiguration: source-configuration + openfeature.dev/featureflagsource: source-configuration labels: app: open-feature-e2e-test status: @@ -13,4 +13,4 @@ spec: - name: open-feature-e2e-test image: nginx:stable-alpine - name: flagd # this part verifies flagd injection happened - image: ghcr.io/open-feature/flagd:v0.6.3 + image: ghcr.io/open-feature/flagd:v0.7.0 diff --git a/test/e2e/kuttl/fsconfig-flagd-proxy-sync/00-assert.yaml b/test/e2e/kuttl/fsconfig-flagd-proxy-sync/00-assert.yaml index 0ce96ba41..e916130f0 100644 --- a/test/e2e/kuttl/fsconfig-flagd-proxy-sync/00-assert.yaml +++ b/test/e2e/kuttl/fsconfig-flagd-proxy-sync/00-assert.yaml @@ -2,3 +2,5 @@ apiVersion: kuttl.dev/v1beta1 kind: TestAssert commands: - command: kubectl wait --for=condition=complete job flagd-query-test -n $NAMESPACE +collectors: + - command: kubectl logs -l job-name=flagd-query-test -n $NAMESPACE \ No newline at end of file diff --git a/test/e2e/kuttl/fsconfig-flagd-proxy-sync/00-install.yaml b/test/e2e/kuttl/fsconfig-flagd-proxy-sync/00-install.yaml index bb7c41d5d..ef5c1fb26 100644 --- a/test/e2e/kuttl/fsconfig-flagd-proxy-sync/00-install.yaml +++ b/test/e2e/kuttl/fsconfig-flagd-proxy-sync/00-install.yaml @@ -1,14 +1,11 @@ --- -apiVersion: core.openfeature.dev/v1alpha3 -kind: FlagSourceConfiguration +apiVersion: core.openfeature.dev/v1beta1 +kind: FeatureFlagSource metadata: name: source-configuration spec: - metricsPort: 8080 evaluator: json defaultSyncProvider: flagd-proxy - # renovate: datasource=github-tags depName=open-feature/flagd/flagd - tag: v0.6.3 sources: - source: end-to-end-test provider: flagd-proxy diff --git a/test/e2e/kuttl/fsconfig-flagd-proxy-sync/01-assert.yaml b/test/e2e/kuttl/fsconfig-flagd-proxy-sync/01-assert.yaml index 39ee90dfa..747459483 100644 --- a/test/e2e/kuttl/fsconfig-flagd-proxy-sync/01-assert.yaml +++ b/test/e2e/kuttl/fsconfig-flagd-proxy-sync/01-assert.yaml @@ -3,7 +3,7 @@ kind: Pod metadata: annotations: openfeature.dev/enabled: "true" - openfeature.dev/flagsourceconfiguration: source-configuration + openfeature.dev/featureflagsource: source-configuration labels: app: open-feature-e2e-test status: @@ -13,4 +13,4 @@ spec: - name: open-feature-e2e-test image: nginx:stable-alpine - name: flagd # this part verifies flagd injection happened - image: ghcr.io/open-feature/flagd:v0.6.3 + image: ghcr.io/open-feature/flagd:v0.7.0 diff --git a/test/e2e/kuttl/fsconfig-k8s-sync/00-assert.yaml b/test/e2e/kuttl/fsconfig-k8s-sync/00-assert.yaml index 0ce96ba41..7e70f3316 100644 --- a/test/e2e/kuttl/fsconfig-k8s-sync/00-assert.yaml +++ b/test/e2e/kuttl/fsconfig-k8s-sync/00-assert.yaml @@ -2,3 +2,5 @@ apiVersion: kuttl.dev/v1beta1 kind: TestAssert commands: - command: kubectl wait --for=condition=complete job flagd-query-test -n $NAMESPACE +collectors: + - command: kubectl logs -l job-name=flagd-query-test -n $NAMESPACE diff --git a/test/e2e/kuttl/fsconfig-k8s-sync/00-install.yaml b/test/e2e/kuttl/fsconfig-k8s-sync/00-install.yaml index 4db30d9c8..03b4b3a8a 100644 --- a/test/e2e/kuttl/fsconfig-k8s-sync/00-install.yaml +++ b/test/e2e/kuttl/fsconfig-k8s-sync/00-install.yaml @@ -1,14 +1,11 @@ --- -apiVersion: core.openfeature.dev/v1alpha3 -kind: FlagSourceConfiguration +apiVersion: core.openfeature.dev/v1beta1 +kind: FeatureFlagSource metadata: name: source-configuration spec: - metricsPort: 8080 evaluator: json defaultSyncProvider: kubernetes - # renovate: datasource=github-tags depName=open-feature/flagd/flagd - tag: v0.6.3 sources: - source: end-to-end-test provider: kubernetes diff --git a/test/e2e/kuttl/fsconfig-k8s-sync/01-assert.yaml b/test/e2e/kuttl/fsconfig-k8s-sync/01-assert.yaml index 8e694b734..f3bf1ed9c 100644 --- a/test/e2e/kuttl/fsconfig-k8s-sync/01-assert.yaml +++ b/test/e2e/kuttl/fsconfig-k8s-sync/01-assert.yaml @@ -4,7 +4,7 @@ metadata: annotations: openfeature.dev/allowkubernetessync: "true" openfeature.dev/enabled: "true" - openfeature.dev/flagsourceconfiguration: source-configuration + openfeature.dev/featureflagsource: source-configuration labels: app: open-feature-e2e-test status: @@ -14,4 +14,4 @@ spec: - name: open-feature-e2e-test image: nginx:stable-alpine - name: flagd # this part verifies flagd injection happened - image: ghcr.io/open-feature/flagd:v0.6.3 + image: ghcr.io/open-feature/flagd:v0.7.0 diff --git a/test/e2e/kuttl/inject-flagd/00-assert.yaml b/test/e2e/kuttl/inject-flagd/00-assert.yaml deleted file mode 100644 index 0ce96ba41..000000000 --- a/test/e2e/kuttl/inject-flagd/00-assert.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: kuttl.dev/v1beta1 -kind: TestAssert -commands: - - command: kubectl wait --for=condition=complete job flagd-query-test -n $NAMESPACE diff --git a/test/e2e/kuttl/inject-flagd/00-install.yaml b/test/e2e/kuttl/inject-flagd/00-install.yaml deleted file mode 100644 index 1f9b5d162..000000000 --- a/test/e2e/kuttl/inject-flagd/00-install.yaml +++ /dev/null @@ -1,123 +0,0 @@ -# This configuration deploys the means to test the mutating injection webhook by proxying through an nginx server -# to assert that flagd is reachable ---- -apiVersion: core.openfeature.dev/v1alpha1 -kind: FeatureFlagConfiguration -metadata: - name: end-to-end-test -spec: - featureFlagSpec: | - { - "flags": { - "simple-flag": { - "state": "ENABLED", - "variants": { - "on": true, - "off": false - }, - "defaultVariant": "on" - } - } - } ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: open-feature-e2e-test-sa -automountServiceAccountToken: true ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: open-feature-e2e-nginx-conf -data: - nginx.conf: | - events {} - http { - server { - location / { - proxy_pass http://127.0.0.1:8013; - } - } - } ---- -# Deployment of nginx using our custom resource -apiVersion: apps/v1 -kind: Deployment -metadata: - name: open-feature-e2e-test-deployment - labels: - app: open-feature-e2e-test -spec: - replicas: 1 - selector: - matchLabels: - app: open-feature-e2e-test - template: - metadata: - labels: - app: open-feature-e2e-test - annotations: - openfeature.dev/enabled: "true" - openfeature.dev/featureflagconfiguration: "end-to-end-test" - spec: - serviceAccountName: open-feature-e2e-test-sa - volumes: - - name: open-feature-e2e-nginx-conf - configMap: - name: open-feature-e2e-nginx-conf - items: - - key: nginx.conf - path: nginx.conf - containers: - - name: open-feature-e2e-test - image: nginx:stable-alpine - ports: - - containerPort: 80 - volumeMounts: - - name: open-feature-e2e-nginx-conf - mountPath: /etc/nginx - readOnly: true ---- -# Service exposed using NodePort -apiVersion: v1 -kind: Service -metadata: - name: open-feature-e2e-test-service -spec: - type: ClusterIP - selector: - app: open-feature-e2e-test - ports: - - protocol: TCP - port: 30000 - targetPort: 80 ---- -apiVersion: batch/v1 -kind: Job -metadata: - name: flagd-query-test -spec: - backoffLimit: 10 - template: - spec: - containers: - - name: test-flagd-endpoint - image: curlimages/curl:8.2.1 - args: - - /bin/sh - - -ec - - | - EXPECTED_RESPONSE_CONTAIN='"value":true,"reason":"STATIC","variant":"on"' - RESPONSE=$(curl -s -X POST "open-feature-e2e-test-service:30000/schema.v1.Service/ResolveBoolean" -d '{"flagKey":"simple-flag","context":{}}' -H "Content-Type: application/json") - RESPONSE="${RESPONSE//[[:space:]]/}" # strip whitespace from response - - if [[ "$RESPONSE" == *"$EXPECTED_RESPONSE_CONTAIN"* ]] - then - exit 0 - fi - - echo "Expected response to contain: $EXPECTED_RESPONSE_CONTAIN" - echo "Got response: $RESPONSE" - exit 1 - restartPolicy: OnFailure diff --git a/test/e2e/kuttl/inject-flagd/01-assert.yaml b/test/e2e/kuttl/inject-flagd/01-assert.yaml deleted file mode 100644 index 32078dd54..000000000 --- a/test/e2e/kuttl/inject-flagd/01-assert.yaml +++ /dev/null @@ -1,18 +0,0 @@ - -apiVersion: v1 -kind: Pod -metadata: - annotations: - openfeature.dev/allowkubernetessync: "true" - openfeature.dev/enabled: "true" - openfeature.dev/featureflagconfiguration: end-to-end-test - labels: - app: open-feature-e2e-test -status: - phase: Running -spec: - containers: - - name: open-feature-e2e-test - image: nginx:stable-alpine - - name: flagd # this part verifies flagd injection happened - image: ghcr.io/open-feature/flagd:v0.6.3 diff --git a/test/e2e/run.sh b/test/e2e/run.sh deleted file mode 100755 index dfdc4300a..000000000 --- a/test/e2e/run.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash - -FAILURE=0 - -for FILE in "$(dirname "${BASH_SOURCE[0]}")"/tests/*; -do - echo "Running ${FILE##*/}"; - ./"${FILE}" - EXIT_CODE=$? - if [ $EXIT_CODE -ne 0 ]; - then - FAILURE=1 - echo "${FILE##*/} failed." - else - echo "${FILE##*/} succeeded." - fi -done - -if [ $FAILURE -eq 1 ]; -then - exit 1 -fi - diff --git a/test/e2e/tests/001.flagd-response.sh b/test/e2e/tests/001.flagd-response.sh deleted file mode 100755 index 996cfe4d5..000000000 --- a/test/e2e/tests/001.flagd-response.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash - -FAILURE=0 - -flagKeys=('simple-flag' 'simple-flag-filepath' 'simple-flag-filepath2') -expectedResponseContain=('value":true,"reason":"STATIC","variant":"on"' '"value":true,"reason":"STATIC","variant":"on"' '"value":true,"reason":"STATIC","variant":"on"') - -for i in "${!flagKeys[@]}"; do - ./"$(dirname "${BASH_SOURCE[0]}")"/../flag-evaluation.sh "${flagKeys[$i]}" "${expectedResponseContain[$i]}" - EXIT_CODE=$? - if [ $EXIT_CODE -ne 0 ]; - then - FAILURE=1 - fi -done - -if [ $FAILURE -eq 1 ]; -then - exit 1 -fi diff --git a/test/e2e/tests/002.crd-configuration-reconciliation.sh b/test/e2e/tests/002.crd-configuration-reconciliation.sh deleted file mode 100755 index 151b0053d..000000000 --- a/test/e2e/tests/002.crd-configuration-reconciliation.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash - -cat < /dev/null # reset state quietly - -exit $EXIT_CODE diff --git a/test/e2e/tests/003.configmap-configuration-reconciliation.sh b/test/e2e/tests/003.configmap-configuration-reconciliation.sh deleted file mode 100755 index 47d8f36c2..000000000 --- a/test/e2e/tests/003.configmap-configuration-reconciliation.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash - -cat < /dev/null # reset state quietly - -exit $EXIT_CODE diff --git a/test/e2e/tests/004.disabled.sh b/test/e2e/tests/004.disabled.sh deleted file mode 100755 index 8655c0eeb..000000000 --- a/test/e2e/tests/004.disabled.sh +++ /dev/null @@ -1,61 +0,0 @@ -#!/bin/bash - -# delete existing deployment -kubectl -n open-feature-operator-system delete deployment open-feature-e2e-test-deployment - -# set openfeature.dev/enabled annotation to false and redeploy -cat < /dev/null -kubectl wait --for=condition=Available=True deploy --all -n 'open-feature-operator-system' - -if [ "$STATUS_CODE" -eq 200 ]; then - echo "Expected curl to nginx reverse proxy to return non 200 status code when openfeature.dev/enabled annotation is false." - exit 1 -else - exit 0 -fi diff --git a/webhooks/common.go b/webhooks/common.go new file mode 100644 index 000000000..a5ac2eef1 --- /dev/null +++ b/webhooks/common.go @@ -0,0 +1,85 @@ +package webhooks + +import ( + "fmt" + "strings" + + api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" + apicommon "github.com/open-feature/open-feature-operator/apis/core/v1beta1/common" + "github.com/open-feature/open-feature-operator/common" + "github.com/open-feature/open-feature-operator/common/types" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func OpenFeatureEnabledAnnotationIndex(o client.Object) []string { + pod, ok := o.(*corev1.Pod) + if !ok { + return []string{"false"} + } + if pod.ObjectMeta.Annotations == nil { + return []string{ + "false", + } + } + val, ok := pod.ObjectMeta.Annotations[fmt.Sprintf("openfeature.dev/%s", common.AllowKubernetesSyncAnnotation)] + if ok && val == "true" { + return []string{ + "true", + } + } + return []string{ + "false", + } +} + +func parseList(s string) []string { + out := []string{} + ss := strings.Split(s, ",") + for i := 0; i < len(ss); i++ { + newS := strings.TrimSpace(ss[i]) + if newS != "" { //function should not add empty values + out = append(out, newS) + } + } + return out +} + +func containsK8sProvider(sources []api.Source) bool { + for _, source := range sources { + if source.Provider.IsKubernetes() { + return true + } + } + return false +} + +func checkOFEnabled(annotations map[string]string) bool { + val, ok := annotations[fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationPrefix, common.EnabledAnnotation)] + return ok && val == "true" +} + +func NewFeatureFlagSourceSpec(env types.EnvConfig) *api.FeatureFlagSourceSpec { + f := false + args := strings.Split(env.SidecarProviderArgs, ",") + // use empty array when arguments are not set + if len(args) == 1 && args[0] == "" { + args = []string{} + } + return &api.FeatureFlagSourceSpec{ + ManagementPort: int32(env.SidecarManagementPort), + Port: int32(env.SidecarPort), + SocketPath: env.SidecarSocketPath, + Evaluator: env.SidecarEvaluator, + Sources: []api.Source{}, + EnvVars: []corev1.EnvVar{}, + SyncProviderArgs: args, + DefaultSyncProvider: apicommon.SyncProviderType(env.SidecarSyncProvider), + EnvVarPrefix: env.SidecarEnvVarPrefix, + LogFormat: env.SidecarLogFormat, + RolloutOnChange: nil, + DebugLogging: &f, + OtelCollectorUri: "", + ProbesEnabled: &env.SidecarProbesEnabled, + } +} diff --git a/webhooks/common_test.go b/webhooks/common_test.go new file mode 100644 index 000000000..d94cfbac8 --- /dev/null +++ b/webhooks/common_test.go @@ -0,0 +1,180 @@ +package webhooks + +import ( + "fmt" + "reflect" + "testing" + + api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" + apicommon "github.com/open-feature/open-feature-operator/apis/core/v1beta1/common" + "github.com/open-feature/open-feature-operator/common" + "github.com/open-feature/open-feature-operator/common/types" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func TestOpenFeatureEnabledAnnotationIndex(t *testing.T) { + + tests := []struct { + name string + o client.Object + want []string + }{ + { + name: "not a pod", + o: &corev1.ConfigMap{}, + want: []string{"false"}, + }, { + name: "no annotations", + o: &corev1.Pod{}, + want: []string{"false"}, + }, { + name: "annotated wrong", + o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{"test/ann": "nope", "openfeature.dev/allowkubernetessync": "false"}}}, + want: []string{"false"}, + }, { + name: "annotated with enabled index", + o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{"openfeature.dev/allowkubernetessync": "true"}}}, + want: []string{"true"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := OpenFeatureEnabledAnnotationIndex(tt.o); !reflect.DeepEqual(got, tt.want) { + t.Errorf("OpenFeatureEnabledAnnotationIndex() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestPodMutator_checkOFEnabled(t *testing.T) { + + tests := []struct { + name string + annotations map[string]string + want bool + }{ + { + name: "enabled", + annotations: map[string]string{fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationPrefix, common.EnabledAnnotation): "true"}, + want: true, + }, { + name: "disabled", + annotations: map[string]string{fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationPrefix, common.EnabledAnnotation): "false"}, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := checkOFEnabled(tt.annotations); got != tt.want { + t.Errorf("checkOFEnabled() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_parseList(t *testing.T) { + + tests := []struct { + name string + s string + want []string + }{ + { + name: "empty string", + s: "", + want: []string{}, + }, { + name: "nice list with spaces", + s: "annotation1, annotation2, annotation4 , annotation3,", + want: []string{"annotation1", "annotation2", "annotation4", "annotation3"}, + }, { + name: "list with no spaces", + s: "annotation1, annotation2,annotation4, annotation3", + want: []string{"annotation1", "annotation2", "annotation4", "annotation3"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := parseList(tt.s); !reflect.DeepEqual(got, tt.want) { + t.Errorf("parseList() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestPodMutator_containsK8sProvider(t *testing.T) { + + tests := []struct { + name string + sources []api.Source + want bool + }{ + { + name: "empty", + sources: []api.Source{}, + want: false, + }, + { + name: "non-kubernetes", + sources: []api.Source{ + {Provider: apicommon.SyncProviderFilepath}, + {Provider: apicommon.SyncProviderGrpc}, + }, + want: false, + }, + { + name: "kubernetes", + sources: []api.Source{ + {Provider: apicommon.SyncProviderFilepath}, + {Provider: apicommon.SyncProviderKubernetes}, + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := containsK8sProvider(tt.sources); got != tt.want { + t.Errorf("containsK8sProvider() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_NewFeatureFlagSourceSpec(t *testing.T) { + env := types.EnvConfig{ + SidecarManagementPort: 80, + SidecarPort: 88, + SidecarSocketPath: "socket-path", + SidecarEvaluator: "evaluator", + SidecarProviderArgs: "arg1,arg2,arg3", + SidecarSyncProvider: "kubernetes", + SidecarEnvVarPrefix: "pre", + SidecarLogFormat: "log", + SidecarProbesEnabled: true, + } + + f := false + tt := true + + expected := &api.FeatureFlagSourceSpec{ + ManagementPort: int32(80), + Port: int32(88), + SocketPath: "socket-path", + Evaluator: "evaluator", + Sources: []api.Source{}, + EnvVars: []corev1.EnvVar{}, + SyncProviderArgs: []string{"arg1", "arg2", "arg3"}, + DefaultSyncProvider: apicommon.SyncProviderKubernetes, + EnvVarPrefix: "pre", + LogFormat: "log", + RolloutOnChange: nil, + DebugLogging: &f, + OtelCollectorUri: "", + ProbesEnabled: &tt, + } + + require.Equal(t, expected, NewFeatureFlagSourceSpec(env)) +} diff --git a/webhooks/error_types.go b/webhooks/error_types.go deleted file mode 100644 index 6ef21407f..000000000 --- a/webhooks/error_types.go +++ /dev/null @@ -1,13 +0,0 @@ -package webhooks - -import ( - goErr "errors" - "net/http" -) - -func (m *PodMutator) IsReady(_ *http.Request) error { - if m.ready { - return nil - } - return goErr.New("pod mutator is not ready") -} diff --git a/webhooks/featureflagconfiguration_webhook.go b/webhooks/featureflagconfiguration_webhook.go deleted file mode 100644 index 56235f5c3..000000000 --- a/webhooks/featureflagconfiguration_webhook.go +++ /dev/null @@ -1,102 +0,0 @@ -package webhooks - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "strings" - - schemas "github.com/open-feature/schemas/json" - "k8s.io/apimachinery/pkg/api/errors" - - "github.com/go-logr/logr" - corev1alpha1 "github.com/open-feature/open-feature-operator/apis/core/v1alpha1" - "github.com/xeipuuv/gojsonschema" - corev1 "k8s.io/api/core/v1" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/webhook/admission" -) - -// NOTE: RBAC not needed here. -//+kubebuilder:rbac:groups="",resources=pods,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:webhook:path=/validate-v1alpha1-featureflagconfiguration,mutating=false,failurePolicy=fail,sideEffects=None,groups=core.openfeature.dev,resources=featureflagconfigurations,verbs=create;update,versions=v1alpha1,name=validate.featureflagconfiguration.openfeature.dev,admissionReviewVersions=v1 - -// FeatureFlagConfigurationValidator annotates Pods -type FeatureFlagConfigurationValidator struct { - Client client.Client - decoder *admission.Decoder - Log logr.Logger -} - -// Handle validates a FeatureFlagConfiguration -func (m *FeatureFlagConfigurationValidator) Handle(ctx context.Context, req admission.Request) admission.Response { - config := corev1alpha1.FeatureFlagConfiguration{} - if err := m.decoder.Decode(req, &config); err != nil { - return admission.Errored(http.StatusBadRequest, err) - } - - if err := m.validateFlagSourceConfiguration(ctx, config); err != nil { - return admission.Denied(err.Error()) - } - - return admission.Allowed("") -} - -// FeatureFlagConfigurationValidator implements admission.DecoderInjector. -// A decoder will be automatically injected. - -// InjectDecoder injects the decoder. -func (m *FeatureFlagConfigurationValidator) InjectDecoder(d *admission.Decoder) error { - m.decoder = d - return nil -} - -func (m *FeatureFlagConfigurationValidator) isJSON(str string) bool { - var js json.RawMessage - return json.Unmarshal([]byte(str), &js) == nil -} - -func validateJSONSchema(schemaJSON string, inputJSON string) error { - schemaLoader := gojsonschema.NewBytesLoader([]byte(schemaJSON)) - valuesLoader := gojsonschema.NewBytesLoader([]byte(inputJSON)) - result, err := gojsonschema.Validate(schemaLoader, valuesLoader) - if err != nil { - return err - } - - if !result.Valid() { - var sb strings.Builder - for _, desc := range result.Errors() { - sb.WriteString(fmt.Sprintf("- %s\n", desc)) - } - return errors.NewBadRequest(sb.String()) - } - return nil -} - -func (m *FeatureFlagConfigurationValidator) validateFlagSourceConfiguration(ctx context.Context, config corev1alpha1.FeatureFlagConfiguration) error { - if config.Spec.FeatureFlagSpec != "" { - if !m.isJSON(config.Spec.FeatureFlagSpec) { - return fmt.Errorf("FeatureFlagSpec is not valid JSON: %s", config.Spec.FeatureFlagSpec) - } - err := validateJSONSchema(schemas.FlagdDefinitions, config.Spec.FeatureFlagSpec) - if err != nil { - return fmt.Errorf("FeatureFlagSpec is not valid JSON: %s", err.Error()) - } - } - - if config.Spec.ServiceProvider != nil && config.Spec.ServiceProvider.Credentials != nil { - // Check the provider and whether it has an existing secret - providerKeySecret := corev1.Secret{} - if err := m.Client.Get(ctx, client.ObjectKey{ - Name: config.Spec.ServiceProvider.Credentials.Name, - Namespace: config.Spec.ServiceProvider.Credentials.Namespace, - }, &providerKeySecret); errors.IsNotFound(err) { - return fmt.Errorf("credentials secret not found") - } - } - - return nil -} diff --git a/webhooks/featureflagconfiguration_webhook_test.go b/webhooks/featureflagconfiguration_webhook_test.go deleted file mode 100644 index f5606fd2c..000000000 --- a/webhooks/featureflagconfiguration_webhook_test.go +++ /dev/null @@ -1,139 +0,0 @@ -package webhooks - -import ( - "context" - "fmt" - "testing" - - "github.com/open-feature/open-feature-operator/apis/core/v1alpha1" - corev1alpha1 "github.com/open-feature/open-feature-operator/apis/core/v1alpha1" - "github.com/stretchr/testify/require" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes/scheme" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client/fake" -) - -func TestFeatureFlagConfigurationWebhook_validateFlagSourceConfiguration(t *testing.T) { - const credentialsName = "credentials-name" - const featureFlagConfigurationName = "test-feature-flag-configuration" - - tests := []struct { - name string - obj corev1alpha1.FeatureFlagConfiguration - secret *corev1.Secret - out error - }{ - { - name: "valid without ServiceProvider", - obj: corev1alpha1.FeatureFlagConfiguration{ - ObjectMeta: metav1.ObjectMeta{ - Name: featureFlagConfigurationName, - Namespace: featureFlagConfigurationNamespace, - }, - Spec: corev1alpha1.FeatureFlagConfigurationSpec{ - FeatureFlagSpec: featureFlagSpec, - }, - }, - out: nil, - }, - { - name: "invalid json", - obj: corev1alpha1.FeatureFlagConfiguration{ - ObjectMeta: metav1.ObjectMeta{ - Name: featureFlagConfigurationName, - Namespace: featureFlagConfigurationNamespace, - }, - Spec: corev1alpha1.FeatureFlagConfigurationSpec{ - FeatureFlagSpec: `{"invalid":json}`, - }, - }, - out: fmt.Errorf("FeatureFlagSpec is not valid JSON: {\"invalid\":json}"), - }, - { - name: "invalid schema", - obj: corev1alpha1.FeatureFlagConfiguration{ - ObjectMeta: metav1.ObjectMeta{ - Name: featureFlagConfigurationName, - Namespace: featureFlagConfigurationNamespace, - }, - Spec: corev1alpha1.FeatureFlagConfigurationSpec{ - FeatureFlagSpec: `{ - "flags":{ - "foo": {} - } - }`, - }, - }, - out: fmt.Errorf("FeatureFlagSpec is not valid JSON: - flags.foo: Must validate one and only one schema (oneOf)\n- flags.foo: state is required\n- flags.foo: defaultVariant is required\n- flags.foo: Must validate all the schemas (allOf)\n"), - }, - { - name: "valid with ServiceProvider", - obj: corev1alpha1.FeatureFlagConfiguration{ - ObjectMeta: metav1.ObjectMeta{ - Name: featureFlagConfigurationName, - Namespace: featureFlagConfigurationNamespace, - }, - Spec: corev1alpha1.FeatureFlagConfigurationSpec{ - FeatureFlagSpec: featureFlagSpec, - ServiceProvider: &corev1alpha1.FeatureFlagServiceProvider{ - Name: "flagd", - Credentials: &corev1.ObjectReference{ - Name: credentialsName, - Namespace: featureFlagConfigurationNamespace, - }, - }, - }, - }, - secret: &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: credentialsName, - Namespace: featureFlagConfigurationNamespace, - }, - }, - out: nil, - }, - { - name: "non-existing secret in ServiceProvider", - obj: corev1alpha1.FeatureFlagConfiguration{ - ObjectMeta: metav1.ObjectMeta{ - Name: featureFlagConfigurationName, - Namespace: featureFlagConfigurationNamespace, - }, - Spec: corev1alpha1.FeatureFlagConfigurationSpec{ - FeatureFlagSpec: featureFlagSpec, - ServiceProvider: &corev1alpha1.FeatureFlagServiceProvider{ - Name: "flagd", - Credentials: &corev1.ObjectReference{ - Name: credentialsName, - Namespace: featureFlagConfigurationNamespace, - }, - }, - }, - }, - out: fmt.Errorf("credentials secret not found"), - }, - } - - err := v1alpha1.AddToScheme(scheme.Scheme) - require.Nil(t, err) - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - validator := FeatureFlagConfigurationValidator{ - Client: fake.NewClientBuilder().WithScheme(scheme.Scheme).Build(), - Log: ctrl.Log.WithName("webhook"), - } - - if tt.secret != nil { - err := validator.Client.Create(context.TODO(), tt.secret) - require.Nil(t, err) - } - - out := validator.validateFlagSourceConfiguration(context.TODO(), tt.obj) - require.Equal(t, tt.out, out) - }) - - } -} diff --git a/webhooks/pod_webhook.go b/webhooks/pod_webhook.go index 4869e34ce..9d02bc64e 100644 --- a/webhooks/pod_webhook.go +++ b/webhooks/pod_webhook.go @@ -3,41 +3,24 @@ package webhooks import ( "context" "encoding/json" + "errors" "fmt" - controllercommon "github.com/open-feature/open-feature-operator/controllers/common" - "github.com/open-feature/open-feature-operator/controllers/common/constant" "net/http" - "reflect" - "strings" "time" - goErr "errors" - "github.com/go-logr/logr" - "github.com/open-feature/open-feature-operator/apis/core/v1alpha1" - "github.com/open-feature/open-feature-operator/pkg/utils" + api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" + "github.com/open-feature/open-feature-operator/common" + "github.com/open-feature/open-feature-operator/common/flagdinjector" + "github.com/open-feature/open-feature-operator/common/flagdproxy" + "github.com/open-feature/open-feature-operator/common/types" + "github.com/open-feature/open-feature-operator/common/utils" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" ) -// we likely want these to be configurable, eventually -const ( - FlagDImagePullPolicy corev1.PullPolicy = "Always" - clusterRoleBindingName string = "open-feature-operator-flagd-kubernetes-sync" - OpenFeatureAnnotationPath = "metadata.annotations.openfeature.dev" - OpenFeatureAnnotationPrefix = "openfeature.dev" - AllowKubernetesSyncAnnotation = "allowkubernetessync" - FlagSourceConfigurationAnnotation = "flagsourceconfiguration" - FeatureFlagConfigurationAnnotation = "featureflagconfiguration" - EnabledAnnotation = "enabled" - ProbeReadiness = "/readyz" - ProbeLiveness = "/healthz" - SourceConfigParam = "--sources" -) - // NOTE: RBAC not needed here. //+kubebuilder:rbac:groups="",resources=pods,verbs=get;list;watch;create;update;patch;delete @@ -52,11 +35,14 @@ type PodMutator struct { decoder *admission.Decoder Log logr.Logger ready bool - FlagdProxyConfig *controllercommon.FlagdProxyConfiguration - FlagdInjector controllercommon.IFlagdContainerInjector + FlagdProxyConfig *flagdproxy.FlagdProxyConfiguration + FlagdInjector flagdinjector.IFlagdContainerInjector + Env types.EnvConfig } // Handle injects the flagd sidecar (if the prerequisites are all met) +// +//nolint:gocyclo func (m *PodMutator) Handle(ctx context.Context, req admission.Request) admission.Response { defer func() { if err := recover(); err != nil { @@ -65,20 +51,13 @@ func (m *PodMutator) Handle(ctx context.Context, req admission.Request) admissio }() pod := &corev1.Pod{} err := m.decoder.Decode(req, pod) - - if pod.Namespace == "" { - pod.Namespace = req.Namespace - } - if err != nil { return admission.Errored(http.StatusBadRequest, err) } - // Check enablement annotations := pod.GetAnnotations() - enabled := m.checkOFEnabled(annotations) - - if !enabled { + // Check enablement + if !checkOFEnabled(annotations) { m.Log.V(2).Info(`openfeature.dev/enabled annotation is not set to "true"`) return admission.Allowed("OpenFeature is disabled") } @@ -89,22 +68,23 @@ func (m *PodMutator) Handle(ctx context.Context, req admission.Request) admissio } // merge any provided flagd specs - flagSourceConfigurationSpec, code, err := m.createFSConfigSpec(ctx, req, annotations, pod) + featureFlagSourceSpec, code, err := m.createFSConfigSpec(ctx, req, annotations, pod) if err != nil { return admission.Errored(code, err) } // Check for the correct clusterrolebinding for the pod if we use the Kubernetes mode - if containsK8sProvider(flagSourceConfigurationSpec.Sources) { + if containsK8sProvider(featureFlagSourceSpec.Sources) { if err := m.FlagdInjector.EnableClusterRoleBinding(ctx, pod.Namespace, pod.Spec.ServiceAccountName); err != nil { return admission.Denied(err.Error()) } } - if err := m.FlagdInjector.InjectFlagd(ctx, &pod.ObjectMeta, &pod.Spec, flagSourceConfigurationSpec); err != nil { - if goErr.Is(err, constant.ErrFlagdProxyNotReady) { + if err := m.FlagdInjector.InjectFlagd(ctx, &pod.ObjectMeta, &pod.Spec, featureFlagSourceSpec); err != nil { + if errors.Is(err, common.ErrFlagdProxyNotReady) { return admission.Denied(err.Error()) } + //test m.Log.Error(err, "unable to inject flagd sidecar") return admission.Errored(http.StatusInternalServerError, err) } @@ -117,72 +97,28 @@ func (m *PodMutator) Handle(ctx context.Context, req admission.Request) admissio return admission.PatchResponseFromRaw(req.Object.Raw, marshaledPod) } -func containsK8sProvider(sources []v1alpha1.Source) bool { - for _, source := range sources { - if source.Provider.IsKubernetes() { - return true - } - } - return false -} - -func (m *PodMutator) createFSConfigSpec(ctx context.Context, req admission.Request, annotations map[string]string, pod *corev1.Pod) (*v1alpha1.FlagSourceConfigurationSpec, int32, error) { +func (m *PodMutator) createFSConfigSpec(ctx context.Context, req admission.Request, annotations map[string]string, pod *corev1.Pod) (*api.FeatureFlagSourceSpec, int32, error) { // Check configuration fscNames := []string{} - val, ok := annotations[fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, FlagSourceConfigurationAnnotation)] + val, ok := annotations[fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationPrefix, common.FeatureFlagSourceAnnotation)] if ok { fscNames = parseList(val) } - flagSourceConfigurationSpec, err := v1alpha1.NewFlagSourceConfigurationSpec() - if err != nil { - m.Log.V(1).Error(err, "unable to parse env var configuration", "webhook", "handle") - return nil, http.StatusBadRequest, err - } + featureFlagSourceSpec := NewFeatureFlagSourceSpec(m.Env) for _, fscName := range fscNames { ns, name := utils.ParseAnnotation(fscName, req.Namespace) - if err != nil { - m.Log.V(1).Info(fmt.Sprintf("failed to parse annotation %s error: %s", fscName, err.Error())) - return nil, http.StatusBadRequest, err - } - fc := m.getFlagSourceConfiguration(ctx, ns, name) - if reflect.DeepEqual(fc, v1alpha1.FlagSourceConfiguration{}) { - m.Log.V(1).Info(fmt.Sprintf("FlagSourceConfiguration could not be found for %s", fscName)) - return nil, http.StatusBadRequest, err - } - flagSourceConfigurationSpec.Merge(&fc.Spec) - } - // maintain backwards compatibility of the openfeature.dev/featureflagconfiguration annotation - ffConfigAnnotation, ffConfigAnnotationOk := pod.GetAnnotations()[fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, FeatureFlagConfigurationAnnotation)] - if ffConfigAnnotationOk { - m.Log.V(1).Info("DEPRECATED: The openfeature.dev/featureflagconfiguration annotation has been superseded by the openfeature.dev/flagsourceconfiguration annotation. " + - "Docs: https://github.com/open-feature/open-feature-operator/blob/main/docs/annotations.md") - if err := m.handleFeatureFlagConfigurationAnnotation(ctx, flagSourceConfigurationSpec, ffConfigAnnotation, req.Namespace); err != nil { - m.Log.Error(err, "unable to handle openfeature.dev/featureflagconfiguration annotation") - return nil, http.StatusInternalServerError, err + fc, err := m.getFeatureFlagSource(ctx, ns, name) + if err != nil { + m.Log.V(1).Info(fmt.Sprintf("FeatureFlagSource could not be found for %s", fscName)) + return nil, http.StatusNotFound, err } + featureFlagSourceSpec.Merge(&fc.Spec) } - return flagSourceConfigurationSpec, 0, nil -} -func (m *PodMutator) checkOFEnabled(annotations map[string]string) bool { - val, ok := annotations[OpenFeatureAnnotationPrefix] - if ok { - m.Log.V(1).Info("DEPRECATED: The openfeature.dev annotation has been superseded by the openfeature.dev/enabled annotation. " + - "Docs: https://github.com/open-feature/open-feature-operator/blob/main/docs/annotations.md") - if val == "enabled" { - return true - } - } - val, ok = annotations[fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, EnabledAnnotation)] - if ok { - if val == "true" { - return true - } - } - return false + return featureFlagSourceSpec, 0, nil } // BackfillPermissions recovers the state of the flagd-kubernetes-sync role binding in the event of upgrade @@ -191,13 +127,13 @@ func (m *PodMutator) BackfillPermissions(ctx context.Context) error { m.ready = true }() for i := 0; i < 5; i++ { - // fetch all pods with the fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, EnabledAnnotation) annotation set to "true" + // fetch all pods with the fmt.Sprintf("%s/%s", OpenFeatureAnnotationPath, EnabledAnnotation) annotation set to "true" podList := &corev1.PodList{} err := m.Client.List(ctx, podList, client.MatchingFields{ - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPath, AllowKubernetesSyncAnnotation): "true", + fmt.Sprintf("%s/%s", common.PodOpenFeatureAnnotationPath, common.AllowKubernetesSyncAnnotation): "true", }) if err != nil { - if !goErr.Is(err, &cache.ErrCacheNotStarted{}) { + if !errors.Is(err, &cache.ErrCacheNotStarted{}) { return err } time.Sleep(1 * time.Second) @@ -212,66 +148,32 @@ func (m *PodMutator) BackfillPermissions(ctx context.Context) error { err, fmt.Sprintf("unable backfill permissions for pod %s/%s", pod.Namespace, pod.Name), "webhook", - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPath, AllowKubernetesSyncAnnotation), + fmt.Sprintf("%s/%s", common.PodOpenFeatureAnnotationPath, common.AllowKubernetesSyncAnnotation), ) } } return nil } - return goErr.New("unable to backfill permissions for the flagd-kubernetes-sync role binding: timeout") -} - -func parseList(s string) []string { - out := []string{} - ss := strings.Split(s, ",") - for i := 0; i < len(ss); i++ { - newS := strings.TrimSpace(ss[i]) - if newS != "" { //function should not add empty values - out = append(out, newS) - } - } - return out + return errors.New("unable to backfill permissions for the flagd-kubernetes-sync role binding: timeout") } -// PodMutator implements admission.DecoderInjector. -// A decoder will be automatically injected. - // InjectDecoder injects the decoder. func (m *PodMutator) InjectDecoder(d *admission.Decoder) error { m.decoder = d return nil } -func (m *PodMutator) getFeatureFlag(ctx context.Context, namespace string, name string) v1alpha1.FeatureFlagConfiguration { - ffConfig := v1alpha1.FeatureFlagConfiguration{} - if err := m.Client.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, &ffConfig); errors.IsNotFound(err) { - return v1alpha1.FeatureFlagConfiguration{} - } - return ffConfig -} - -func (m *PodMutator) getFlagSourceConfiguration(ctx context.Context, namespace string, name string) v1alpha1.FlagSourceConfiguration { - fcConfig := v1alpha1.FlagSourceConfiguration{} - if err := m.Client.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, &fcConfig); errors.IsNotFound(err) { - return v1alpha1.FlagSourceConfiguration{} +func (m *PodMutator) getFeatureFlagSource(ctx context.Context, namespace string, name string) (*api.FeatureFlagSource, error) { + fcConfig := &api.FeatureFlagSource{} + if err := m.Client.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, fcConfig); err != nil { + return nil, err } - return fcConfig + return fcConfig, nil } -func OpenFeatureEnabledAnnotationIndex(o client.Object) []string { - pod := o.(*corev1.Pod) - if pod.ObjectMeta.Annotations == nil { - return []string{ - "false", - } - } - val, ok := pod.ObjectMeta.Annotations[fmt.Sprintf("openfeature.dev/%s", AllowKubernetesSyncAnnotation)] - if ok && val == "true" { - return []string{ - "true", - } - } - return []string{ - "false", +func (m *PodMutator) IsReady(_ *http.Request) error { + if m.ready { + return nil } + return errors.New("pod mutator is not ready") } diff --git a/webhooks/pod_webhook_component_test.go b/webhooks/pod_webhook_component_test.go deleted file mode 100644 index 484de8b43..000000000 --- a/webhooks/pod_webhook_component_test.go +++ /dev/null @@ -1,841 +0,0 @@ -package webhooks - -import ( - "context" - "encoding/json" - "fmt" - controllercommon "github.com/open-feature/open-feature-operator/controllers/common" - "reflect" - "testing" - "time" - - jsonpatch "github.com/evanphx/json-patch/v5" - "github.com/go-logr/logr/testr" - "github.com/open-feature/open-feature-operator/apis/core/v1alpha1" - corev1alpha2 "github.com/open-feature/open-feature-operator/apis/core/v1alpha2" - corev1alpha3 "github.com/open-feature/open-feature-operator/apis/core/v1alpha3" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - admissionv1 "k8s.io/api/admission/v1" - corev1 "k8s.io/api/core/v1" - v1 "k8s.io/api/rbac/v1" - "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme" - errors2 "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" - clientgoscheme "k8s.io/client-go/kubernetes/scheme" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/client/fake" - "sigs.k8s.io/controller-runtime/pkg/webhook/admission" -) - -var ( - k8sClient client.Client - testCtx context.Context - mutator *PodMutator -) - -const ( - mutatePodNamespace = "test-mutate-pod" - defaultPodName = "test-pod" - defaultPodServiceAccountName = "test-pod-service-account" - featureFlagConfigurationName = "test-feature-flag-configuration" - featureFlagConfigurationName2 = "test-feature-flag-configuration-2" - flagSourceConfigurationName = "test-flag-source-configuration" - flagSourceConfigurationName2 = "test-flag-source-configuration-2" - flagSourceConfigurationName3 = "test-flag-source-configuration-3" - flagSourceConfigGrpc = "test-flag-source-grpc" - existingPod1Name = "existing-pod-1" - existingPod1ServiceAccountName = "existing-pod-1-service-account" - existingPod2Name = "existing-pod-2" - existingPod2ServiceAccountName = "existing-pod-2-service-account" - featureFlagConfigurationNamespace = "test-validate-featureflagconfiguration" - featureFlagSpec = ` - { - "flags": { - "new-welcome-message": { - "state": "ENABLED", - "variants": { - "on": true, - "off": false - }, - "defaultVariant": "on" - } - } - }` -) - -func TestPodMutationWebhook_Component(t *testing.T) { - setupTests(t) - t.Run("should backfill role binding subjects when annotated pods already exist in the cluster", func(t *testing.T) { - // this integration test confirms the proper execution of the podMutator.BackfillPermissions method - // this method is responsible for backfilling the subjects of the open-feature-operator-flagd-kubernetes-sync - // cluster role binding, for previously existing pods on startup - // a retry is required on this test as the backfilling occurs asynchronously - var finalError error - for i := 0; i < 3; i++ { - pod1 := getPod(existingPod1Name, t) - pod2 := getPod(existingPod2Name, t) - - handleMutation(t, pod1) - handleMutation(t, pod2) - - rb := getRoleBinding(clusterRoleBindingName, t) - - unexpectedServiceAccount := "" - for _, subject := range rb.Subjects { - if !reflect.DeepEqual(subject, v1.Subject{ - Kind: "ServiceAccount", - APIGroup: "", - Name: existingPod1ServiceAccountName, - Namespace: mutatePodNamespace, - }) && - !reflect.DeepEqual(subject, v1.Subject{ - Kind: "ServiceAccount", - APIGroup: "", - Name: existingPod2ServiceAccountName, - Namespace: mutatePodNamespace, - }) { - unexpectedServiceAccount = subject.Name - } - } - if unexpectedServiceAccount != "" { - finalError = fmt.Errorf("unexpected subject found in role binding, name: %s", unexpectedServiceAccount) - time.Sleep(1 * time.Second) - continue - } - finalError = nil - break - } - require.Nil(t, finalError) - }) - - t.Run("should update cluster role binding's subjects", func(t *testing.T) { - pod := testPod(defaultPodName, defaultPodServiceAccountName, map[string]string{ - OpenFeatureAnnotationPrefix: "enabled", - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, FeatureFlagConfigurationAnnotation): fmt.Sprintf("%s/%s", mutatePodNamespace, featureFlagConfigurationName), - }) - err := k8sClient.Create(testCtx, pod) - require.Nil(t, err) - defer podMutationWebhookCleanup(t) - - handleMutation(t, pod) - - crb := &v1.ClusterRoleBinding{} - err = k8sClient.Get(testCtx, client.ObjectKey{Name: clusterRoleBindingName}, crb) - require.Nil(t, err) - - require.Contains(t, crb.Subjects, v1.Subject{ - Kind: "ServiceAccount", - APIGroup: "", - Name: defaultPodServiceAccountName, - Namespace: mutatePodNamespace, - }) - }) - - t.Run("should create flagd sidecar", func(t *testing.T) { - flagConfig, _ := v1alpha1.NewFlagSourceConfigurationSpec() - pod := testPod(defaultPodName, defaultPodServiceAccountName, map[string]string{ - OpenFeatureAnnotationPrefix: "enabled", - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, FeatureFlagConfigurationAnnotation): fmt.Sprintf("%s/%s", mutatePodNamespace, featureFlagConfigurationName), - }) - err := k8sClient.Create(testCtx, pod) - defer podMutationWebhookCleanup(t) - require.Nil(t, err) - pod = handleMutation(t, pod) - require.Equal(t, "true", pod.Annotations["openfeature.dev/allowkubernetessync"]) - require.Equal(t, 2, len(pod.Spec.Containers)) - require.Equal(t, pod.Spec.Containers[1].Name, "flagd") - require.Equal(t, pod.Spec.Containers[1].Image, fmt.Sprintf("%s:%s", flagConfig.Image, flagConfig.Tag)) - require.Equal(t, pod.Spec.Containers[1].Args, []string{ - "start", "--sources", "[{\"uri\":\"test-mutate-pod/test-feature-flag-configuration\",\"provider\":\"kubernetes\"}]", - }) - require.Equal(t, pod.Spec.Containers[1].ImagePullPolicy, FlagDImagePullPolicy) - require.Equal(t, pod.Spec.Containers[1].Env, []corev1.EnvVar{ - {Name: "FLAGD_LOG_LEVEL", Value: "dev"}, - }) - - require.Equal(t, []corev1.ContainerPort{ - { - Name: "metrics", - ContainerPort: 8014, - }, - }, pod.Spec.Containers[1].Ports) - - // Validate probes. Default config will set them - liveness := pod.Spec.Containers[1].LivenessProbe - require.NotNil(t, liveness) - require.Equal(t, ProbeLiveness, liveness.HTTPGet.Path) - - readiness := pod.Spec.Containers[1].ReadinessProbe - require.NotNil(t, readiness) - require.Equal(t, ProbeReadiness, readiness.HTTPGet.Path) - - }) - - t.Run("should create flagd sidecar even if openfeature.dev/featureflagconfiguration annotation isn't present", func(t *testing.T) { - pod := testPod(defaultPodName, defaultPodServiceAccountName, map[string]string{ - OpenFeatureAnnotationPrefix: "enabled", - }) - - err := k8sClient.Create(testCtx, pod) - require.Nil(t, err) - defer podMutationWebhookCleanup(t) - pod = handleMutation(t, pod) - - require.Equal(t, 2, len(pod.Spec.Containers)) - }) - - t.Run("should not create flagd sidecar if openfeature.dev annotation is disabled", func(t *testing.T) { - pod := testPod(defaultPodName, defaultPodServiceAccountName, map[string]string{ - OpenFeatureAnnotationPrefix: "disabled", - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, FeatureFlagConfigurationAnnotation): fmt.Sprintf("%s/%s", mutatePodNamespace, featureFlagConfigurationName), - }) - err := k8sClient.Create(testCtx, pod) - require.Nil(t, err) - defer podMutationWebhookCleanup(t) - - pod = handleMutation(t, pod) - - require.Equal(t, len(pod.Spec.Containers), 1) - - }) - - t.Run("should fail if pod has no owner references", func(t *testing.T) { - pod := testPod(defaultPodName, defaultPodServiceAccountName, map[string]string{ - OpenFeatureAnnotationPrefix: "enabled", - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, FeatureFlagConfigurationAnnotation): fmt.Sprintf("%s/%s", mutatePodNamespace, featureFlagConfigurationName), - }) - pod.OwnerReferences = nil - err := k8sClient.Create(testCtx, pod) - require.Nil(t, err) - defer podMutationWebhookCleanup(t) - handleError(t, pod) - }) - - t.Run("should fail if service account not found", func(t *testing.T) { - pod := testPod(defaultPodName, defaultPodServiceAccountName, map[string]string{ - OpenFeatureAnnotationPrefix: "enabled", - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, FeatureFlagConfigurationAnnotation): fmt.Sprintf("%s/%s", mutatePodNamespace, featureFlagConfigurationName), - }) - pod.Spec.ServiceAccountName = "foo" - err := k8sClient.Create(testCtx, pod) - require.Nil(t, err) - defer podMutationWebhookCleanup(t) - handleError(t, pod) - }) - - t.Run("should create config map if sync provider is filepath", func(t *testing.T) { - ffConfig := &v1alpha1.FeatureFlagConfiguration{} - err := k8sClient.Get( - testCtx, client.ObjectKey{Name: featureFlagConfigurationName, Namespace: mutatePodNamespace}, ffConfig, - ) - require.Nil(t, err) - - ffConfig.Spec = v1alpha1.FeatureFlagConfigurationSpec{ - SyncProvider: &v1alpha1.FeatureFlagSyncProvider{ - Name: string(v1alpha1.SyncProviderFilepath), - }, - } - err = k8sClient.Update(testCtx, ffConfig) - require.Nil(t, err) - - pod := testPod(defaultPodName, defaultPodServiceAccountName, map[string]string{ - OpenFeatureAnnotationPrefix: "enabled", - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, FeatureFlagConfigurationAnnotation): fmt.Sprintf("%s/%s", mutatePodNamespace, featureFlagConfigurationName), - }) - err = k8sClient.Create(testCtx, pod) - require.Nil(t, err) - - defer func() { - podMutationWebhookCleanup(t) - // reset FeatureFlagConfiguration - ffConfig.Spec.SyncProvider = nil - err = k8sClient.Update(testCtx, ffConfig) - require.Nil(t, err) - }() - - handleMutation(t, pod) - - cm := &corev1.ConfigMap{} - err = k8sClient.Get(testCtx, client.ObjectKey{ - Name: featureFlagConfigurationName, - Namespace: mutatePodNamespace, - }, cm) - require.Nil(t, err) - - require.Equal(t, cm.Name, featureFlagConfigurationName) - require.Equal(t, cm.Namespace, mutatePodNamespace) - require.EqualValues(t, map[string]string{ - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, FeatureFlagConfigurationAnnotation): featureFlagConfigurationName, - }, cm.Annotations) - require.Equal(t, 2, len(cm.OwnerReferences)) - - require.Equal(t, cm.Data, map[string]string{ - fmt.Sprintf("%s_%s.flagd.json", mutatePodNamespace, featureFlagConfigurationName): ffConfig.Spec.FeatureFlagSpec, - }) - - }) - - t.Run("should not panic if flagDSpec isn't provided", func(t *testing.T) { - ffConfigName := "feature-flag-configuration-panic-test" - ffConfig := &v1alpha1.FeatureFlagConfiguration{} - ffConfig.Namespace = mutatePodNamespace - ffConfig.Name = ffConfigName - ffConfig.Spec.FeatureFlagSpec = featureFlagSpec - err := k8sClient.Create(testCtx, ffConfig) - require.Nil(t, err) - - pod := testPod(defaultPodName, defaultPodServiceAccountName, map[string]string{ - OpenFeatureAnnotationPrefix: "enabled", - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, FeatureFlagConfigurationAnnotation): fmt.Sprintf("%s/%s", mutatePodNamespace, ffConfigName), - }) - err = k8sClient.Create(testCtx, pod) - require.Nil(t, err) - - podMutationWebhookCleanup(t) - err = k8sClient.Delete(testCtx, ffConfig, client.GracePeriodSeconds(0)) - require.Nil(t, err) - }) - - t.Run(`should create flagd sidecar if openfeature.dev/enabled annotation is "true"`, func(t *testing.T) { - pod := testPod(defaultPodName, defaultPodServiceAccountName, map[string]string{ - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, EnabledAnnotation): "true", - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, FeatureFlagConfigurationAnnotation): fmt.Sprintf("%s/%s", mutatePodNamespace, featureFlagConfigurationName), - }) - err := k8sClient.Create(testCtx, pod) - require.Nil(t, err) - defer podMutationWebhookCleanup(t) - - pod = handleMutation(t, pod) - - require.Equal(t, 2, len(pod.Spec.Containers)) - - }) - - t.Run(`should only write non default flagsourceconfiguration env vars to the flagd container`, func(t *testing.T) { - pod := testPod(defaultPodName, defaultPodServiceAccountName, map[string]string{ - OpenFeatureAnnotationPrefix: "enabled", - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, FeatureFlagConfigurationAnnotation): fmt.Sprintf("%s/%s", mutatePodNamespace, featureFlagConfigurationName), - "openfeature.dev/flagsourceconfiguration": fmt.Sprintf("%s/%s", mutatePodNamespace, flagSourceConfigurationName), - }) - err := k8sClient.Create(testCtx, pod) - require.Nil(t, err) - defer podMutationWebhookCleanup(t) - - pod = handleMutation(t, pod) - require.Equal(t, []corev1.EnvVar{ - {Name: "FLAGD_METRICS_PORT", Value: "8081"}, - {Name: "FLAGD_PORT", Value: "8080"}, - {Name: "FLAGD_EVALUATOR", Value: "yaml"}, - {Name: "FLAGD_SOCKET_PATH", Value: "/tmp/flag-source.sock"}, - {Name: "FLAGD_LOG_FORMAT", Value: "console"}, - }, - pod.Spec.Containers[1].Env) - - }) - - t.Run(`should use env var configuration to overwrite flagsourceconfiguration defaults`, func(t *testing.T) { - t.Setenv(v1alpha1.SidecarEnvVarPrefix, "MY_SIDECAR") - t.Setenv(fmt.Sprintf("%s_%s", v1alpha1.InputConfigurationEnvVarPrefix, v1alpha1.SidecarMetricPortEnvVar), "10") - t.Setenv(fmt.Sprintf("%s_%s", v1alpha1.InputConfigurationEnvVarPrefix, v1alpha1.SidecarPortEnvVar), "20") - t.Setenv(fmt.Sprintf("%s_%s", v1alpha1.InputConfigurationEnvVarPrefix, v1alpha1.SidecarSocketPathEnvVar), "socket") - t.Setenv(fmt.Sprintf("%s_%s", v1alpha1.InputConfigurationEnvVarPrefix, v1alpha1.SidecarEvaluatorEnvVar), "evaluator") - t.Setenv(fmt.Sprintf("%s_%s", v1alpha1.InputConfigurationEnvVarPrefix, v1alpha1.SidecarImageEnvVar), "image") - t.Setenv(fmt.Sprintf("%s_%s", v1alpha1.InputConfigurationEnvVarPrefix, v1alpha1.SidecarVersionEnvVar), "version") - t.Setenv(fmt.Sprintf("%s_%s", v1alpha1.InputConfigurationEnvVarPrefix, v1alpha1.SidecarDefaultSyncProviderEnvVar), "filepath") - t.Setenv(fmt.Sprintf("%s_%s", v1alpha1.InputConfigurationEnvVarPrefix, v1alpha1.SidecarProviderArgsEnvVar), "key=value,key2=value2") - t.Setenv(fmt.Sprintf("%s_%s", v1alpha1.InputConfigurationEnvVarPrefix, v1alpha1.SidecarLogFormatEnvVar), "yaml") - - // Override probes - disabled - t.Setenv(fmt.Sprintf("%s_%s", v1alpha1.InputConfigurationEnvVarPrefix, v1alpha1.SidecarProbesEnabledVar), "false") - - pod := testPod(defaultPodName, defaultPodServiceAccountName, map[string]string{ - OpenFeatureAnnotationPrefix: "enabled", - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, FeatureFlagConfigurationAnnotation): fmt.Sprintf("%s/%s", mutatePodNamespace, featureFlagConfigurationName), - }) - err := k8sClient.Create(testCtx, pod) - require.Nil(t, err) - defer podMutationWebhookCleanup(t) - - pod = handleMutation(t, pod) - require.Equal(t, pod.Spec.Containers[1].Env, []corev1.EnvVar{ - {Name: "MY_SIDECAR_METRICS_PORT", Value: "10"}, - {Name: "MY_SIDECAR_PORT", Value: "20"}, - {Name: "MY_SIDECAR_EVALUATOR", Value: "evaluator"}, - {Name: "MY_SIDECAR_SOCKET_PATH", Value: "socket"}, - {Name: "MY_SIDECAR_LOG_FORMAT", Value: "yaml"}, - }) - require.Equal(t, pod.Spec.Containers[1].Image, "image:version") - require.Equal(t, pod.Spec.Containers[1].Args, []string{ - "start", - SourceConfigParam, - "[{\"uri\":\"/etc/flagd/test-mutate-pod_test-feature-flag-configuration/test-mutate-pod_test-feature-flag-configuration.flagd.json\",\"provider\":\"file\"}]", - "--sync-provider-args", - "key=value", - "--sync-provider-args", - "key2=value2", - }) - - // Validate probes - disabled - require.Nil(t, pod.Spec.Containers[1].LivenessProbe) - require.Nil(t, pod.Spec.Containers[1].ReadinessProbe) - - }) - - t.Run(`should overwrite env var configuration with flagsourceconfiguration values`, func(t *testing.T) { - t.Setenv(v1alpha1.SidecarEnvVarPrefix, "") - t.Setenv(fmt.Sprintf("%s_%s", v1alpha1.InputConfigurationEnvVarPrefix, v1alpha1.SidecarMetricPortEnvVar), "") - t.Setenv(fmt.Sprintf("%s_%s", v1alpha1.InputConfigurationEnvVarPrefix, v1alpha1.SidecarPortEnvVar), "") - t.Setenv(fmt.Sprintf("%s_%s", v1alpha1.InputConfigurationEnvVarPrefix, v1alpha1.SidecarSocketPathEnvVar), "") - t.Setenv(fmt.Sprintf("%s_%s", v1alpha1.InputConfigurationEnvVarPrefix, v1alpha1.SidecarEvaluatorEnvVar), "") - t.Setenv(fmt.Sprintf("%s_%s", v1alpha1.InputConfigurationEnvVarPrefix, v1alpha1.SidecarImageEnvVar), "") - t.Setenv(fmt.Sprintf("%s_%s", v1alpha1.InputConfigurationEnvVarPrefix, v1alpha1.SidecarVersionEnvVar), "") - t.Setenv(fmt.Sprintf("%s_%s", v1alpha1.InputConfigurationEnvVarPrefix, v1alpha1.SidecarDefaultSyncProviderEnvVar), "") - t.Setenv(fmt.Sprintf("%s_%s", v1alpha1.InputConfigurationEnvVarPrefix, v1alpha1.SidecarProviderArgsEnvVar), "key=value,key2=value2") - t.Setenv(fmt.Sprintf("%s_%s", v1alpha1.InputConfigurationEnvVarPrefix, v1alpha1.SidecarLogFormatEnvVar), "") - - pod := testPod(defaultPodName, defaultPodServiceAccountName, map[string]string{ - OpenFeatureAnnotationPrefix: "enabled", - "openfeature.dev/flagsourceconfiguration": fmt.Sprintf("%s/%s", mutatePodNamespace, flagSourceConfigurationName), - }) - err := k8sClient.Create(testCtx, pod) - require.Nil(t, err) - defer podMutationWebhookCleanup(t) - - pod = handleMutation(t, pod) - require.Equal(t, pod.Spec.Containers[1].Env, []corev1.EnvVar{ - {Name: "FLAGD_METRICS_PORT", Value: "8081"}, - {Name: "FLAGD_PORT", Value: "8080"}, - {Name: "FLAGD_EVALUATOR", Value: "yaml"}, - {Name: "FLAGD_SOCKET_PATH", Value: "/tmp/flag-source.sock"}, - {Name: "FLAGD_LOG_FORMAT", Value: "console"}, - }) - require.Equal(t, pod.Spec.Containers[1].Image, "new-image:latest") - require.Equal(t, pod.Spec.Containers[1].Args, []string{ - "start", - SourceConfigParam, - "[{\"uri\":\"not-real.com\",\"provider\":\"http\"}]", - "--sync-provider-args", - "key=value", - "--sync-provider-args", - "key2=value2", - "--sync-provider-args", - "key3=val3", - }) - }) - - t.Run("should create flagd sidecar using flagsourceconfiguration", func(t *testing.T) { - t.Setenv(v1alpha1.SidecarEnvVarPrefix, "") - t.Setenv(fmt.Sprintf("%s_%s", v1alpha1.InputConfigurationEnvVarPrefix, v1alpha1.SidecarMetricPortEnvVar), "") - t.Setenv(fmt.Sprintf("%s_%s", v1alpha1.InputConfigurationEnvVarPrefix, v1alpha1.SidecarPortEnvVar), "") - t.Setenv(fmt.Sprintf("%s_%s", v1alpha1.InputConfigurationEnvVarPrefix, v1alpha1.SidecarSocketPathEnvVar), "") - t.Setenv(fmt.Sprintf("%s_%s", v1alpha1.InputConfigurationEnvVarPrefix, v1alpha1.SidecarEvaluatorEnvVar), "") - t.Setenv(fmt.Sprintf("%s_%s", v1alpha1.InputConfigurationEnvVarPrefix, v1alpha1.SidecarImageEnvVar), "") - t.Setenv(fmt.Sprintf("%s_%s", v1alpha1.InputConfigurationEnvVarPrefix, v1alpha1.SidecarVersionEnvVar), "") - t.Setenv(fmt.Sprintf("%s_%s", v1alpha1.InputConfigurationEnvVarPrefix, v1alpha1.SidecarDefaultSyncProviderEnvVar), "") - t.Setenv(fmt.Sprintf("%s_%s", v1alpha1.InputConfigurationEnvVarPrefix, v1alpha1.SidecarProviderArgsEnvVar), "") - flagConfig, _ := v1alpha1.NewFlagSourceConfigurationSpec() - pod := testPod(defaultPodName, defaultPodServiceAccountName, map[string]string{ - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, EnabledAnnotation): "true", - "openfeature.dev/flagsourceconfiguration": fmt.Sprintf("%s/%s", mutatePodNamespace, flagSourceConfigurationName2), - }) - err := k8sClient.Create(testCtx, pod) - require.Nil(t, err) - defer podMutationWebhookCleanup(t) - - pod = handleMutation(t, pod) - require.Equal(t, pod.Annotations["openfeature.dev/allowkubernetessync"], "true") - require.Equal(t, len(pod.Spec.Containers), 2) - require.Equal(t, pod.Spec.Containers[1].Name, "flagd") - require.Equal(t, pod.Spec.Containers[1].Image, fmt.Sprintf("%s:%s", flagConfig.Image, flagConfig.Tag)) - require.Equal(t, pod.Spec.Containers[1].Args, []string{ - "start", - SourceConfigParam, - "[{\"uri\":\"test-mutate-pod/test-feature-flag-configuration\",\"provider\":\"kubernetes\"}," + - "{\"uri\":\"/etc/flagd/test-mutate-pod_test-feature-flag-configuration-2/test-mutate-pod_test-feature-flag-configuration-2.flagd.json\",\"provider\":\"file\"}]", - }) - require.Equal(t, pod.Spec.Containers[1].ImagePullPolicy, FlagDImagePullPolicy) - require.Equal(t, pod.Spec.Containers[1].Ports, []corev1.ContainerPort{ - { - Name: "metrics", - ContainerPort: 8014, - }, - }) - - }) - - t.Run("should not create flagd sidecar if flagsourceconfiguration does not exist", func(t *testing.T) { - pod := testPod(defaultPodName, defaultPodServiceAccountName, map[string]string{ - OpenFeatureAnnotationPrefix: "enabled", - "openfeature.dev/flagsourceconfiguration": "im-not-real", - }) - err := k8sClient.Create(testCtx, pod) - require.Nil(t, err) - defer podMutationWebhookCleanup(t) - handleError(t, pod) - }) - - t.Run("should not create flagd sidecar if flagsourceconfiguration contains a source that does not exist", func(t *testing.T) { - pod := testPod(defaultPodName, defaultPodServiceAccountName, map[string]string{ - OpenFeatureAnnotationPrefix: "enabled", - "openfeature.dev/flagsourceconfiguration": fmt.Sprintf("%s/%s", mutatePodNamespace, flagSourceConfigurationName3), - }) - err := k8sClient.Create(testCtx, pod) - require.Nil(t, err) - defer podMutationWebhookCleanup(t) - handleError(t, pod) - }) - - t.Run(`should use defaultSyncProvider if one isn't provided`, func(t *testing.T) { - t.Setenv(fmt.Sprintf("%s_%s", v1alpha1.InputConfigurationEnvVarPrefix, v1alpha1.SidecarDefaultSyncProviderEnvVar), "filepath") - - pod := testPod(defaultPodName, defaultPodServiceAccountName, map[string]string{ - OpenFeatureAnnotationPrefix: "enabled", - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, FlagSourceConfigurationAnnotation): fmt.Sprintf("%s/%s", mutatePodNamespace, flagSourceConfigurationName2), - }) - err := k8sClient.Create(testCtx, pod) - require.Nil(t, err) - defer podMutationWebhookCleanup(t) - - pod = handleMutation(t, pod) - require.Equal(t, pod.Spec.Containers[1].Args, []string{ - "start", - SourceConfigParam, - "[{\"uri\":\"/etc/flagd/test-mutate-pod_test-feature-flag-configuration/test-mutate-pod_test-feature-flag-configuration.flagd.json\",\"provider\":\"file\"}," + - "{\"uri\":\"/etc/flagd/test-mutate-pod_test-feature-flag-configuration-2/test-mutate-pod_test-feature-flag-configuration-2.flagd.json\",\"provider\":\"file\"}]", - }) - - }) - - t.Run("should create a valid grpc source configuration", func(t *testing.T) { - pod := testPod(defaultPodName, defaultPodServiceAccountName, map[string]string{ - OpenFeatureAnnotationPrefix: "enabled", - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, FlagSourceConfigurationAnnotation): fmt.Sprintf("%s/%s", mutatePodNamespace, flagSourceConfigGrpc), - }) - err := k8sClient.Create(testCtx, pod) - require.Nil(t, err) - defer podMutationWebhookCleanup(t) - - pod = handleMutation(t, pod) - require.Equal(t, pod.Spec.Containers[1].Args, []string{ - "start", - SourceConfigParam, - "[{\"uri\":\"grpc-service:9090\",\"provider\":\"grpc\",\"certPath\":\"/tmp/certs\",\"tls\":true,\"providerID\":\"myapp\",\"selector\":\"source=database\"}]", - }) - }) -} - -func handleError(t *testing.T, pod *corev1.Pod) { - _, res := triggerHandler(t, pod) - require.False(t, res.Allowed) -} - -// calls handle of the webhook and returns the mutated pod according to the resulting patch -func handleMutation(t *testing.T, pod *corev1.Pod) *corev1.Pod { - - rawPod, res := triggerHandler(t, pod) - - data, err := json.Marshal(res.Patches) - assert.Nil(t, err) - - patch, err := jsonpatch.DecodePatch(data) - assert.Nil(t, err) - - patchedPod, err := patch.Apply(rawPod) - assert.Nil(t, err) - - newPod := &corev1.Pod{} - err = json.Unmarshal(patchedPod, newPod) - assert.Nil(t, err) - - return newPod -} - -func triggerHandler(t *testing.T, pod *corev1.Pod) ([]byte, admission.Response) { - rawPod, err := json.Marshal(pod) - require.Nil(t, err) - req := admission.Request{ - AdmissionRequest: admissionv1.AdmissionRequest{ - UID: pod.UID, - Object: runtime.RawExtension{ - Raw: rawPod, - Object: pod, - }, - }, - } - - res := mutator.Handle(context.TODO(), req) - return rawPod, res -} - -func setupTests(t *testing.T) { - - utilruntime.Must(clientgoscheme.AddToScheme(scheme.Scheme)) - utilruntime.Must(v1alpha1.AddToScheme(scheme.Scheme)) - utilruntime.Must(corev1alpha2.AddToScheme(scheme.Scheme)) - utilruntime.Must(corev1alpha3.AddToScheme(scheme.Scheme)) - - annotationsSyncIndexer := func(obj client.Object) []string { - res := obj.GetAnnotations()[fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, AllowKubernetesSyncAnnotation)] - return []string{res} - } - - featureflagIndexer := func(obj client.Object) []string { - res := obj.GetAnnotations()["openfeature.dev/featureflagconfiguration"] - return []string{res} - } - enabledIndexer := func(obj client.Object) []string { - res := obj.GetAnnotations()["openfeature.dev/enabled"] - return []string{res} - } - - k8sClient = fake.NewClientBuilder(). - WithScheme(scheme.Scheme). - WithIndex( - &corev1.Pod{}, - "metadata.annotations.openfeature.dev/allowkubernetessync", - annotationsSyncIndexer). - WithIndex( - &corev1.Pod{}, - "metadata.annotations.openfeature.dev/featureflagconfiguration", - featureflagIndexer). - WithIndex( - &corev1.Pod{}, - "metadata.annotations.openfeature.dev/enabled", - enabledIndexer). - Build() - - decoder, err := admission.NewDecoder(scheme.Scheme) - require.Nil(t, err) - - setupValidateFeatureFlagConfigurationResources(t) - setupPreviouslyExistingPods(t) - setupMutatePodResources(t) - - mutator = &PodMutator{ - Client: k8sClient, - decoder: decoder, - Log: testr.New(t), - FlagdInjector: &controllercommon.FlagdContainerInjector{ - Client: k8sClient, - Logger: testr.New(t), - FlagDResourceRequirements: corev1.ResourceRequirements{}, - }, - ready: false, - } - -} - -func setupValidateFeatureFlagConfigurationResources(t *testing.T) { - ns := &corev1.Namespace{} - ns.Name = featureFlagConfigurationNamespace - err := k8sClient.Create(testCtx, ns) - require.Nil(t, err) -} - -// // Sets up environment to simulate an upgrade, with an existing pod already in the cluster -func setupPreviouslyExistingPods(t *testing.T) { - ns := &corev1.Namespace{} - ns.Name = mutatePodNamespace - err := k8sClient.Create(testCtx, ns) - require.Nil(t, err) - - svcAccount := &corev1.ServiceAccount{} - svcAccount.Namespace = mutatePodNamespace - svcAccount.Name = existingPod1ServiceAccountName - err = k8sClient.Create(testCtx, svcAccount) - require.Nil(t, err) - - svcAccount = &corev1.ServiceAccount{} - svcAccount.Namespace = mutatePodNamespace - svcAccount.Name = existingPod2ServiceAccountName - err = k8sClient.Create(testCtx, svcAccount) - require.Nil(t, err) - - clusterRoleBinding := &v1.ClusterRoleBinding{} - clusterRoleBinding.Name = clusterRoleBindingName - clusterRoleBinding.APIVersion = "rbac.authorization.k8s.io/v1" - clusterRoleBinding.RoleRef = v1.RoleRef{ - APIGroup: "", - Kind: "ClusterRole", - Name: clusterRoleBindingName, - } - err = k8sClient.Create(testCtx, clusterRoleBinding) - require.Nil(t, err) - - existingPod := testPod(existingPod1Name, existingPod1ServiceAccountName, map[string]string{ - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, EnabledAnnotation): "true", - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, FeatureFlagConfigurationAnnotation): fmt.Sprintf("%s/%s", mutatePodNamespace, featureFlagConfigurationName), - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, AllowKubernetesSyncAnnotation): "true", - }) - err = k8sClient.Create(testCtx, existingPod) - require.Nil(t, err) - - existingPod = testPod(existingPod2Name, existingPod2ServiceAccountName, map[string]string{ - OpenFeatureAnnotationPrefix: "enabled", - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, FeatureFlagConfigurationAnnotation): fmt.Sprintf("%s/%s", mutatePodNamespace, featureFlagConfigurationName), - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, AllowKubernetesSyncAnnotation): "true", - }) - err = k8sClient.Create(testCtx, existingPod) - require.Nil(t, err) -} - -func setupMutatePodResources(t *testing.T) { - svcAccount := &corev1.ServiceAccount{} - svcAccount.Namespace = mutatePodNamespace - svcAccount.Name = defaultPodServiceAccountName - err := k8sClient.Create(testCtx, svcAccount) - require.Nil(t, err) - - ffConfig := &v1alpha1.FeatureFlagConfiguration{} - ffConfig.Namespace = mutatePodNamespace - ffConfig.Name = featureFlagConfigurationName - ffConfig.Spec.FlagDSpec = &v1alpha1.FlagDSpec{Envs: []corev1.EnvVar{ - {Name: "LOG_LEVEL", Value: "dev"}, - }} - ffConfig.Spec.FeatureFlagSpec = featureFlagSpec - err = k8sClient.Create(testCtx, ffConfig) - require.Nil(t, err) - - fsConfig := &v1alpha1.FlagSourceConfiguration{} - fsConfig.Namespace = mutatePodNamespace - fsConfig.Name = flagSourceConfigurationName - fsConfig.Spec.Port = 8080 - fsConfig.Spec.Evaluator = "yaml" - fsConfig.Spec.Image = "new-image" - fsConfig.Spec.Tag = "latest" - fsConfig.Spec.MetricsPort = 8081 - fsConfig.Spec.SocketPath = "/tmp/flag-source.sock" - fsConfig.Spec.SyncProviderArgs = []string{ - "key3=val3", - } - fsConfig.Spec.LogFormat = "console" - fsConfig.Spec.Sources = []v1alpha1.Source{ - { - Source: "not-real.com", - Provider: "http", - }, - } - err = k8sClient.Create(testCtx, fsConfig) - require.Nil(t, err) - - ffConfig2 := &v1alpha1.FeatureFlagConfiguration{} - ffConfig2.Namespace = mutatePodNamespace - ffConfig2.Name = featureFlagConfigurationName2 - ffConfig2.Spec.FeatureFlagSpec = featureFlagSpec - err = k8sClient.Create(testCtx, ffConfig2) - require.Nil(t, err) - - fsConfig2 := &v1alpha1.FlagSourceConfiguration{} - fsConfig2.Namespace = mutatePodNamespace - fsConfig2.Name = flagSourceConfigurationName2 - fsConfig2.Spec.Sources = []v1alpha1.Source{ - { - Source: fmt.Sprintf("%s/%s", mutatePodNamespace, featureFlagConfigurationName), - }, - { - Source: fmt.Sprintf("%s/%s", mutatePodNamespace, featureFlagConfigurationName2), - Provider: v1alpha1.SyncProviderFilepath, - }, - } - err = k8sClient.Create(testCtx, fsConfig2) - require.Nil(t, err) - - fsConfig3 := &v1alpha1.FlagSourceConfiguration{} - fsConfig3.Namespace = mutatePodNamespace - fsConfig3.Name = flagSourceConfigurationName3 - fsConfig3.Spec.Sources = []v1alpha1.Source{ - { - Source: fmt.Sprintf("%s/%s", mutatePodNamespace, featureFlagConfigurationName2), - Provider: v1alpha1.SyncProviderKubernetes, - }, - { - Source: "i don't exist", - Provider: v1alpha1.SyncProviderFilepath, - }, - } - err = k8sClient.Create(testCtx, fsConfig3) - require.Nil(t, err) - - fsConfigGrpc := &v1alpha1.FlagSourceConfiguration{} - fsConfigGrpc.Namespace = mutatePodNamespace - fsConfigGrpc.Name = flagSourceConfigGrpc - fsConfigGrpc.Spec.Sources = []v1alpha1.Source{ - { - Source: "grpc-service:9090", - Provider: v1alpha1.SyncProviderGrpc, - TLS: true, - ProviderID: "myapp", - Selector: "source=database", - CertPath: "/tmp/certs", - }, - } - err = k8sClient.Create(testCtx, fsConfigGrpc) - require.Nil(t, err) -} - -func testPod(podName string, serviceAccountName string, annotations map[string]string) *corev1.Pod { - pod := &corev1.Pod{} - pod.Namespace = mutatePodNamespace - pod.Name = podName - pod.Annotations = annotations - - pod.Spec.Containers = []corev1.Container{ - { - Name: "container1", - Image: "ubuntu", - }, - } - pod.Spec.ServiceAccountName = serviceAccountName - - // In reality something like a Deployment would take ownership of pod creation. - // A limitation of envtest is that inbuilt kubernetes controllers like deployment controllers aren't available. - // Below simulates a pod that has ownership. - pod.OwnerReferences = []metav1.OwnerReference{ - { - Name: "simulated-owner", - Kind: "deployment", - APIVersion: "v1", - UID: "1f08bbbf-edb4-452a-9ffd-1898dd24c5b8", - }, - } - return pod -} - -func getPod(podName string, t *testing.T) *corev1.Pod { - pod := &corev1.Pod{} - name := types.NamespacedName{ - Namespace: mutatePodNamespace, - Name: podName, - } - err := k8sClient.Get(testCtx, name, pod) - require.Nil(t, err) - return pod -} - -func getRoleBinding(roleBindingName string, t *testing.T) *v1.ClusterRoleBinding { - roleBinding := &v1.ClusterRoleBinding{} - name := types.NamespacedName{ - Name: roleBindingName, - } - err := k8sClient.Get(testCtx, name, roleBinding) - require.Nil(t, err) - return roleBinding -} - -func podMutationWebhookCleanup(t *testing.T) { - pod := &corev1.Pod{} - pod.Namespace = mutatePodNamespace - pod.Name = defaultPodName - err := k8sClient.Delete(testCtx, pod, client.GracePeriodSeconds(0)) - require.Nil(t, err) - require.Eventually(t, func() bool { - err = k8sClient.Get(testCtx, types.NamespacedName{ - Name: defaultPodName, Namespace: mutatePodNamespace, - }, pod) - return errors2.IsNotFound(err) - }, time.Second, 10*time.Millisecond) -} diff --git a/webhooks/pod_webhook_deprecated.go b/webhooks/pod_webhook_deprecated.go deleted file mode 100644 index 4cc70560c..000000000 --- a/webhooks/pod_webhook_deprecated.go +++ /dev/null @@ -1,57 +0,0 @@ -package webhooks - -import ( - "context" - "fmt" - "reflect" - - v1alpha1 "github.com/open-feature/open-feature-operator/apis/core/v1alpha1" - "github.com/open-feature/open-feature-operator/pkg/utils" -) - -func (m *PodMutator) handleFeatureFlagConfigurationAnnotation(ctx context.Context, fcConfig *v1alpha1.FlagSourceConfigurationSpec, ffconfigAnnotation string, defaultNamespace string) error { - for _, ffName := range parseList(ffconfigAnnotation) { - ns, name := utils.ParseAnnotation(ffName, defaultNamespace) - fsConfig := m.getFeatureFlag(ctx, ns, name) - if reflect.DeepEqual(fsConfig, v1alpha1.FeatureFlagConfiguration{}) { - return fmt.Errorf("FeatureFlagConfiguration %s not found", ffName) - } - if fsConfig.Spec.FlagDSpec != nil { - if len(fsConfig.Spec.FlagDSpec.Envs) > 0 { - fcConfig.EnvVars = append(fsConfig.Spec.FlagDSpec.Envs, fcConfig.EnvVars...) - } - if fsConfig.Spec.FlagDSpec.MetricsPort != 0 && fcConfig.MetricsPort == v1alpha1.DefaultMetricPort { - fcConfig.MetricsPort = fsConfig.Spec.FlagDSpec.MetricsPort - } - } - switch { - case fsConfig.Spec.SyncProvider == nil: - fcConfig.Sources = append(fcConfig.Sources, v1alpha1.Source{ - Provider: fcConfig.DefaultSyncProvider, - Source: ffName, - }) - case v1alpha1.SyncProviderType(fsConfig.Spec.SyncProvider.Name).IsKubernetes(): - fcConfig.Sources = append(fcConfig.Sources, v1alpha1.Source{ - Provider: v1alpha1.SyncProviderKubernetes, - Source: ffName, - }) - case v1alpha1.SyncProviderType(fsConfig.Spec.SyncProvider.Name).IsFilepath(): - fcConfig.Sources = append(fcConfig.Sources, v1alpha1.Source{ - Provider: v1alpha1.SyncProviderFilepath, - Source: ffName, - }) - case v1alpha1.SyncProviderType(fsConfig.Spec.SyncProvider.Name).IsHttp(): - if fsConfig.Spec.SyncProvider.HttpSyncConfiguration == nil { - return fmt.Errorf("FeatureFlagConfiguration %s is missing HttpSyncConfiguration", ffName) - } - fcConfig.Sources = append(fcConfig.Sources, v1alpha1.Source{ - Provider: v1alpha1.SyncProviderHttp, - Source: fsConfig.Spec.SyncProvider.HttpSyncConfiguration.Target, - HttpSyncBearerToken: fsConfig.Spec.SyncProvider.HttpSyncConfiguration.BearerToken, - }) - default: - return fmt.Errorf("FeatureFlagConfiguration %s configuration is unrecognized", ffName) - } - } - return nil -} diff --git a/webhooks/pod_webhook_test.go b/webhooks/pod_webhook_test.go index 80f559bbd..ec3413257 100644 --- a/webhooks/pod_webhook_test.go +++ b/webhooks/pod_webhook_test.go @@ -5,18 +5,16 @@ import ( "encoding/json" "errors" "fmt" - "github.com/golang/mock/gomock" - "github.com/open-feature/open-feature-operator/controllers/common/constant" - commonmock "github.com/open-feature/open-feature-operator/controllers/common/mock" "net/http" "reflect" "testing" "github.com/go-logr/logr/testr" - "github.com/open-feature/open-feature-operator/apis/core/v1alpha1" - "github.com/open-feature/open-feature-operator/apis/core/v1alpha2" - "github.com/open-feature/open-feature-operator/apis/core/v1alpha3" - "github.com/open-feature/open-feature-operator/pkg/utils" + "github.com/golang/mock/gomock" + api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" + apicommon "github.com/open-feature/open-feature-operator/apis/core/v1beta1/common" + "github.com/open-feature/open-feature-operator/common" + flagdinjectorfake "github.com/open-feature/open-feature-operator/common/flagdinjector/fake" "github.com/stretchr/testify/require" admissionv1 "k8s.io/api/admission/v1" corev1 "k8s.io/api/core/v1" @@ -31,35 +29,11 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook/admission" ) -func TestOpenFeatureEnabledAnnotationIndex(t *testing.T) { - - tests := []struct { - name string - o client.Object - want []string - }{ - { - name: "no annotations", - o: &corev1.Pod{}, - want: []string{"false"}, - }, { - name: "annotated wrong", - o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{"test/ann": "nope", "openfeature.dev/allowkubernetessync": "false"}}}, - want: []string{"false"}, - }, { - name: "annotated with enabled index", - o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{"openfeature.dev/allowkubernetessync": "true"}}}, - want: []string{"true"}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := OpenFeatureEnabledAnnotationIndex(tt.o); !reflect.DeepEqual(got, tt.want) { - t.Errorf("OpenFeatureEnabledAnnotationIndex() = %v, want %v", got, tt.want) - } - }) - } -} +const ( + mutatePodNamespace = "test-mutate-pod" + defaultPodServiceAccountName = "test-pod-service-account" + featureFlagSourceName = "test-feature-flag-source" +) func TestPodMutator_BackfillPermissions(t *testing.T) { const ( @@ -72,7 +46,7 @@ func TestPodMutator_BackfillPermissions(t *testing.T) { name string mutator *PodMutator wantErr bool - setup func(injector *commonmock.MockIFlagdContainerInjector) + setup func(injector *flagdinjectorfake.MockFlagdContainerInjector) }{ { name: "no annotated pod", @@ -94,14 +68,14 @@ func TestPodMutator_BackfillPermissions(t *testing.T) { Name: pod, Namespace: ns, Annotations: map[string]string{ - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, EnabledAnnotation): "true", - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, FeatureFlagConfigurationAnnotation): fmt.Sprintf("%s/%s", mutatePodNamespace, featureFlagConfigurationName), - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, AllowKubernetesSyncAnnotation): "true", + fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationPrefix, common.EnabledAnnotation): "true", + fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationPrefix, common.FeatureFlagSourceAnnotation): fmt.Sprintf("%s/%s", mutatePodNamespace, featureFlagSourceName), + fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationPrefix, common.AllowKubernetesSyncAnnotation): "true", }}, }, ), }, - setup: func(injector *commonmock.MockIFlagdContainerInjector) { + setup: func(injector *flagdinjectorfake.MockFlagdContainerInjector) { injector.EXPECT().EnableClusterRoleBinding( gomock.Any(), ns, @@ -120,9 +94,9 @@ func TestPodMutator_BackfillPermissions(t *testing.T) { Name: pod + "-1", Namespace: ns, Annotations: map[string]string{ - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, EnabledAnnotation): "true", - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, FeatureFlagConfigurationAnnotation): fmt.Sprintf("%s/%s", mutatePodNamespace, featureFlagConfigurationName), - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, AllowKubernetesSyncAnnotation): "true", + fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationPrefix, common.EnabledAnnotation): "true", + fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationPrefix, common.FeatureFlagSourceAnnotation): fmt.Sprintf("%s/%s", mutatePodNamespace, featureFlagSourceName), + fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationPrefix, common.AllowKubernetesSyncAnnotation): "true", }}, }, &corev1.Pod{ @@ -130,14 +104,14 @@ func TestPodMutator_BackfillPermissions(t *testing.T) { Name: pod + "-2", Namespace: ns, Annotations: map[string]string{ - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, EnabledAnnotation): "true", - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, FeatureFlagConfigurationAnnotation): fmt.Sprintf("%s/%s", mutatePodNamespace, featureFlagConfigurationName), - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, AllowKubernetesSyncAnnotation): "true", + fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationPrefix, common.EnabledAnnotation): "true", + fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationPrefix, common.FeatureFlagSourceAnnotation): fmt.Sprintf("%s/%s", mutatePodNamespace, featureFlagSourceName), + fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationPrefix, common.AllowKubernetesSyncAnnotation): "true", }}, }, ), }, - setup: func(injector *commonmock.MockIFlagdContainerInjector) { + setup: func(injector *flagdinjectorfake.MockFlagdContainerInjector) { // make the mock return an error - in this case we still expect the number of invocations // to match the number of pods injector.EXPECT().EnableClusterRoleBinding( @@ -158,9 +132,9 @@ func TestPodMutator_BackfillPermissions(t *testing.T) { Name: pod, Namespace: ns, Annotations: map[string]string{ - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, EnabledAnnotation): "true", - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, FeatureFlagConfigurationAnnotation): fmt.Sprintf("%s/%s", mutatePodNamespace, featureFlagConfigurationName), - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, AllowKubernetesSyncAnnotation): "true", + fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationPrefix, common.EnabledAnnotation): "true", + fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationPrefix, common.FeatureFlagSourceAnnotation): fmt.Sprintf("%s/%s", mutatePodNamespace, featureFlagSourceName), + fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationPrefix, common.AllowKubernetesSyncAnnotation): "true", }}, Spec: corev1.PodSpec{ServiceAccountName: "my-service-account"}, }, @@ -169,9 +143,9 @@ func TestPodMutator_BackfillPermissions(t *testing.T) { Name: name, Namespace: ns, Annotations: map[string]string{ - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, EnabledAnnotation): "true", - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, FeatureFlagConfigurationAnnotation): fmt.Sprintf("%s/%s", mutatePodNamespace, featureFlagConfigurationName), - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, AllowKubernetesSyncAnnotation): "true", + fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationPrefix, common.EnabledAnnotation): "true", + fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationPrefix, common.FeatureFlagSourceAnnotation): fmt.Sprintf("%s/%s", mutatePodNamespace, featureFlagSourceName), + fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationPrefix, common.AllowKubernetesSyncAnnotation): "true", }}, }, &rbac.ClusterRoleBinding{ @@ -181,7 +155,7 @@ func TestPodMutator_BackfillPermissions(t *testing.T) { }, ), }, - setup: func(injector *commonmock.MockIFlagdContainerInjector) { + setup: func(injector *flagdinjectorfake.MockFlagdContainerInjector) { injector.EXPECT().EnableClusterRoleBinding(context.Background(), ns, "my-service-account").Times(1) }, wantErr: false, @@ -196,9 +170,9 @@ func TestPodMutator_BackfillPermissions(t *testing.T) { Name: pod, Namespace: ns, Annotations: map[string]string{ - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, EnabledAnnotation): "true", - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, FeatureFlagConfigurationAnnotation): fmt.Sprintf("%s/%s", mutatePodNamespace, featureFlagConfigurationName), - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, AllowKubernetesSyncAnnotation): "true", + fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationPrefix, common.EnabledAnnotation): "true", + fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationPrefix, common.FeatureFlagSourceAnnotation): fmt.Sprintf("%s/%s", mutatePodNamespace, featureFlagSourceName), + fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationPrefix, common.AllowKubernetesSyncAnnotation): "true", }}, }, &corev1.ServiceAccount{ @@ -206,9 +180,9 @@ func TestPodMutator_BackfillPermissions(t *testing.T) { Name: name, Namespace: ns, Annotations: map[string]string{ - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, EnabledAnnotation): "true", - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, FeatureFlagConfigurationAnnotation): fmt.Sprintf("%s/%s", mutatePodNamespace, featureFlagConfigurationName), - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, AllowKubernetesSyncAnnotation): "true", + fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationPrefix, common.EnabledAnnotation): "true", + fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationPrefix, common.FeatureFlagSourceAnnotation): fmt.Sprintf("%s/%s", mutatePodNamespace, featureFlagSourceName), + fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationPrefix, common.AllowKubernetesSyncAnnotation): "true", }}, }, &rbac.ClusterRoleBinding{ @@ -226,7 +200,7 @@ func TestPodMutator_BackfillPermissions(t *testing.T) { ), }, wantErr: false, - setup: func(injector *commonmock.MockIFlagdContainerInjector) { + setup: func(injector *flagdinjectorfake.MockFlagdContainerInjector) { injector.EXPECT().EnableClusterRoleBinding(context.Background(), ns, "").Times(1) }, }, @@ -236,7 +210,7 @@ func TestPodMutator_BackfillPermissions(t *testing.T) { t.Run(tt.name, func(t *testing.T) { ctrl := gomock.NewController(t) - mockInjector := commonmock.NewMockIFlagdContainerInjector(ctrl) + mockInjector := flagdinjectorfake.NewMockFlagdContainerInjector(ctrl) if tt.setup != nil { tt.setup(mockInjector) @@ -263,8 +237,8 @@ func TestPodMutator_Handle(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "myAnnotatedPod", Annotations: map[string]string{ - OpenFeatureAnnotationPrefix: "enabled", - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, FeatureFlagConfigurationAnnotation): fmt.Sprintf("%s/%s", mutatePodNamespace, featureFlagConfigurationName), + fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationPrefix, common.EnabledAnnotation): "true", + fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationPrefix, common.FeatureFlagSourceAnnotation): fmt.Sprintf("%s/%s", mutatePodNamespace, featureFlagSourceName), }, }, }) @@ -275,8 +249,8 @@ func TestPodMutator_Handle(t *testing.T) { Name: "myAnnotatedPod", Namespace: mutatePodNamespace, Annotations: map[string]string{ - OpenFeatureAnnotationPrefix: "enabled", - fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, FeatureFlagConfigurationAnnotation): fmt.Sprintf("%s/%s", mutatePodNamespace, featureFlagConfigurationName), + fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationPrefix, common.EnabledAnnotation): "true", + fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationPrefix, common.FeatureFlagSourceAnnotation): fmt.Sprintf("%s/%s", mutatePodNamespace, featureFlagSourceName), }, OwnerReferences: []metav1.OwnerReference{{UID: "123"}}, }, @@ -291,7 +265,7 @@ func TestPodMutator_Handle(t *testing.T) { req admission.Request wantCode int32 allow bool - setup func(mockInjector *commonmock.MockIFlagdContainerInjector) + setup func(mockInjector *flagdinjectorfake.MockFlagdContainerInjector) }{ { name: "successful request pod not annotated", @@ -331,20 +305,21 @@ func TestPodMutator_Handle(t *testing.T) { }, }, wantCode: http.StatusForbidden, + allow: false, }, { name: "forbidden request pod annotated with owner, but cluster role binding cannot be enabled", mutator: &PodMutator{ Client: NewClient(false, - &v1alpha1.FeatureFlagConfiguration{ + &api.FeatureFlagSource{ ObjectMeta: metav1.ObjectMeta{ - Name: featureFlagConfigurationName, + Name: featureFlagSourceName, Namespace: mutatePodNamespace, }, - Spec: v1alpha1.FeatureFlagConfigurationSpec{ - FlagDSpec: &v1alpha1.FlagDSpec{Envs: []corev1.EnvVar{ - {Name: "LOG_LEVEL", Value: "dev"}, - }}, + Spec: api.FeatureFlagSourceSpec{ + Sources: []api.Source{ + {Provider: apicommon.SyncProviderKubernetes}, + }, }, }, ), @@ -361,7 +336,7 @@ func TestPodMutator_Handle(t *testing.T) { }, }, }, - setup: func(mockInjector *commonmock.MockIFlagdContainerInjector) { + setup: func(mockInjector *flagdinjectorfake.MockFlagdContainerInjector) { mockInjector.EXPECT(). EnableClusterRoleBinding( gomock.Any(), @@ -370,21 +345,18 @@ func TestPodMutator_Handle(t *testing.T) { ).Return(errors.New("error")).Times(1) }, wantCode: http.StatusForbidden, + allow: false, }, { name: "forbidden request pod annotated with owner, but flagd proxy is not ready", mutator: &PodMutator{ Client: NewClient(false, - &v1alpha1.FeatureFlagConfiguration{ + &api.FeatureFlagSource{ ObjectMeta: metav1.ObjectMeta{ - Name: featureFlagConfigurationName, + Name: featureFlagSourceName, Namespace: mutatePodNamespace, }, - Spec: v1alpha1.FeatureFlagConfigurationSpec{ - FlagDSpec: &v1alpha1.FlagDSpec{Envs: []corev1.EnvVar{ - {Name: "LOG_LEVEL", Value: "dev"}, - }}, - }, + Spec: api.FeatureFlagSourceSpec{}, }, ), decoder: decoder, @@ -400,26 +372,19 @@ func TestPodMutator_Handle(t *testing.T) { }, }, }, - setup: func(mockInjector *commonmock.MockIFlagdContainerInjector) { - mockInjector.EXPECT(). - EnableClusterRoleBinding( - gomock.Any(), - antPod.Namespace, - antPod.Spec.ServiceAccountName, - ).Return(nil).Times(1) - + setup: func(mockInjector *flagdinjectorfake.MockFlagdContainerInjector) { mockInjector.EXPECT(). InjectFlagd( gomock.Any(), gomock.AssignableToTypeOf(&antPod.ObjectMeta), gomock.AssignableToTypeOf(&antPod.Spec), - gomock.AssignableToTypeOf(&v1alpha1.FlagSourceConfigurationSpec{}), - ).Return(constant.ErrFlagdProxyNotReady).Times(1) + gomock.AssignableToTypeOf(&api.FeatureFlagSourceSpec{}), + ).Return(common.ErrFlagdProxyNotReady).Times(1) }, wantCode: http.StatusForbidden, }, { - name: "forbidden request pod annotated with owner, but feature flag configuration is not available", + name: "forbidden request pod annotated with owner, but FeatureFlagSource is not available", mutator: &PodMutator{ Client: NewClient(false), decoder: decoder, @@ -435,23 +400,7 @@ func TestPodMutator_Handle(t *testing.T) { }, }, }, - setup: func(mockInjector *commonmock.MockIFlagdContainerInjector) { - mockInjector.EXPECT(). - EnableClusterRoleBinding( - gomock.Any(), - antPod.Namespace, - antPod.Spec.ServiceAccountName, - ).Return(nil).Times(1) - - mockInjector.EXPECT(). - InjectFlagd( - gomock.Any(), - gomock.AssignableToTypeOf(&antPod.ObjectMeta), - gomock.AssignableToTypeOf(&antPod.Spec), - gomock.AssignableToTypeOf(&v1alpha1.FlagSourceConfigurationSpec{}), - ).Return(constant.ErrFlagdProxyNotReady).Times(1) - }, - wantCode: http.StatusInternalServerError, + wantCode: http.StatusNotFound, }, { name: "happy path: request pod annotated configured for env var", @@ -465,20 +414,16 @@ func TestPodMutator_Handle(t *testing.T) { }, }, &rbac.ClusterRoleBinding{ - ObjectMeta: metav1.ObjectMeta{Name: clusterRoleBindingName}, + ObjectMeta: metav1.ObjectMeta{Name: common.ClusterRoleBindingName}, Subjects: nil, RoleRef: rbac.RoleRef{}, }, - &v1alpha1.FeatureFlagConfiguration{ + &api.FeatureFlagSource{ ObjectMeta: metav1.ObjectMeta{ - Name: featureFlagConfigurationName, + Name: featureFlagSourceName, Namespace: mutatePodNamespace, }, - Spec: v1alpha1.FeatureFlagConfigurationSpec{ - FlagDSpec: &v1alpha1.FlagDSpec{Envs: []corev1.EnvVar{ - {Name: "LOG_LEVEL", Value: "dev"}, - }}, - }, + Spec: api.FeatureFlagSourceSpec{}, }, ), decoder: decoder, @@ -493,20 +438,13 @@ func TestPodMutator_Handle(t *testing.T) { }, }, }, - setup: func(mockInjector *commonmock.MockIFlagdContainerInjector) { - mockInjector.EXPECT(). - EnableClusterRoleBinding( - gomock.Any(), - antPod.Namespace, - antPod.Spec.ServiceAccountName, - ).Return(nil).Times(1) - + setup: func(mockInjector *flagdinjectorfake.MockFlagdContainerInjector) { mockInjector.EXPECT(). InjectFlagd( gomock.Any(), gomock.AssignableToTypeOf(&antPod.ObjectMeta), gomock.AssignableToTypeOf(&antPod.Spec), - gomock.AssignableToTypeOf(&v1alpha1.FlagSourceConfigurationSpec{}), + gomock.AssignableToTypeOf(&api.FeatureFlagSourceSpec{}), ).Return(nil).Times(1) }, allow: true, @@ -535,7 +473,7 @@ func TestPodMutator_Handle(t *testing.T) { t.Run(tt.name, func(t *testing.T) { ctrl := gomock.NewController(t) - mockFlagdInjector := commonmock.NewMockIFlagdContainerInjector(ctrl) + mockFlagdInjector := flagdinjectorfake.NewMockFlagdContainerInjector(ctrl) m := tt.mutator @@ -555,127 +493,17 @@ func TestPodMutator_Handle(t *testing.T) { } } -func TestPodMutator_checkOFEnabled(t *testing.T) { - - tests := []struct { - name string - mutator PodMutator - annotations map[string]string - want bool - }{ - { - name: "deprecated enabled", - mutator: PodMutator{ - Log: testr.New(t), - }, - annotations: map[string]string{OpenFeatureAnnotationPrefix: "enabled"}, - want: true, - }, - { - name: "enabled", - mutator: PodMutator{ - Log: testr.New(t), - }, - annotations: map[string]string{fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, EnabledAnnotation): "true"}, - want: true, - }, { - name: "disabled", - mutator: PodMutator{ - Log: testr.New(t), - }, - annotations: map[string]string{fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, EnabledAnnotation): "false"}, - want: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - m := &tt.mutator - if got := m.checkOFEnabled(tt.annotations); got != tt.want { - t.Errorf("checkOFEnabled() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_parseAnnotation(t *testing.T) { - tests := []struct { - name string - s string - defaultNs string - wantNs string - want string - }{ - { - name: "no namespace", - s: "test", - defaultNs: "ofo", - wantNs: "ofo", - want: "test", - }, - { - name: "namespace", - s: "myns/test", - defaultNs: "ofo", - wantNs: "myns", - want: "test", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, got1 := utils.ParseAnnotation(tt.s, tt.defaultNs) - if got != tt.wantNs { - t.Errorf("parseAnnotation() got = %v, want %v", got, tt.wantNs) - } - if got1 != tt.want { - t.Errorf("parseAnnotation() got1 = %v, want %v", got1, tt.want) - } - }) - } -} - -func Test_parseList(t *testing.T) { - - tests := []struct { - name string - s string - want []string - }{ - { - name: "empty string", - s: "", - want: []string{}, - }, { - name: "nice list with spaces", - s: "annotation1, annotation2, annotation4 , annotation3,", - want: []string{"annotation1", "annotation2", "annotation4", "annotation3"}, - }, { - name: "list with no spaces", - s: "annotation1, annotation2,annotation4, annotation3", - want: []string{"annotation1", "annotation2", "annotation4", "annotation3"}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := parseList(tt.s); !reflect.DeepEqual(got, tt.want) { - t.Errorf("parseList() = %v, want %v", got, tt.want) - } - }) - } -} - func NewClient(withIndexes bool, objs ...client.Object) client.Client { utilruntime.Must(clientgoscheme.AddToScheme(scheme.Scheme)) - utilruntime.Must(v1alpha1.AddToScheme(scheme.Scheme)) - utilruntime.Must(v1alpha2.AddToScheme(scheme.Scheme)) - utilruntime.Must(v1alpha3.AddToScheme(scheme.Scheme)) + utilruntime.Must(api.AddToScheme(scheme.Scheme)) annotationsSyncIndexer := func(obj client.Object) []string { - res := obj.GetAnnotations()[fmt.Sprintf("%s/%s", OpenFeatureAnnotationPrefix, AllowKubernetesSyncAnnotation)] + res := obj.GetAnnotations()[fmt.Sprintf("%s/%s", common.OpenFeatureAnnotationPrefix, common.AllowKubernetesSyncAnnotation)] return []string{res} } featureflagIndexer := func(obj client.Object) []string { - res := obj.GetAnnotations()["openfeature.dev/featureflagconfiguration"] + res := obj.GetAnnotations()["openfeature.dev/featureflag"] return []string{res} } @@ -691,9 +519,22 @@ func NewClient(withIndexes bool, objs ...client.Object) client.Client { annotationsSyncIndexer). WithIndex( &corev1.Pod{}, - "metadata.annotations.openfeature.dev/featureflagconfiguration", + "metadata.annotations.openfeature.dev/featureflag", featureflagIndexer). Build() } return fakeClient.Build() } + +func TestPodMutator_IsReady(t *testing.T) { + + podMutator := PodMutator{ + ready: true, + } + + require.Nil(t, podMutator.IsReady(nil)) + + podMutator.ready = false + + require.NotNil(t, podMutator.IsReady(nil)) +}