diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e64b5e1bab..f26041515b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,9 +1,5 @@ version: 2 updates: - - package-ecosystem: docker - directory: "/images/ansible-operator" - schedule: - interval: daily - package-ecosystem: docker directory: "/images/custom-scorecard-tests" schedule: diff --git a/.github/workflows/deploy-manual.yml b/.github/workflows/deploy-manual.yml deleted file mode 100644 index 33fb6ae3d5..0000000000 --- a/.github/workflows/deploy-manual.yml +++ /dev/null @@ -1,81 +0,0 @@ -name: deploy-manual - -on: - workflow_dispatch: - inputs: - ansible_operator_base_tag: - description: ansible-operator-base image tag, ex. "6e1b47e6ca7c507b8ecf197a8edcd412dd64d85d" - required: false - -jobs: - # Build the ansible-operator-base image. - ansible-operator-base: - runs-on: ubuntu-22.04 - environment: deploy - steps: - - - name: set up qemu - uses: docker/setup-qemu-action@v2 - - - name: set up buildx - uses: docker/setup-buildx-action@v3 - - - name: quay.io login - uses: docker/login-action@v2 - with: - username: ${{ secrets.QUAY_USERNAME }} - password: ${{ secrets.QUAY_PASSWORD }} - registry: quay.io - - - name: checkout - uses: actions/checkout@v3 - with: - fetch-depth: 1 - - # Copied this for 2.11 rather than use a matrix because eventually 2.11 will be default and this will be removed. - - name: create base tag - id: base_tag - run: | - set -e - IMG=quay.io/${{ github.repository_owner }}/ansible-operator-base - TAG="${{ github.event.inputs.ansible_operator_base_tag }}" - GIT_COMMIT=$(git rev-parse HEAD) - if [[ "$TAG" == "" ]]; then - TAG="$(git branch --show-current)-${GIT_COMMIT}" - fi - echo "tag=${IMG}:${TAG}" >> $GITHUB_OUTPUT - echo "git_commit=${GIT_COMMIT}" >> $GITHUB_OUTPUT - - - name: build and push ansible dep image - uses: docker/build-push-action@v3 - with: - file: ./images/ansible-operator/base.Dockerfile - context: ./images/ansible-operator - platforms: linux/amd64,linux/arm64,linux/ppc64le,linux/s390x - push: true - tags: ${{ steps.base_tag.outputs.tag }} - build-args: | - GIT_COMMIT=${{ steps.base_tag.outputs.git_commit }} - - # This change will be staged and committed in the PR pushed below. - # The script below will fail if no change was made. - - name: update base of ansible-operator - id: update - run: | - set -ex - sed -i -E 's|FROM quay\.io/operator-framework/ansible-operator-base:.+|FROM ${{ steps.base_tag.outputs.tag }}|g' images/ansible-operator/Dockerfile - git diff --exit-code --quiet && echo "Failed to update images/ansible-operator/Dockerfile" && exit 1 - REF="${{ github.event.ref }}" - echo "branch_name=${REF##*/}" >> $GITHUB_OUTPUT - - - name: create PR for ansible-operator Dockerfile - uses: peter-evans/create-pull-request@v3 - with: - title: "[${{ steps.update.outputs.branch_name }}] image(ansible-operator): bump base to ${{ steps.base_tag.outputs.tag }}" - commit-message: | - [${{ steps.update.outputs.branch_name }}] image(ansible-operator): bump base to ${{ steps.base_tag.outputs.tag }} - - Signed-off-by: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com> - body: "New ansible-operator-base image built by https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" - delete-branch: true - branch-suffix: short-commit-hash diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 860b42465f..a8067db28c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -73,7 +73,7 @@ jobs: environment: deploy strategy: matrix: - id: ["operator-sdk", "helm-operator", "scorecard-test", "ansible-operator"] + id: ["operator-sdk", "helm-operator", "scorecard-test"] steps: - name: set up qemu diff --git a/.github/workflows/freshen-images.yml b/.github/workflows/freshen-images.yml index ec0e32edb3..0ebf7b583a 100644 --- a/.github/workflows/freshen-images.yml +++ b/.github/workflows/freshen-images.yml @@ -32,7 +32,7 @@ jobs: strategy: matrix: # TODO(estroz): support scorecard-test-kuttl rebuilds. - id: ["operator-sdk", "ansible-operator", "helm-operator", "scorecard-test"] + id: ["operator-sdk", "helm-operator", "scorecard-test"] steps: - name: set up qemu uses: docker/setup-qemu-action@v2 diff --git a/.github/workflows/freshen-images/build.sh b/.github/workflows/freshen-images/build.sh index f3349fdca9..3080279240 100755 --- a/.github/workflows/freshen-images/build.sh +++ b/.github/workflows/freshen-images/build.sh @@ -22,7 +22,7 @@ IMAGE_DO=--load # Space-separated list of git tags. # The --tags arg can be comma-separated. TAGS= -# ID of the image, ex. operator-sdk, ansible-operator. +# ID of the image, ex. operator-sdk. IMAGE_ID= # Update all images. FORCE=0 @@ -78,20 +78,6 @@ git clone https://github.com/operator-framework/operator-sdk.git $tmp trap "rm -rf $tmp" EXIT pushd $tmp -case $IMAGE_ID in -ansible-operator) - # ansible-operator has a base image that must be rebuilt in advance if necessary. - # This script will detect that the base is fresh when inspecting ansible-operator's - # Dockerfile and build it. - for i in ${!TAGS[*]}; do - if (($i=0)); then - build_ansible_base ${TAGS[$i]} "$PLATFORMS" true - else - build_ansible_base ${TAGS[$i]} "$PLATFORMS" false - fi - done -;; -esac # Build the image defined by IMAGE_ID for each tag for a set of platforms. for i in ${!TAGS[*]}; do diff --git a/.github/workflows/freshen-images/lib.sh b/.github/workflows/freshen-images/lib.sh index 2e6d550dfe..21e7966352 100755 --- a/.github/workflows/freshen-images/lib.sh +++ b/.github/workflows/freshen-images/lib.sh @@ -48,38 +48,6 @@ function is_dockerfile_fresh() { done } -# Build an image at path ./images/ansible-operator/base.Dockerfile checked out at git tag $1 -# for all platforms in $2. Semantics are otherwise the same as build_generic. -function build_ansible_base() { - local tag=$1 - local platforms=$2 - local buildlatest=$3 - local dockerfile=./images/ansible-operator/base.Dockerfile - - - git checkout refs/tags/$tag - local ansible_base_image_tag=$(grep -oP 'FROM \K(quay\.io/operator-framework/ansible-operator-base:.+)' ./images/ansible-operator/Dockerfile) - # Attempt to get the git ref that built this image from the git_commit image label, - # falling back to parsing it from the image tag, which typically contains a git ref - # as the last hyphen-delimit element. - local ansible_base_git_ref=$(docker inspect --format '{{ index .Config.Labels "git_commit" }}' $ansible_base_image_tag) - if [[ $ansible_base_git_ref == "devel" || $ansible_base_git_ref == "" ]]; then - ansible_base_git_ref=$(echo $ansible_base_image_tag | sed -E 's|.+:.+-(.+)|\1|') - fi - git checkout $ansible_base_git_ref - if is_dockerfile_fresh "$dockerfile"; then - echo "Skipping build of $dockerfile, it is FRESH!" - else - # dockerfile is not fresh, rebuildng image - if $buildlatest; then - echo "Rebuilding image [$ansible_base_image_tag] and latest for [$platforms]" - _buildx --tag $ansible_base_image_tag --tag quay.io/operator-framework/ansible-operator-base:latest --platform "$platforms" --file "$dockerfile" $IMAGE_DO --build-arg GIT_COMMIT=$ansible_base_git_ref ./images/ansible-operator - else - echo "Rebuilding image [$ansible_base_image_tag] for [$platforms]" - _buildx --tag $ansible_base_image_tag --platform "$platforms" --file "$dockerfile" $IMAGE_DO --build-arg GIT_COMMIT=$ansible_base_git_ref ./images/ansible-operator - fi - fi -} # Build an image at path ./images/$2/Dockerfile checked out at git tag $1 # for all platforms in $3. Tag is assumed to be "v"+semver; the image is tagged diff --git a/.github/workflows/test-ansible.yml b/.github/workflows/test-ansible.yml deleted file mode 100644 index f6d0f0dd84..0000000000 --- a/.github/workflows/test-ansible.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: ansible -on: - pull_request: {} - -jobs: - check_docs_only: - name: check_docs_only - runs-on: ubuntu-22.04 - outputs: - skip: ${{ steps.check_docs_only.outputs.skip }} - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - id: check_docs_only - # Since PR's are squashed prior to merging to the branch checked out (default branch), - # HEAD^ will resolve to the previous point in history. - run: | - REF="HEAD^" - [[ -z "${{ github.base_ref }}" ]] || REF=$(git show-ref ${{ github.base_ref }} | head -1 | cut -d' ' -f2) - echo "::set-output name=skip::$(.github/workflows/check-docs-only.sh $REF)" - - e2e: - name: e2e - runs-on: ubuntu-22.04 - needs: check_docs_only - if: needs.check_docs_only.outputs.skip != 'true' - steps: - - uses: actions/setup-go@v3 - with: - go-version: 1.19 - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - run: sudo rm -rf /usr/local/bin/kustomize - - run: make test-e2e-ansible - - e2e-molecule: - name: e2e-molecule - runs-on: ubuntu-22.04 - needs: check_docs_only - if: needs.check_docs_only.outputs.skip != 'true' - steps: - - uses: actions/setup-go@v3 - with: - go-version: 1.19 - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - run: sudo rm -rf /usr/local/bin/kustomize - - uses: actions/setup-python@v4 - with: - python-version: '3.9' - - name: Run test e2e ansible molecule - run: | - env - pip3 install --user --upgrade setuptools pip - pip3 install --user ansible-core~=2.15.0 - make test-e2e-ansible-molecule diff --git a/cmd/ansible-operator/main.go b/cmd/ansible-operator/main.go deleted file mode 100644 index 567e8925be..0000000000 --- a/cmd/ansible-operator/main.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2020 The Operator-SDK Authors -// -// 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 main - -import ( - "log" - - "github.com/spf13/cobra" - _ "k8s.io/client-go/plugin/pkg/client/auth" - - "github.com/operator-framework/operator-sdk/internal/cmd/ansible-operator/run" - "github.com/operator-framework/operator-sdk/internal/cmd/ansible-operator/version" -) - -func main() { - root := cobra.Command{ - Short: "Reconcile an Ansible operator project using ansible-runner", - Long: `This binary runs an Ansible operator that reconciles Kubernetes resources -managed by the ansible-runner program. It can be run either directly or from an Ansible -operator project's image entrypoint -`, - Use: "ansible-operator", - } - - root.AddCommand(run.NewCmd()) - root.AddCommand(version.NewCmd()) - - if err := root.Execute(); err != nil { - log.Fatal(err) - } -} diff --git a/hack/generate/samples/generate_testdata.go b/hack/generate/samples/generate_testdata.go index 421810afeb..e30308aa69 100644 --- a/hack/generate/samples/generate_testdata.go +++ b/hack/generate/samples/generate_testdata.go @@ -19,7 +19,6 @@ import ( "os" "path/filepath" - "github.com/operator-framework/operator-sdk/hack/generate/samples/internal/ansible" "github.com/operator-framework/operator-sdk/hack/generate/samples/internal/helm" log "github.com/sirupsen/logrus" @@ -57,9 +56,6 @@ func main() { log.Infof("creating Helm Memcached Sample") helm.GenerateMemcachedSamples(binaryPath, samplesPath) - log.Infof("creating Ansible Memcached Sample") - ansible.GenerateMemcachedSamples(binaryPath, samplesPath) - log.Infof("creating Go Memcached Sample with Webhooks") golang.GenerateMemcachedSamples(binaryPath, samplesPath) } diff --git a/hack/generate/samples/internal/ansible/advanced_molecule.go b/hack/generate/samples/internal/ansible/advanced_molecule.go deleted file mode 100644 index dde2879737..0000000000 --- a/hack/generate/samples/internal/ansible/advanced_molecule.go +++ /dev/null @@ -1,600 +0,0 @@ -// Copyright 2020 The Operator-SDK Authors -// -// 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 ansible - -import ( - "fmt" - "os/exec" - "path/filepath" - "strings" - - log "github.com/sirupsen/logrus" - kbutil "sigs.k8s.io/kubebuilder/v3/pkg/plugin/util" - - "github.com/operator-framework/operator-sdk/hack/generate/samples/internal/pkg" -) - -// AdvancedMolecule defines the context for the sample -type AdvancedMolecule struct { - ctx *pkg.SampleContext -} - -// GenerateAdvancedMoleculeSample will call all actions to create the directory and generate the sample -// The Context to run the samples are not the same in the e2e test. In this way, note that it should NOT -// be called in the e2e tests since it will call the Prepare() to set the sample context and generate the files -// in the testdata directory. The e2e tests only ought to use the Run() method with the TestContext. -func GenerateAdvancedMoleculeSample(binaryPath, samplesPath string) { - ctx, err := pkg.NewSampleContext(binaryPath, filepath.Join(samplesPath, "advanced-molecule-operator"), - "GO111MODULE=on") - pkg.CheckError("generating Ansible Molecule Advanced Operator context", err) - - molecule := AdvancedMolecule{&ctx} - molecule.Prepare() - molecule.Run() -} - -// Prepare the Context for the Memcached Ansible Sample -// Note that sample directory will be re-created and the context data for the sample -// will be set such as the domain and GVK. -func (ma *AdvancedMolecule) Prepare() { - log.Infof("destroying directory for memcached Ansible samples") - ma.ctx.Destroy() - - log.Infof("creating directory") - err := ma.ctx.Prepare() - pkg.CheckError("creating directory for Advanced Molecule Sample", err) - - log.Infof("setting domain and GVK") - // nolint:goconst - ma.ctx.Domain = "example.com" - // nolint:goconst - ma.ctx.Version = "v1alpha1" - ma.ctx.Group = "test" - ma.ctx.Kind = "InventoryTest" -} - -// Run the steps to create the Memcached Ansible Sample -func (ma *AdvancedMolecule) Run() { - log.Infof("creating the project") - err := ma.ctx.Init( - "--plugins", "ansible", - "--group", ma.ctx.Group, - "--version", ma.ctx.Version, - "--kind", ma.ctx.Kind, - "--domain", ma.ctx.Domain, - "--generate-role", - "--generate-playbook") - pkg.CheckError("creating the project", err) - - log.Infof("enabling multigroup support") - err = ma.ctx.AllowProjectBeMultiGroup() - pkg.CheckError("updating PROJECT file", err) - - inventoryRoleTask := filepath.Join(ma.ctx.Dir, "roles", "inventorytest", "tasks", "main.yml") - log.Infof("inserting code to inventory role task") - const inventoryRoleTaskFragment = ` -- when: sentinel | test - block: - - kubernetes.core.k8s: - definition: - apiVersion: v1 - kind: ConfigMap - metadata: - name: inventory-cm - namespace: '{{ meta.namespace }}' - data: - sentinel: '{{ sentinel }}' - groups: '{{ groups | to_nice_yaml }}'` - err = kbutil.ReplaceInFile( - inventoryRoleTask, - "# tasks file for InventoryTest", - inventoryRoleTaskFragment) - pkg.CheckError("replacing inventory task", err) - - log.Infof("updating inventorytest sample") - err = kbutil.ReplaceInFile( - filepath.Join(ma.ctx.Dir, "config", "samples", "test_v1alpha1_inventorytest.yaml"), - "name: inventorytest-sample", - inventorysampleFragment) - pkg.CheckError("updating inventorytest sample", err) - - log.Infof("updating spec of inventorytest sample") - err = kbutil.ReplaceInFile( - filepath.Join(ma.ctx.Dir, "config", "samples", "test_v1alpha1_inventorytest.yaml"), - "# TODO(user): Add fields here", - "size: 3") - pkg.CheckError("updating spec of inventorytest sample", err) - - ma.addPlaybooks() - ma.updatePlaybooks() - ma.addMocksFromTestdata() - ma.updateDockerfile() - ma.updateConfig() -} - -func (ma *AdvancedMolecule) updateConfig() { - log.Infof("adding customized roles") - const cmRolesFragment = ` ## - ## Base operator rules - ## - - apiGroups: - - "" - resources: - - configmaps - - namespaces - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - - apiGroups: - - apps - resources: - - configmaps - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -#+kubebuilder:scaffold:rules` - err := kbutil.ReplaceInFile( - filepath.Join(ma.ctx.Dir, "config", "rbac", "role.yaml"), - "#+kubebuilder:scaffold:rules", - cmRolesFragment) - pkg.CheckError("adding customized roles", err) - - log.Infof("adding manager arg") - const ansibleVaultArg = ` - - --ansible-args='--vault-password-file /opt/ansible/pwd.yml'` - err = kbutil.InsertCode( - filepath.Join(ma.ctx.Dir, "config", "manager", "manager.yaml"), - "- --leader-election-id=advanced-molecule-operator", - ansibleVaultArg) - pkg.CheckError("adding manager arg", err) - - log.Infof("adding manager env") - const managerEnv = ` - - name: ANSIBLE_DEBUG_LOGS - value: "TRUE" - - name: ANSIBLE_INVENTORY - value: /opt/ansible/inventory` - err = kbutil.InsertCode( - filepath.Join(ma.ctx.Dir, "config", "manager", "manager.yaml"), - "value: explicit", - managerEnv) - pkg.CheckError("adding manager env", err) - - log.Infof("adding vaulting args to the proxy auth") - const managerAuthArgs = ` - - "--ansible-args='--vault-password-file /opt/ansible/pwd.yml'"` - err = kbutil.InsertCode( - filepath.Join(ma.ctx.Dir, "config", "default", "manager_auth_proxy_patch.yaml"), - "- \"--leader-election-id=advanced-molecule-operator\"", - managerAuthArgs) - pkg.CheckError("adding vaulting args to the proxy auth", err) - - log.Infof("adding task to not pull image to the config/testing") - err = kbutil.ReplaceInFile( - filepath.Join(ma.ctx.Dir, "config", "testing", "kustomization.yaml"), - "- manager_image.yaml", - "- manager_image.yaml\n- pull_policy/Never.yaml") - pkg.CheckError("adding task to not pull image to the config/testing", err) -} - -func (ma *AdvancedMolecule) addMocksFromTestdata() { - log.Infof("adding ansible.cfg") - cmd := exec.Command("cp", "../../../hack/generate/samples/internal/ansible/testdata/ansible.cfg", ma.ctx.Dir) - _, err := ma.ctx.Run(cmd) - pkg.CheckError("adding ansible.cfg", err) - - log.Infof("adding plugins/") - cmd = exec.Command("cp", "-r", "../../../hack/generate/samples/internal/ansible/testdata/plugins/", filepath.Join(ma.ctx.Dir, "plugins/")) - _, err = ma.ctx.Run(cmd) - pkg.CheckError("adding plugins/", err) - - log.Infof("adding fixture_collection/") - cmd = exec.Command("cp", "-r", "../../../hack/generate/samples/internal/ansible/testdata/fixture_collection/", filepath.Join(ma.ctx.Dir, "fixture_collection/")) - _, err = ma.ctx.Run(cmd) - pkg.CheckError("adding fixture_collection/", err) - - log.Infof("replacing watches.yaml") - cmd = exec.Command("cp", "-r", "../../../hack/generate/samples/internal/ansible/testdata/watches.yaml", ma.ctx.Dir) - _, err = ma.ctx.Run(cmd) - pkg.CheckError("replacing watches.yaml", err) - - log.Infof("adding tasks/") - cmd = exec.Command("cp", "-r", "../../../hack/generate/samples/internal/ansible/testdata/tasks/", filepath.Join(ma.ctx.Dir, "molecule/default/")) - _, err = ma.ctx.Run(cmd) - pkg.CheckError("adding tasks/", err) - - log.Infof("adding secret playbook") - cmd = exec.Command("cp", "-r", "../../../hack/generate/samples/internal/ansible/testdata/secret.yml", filepath.Join(ma.ctx.Dir, "playbooks/secret.yml")) - _, err = ma.ctx.Run(cmd) - pkg.CheckError("adding secret playbook", err) - - log.Infof("adding inventory/") - cmd = exec.Command("cp", "-r", "../../../hack/generate/samples/internal/ansible/testdata/inventory/", filepath.Join(ma.ctx.Dir, "inventory/")) - _, err = ma.ctx.Run(cmd) - pkg.CheckError("adding inventory/", err) - - log.Infof("adding finalizer for finalizerconcurrencytest") - cmd = exec.Command("cp", "../../../hack/generate/samples/internal/ansible/testdata/playbooks/finalizerconcurrencyfinalizer.yml", filepath.Join(ma.ctx.Dir, "playbooks/finalizerconcurrencyfinalizer.yml")) - _, err = ma.ctx.Run(cmd) - pkg.CheckError("adding finalizer for finalizerconccurencytest", err) - -} - -func (ma *AdvancedMolecule) updateDockerfile() { - log.Infof("replacing project Dockerfile to use ansible base image with the dev tag") - err := kbutil.ReplaceRegexInFile( - filepath.Join(ma.ctx.Dir, "Dockerfile"), - "quay.io/operator-framework/ansible-operator:.*", - "quay.io/operator-framework/ansible-operator:dev") - pkg.CheckError("replacing Dockerfile", err) - - log.Infof("inserting code to Dockerfile") - const dockerfileFragment = ` - -# Customizations done to check advanced scenarios -COPY inventory/ ${HOME}/inventory/ -COPY plugins/ ${HOME}/plugins/ -COPY ansible.cfg /etc/ansible/ansible.cfg -COPY fixture_collection/ /tmp/fixture_collection/ -USER root -RUN chmod -R ug+rwx /tmp/fixture_collection -USER 1001 -RUN ansible-galaxy collection build /tmp/fixture_collection/ --output-path /tmp/fixture_collection/ \ - && ansible-galaxy collection install /tmp/fixture_collection/operator_sdk-test_fixtures-0.0.0.tar.gz -RUN echo abc123 > /opt/ansible/pwd.yml \ - && ansible-vault encrypt_string --vault-password-file /opt/ansible/pwd.yml 'thisisatest' --name 'the_secret' > /opt/ansible/vars.yml -` - err = kbutil.InsertCode( - filepath.Join(ma.ctx.Dir, "Dockerfile"), - "COPY playbooks/ ${HOME}/playbooks/", - dockerfileFragment) - pkg.CheckError("replacing Dockerfile", err) -} - -func (ma *AdvancedMolecule) updatePlaybooks() { - log.Infof("adding playbook for argstest") - const argsPlaybook = `--- -- hosts: localhost - gather_facts: no - collections: - - kubernetes.core - tasks: - - name: Get the decrypted message variable - include_vars: - file: /opt/ansible/vars.yml - name: the_secret - - name: Create configmap - k8s: - definition: - apiVersion: v1 - kind: ConfigMap - metadata: - name: '{{ meta.name }}' - namespace: '{{ meta.namespace }}' - data: - msg: The decrypted value is {{the_secret.the_secret}} -` - err := kbutil.ReplaceInFile( - filepath.Join(ma.ctx.Dir, "playbooks", "argstest.yml"), - originalPlaybookFragment, - argsPlaybook) - pkg.CheckError("adding playbook for argstest", err) - - log.Infof("adding playbook for casetest") - const casePlaybook = `--- -- hosts: localhost - gather_facts: no - collections: - - kubernetes.core - tasks: - - name: Create configmap - k8s: - definition: - apiVersion: v1 - kind: ConfigMap - metadata: - name: '{{ meta.name }}' - namespace: '{{ meta.namespace }}' - data: - shouldBeCamel: '{{ camelCaseVar | default("false") }}' -` - err = kbutil.ReplaceInFile( - filepath.Join(ma.ctx.Dir, "playbooks", "casetest.yml"), - originalPlaybookFragment, - casePlaybook) - pkg.CheckError("adding playbook for casetest", err) - - log.Infof("adding playbook for inventorytest") - const inventoryPlaybook = `--- -- hosts: test - gather_facts: no - tasks: - - import_role: - name: "inventorytest" - -- hosts: localhost - gather_facts: no - tasks: - - command: echo hello - - debug: msg='{{ "hello" | test }}'` - err = kbutil.ReplaceInFile( - filepath.Join(ma.ctx.Dir, "playbooks", "inventorytest.yml"), - "---\n- hosts: localhost\n gather_facts: no\n collections:\n - kubernetes.core\n - operator_sdk.util\n tasks:\n - import_role:\n name: \"inventorytest\"", - inventoryPlaybook) - pkg.CheckError("adding playbook for inventorytest", err) - - log.Infof("adding playbook for reconciliationtest") - const reconciliationPlaybook = `--- -- hosts: localhost - gather_facts: no - collections: - - kubernetes.core - tasks: - - name: retrieve configmap - k8s_info: - api_version: v1 - kind: ConfigMap - namespace: '{{ meta.namespace }}' - name: '{{ meta.name }}' - register: configmap - - - name: create configmap - k8s: - definition: - apiVersion: v1 - kind: ConfigMap - metadata: - name: '{{ meta.name }}' - namespace: '{{ meta.namespace }}' - data: - iterations: '1' - when: configmap.resources|length == 0 - - - name: Update ConfigMap - k8s: - definition: - apiVersion: v1 - kind: ConfigMap - metadata: - name: '{{ meta.name }}' - namespace: '{{ meta.namespace }}' - data: - iterations: '{{ (configmap.resources.0.data.iterations|int) + 1 }}' - when: configmap.resources|length > 0 and (configmap.resources.0.data.iterations|int) < 5 - - - name: retrieve configmap - k8s_info: - api_version: v1 - kind: ConfigMap - namespace: '{{ meta.namespace }}' - name: '{{ meta.name }}' - register: configmap - - - name: Using the requeue_after module - operator_sdk.util.requeue_after: - time: 1s - when: configmap.resources|length > 0 and (configmap.resources.0.data.iterations|int) < 5 -` - err = kbutil.ReplaceInFile( - filepath.Join(ma.ctx.Dir, "playbooks", "reconciliationtest.yml"), - originalPlaybookFragment, - reconciliationPlaybook) - pkg.CheckError("adding playbook for reconciliationtest", err) - - log.Infof("adding playbook for selectortest") - const selectorPlaybook = `--- -- hosts: localhost - gather_facts: no - collections: - - kubernetes.core - tasks: - - name: Create configmap - k8s: - definition: - apiVersion: v1 - kind: ConfigMap - metadata: - name: '{{ meta.name }}' - namespace: '{{ meta.namespace }}' - data: - hello: "world" -` - err = kbutil.ReplaceInFile( - filepath.Join(ma.ctx.Dir, "playbooks", "selectortest.yml"), - originalPlaybookFragment, - selectorPlaybook) - pkg.CheckError("adding playbook for selectortest", err) - - log.Infof("adding playbook for subresourcestest") - const subresourcesPlaybook = `--- -- hosts: localhost - gather_facts: no - collections: - - kubernetes.core - - operator_sdk.util - - tasks: - - name: Deploy busybox pod - k8s: - definition: - apiVersion: v1 - kind: Pod - metadata: - name: '{{ meta.name }}-busybox' - namespace: '{{ meta.namespace }}' - spec: - containers: - - image: busybox - name: sleep - args: - - "/bin/sh" - - "-c" - - "while true ; do echo '{{ log_message }}' ; sleep 5 ; done" - wait: yes - - - name: Execute command in busybox pod - k8s_exec: - namespace: '{{ meta.namespace }}' - pod: '{{ meta.name }}-busybox' - command: '{{ exec_command }}' - register: exec_result - - - name: Get logs from busybox pod - k8s_log: - name: '{{ meta.name }}-busybox' - namespace: '{{ meta.namespace }}' - register: log_result - - - name: Write results to resource status - k8s_status: - api_version: test.example.com/v1alpha1 - kind: SubresourcesTest - name: '{{ meta.name }}' - namespace: '{{ meta.namespace }}' - status: - execCommandStdout: '{{ exec_result.stdout.strip() }}' - execCommandStderr: '{{ exec_result.stderr.strip() }}' - logs: '{{ log_result.log }}' -` - err = kbutil.ReplaceInFile( - filepath.Join(ma.ctx.Dir, "playbooks", "subresourcestest.yml"), - originalPlaybookFragment, - subresourcesPlaybook) - pkg.CheckError("adding playbook for subresourcestest", err) - - log.Infof("adding playbook for clusterannotationtest") - const clusterAnnotationTest = `--- -- hosts: localhost - gather_facts: no - collections: - - kubernetes.core - tasks: - - - name: create externalnamespace - k8s: - name: "externalnamespace" - api_version: v1 - kind: "Namespace" - definition: - metadata: - labels: - foo: bar - - - name: create configmap - k8s: - definition: - apiVersion: v1 - kind: ConfigMap - metadata: - namespace: "externalnamespace" - name: '{{ meta.name }}' - data: - foo: bar -` - err = kbutil.ReplaceInFile( - filepath.Join(ma.ctx.Dir, "playbooks", "clusterannotationtest.yml"), - originalPlaybookFragment, - clusterAnnotationTest) - pkg.CheckError("adding playbook for clusterannotationtest", err) - - log.Infof("adding playbook for finalizerconcurrencytest") - const finalizerConcurrencyTest = `--- -- hosts: localhost - gather_facts: no - collections: - - kubernetes.core - - operator_sdk.util - - tasks: - - debug: - msg: "Pausing until configmap exists" - - - name: Wait for configmap - k8s_info: - apiVersion: v1 - kind: ConfigMap - name: unpause-reconciliation - namespace: osdk-test - wait: yes - wait_sleep: 10 - wait_timeout: 360 - - - debug: - msg: "Unpause!" -` - err = kbutil.ReplaceInFile( - filepath.Join(ma.ctx.Dir, "playbooks", "finalizerconcurrencytest.yml"), - originalPlaybookFragment, - finalizerConcurrencyTest) - pkg.CheckError("adding playbook for finalizerconcurrencytest", err) -} - -func (ma *AdvancedMolecule) addPlaybooks() { - allPlaybookKinds := []string{ - "ArgsTest", - "CaseTest", - "CollectionTest", - "ClusterAnnotationTest", - "FinalizerConcurrencyTest", - "ReconciliationTest", - "SelectorTest", - "SubresourcesTest", - } - - // Create API - for _, k := range allPlaybookKinds { - logMsgForKind := fmt.Sprintf("creating an API %s", k) - log.Infof(logMsgForKind) - err := ma.ctx.CreateAPI( - "--group", ma.ctx.Group, - "--version", ma.ctx.Version, - "--kind", k, - "--generate-playbook") - pkg.CheckError(logMsgForKind, err) - - k = strings.ToLower(k) - task := fmt.Sprintf("%s_test.yml", k) - logMsgForKind = fmt.Sprintf("removing FIXME assert from %s", task) - log.Infof(logMsgForKind) - err = kbutil.ReplaceInFile( - filepath.Join(ma.ctx.Dir, "molecule", "default", "tasks", task), - fixmeAssert, - "") - pkg.CheckError(logMsgForKind, err) - } -} - -const originalPlaybookFragment = `--- -- hosts: localhost - gather_facts: no - collections: - - kubernetes.core - - operator_sdk.util - tasks: [] -` - -const inventorysampleFragment = `name: inventorytest-sample - annotations: - "ansible.sdk.operatorframework.io/verbosity": "0"` diff --git a/hack/generate/samples/internal/ansible/constants.go b/hack/generate/samples/internal/ansible/constants.go deleted file mode 100644 index d073114cab..0000000000 --- a/hack/generate/samples/internal/ansible/constants.go +++ /dev/null @@ -1,588 +0,0 @@ -// Copyright 2020 The Operator-SDK Authors -// -// 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 ansible - -const roleFragment = ` -- name: start memcached - kubernetes.core.k8s: - definition: - kind: Deployment - apiVersion: apps/v1 - metadata: - name: '{{ ansible_operator_meta.name }}-memcached' - namespace: '{{ ansible_operator_meta.namespace }}' - labels: - app: memcached - spec: - securityContext: - runAsNonRoot: true - seccompProfile: - type: RuntimeDefault - replicas: "{{size}}" - selector: - matchLabels: - app: memcached - template: - metadata: - labels: - app: memcached - spec: - containers: - - name: memcached - securityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - "ALL" - command: - - memcached - - -m=64 - - -o - - modern - - -v - image: "docker.io/memcached:1.4.36-alpine" - ports: - - containerPort: 11211 - readinessProbe: - tcpSocket: - port: 11211 - initialDelaySeconds: 3 - periodSeconds: 3 - -- name: Check if config exists - ansible.builtin.stat: - path: /tmp/metricsbumped - register: metricsbumped - -# Only run once -- block: - - ansible.builtin.file: - path: /tmp/metricsbumped - state: touch - # Sanity - - name: create sanity_counter - operator_sdk.util.osdk_metric: - name: sanity_counter - description: ensure counter can be created - counter: {} - - - name: create sanity_gauge - operator_sdk.util.osdk_metric: - name: sanity_gauge - description: ensure gauge can be created - gauge: {} - - - name: create sanity_histogram - operator_sdk.util.osdk_metric: - name: sanity_histogram - description: ensure histogram can be created - histogram: {} - - - name: create sanity_summary - operator_sdk.util.osdk_metric: - name: sanity_summary - description: ensure summary can be created - summary: {} - - # Counter - - name: Counter increment test setup - operator_sdk.util.osdk_metric: - name: counter_inc_test - description: create counter to be incremented - counter: {} - - - name: Execute Counter increment test - operator_sdk.util.osdk_metric: - name: counter_inc_test - description: increment counter - counter: - increment: yes - - - name: Counter add test setup - operator_sdk.util.osdk_metric: - name: counter_add_test - description: create counter to be added to - counter: {} - - - name: Counter add test exe - operator_sdk.util.osdk_metric: - name: counter_add_test - description: create counter to be incremented - counter: - add: 2 - - # Gauge - - name: Gauge set test - operator_sdk.util.osdk_metric: - name: gauge_set_test - description: create and set a gauge t0 5 - gauge: - set: 5 - - - name: Gauge add test setup - operator_sdk.util.osdk_metric: - name: gauge_add_test - description: create a gauge - gauge: {} - - - name: Gauge add test - operator_sdk.util.osdk_metric: - name: gauge_add_test - description: Add 7 to the gauge - gauge: - add: 7 - - - name: Gauge subtract test setup - operator_sdk.util.osdk_metric: - name: gauge_sub_test - description: create a gauge - gauge: {} - - - name: Gauge sub test - operator_sdk.util.osdk_metric: - name: gauge_sub_test - description: Add 7 to the gauge - gauge: - subtract: 7 - - - name: Gauge time test - operator_sdk.util.osdk_metric: - name: gauge_time_test - description: set the gauge to current time - gauge: - set_to_current_time: yes - - # Summary - - name: Summary test setup - operator_sdk.util.osdk_metric: - name: summary_test - description: create a summary - summary: {} - - - name: Summary test - operator_sdk.util.osdk_metric: - name: summary_test - description: observe a summary - summary: - observe: 2 - - # Histogram - - name: Histogram test setup - operator_sdk.util.osdk_metric: - name: histogram_test - description: create a histogram - histogram: {} - - - name: Histogram test - operator_sdk.util.osdk_metric: - name: histogram_test - description: observe a histogram - histogram: - observe: 2 - when: not metricsbumped.stat.exists -` -const defaultsFragment = `size: 1` - -const moleculeTaskFragment = `- name: Load CR - set_fact: - custom_resource: "{{ lookup('template', '/'.join([samples_dir, cr_file])) | from_yaml }}" - vars: - cr_file: 'cache_v1alpha1_memcached.yaml' - -- name: Create the cache.example.com/v1alpha1.Memcached - k8s: - state: present - namespace: '{{ namespace }}' - definition: '{{ custom_resource }}' - wait: yes - wait_timeout: 300 - wait_condition: - type: Successful - status: "True" - -- name: Wait 2 minutes for memcached deployment - debug: - var: deploy - until: - - deploy is defined - - deploy.status is defined - - deploy.status.replicas is defined - - deploy.status.replicas == deploy.status.get("availableReplicas", 0) - retries: 12 - delay: 10 - vars: - deploy: '{{ lookup("k8s", - kind="Deployment", - api_version="apps/v1", - namespace=namespace, - label_selector="app=memcached" - )}}' - -- name: Verify custom status exists - assert: - that: debug_cr.status.get("test") == "hello world" - vars: - debug_cr: '{{ lookup("k8s", - kind=custom_resource.kind, - api_version=custom_resource.apiVersion, - namespace=namespace, - resource_name=custom_resource.metadata.name - )}}' - -- when: molecule_yml.scenario.name == "test-local" - block: - - name: Restart the operator by killing the pod - k8s: - state: absent - definition: - api_version: v1 - kind: Pod - metadata: - namespace: '{{ namespace }}' - name: '{{ pod.metadata.name }}' - vars: - pod: '{{ q("k8s", api_version="v1", kind="Pod", namespace=namespace, label_selector="name=%s").0 }}' - - - name: Wait 2 minutes for operator deployment - debug: - var: deploy - until: - - deploy is defined - - deploy.status is defined - - deploy.status.replicas is defined - - deploy.status.replicas == deploy.status.get("availableReplicas", 0) - retries: 12 - delay: 10 - vars: - deploy: '{{ lookup("k8s", - kind="Deployment", - api_version="apps/v1", - namespace=namespace, - resource_name="%s" - )}}' - - - name: Wait for reconciliation to have a chance at finishing - pause: - seconds: 15 - - - name: Delete the service that is created. - k8s: - kind: Service - api_version: v1 - namespace: '{{ namespace }}' - name: test-service - state: absent - - - name: Verify that test-service was re-created - debug: - var: service - until: service - retries: 12 - delay: 10 - vars: - service: '{{ lookup("k8s", - kind="Service", - api_version="v1", - namespace=namespace, - resource_name="test-service", - )}}' - -- name: Delete the custom resource - k8s: - state: absent - namespace: '{{ namespace }}' - definition: '{{ custom_resource }}' - -- name: Wait for the custom resource to be deleted - k8s_info: - api_version: '{{ custom_resource.apiVersion }}' - kind: '{{ custom_resource.kind }}' - namespace: '{{ namespace }}' - name: '{{ custom_resource.metadata.name }}' - register: cr - retries: 10 - delay: 6 - until: not cr.resources - failed_when: cr.resources - -- name: Verify the Deployment was deleted (wait 30s) - assert: - that: not lookup('k8s', kind='Deployment', api_version='apps/v1', namespace=namespace, label_selector='app=memcached') - retries: 10 - delay: 3 -` - -const memcachedCustomStatusMoleculeTarget = `- name: Verify custom status exists - assert: - that: debug_cr.status.get("test") == "hello world" - vars: - debug_cr: '{{ lookup("k8s", - kind=custom_resource.kind, - api_version=custom_resource.apiVersion, - namespace=namespace, - resource_name=custom_resource.metadata.name - )}}'` - -// false positive: G101: Potential hardcoded credentials (gosec) -// nolint:gosec -const testSecretMoleculeCheck = ` - -# This will verify that the secret role was executed -- name: Verify that test-service was created - assert: - that: lookup('k8s', kind='Service', api_version='v1', namespace=namespace, resource_name='test-service') -` - -const testFooMoleculeCheck = ` - -- name: Verify that project testing-foo was created - assert: - that: lookup('k8s', kind='Namespace', api_version='v1', resource_name='testing-foo') - when: "'project.openshift.io' in lookup('k8s', cluster_info='api_groups')" -` - -// false positive: G101: Potential hardcoded credentials (gosec) -// nolint:gosec -const originalTaskSecret = `--- -# tasks file for Secret -` - -// false positive: G101: Potential hardcoded credentials (gosec) -// nolint:gosec -const taskForSecret = `- name: Create test service - kubernetes.core.k8s: - definition: - kind: Service - api_version: v1 - metadata: - name: test-service - namespace: default - spec: - ports: - - protocol: TCP - port: 8332 - targetPort: 8332 - name: rpc - -` - -// false positive: G101: Potential hardcoded credentials (gosec) -// nolint:gosec -const manageStatusFalseForRoleSecret = `role: secret - manageStatus: false` - -const fixmeAssert = ` -- name: Add assertions here - assert: - that: false - fail_msg: FIXME Add real assertions for your operator -` - -const originaMemcachedMoleculeTask = `- name: Create the cache.example.com/v1alpha1.Memcached - k8s: - state: present - namespace: '{{ namespace }}' - definition: "{{ lookup('template', '/'.join([samples_dir, cr_file])) | from_yaml }}" - wait: yes - wait_timeout: 300 - wait_condition: - type: Successful - status: "True" - vars: - cr_file: 'cache_v1alpha1_memcached.yaml' - -- name: Add assertions here - assert: - that: false - fail_msg: FIXME Add real assertions for your operator` - -const targetMoleculeCheckDeployment = `- name: Wait 2 minutes for memcached deployment - debug: - var: deploy - until: - - deploy is defined - - deploy.status is defined - - deploy.status.replicas is defined - - deploy.status.replicas == deploy.status.get("availableReplicas", 0) - retries: 12 - delay: 10 - vars: - deploy: '{{ lookup("k8s", - kind="Deployment", - api_version="apps/v1", - namespace=namespace, - label_selector="app=memcached" - )}}'` - -const molecuTaskToCheckConfigMap = ` -- name: Create ConfigMap that the Operator should delete - k8s: - definition: - apiVersion: v1 - kind: ConfigMap - metadata: - name: deleteme - namespace: '{{ namespace }}' - data: - delete: me -` - -const memcachedWithBlackListTask = ` -- operator_sdk.util.k8s_status: - api_version: cache.example.com/v1alpha1 - kind: Memcached - name: "{{ ansible_operator_meta.name }}" - namespace: "{{ ansible_operator_meta.namespace }}" - status: - test: "hello world" - -- kubernetes.core.k8s: - definition: - kind: Secret - apiVersion: v1 - metadata: - name: test-secret - namespace: "{{ ansible_operator_meta.namespace }}" - data: - test: aGVsbG8K -- name: Get cluster api_groups - set_fact: - api_groups: "{{ lookup('kubernetes.core.k8s', cluster_info='api_groups', kubeconfig=lookup('env', 'K8S_AUTH_KUBECONFIG')) }}" - -- name: create project if projects are available - kubernetes.core.k8s: - definition: - apiVersion: project.openshift.io/v1 - kind: Project - metadata: - name: testing-foo - when: "'project.openshift.io' in api_groups" - -- name: Create ConfigMap to test blacklisted watches - kubernetes.core.k8s: - definition: - kind: ConfigMap - apiVersion: v1 - metadata: - name: test-blacklist-watches - namespace: "{{ ansible_operator_meta.namespace }}" - data: - arbitrary: afdasdfsajsafj - state: present` - -const taskToDeleteConfigMap = `- name: delete configmap for test - kubernetes.core.k8s: - kind: ConfigMap - api_version: v1 - name: deleteme - namespace: default - state: absent` - -const memcachedWatchCustomizations = `playbook: playbooks/memcached.yml - finalizer: - name: cache.example.com/finalizer - role: memfin - blacklist: - - group: "" - version: v1 - kind: ConfigMap` - -const rolesForBaseOperator = ` - ## - ## Apply customize roles for base operator - ## - - apiGroups: - - "" - resources: - - configmaps - - services - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -#+kubebuilder:scaffold:rules -` - -const customMetricsTest = ` -- name: Search for all running pods - kubernetes.core.k8s_info: - kind: Pod - label_selectors: - - "control-plane = controller-manager" - register: output -- name: Curl the metrics from the manager - kubernetes.core.k8s_exec: - namespace: default - container: manager - pod: "{{ output.resources[0].metadata.name }}" - command: curl localhost:8080/metrics - register: metrics_output - -- name: Assert sanity metrics were created - assert: - that: - - "'sanity_counter 0' in metrics_output.stdout" - - "'sanity_gauge 0' in metrics_output.stdout" - - "'sanity_histogram_bucket' in metrics_output.stdout" - - "'sanity_summary summary' in metrics_output.stdout" - -- name: Assert Counter works as expected - assert: - that: - - "'counter_inc_test 1' in metrics_output.stdout" - - "'counter_add_test 2' in metrics_output.stdout" - -- name: Assert Gauge works as expected - assert: - that: - - "'gauge_set_test 5' in metrics_output.stdout" - - "'gauge_add_test 7' in metrics_output.stdout" - - "'gauge_sub_test -7' in metrics_output.stdout" - # result is epoch time in seconds so the first digit is good until 2033 - - "'gauge_time_test 1' in metrics_output.stdout" - -- name: Assert Summary works as expected - assert: - that: - - "'summary_test_sum 2' in metrics_output.stdout" - -- name: Assert Histogram works as expected - assert: - that: - - "'histogram_test_sum 2' in metrics_output.stdout" - -` - -const watchNamespacePatch = `--- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: controller-manager - namespace: system -spec: - template: - spec: - containers: - - name: manager - env: - - name: WATCH_NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace -` diff --git a/hack/generate/samples/internal/ansible/generate.go b/hack/generate/samples/internal/ansible/generate.go deleted file mode 100644 index 2d0321e036..0000000000 --- a/hack/generate/samples/internal/ansible/generate.go +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2021 The Operator-SDK Authors -// -// 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 ansible - -import ( - "path/filepath" -) - -func GenerateMemcachedSamples(binaryPath, rootPath string) { - GenerateMemcachedSample(binaryPath, filepath.Join(rootPath, "ansible")) -} diff --git a/hack/generate/samples/internal/ansible/memcached.go b/hack/generate/samples/internal/ansible/memcached.go deleted file mode 100644 index 297b390a57..0000000000 --- a/hack/generate/samples/internal/ansible/memcached.go +++ /dev/null @@ -1,136 +0,0 @@ -// Copyright 2020 The Operator-SDK Authors -// -// 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 ansible - -import ( - "fmt" - "path/filepath" - "strings" - - log "github.com/sirupsen/logrus" - kbutil "sigs.k8s.io/kubebuilder/v3/pkg/plugin/util" - - "github.com/operator-framework/operator-sdk/hack/generate/samples/internal/pkg" -) - -// Memcached defines the context for the sample -type Memcached struct { - ctx *pkg.SampleContext -} - -// GenerateMemcachedSample will call all actions to create the directory and generate the sample -// The Context to run the samples are not the same in the e2e test. In this way, note that it should NOT -// be called in the e2e tests since it will call the Prepare() to set the sample context and generate the files -// in the testdata directory. The e2e tests only ought to use the Run() method with the TestContext. -func GenerateMemcachedSample(binaryPath, samplesPath string) { - ctx, err := pkg.NewSampleContext(binaryPath, filepath.Join(samplesPath, "memcached-operator"), - "GO111MODULE=on") - pkg.CheckError("generating Ansible memcached context", err) - - memcached := Memcached{&ctx} - memcached.Prepare() - memcached.Run() -} - -// Prepare the Context for the Memcached Ansible Sample -// Note that sample directory will be re-created and the context data for the sample -// will be set such as the domain and GVK. -func (ma *Memcached) Prepare() { - log.Infof("destroying directory for memcached Ansible samples") - ma.ctx.Destroy() - - log.Infof("creating directory") - err := ma.ctx.Prepare() - pkg.CheckError("creating directory for Ansible Sample", err) - - log.Infof("setting domain and GVK") - ma.ctx.Domain = "example.com" - ma.ctx.Version = "v1alpha1" - ma.ctx.Group = "cache" - ma.ctx.Kind = "Memcached" -} - -// Run the steps to create the Memcached Ansible Sample -func (ma *Memcached) Run() { - log.Infof("creating the project") - err := ma.ctx.Init( - "--plugins", "ansible", - "--group", ma.ctx.Group, - "--version", ma.ctx.Version, - "--kind", ma.ctx.Kind, - "--domain", ma.ctx.Domain, - "--generate-role", - "--generate-playbook") - pkg.CheckError("creating the project", err) - - log.Infof("customizing the sample") - err = kbutil.UncommentCode( - filepath.Join(ma.ctx.Dir, "config", "default", "kustomization.yaml"), - "#- ../prometheus", "#") - pkg.CheckError("enabling prometheus metrics", err) - - err = ma.ctx.UncommentRestrictivePodStandards() - pkg.CheckError("creating the bundle", err) - - ma.addingAnsibleTask() - ma.addingMoleculeMockData() - - log.Infof("creating the bundle") - err = ma.ctx.GenerateBundle() - pkg.CheckError("creating the bundle", err) - - log.Infof("striping bundle annotations") - err = ma.ctx.StripBundleAnnotations() - pkg.CheckError("striping bundle annotations", err) - - log.Infof("setting createdAt annotation") - csv := filepath.Join(ma.ctx.Dir, "bundle", "manifests", ma.ctx.ProjectName+".clusterserviceversion.yaml") - err = kbutil.ReplaceRegexInFile(csv, "createdAt:.*", createdAt) - pkg.CheckError("setting createdAt annotation", err) -} - -// addingMoleculeMockData will customize the molecule data -func (ma *Memcached) addingMoleculeMockData() { - log.Infof("adding molecule test for Ansible task") - moleculeTaskPath := filepath.Join(ma.ctx.Dir, "molecule", "default", "tasks", - fmt.Sprintf("%s_test.yml", strings.ToLower(ma.ctx.Kind))) - - err := kbutil.ReplaceInFile(moleculeTaskPath, - originaMemcachedMoleculeTask, fmt.Sprintf(moleculeTaskFragment, ma.ctx.ProjectName, ma.ctx.ProjectName)) - pkg.CheckError("replacing molecule default tasks", err) -} - -// addingAnsibleTask will add the Ansible Task and update the sample -func (ma *Memcached) addingAnsibleTask() { - log.Infof("adding Ansible task and variable") - err := kbutil.InsertCode(filepath.Join(ma.ctx.Dir, "roles", strings.ToLower(ma.ctx.Kind), - "tasks", "main.yml"), - fmt.Sprintf("# tasks file for %s", ma.ctx.Kind), - roleFragment) - pkg.CheckError("adding task", err) - - err = kbutil.ReplaceInFile(filepath.Join(ma.ctx.Dir, "roles", strings.ToLower(ma.ctx.Kind), - "defaults", "main.yml"), - fmt.Sprintf("# defaults file for %s", ma.ctx.Kind), - defaultsFragment) - pkg.CheckError("adding defaulting", err) - - err = kbutil.ReplaceInFile(filepath.Join(ma.ctx.Dir, "config", "samples", - fmt.Sprintf("%s_%s_%s.yaml", ma.ctx.Group, ma.ctx.Version, strings.ToLower(ma.ctx.Kind))), - "# TODO(user): Add fields here", "size: 1") - pkg.CheckError("updating sample CR", err) -} - -const createdAt = `createdAt: "2022-11-08T17:26:37Z"` diff --git a/hack/generate/samples/internal/ansible/memcached_molecule.go b/hack/generate/samples/internal/ansible/memcached_molecule.go deleted file mode 100644 index 1f2bbae68a..0000000000 --- a/hack/generate/samples/internal/ansible/memcached_molecule.go +++ /dev/null @@ -1,210 +0,0 @@ -// Copyright 2020 The Operator-SDK Authors -// -// 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 ansible - -import ( - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - - log "github.com/sirupsen/logrus" - kbutil "sigs.k8s.io/kubebuilder/v3/pkg/plugin/util" - - "github.com/operator-framework/operator-sdk/hack/generate/samples/internal/pkg" -) - -// MemcachedMolecule defines the context for the sample -type MemcachedMolecule struct { - ctx *pkg.SampleContext - Base Memcached -} - -// GenerateMoleculeSample will call all actions to create the directory and generate the sample -// The Context to run the samples are not the same in the e2e test. In this way, note that it should NOT -// be called in the e2e tests since it will call the Prepare() to set the sample context and generate the files -// in the testdata directory. The e2e tests only ought to use the Run() method with the TestContext. -func GenerateMoleculeSample(binaryPath, samplesPath string) { - ctx, err := pkg.NewSampleContext(binaryPath, filepath.Join(samplesPath, "memcached-molecule-operator"), - "GO111MODULE=on") - pkg.CheckError("generating Ansible Moleule memcached context", err) - - molecule := MemcachedMolecule{ctx: &ctx, Base: Memcached{&ctx}} - molecule.Prepare() - molecule.Run() -} - -// Prepare the Context for the Memcached Ansible Sample -// Note that sample directory will be re-created and the context data for the sample -// will be set such as the domain and GVK. -func (ma *MemcachedMolecule) Prepare() { - log.Infof("destroying directory for memcached Ansible samples") - ma.ctx.Destroy() - - log.Infof("creating directory") - err := ma.ctx.Prepare() - pkg.CheckError("creating directory for Ansible Sample", err) - - log.Infof("setting domain and GVK") - ma.ctx.Domain = "example.com" - ma.ctx.Version = "v1alpha1" - ma.ctx.Group = "cache" - ma.ctx.Kind = "Memcached" -} - -// Run the steps to create the Memcached Ansible Sample -func (ma *MemcachedMolecule) Run() { - ma.Base.Run() - - moleculeTaskPath := filepath.Join(ma.ctx.Dir, "molecule", "default", "tasks", - fmt.Sprintf("%s_test.yml", strings.ToLower(ma.ctx.Kind))) - - log.Infof("insert molecule task to ensure that ConfigMap will be deleted") - err := kbutil.InsertCode(moleculeTaskPath, targetMoleculeCheckDeployment, molecuTaskToCheckConfigMap) - pkg.CheckError("replacing memcached task to add config map check", err) - - log.Infof("insert molecule task to ensure to check secret") - err = kbutil.InsertCode(moleculeTaskPath, memcachedCustomStatusMoleculeTarget, testSecretMoleculeCheck) - pkg.CheckError("replacing memcached task to add secret check", err) - - log.Infof("insert molecule task to ensure to foo ") - err = kbutil.InsertCode(moleculeTaskPath, testSecretMoleculeCheck, testFooMoleculeCheck) - pkg.CheckError("replacing memcached task to add foo check", err) - - log.Infof("insert molecule task to check custom metrics") - err = kbutil.InsertCode(moleculeTaskPath, testFooMoleculeCheck, customMetricsTest) - - pkg.CheckError("replacing memcached task to add foo check", err) - - log.Infof("replacing project Dockerfile to use ansible base image with the dev tag") - err = kbutil.ReplaceRegexInFile(filepath.Join(ma.ctx.Dir, "Dockerfile"), "quay.io/operator-framework/ansible-operator:.*", "quay.io/operator-framework/ansible-operator:dev") - pkg.CheckError("replacing Dockerfile", err) - - log.Infof("adding RBAC permissions") - err = kbutil.ReplaceInFile(filepath.Join(ma.ctx.Dir, "config", "rbac", "role.yaml"), - "#+kubebuilder:scaffold:rules", rolesForBaseOperator) - pkg.CheckError("replacing in role.yml", err) - - log.Infof("adding Memcached mock task to the role with black list") - err = kbutil.InsertCode(filepath.Join(ma.ctx.Dir, "roles", strings.ToLower(ma.ctx.Kind), "tasks", "main.yml"), - roleFragment, memcachedWithBlackListTask) - pkg.CheckError("replacing in tasks/main.yml", err) - - log.Infof("creating an API definition Foo") - err = ma.ctx.CreateAPI( - "--group", ma.ctx.Group, - "--version", ma.ctx.Version, - "--kind", "Foo", - "--generate-role") - pkg.CheckError("creating api", err) - - log.Infof("updating spec of foo sample") - err = kbutil.ReplaceInFile( - filepath.Join(ma.ctx.Dir, "config", "samples", "cache_v1alpha1_foo.yaml"), - "# TODO(user): Add fields here", - "foo: bar") - pkg.CheckError("updating spec of cache_v1alpha1_foo.yaml", err) - - log.Infof("creating an API definition to add a task to delete the config map") - err = ma.ctx.CreateAPI( - "--group", ma.ctx.Group, - "--version", ma.ctx.Version, - "--kind", "Memfin", - "--generate-role") - pkg.CheckError("creating api", err) - - log.Infof("updating spec of Memfin sample") - err = kbutil.ReplaceInFile( - filepath.Join(ma.ctx.Dir, "config", "samples", "cache_v1alpha1_memfin.yaml"), - "# TODO(user): Add fields here", - "foo: bar") - pkg.CheckError("updating spec of cache_v1alpha1_memfin.yaml ", err) - - log.Infof("adding task to delete config map") - err = kbutil.ReplaceInFile(filepath.Join(ma.ctx.Dir, "roles", "memfin", "tasks", "main.yml"), - "# tasks file for Memfin", taskToDeleteConfigMap) - pkg.CheckError("replacing in tasks/main.yml", err) - - log.Infof("adding to watches finalizer and blacklist") - err = kbutil.ReplaceInFile(filepath.Join(ma.ctx.Dir, "watches.yaml"), - "playbook: playbooks/memcached.yml", memcachedWatchCustomizations) - pkg.CheckError("replacing in watches", err) - - log.Infof("enabling multigroup support") - err = ma.ctx.AllowProjectBeMultiGroup() - pkg.CheckError("updating PROJECT file", err) - - log.Infof("creating core Secret API") - err = ma.ctx.CreateAPI( - // the tool do not allow we crate an API with a group nil for v2+ - // which is required here to mock the tests. - // however, it is done already for v3+. More info: https://github.com/kubernetes-sigs/kubebuilder/issues/1404 - // and the tests should be changed when the tool allows we create API's for core types. - // todo: replace the ignore value when the tool provide a solution for it. - "--group", "ignore", - "--version", "v1", - "--kind", "Secret", - "--generate-role") - pkg.CheckError("creating api", err) - - log.Infof("updating spec of ignore sample") - err = kbutil.ReplaceInFile( - filepath.Join(ma.ctx.Dir, "config", "samples", "ignore_v1_secret.yaml"), - "# TODO(user): Add fields here", - "foo: bar") - pkg.CheckError("updating spec of ignore_v1_secret.yaml", err) - - log.Infof("removing ignore group for the secret from watches as an workaround to work with core types") - err = kbutil.ReplaceInFile(filepath.Join(ma.ctx.Dir, "watches.yaml"), - "ignore.example.com", "\"\"") - pkg.CheckError("replacing the watches file", err) - - log.Infof("removing molecule test for the Secret since it is a core type") - cmd := exec.Command("rm", "-rf", filepath.Join(ma.ctx.Dir, "molecule", "default", "tasks", "secret_test.yml")) - _, err = ma.ctx.Run(cmd) - pkg.CheckError("removing secret test file", err) - - log.Infof("adding Secret task to the role") - err = kbutil.ReplaceInFile(filepath.Join(ma.ctx.Dir, "roles", "secret", "tasks", "main.yml"), - originalTaskSecret, taskForSecret) - pkg.CheckError("replacing in secret/tasks/main.yml file", err) - - log.Infof("adding ManageStatus == false for role secret") - err = kbutil.ReplaceInFile(filepath.Join(ma.ctx.Dir, "watches.yaml"), - "role: secret", manageStatusFalseForRoleSecret) - pkg.CheckError("replacing in watches.yaml", err) - - // prevent high load of controller caused by watching all the secrets in the cluster - watchNamespacePatchFileName := "watch_namespace_patch.yaml" - log.Info("adding WATCH_NAMESPACE env patch to watch own namespace") - err = os.WriteFile(filepath.Join(ma.ctx.Dir, "config", "testing", watchNamespacePatchFileName), []byte(watchNamespacePatch), 0644) - pkg.CheckError("adding watch_namespace_patch.yaml", err) - - log.Info("adding WATCH_NAMESPACE env patch to patch list to be applied") - err = kbutil.InsertCode(filepath.Join(ma.ctx.Dir, "config", "testing", "kustomization.yaml"), "patchesStrategicMerge:", - fmt.Sprintf("\n- %s", watchNamespacePatchFileName)) - pkg.CheckError("inserting in kustomization.yaml", err) - - log.Infof("removing FIXME asserts from memfin_test.yml") - err = kbutil.ReplaceInFile(filepath.Join(ma.ctx.Dir, "molecule", "default", "tasks", "memfin_test.yml"), - fixmeAssert, "") - pkg.CheckError("replacing memfin_test.yml", err) - - log.Infof("removing FIXME asserts from foo_test.yml") - err = kbutil.ReplaceInFile(filepath.Join(ma.ctx.Dir, "molecule", "default", "tasks", "foo_test.yml"), - fixmeAssert, "") - pkg.CheckError("replacing foo_test.yml", err) -} diff --git a/hack/generate/samples/internal/ansible/testdata/ansible.cfg b/hack/generate/samples/internal/ansible/testdata/ansible.cfg deleted file mode 100644 index 4dcc9a98e1..0000000000 --- a/hack/generate/samples/internal/ansible/testdata/ansible.cfg +++ /dev/null @@ -1,10 +0,0 @@ -[defaults] -inventory_plugins = /opt/ansible/plugins/inventory -stdout_callback = yaml -callback_whitelist = profile_tasks,timer -module_utils = /opt/ansible/module_utils -roles_path = /opt/ansible/roles -library = /opt/ansible/library -inventory = /opt/ansible/inventory -filter_plugins = /opt/ansible/plugins/filter -remote_tmp = /tmp/ansible diff --git a/hack/generate/samples/internal/ansible/testdata/fixture_collection/galaxy.yml b/hack/generate/samples/internal/ansible/testdata/fixture_collection/galaxy.yml deleted file mode 100644 index 0f2699d96a..0000000000 --- a/hack/generate/samples/internal/ansible/testdata/fixture_collection/galaxy.yml +++ /dev/null @@ -1,6 +0,0 @@ -namespace: operator_sdk -name: test_fixtures -version: 0.0.0 -readme: README.md -authors: -- your name diff --git a/hack/generate/samples/internal/ansible/testdata/fixture_collection/roles/dummy/tasks/main.yml b/hack/generate/samples/internal/ansible/testdata/fixture_collection/roles/dummy/tasks/main.yml deleted file mode 100644 index 4543ce2b84..0000000000 --- a/hack/generate/samples/internal/ansible/testdata/fixture_collection/roles/dummy/tasks/main.yml +++ /dev/null @@ -1,12 +0,0 @@ ---- -- name: Create ConfigMap - kubernetes.core.k8s: - definition: - kind: ConfigMap - apiVersion: v1 - metadata: - name: test-this-collection - namespace: "{{ meta.namespace }}" - data: - did_it_work: "indeed" - state: present diff --git a/hack/generate/samples/internal/ansible/testdata/inventory/group_vars/test.yml b/hack/generate/samples/internal/ansible/testdata/inventory/group_vars/test.yml deleted file mode 100644 index b89764e02f..0000000000 --- a/hack/generate/samples/internal/ansible/testdata/inventory/group_vars/test.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- - -sentinel: test diff --git a/hack/generate/samples/internal/ansible/testdata/inventory/hosts b/hack/generate/samples/internal/ansible/testdata/inventory/hosts deleted file mode 100644 index fd2ec1a607..0000000000 --- a/hack/generate/samples/internal/ansible/testdata/inventory/hosts +++ /dev/null @@ -1,5 +0,0 @@ -[test] -127.0.0.1 ansible_connection=local - -[all:vars] -ansible_python_interpreter=/usr/bin/python3 \ No newline at end of file diff --git a/hack/generate/samples/internal/ansible/testdata/playbooks/finalizerconcurrencyfinalizer.yml b/hack/generate/samples/internal/ansible/testdata/playbooks/finalizerconcurrencyfinalizer.yml deleted file mode 100644 index 2cc57f3b11..0000000000 --- a/hack/generate/samples/internal/ansible/testdata/playbooks/finalizerconcurrencyfinalizer.yml +++ /dev/null @@ -1,34 +0,0 @@ ---- -- hosts: localhost - gather_facts: no - collections: - - kubernetes.core - - operator_sdk.util - - tasks: - - debug: - msg: "Pausing until configmap exists" - - - name: Wait for configmap - k8s_info: - api_version: v1 - kind: ConfigMap - name: finalizer-concurrency-results - namespace: osdk-test - wait: yes - wait_sleep: 10 - wait_timeout: 30 - - - name: Update configmap - k8s: - state: present - force: yes - definition: - apiVersion: v1 - kind: ConfigMap - metadata: - name: finalizer-concurrency-results - namespace: osdk-test - data: - finalizer: "success" - wait: yes diff --git a/hack/generate/samples/internal/ansible/testdata/plugins/filter/test.py b/hack/generate/samples/internal/ansible/testdata/plugins/filter/test.py deleted file mode 100644 index 2058f71c2d..0000000000 --- a/hack/generate/samples/internal/ansible/testdata/plugins/filter/test.py +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/python3 - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - - -def test(sentinel): - return sentinel == 'test' - - -class FilterModule(object): - ''' Fake test plugin for ansible-operator ''' - - def filters(self): - return { - 'test': test - } diff --git a/hack/generate/samples/internal/ansible/testdata/secret.yml b/hack/generate/samples/internal/ansible/testdata/secret.yml deleted file mode 100644 index 30374b88e7..0000000000 --- a/hack/generate/samples/internal/ansible/testdata/secret.yml +++ /dev/null @@ -1,22 +0,0 @@ ---- -- hosts: localhost - gather_facts: no - collections: - - kubernetes.core - - tasks: - - meta: end_play - when: not (__secret.metadata.get('labels', {}).reconcile|default(false)|bool) - - # This is for testing, but never do this with real secrets - - name: Populate configmap with contents of secret - k8s: - definition: | - apiVersion: v1 - kind: ConfigMap - metadata: - name: '{{ meta.name }}' - namespace: '{{ meta.namespace }}' - data: - '{{ item.key }}': '{{ item.value | b64decode }}' - with_dict: '{{ __secret.data }}' diff --git a/hack/generate/samples/internal/ansible/testdata/tasks/argstest_test.yml b/hack/generate/samples/internal/ansible/testdata/tasks/argstest_test.yml deleted file mode 100644 index 180e1ffc4d..0000000000 --- a/hack/generate/samples/internal/ansible/testdata/tasks/argstest_test.yml +++ /dev/null @@ -1,25 +0,0 @@ ---- -- name: Create the test.example.com/v1alpha1.ArgsTest - k8s: - state: present - definition: - apiVersion: test.example.com/v1alpha1 - kind: ArgsTest - metadata: - name: args-test - namespace: '{{ namespace }}' - spec: - field: value - wait: yes - wait_timeout: 300 - wait_condition: - type: Successful - status: "True" - register: args_test - -- name: Assert sentinel ConfigMap has been created for Molecule Test - assert: - that: cm.data.msg == "The decrypted value is thisisatest" - vars: - cm: "{{ q('k8s', api_version='v1', kind='ConfigMap', namespace=namespace, - resource_name='args-test').0 }}" diff --git a/hack/generate/samples/internal/ansible/testdata/tasks/casetest_test.yml b/hack/generate/samples/internal/ansible/testdata/tasks/casetest_test.yml deleted file mode 100644 index 427a440b2d..0000000000 --- a/hack/generate/samples/internal/ansible/testdata/tasks/casetest_test.yml +++ /dev/null @@ -1,24 +0,0 @@ ---- -- name: Create the test.example.com/v1alpha1.CaseTest - k8s: - state: present - definition: - apiVersion: test.example.com/v1alpha1 - kind: CaseTest - metadata: - name: case-test - namespace: '{{ namespace }}' - spec: - camelCaseVar: "true" - wait: yes - wait_timeout: 300 - wait_condition: - type: Successful - status: "True" - register: case_test - -- name: Assert sentinel ConfigMap has been created for Molecule Test - assert: - that: cm.data.shouldBeCamel == 'true' - vars: - cm: "{{ q('k8s', api_version='v1', kind='ConfigMap', namespace=namespace, resource_name='case-test').0 }}" diff --git a/hack/generate/samples/internal/ansible/testdata/tasks/clusterannotationtest_test.yml b/hack/generate/samples/internal/ansible/testdata/tasks/clusterannotationtest_test.yml deleted file mode 100644 index 668c989ada..0000000000 --- a/hack/generate/samples/internal/ansible/testdata/tasks/clusterannotationtest_test.yml +++ /dev/null @@ -1,51 +0,0 @@ ---- -- name: Create the test.example.com/v1alpha1.ClusterAnnotationTest - k8s: - state: present - namespace: '{{ namespace }}' - definition: "{{ lookup('template', '/'.join([samples_dir, cr_file])) | from_yaml }}" - wait: yes - wait_timeout: 300 - wait_condition: - type: Successful - status: "True" - vars: - cr_file: 'test_v1alpha1_clusterannotationtest.yaml' - -- name: retrieve configmap - k8s_info: - api_version: v1 - kind: ConfigMap - namespace: "externalnamespace" - name: "clusterannotationtest-sample" - register: configmap - until: (configmap.resources | length) == 1 - -- assert: - that: - - configmap.resources[0].metadata.annotations["operator-sdk/primary-resource"] == primary - - configmap.resources[0].metadata.annotations["operator-sdk/primary-resource-type"] == primary_type - vars: - primary: "osdk-test/clusterannotationtest-sample" - primary_type: "ClusterAnnotationTest.test.example.com" - -- name: change the namespace labels - k8s: - name: "externalnamespace" - api_version: v1 - kind: "Namespace" - wait: yes - definition: - metadata: - labels: - foo: baz - -- name: Make sure the label is changed back - k8s_info: - api_version: v1 - kind: Namespace - name: "externalnamespace" - register: external_namespace - until: external_namespace.resources[0].metadata.labels["foo"] == "bar" - retries: 6 - diff --git a/hack/generate/samples/internal/ansible/testdata/tasks/collectiontest_test.yml b/hack/generate/samples/internal/ansible/testdata/tasks/collectiontest_test.yml deleted file mode 100644 index 9623d2ad69..0000000000 --- a/hack/generate/samples/internal/ansible/testdata/tasks/collectiontest_test.yml +++ /dev/null @@ -1,26 +0,0 @@ ---- -- name: Create the test.example.com/v1alpha1.CollectionTest - k8s: - state: present - namespace: '{{ namespace }}' - definition: - apiVersion: test.example.com/v1alpha1 - kind: CollectionTest - metadata: - name: collection-test - wait: yes - wait_timeout: 300 - wait_condition: - type: Successful - status: "True" - -- name: Assert ConfigMap has been created by collection Role - assert: - that: cm.data.did_it_work == 'indeed' - vars: - cm: "{{ q('k8s', - api_version='v1', - kind='ConfigMap', - namespace=namespace, - resource_name='test-this-collection' - ).0 }}" diff --git a/hack/generate/samples/internal/ansible/testdata/tasks/finalizerconcurrencytest_test.yml b/hack/generate/samples/internal/ansible/testdata/tasks/finalizerconcurrencytest_test.yml deleted file mode 100644 index 8131669c6c..0000000000 --- a/hack/generate/samples/internal/ansible/testdata/tasks/finalizerconcurrencytest_test.yml +++ /dev/null @@ -1,58 +0,0 @@ ---- -# TODO(asmacdo) this should be the only task. the other is getting magiced in -- name: Create the test.example.com/v1alpha1.FinalizerConcurrencyTest - k8s: - state: present - definition: - apiVersion: test.example.com/v1alpha1 - kind: FinalizerConcurrencyTest - metadata: - name: finalizer-concurrency-test - namespace: '{{ namespace }}' - wait: no - -- name: While reconcile is paused, delete the CR - k8s: - state: absent - definition: - apiVersion: test.example.com/v1alpha1 - kind: FinalizerConcurrencyTest - metadata: - name: finalizer-concurrency-test - namespace: '{{ namespace }}' - wait: no - -- name: Create a configmap to allow reconciliation to unpause - k8s: - state: present - definition: - apiVersion: v1 - kind: ConfigMap - metadata: - name: finalizer-concurrency-results - namespace: osdk-test - wait: no - -- name: Wait for the custom resource to be deleted - k8s_info: - api_version: test.example.com/v1alpha1 - kind: FinalizerConcurrencyTest - namespace: osdk-test # TODO(asmacdo) Fixme - name: finalizer-concurrency-test - register: cr - retries: 10 - delay: 6 - until: not cr.resources - failed_when: cr.resources - -- name: Retrive the cm - k8s_info: - api_version: v1 - kind: ConfigMap - name: finalizer-concurrency-results - namespace: osdk-test - register: finalizer_test - -- name: Assert that finalizer ran - assert: - that: finalizer_test.resources.0.data.finalizer== 'success' diff --git a/hack/generate/samples/internal/ansible/testdata/tasks/inventorytest_test.yml b/hack/generate/samples/internal/ansible/testdata/tasks/inventorytest_test.yml deleted file mode 100644 index 649713e392..0000000000 --- a/hack/generate/samples/internal/ansible/testdata/tasks/inventorytest_test.yml +++ /dev/null @@ -1,22 +0,0 @@ ---- -- name: Create the test.example.com/v1alpha1.InventoryTest - k8s: - state: present - namespace: '{{ namespace }}' - definition: '{{ custom_resource }}' - wait: yes - wait_timeout: 300 - wait_condition: - type: Successful - status: "True" - vars: - custom_resource: "{{ lookup('template', '/'.join([ - config_dir, - 'samples/test_v1alpha1_inventorytest.yaml' - ])) | from_yaml }}" - -- name: Assert sentinel ConfigMap has been created for Molecule Test - assert: - that: cm.data.sentinel == 'test' - vars: - cm: "{{ q('k8s', api_version='v1', kind='ConfigMap', namespace=namespace, resource_name='inventory-cm').0 }}" diff --git a/hack/generate/samples/internal/ansible/testdata/tasks/reconciliationtest_test.yml b/hack/generate/samples/internal/ansible/testdata/tasks/reconciliationtest_test.yml deleted file mode 100644 index b415b57973..0000000000 --- a/hack/generate/samples/internal/ansible/testdata/tasks/reconciliationtest_test.yml +++ /dev/null @@ -1,32 +0,0 @@ ---- -- name: Create the test.example.com/v1alpha1.ReconciliationTest - k8s: - state: present - definition: - apiVersion: test.example.com/v1alpha1 - kind: ReconciliationTest - metadata: - name: reconciliation-test - namespace: '{{ namespace }}' - spec: - field: value - wait: yes - wait_timeout: 300 - wait_condition: - type: Successful - status: "True" - register: reconciliation_test -- name: Retreive the number of iterations on the ConfigMap - debug: var=cm.data.iterations - retries: 20 - delay: 2 - until: "cm.data.iterations|int == 5" - vars: - cm: "{{ q('k8s', api_version='v1', kind='ConfigMap', namespace=namespace, - resource_name='reconciliation-test').0 }}" -- name: Assert sentinel ConfigMap has been created for Molecule Test - assert: - that: "cm.data.iterations|int == 5" - vars: - cm: "{{ q('k8s', api_version='v1', kind='ConfigMap', namespace=namespace, - resource_name='reconciliation-test').0 }}" diff --git a/hack/generate/samples/internal/ansible/testdata/tasks/secretstest_test.yml b/hack/generate/samples/internal/ansible/testdata/tasks/secretstest_test.yml deleted file mode 100644 index 70c1bc9987..0000000000 --- a/hack/generate/samples/internal/ansible/testdata/tasks/secretstest_test.yml +++ /dev/null @@ -1,56 +0,0 @@ ---- -- name: Create the v1.Secret - k8s: - state: present - definition: - apiVersion: v1 - kind: Secret - metadata: - name: test-secret - namespace: '{{ namespace }}' - labels: - reconcile: "yes" - data: - test: '{{ "test" | b64encode }}' - -- name: Wait for the corresponding configmap to be created - k8s_info: - api_version: v1 - kind: ConfigMap - name: test-secret - namespace: '{{ namespace }}' - register: result - until: result.resources - retries: 20 - -- name: Assert that the configmap has the proper content - assert: - that: result.resources.0.data.test == "test" - -- name: Update the v1.Secret - k8s: - state: present - definition: - apiVersion: v1 - kind: Secret - metadata: - name: test-secret - namespace: '{{ namespace }}' - labels: - reconcile: "yes" - data: - new: '{{ "content" | b64encode }}' - -- name: Wait for the corresponding key to be created - k8s_info: - api_version: v1 - kind: ConfigMap - name: test-secret - namespace: '{{ namespace }}' - register: result - until: result.resources.0.data.new is defined - retries: 20 - -- name: Assert that the configmap has the proper content - assert: - that: result.resources.0.data.new == 'content' diff --git a/hack/generate/samples/internal/ansible/testdata/tasks/selectortest_test.yml b/hack/generate/samples/internal/ansible/testdata/tasks/selectortest_test.yml deleted file mode 100644 index e766d87f6e..0000000000 --- a/hack/generate/samples/internal/ansible/testdata/tasks/selectortest_test.yml +++ /dev/null @@ -1,51 +0,0 @@ ---- -- name: Create the test.example.com/v1alpha1.SelectorTest - k8s: - state: present - definition: - apiVersion: test.example.com/v1alpha1 - kind: SelectorTest - metadata: - name: selector-test - namespace: '{{ namespace }}' - labels: - testLabel: testValue - spec: - field: value - wait: yes - wait_timeout: 300 - wait_condition: - type: Successful - status: "True" - register: selector_test - -- name: Assert sentinel ConfigMap has been created for Molecule Test - assert: - that: cm.data.hello == 'world' - vars: - cm: "{{ q('k8s', api_version='v1', kind='ConfigMap', namespace=namespace, - resource_name='selector-test').0 }}" - -- name: Create the test.example.com/v1alpha1.SelectorTest - k8s: - state: present - definition: - apiVersion: test.example.com/v1alpha1 - kind: SelectorTest - metadata: - name: selector-test-fail - namespace: '{{ namespace }}' - spec: - field: value - register: selector_test - -- name: Wait for 30 seconds - wait_for: - timeout: 30 - -- name: Assert sentinel ConfigMap has not been created for Molecule Test - assert: - that: not cm - vars: - cm: "{{ q('k8s', api_version='v1', kind='ConfigMap', namespace=namespace, - resource_name='selector-test-fail')}}" diff --git a/hack/generate/samples/internal/ansible/testdata/tasks/subresourcestest_test.yml b/hack/generate/samples/internal/ansible/testdata/tasks/subresourcestest_test.yml deleted file mode 100644 index 7a12b945c8..0000000000 --- a/hack/generate/samples/internal/ansible/testdata/tasks/subresourcestest_test.yml +++ /dev/null @@ -1,26 +0,0 @@ ---- -- name: Create the test.example.com/v1alpha1.SubresourcesTest - k8s: - state: present - definition: - apiVersion: test.example.com/v1alpha1 - kind: SubresourcesTest - metadata: - name: subresources-test - namespace: '{{ namespace }}' - spec: - execCommand: "echo 'hello world'" - logMessage: "Running..." - wait: yes - wait_timeout: 300 - wait_condition: - type: Successful - status: "True" - register: subresources_test - -- name: Assert stdout and stderr are properly set in status - assert: - that: - - subresources_test.result.status.execCommandStderr == "" - - subresources_test.result.status.execCommandStdout == "hello world" - - "'Running' in subresources_test.result.status.logs" diff --git a/hack/generate/samples/internal/ansible/testdata/watches.yaml b/hack/generate/samples/internal/ansible/testdata/watches.yaml deleted file mode 100644 index 1adfbd9fa4..0000000000 --- a/hack/generate/samples/internal/ansible/testdata/watches.yaml +++ /dev/null @@ -1,84 +0,0 @@ ---- -# Use the 'create api' subcommand to add watches to this file. -- version: v1alpha1 - group: test.example.com - kind: InventoryTest - playbook: playbooks/inventorytest.yml - vars: - meta: '{{ ansible_operator_meta }}' - -- version: v1alpha1 - group: test.example.com - kind: CollectionTest - role: operator_sdk.test_fixtures.dummy - vars: - meta: '{{ ansible_operator_meta }}' - -- version: v1alpha1 - group: test.example.com - kind: SubresourcesTest - playbook: playbooks/subresourcestest.yml - vars: - meta: '{{ ansible_operator_meta }}' - -- version: v1 - group: "" - kind: Secret - playbook: playbooks/secret.yml - manageStatus: false - selector: - matchExpressions: - - {key: reconcile, operator: Exists, values: []} - vars: - meta: '{{ ansible_operator_meta }}' - -- version: v1alpha1 - group: test.example.com - kind: SelectorTest - playbook: playbooks/selectortest.yml - selector: - matchExpressions: - - {key: testLabel, operator: Exists, values: []} - vars: - meta: '{{ ansible_operator_meta }}' - -- version: v1alpha1 - group: test.example.com - kind: CaseTest - playbook: playbooks/casetest.yml - snakeCaseParameters: false - vars: - meta: '{{ ansible_operator_meta }}' - -- version: v1alpha1 - group: test.example.com - kind: ArgsTest - playbook: playbooks/argstest.yml - vars: - meta: '{{ ansible_operator_meta }}' - -- version: v1alpha1 - group: test.example.com - kind: ReconciliationTest - playbook: playbooks/reconciliationtest.yml - vars: - meta: '{{ ansible_operator_meta }}' - -- version: v1alpha1 - group: test.example.com - kind: ClusterAnnotationTest - playbook: playbooks/clusterannotationtest.yml - watchClusterScopedResources: true - vars: - meta: '{{ ansible_operator_meta }}' - -- version: v1alpha1 - group: test.example.com - kind: FinalizerConcurrencyTest - playbook: playbooks/finalizerconcurrencytest.yml - finalizer: - name: test.example.com/finalizer - playbook: playbooks/finalizerconcurrencyfinalizer.yml - vars: - meta: '{{ ansible_operator_meta }}' -#+kubebuilder:scaffold:watch diff --git a/hack/generate/samples/molecule/generate.go b/hack/generate/samples/molecule/generate.go deleted file mode 100644 index 5deceedb52..0000000000 --- a/hack/generate/samples/molecule/generate.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2020 The Operator-SDK Authors -// -// 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 main - -import ( - "flag" - "os" - "path/filepath" - "strings" - - log "github.com/sirupsen/logrus" - - "github.com/operator-framework/operator-sdk/hack/generate/samples/internal/ansible" - "github.com/operator-framework/operator-sdk/internal/testutils" -) - -// This generate is used to run the e2e molecule tests -func main() { - var ( - // binaryPath allow inform the binary that should be used. - // By default it is operator-sdk - binaryPath string - - // samplesRoot is the path provided to generate the molecule sample - samplesRoot string - - // sample is the name of the mock was selected to be generated - sample string - ) - - flag.StringVar(&binaryPath, "bin", testutils.BinaryName, "Binary path that should be used") - flag.StringVar(&samplesRoot, "samples-root", "", "Path where molecule samples should be generated") - flag.StringVar(&sample, "sample", "", "To generate only the selected option. Options: [advanced, memcached]") - - flag.Parse() - - // Make the binary path absolute if pathed, for reproducibility and debugging purposes. - if dir, _ := filepath.Split(binaryPath); dir != "" { - tmp, err := filepath.Abs(binaryPath) - if err != nil { - log.Fatalf("Failed to make binary path %q absolute: %v", binaryPath, err) - } - binaryPath = tmp - } - - // If no path be provided then the Molecule sample will be create in the testdata/ansible dir - // It can be helpful to check the mock data used in the e2e molecule tests as to develop this sample - // By default this mock is ignored in the .gitignore - if strings.TrimSpace(samplesRoot) == "" { - currentPath, err := os.Getwd() - if err != nil { - log.Fatal(err) - } - samplesRoot = filepath.Join(currentPath, "testdata", "ansible") - } - - log.Infof("creating Ansible Molecule Mock Samples under %s", samplesRoot) - - if sample == "" || sample == "memcached" { - ansible.GenerateMoleculeSample(binaryPath, samplesRoot) - } - - if sample == "" || sample == "advanced" { - ansible.GenerateAdvancedMoleculeSample(binaryPath, samplesRoot) - } -} diff --git a/hack/tests/e2e-ansible-molecule.sh b/hack/tests/e2e-ansible-molecule.sh deleted file mode 100755 index 8c1410193f..0000000000 --- a/hack/tests/e2e-ansible-molecule.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env bash - -source hack/lib/common.sh - -# load_image_if_kind -# -# load_image_if_kind loads an image into all nodes in a kind cluster. -# -function load_image_if_kind() { - local cluster=${KIND_CLUSTER:-kind} - if [[ "$(kubectl config current-context)" == "kind-${cluster}" ]]; then - kind load docker-image --name "${cluster}" "$1" - fi -} - -set -eu - -header_text "Running ansible molecule tests in a python3 virtual environment" - -# Set up a python3.8 virtual environment. -ENVDIR="$(mktemp -d)" -trap_add "set +u; deactivate; set -u; rm -rf $ENVDIR" EXIT -python3 -m venv "$ENVDIR" -set +u; source "${ENVDIR}/bin/activate"; set -u - -# Install dependencies. -TMPDIR="$(mktemp -d)" -trap_add "rm -rf $TMPDIR" EXIT -pip3 install pyasn1==0.4.7 pyasn1-modules==0.2.6 idna==2.8 ipaddress==1.0.23 -pip3 install cryptography molecule==5.1.0 -pip3 install ansible-lint yamllint -pip3 install docker kubernetes jmespath -ansible-galaxy collection install 'kubernetes.core:==2.4.0' -ansible-galaxy collection install 'operator_sdk.util:==0.4.0' -ansible-galaxy collection install 'community.docker:==3.4.0' - -header_text "Copying molecule testdata scenarios" -ROOTDIR="$(pwd)" -cp -r $ROOTDIR/testdata/ansible/memcached-molecule-operator/ $TMPDIR/memcached-molecule-operator -cp -r $ROOTDIR/testdata/ansible/advanced-molecule-operator/ $TMPDIR/advanced-molecule-operator - -pushd $TMPDIR/memcached-molecule-operator - -header_text "Running Kind test with memcached-molecule-operator" -make kustomize -if [ -f ./bin/kustomize ] ; then - KUSTOMIZE="$(realpath ./bin/kustomize)" -else - KUSTOMIZE="$(which kustomize)" -fi -KUSTOMIZE_PATH=${KUSTOMIZE} TEST_OPERATOR_NAMESPACE=default molecule test -s kind -popd - -header_text "Running Default test with advanced-molecule-operator" - -make test-e2e-setup -pushd $TMPDIR/advanced-molecule-operator - -make kustomize -if [ -f ./bin/kustomize ] ; then - KUSTOMIZE="$(realpath ./bin/kustomize)" -else - KUSTOMIZE="$(which kustomize)" -fi - -DEST_IMAGE="quay.io/example/advanced-molecule-operator:v0.0.1" -docker build -t "$DEST_IMAGE" --no-cache . -load_image_if_kind "$DEST_IMAGE" -KUSTOMIZE_PATH=$KUSTOMIZE OPERATOR_PULL_POLICY=Never OPERATOR_IMAGE=${DEST_IMAGE} TEST_OPERATOR_NAMESPACE=osdk-test molecule test -popd diff --git a/images/ansible-operator/Dockerfile b/images/ansible-operator/Dockerfile deleted file mode 100644 index 5abb5acd36..0000000000 --- a/images/ansible-operator/Dockerfile +++ /dev/null @@ -1,37 +0,0 @@ -# Build the manager binary -FROM --platform=$BUILDPLATFORM golang:1.19 as builder -ARG TARGETARCH - -WORKDIR /workspace -# Copy the Go Modules manifests -COPY go.mod go.mod -COPY go.sum go.sum -# cache deps before building and copying source so that we don't need to re-download as much -# and so that source changes don't invalidate our downloaded layer -RUN go mod download - -# Copy the go source -COPY . . - -# Build -RUN GOOS=linux GOARCH=$TARGETARCH make build/ansible-operator - -# Final image. -FROM quay.io/operator-framework/ansible-operator-base:master-b247ac8390699c2773625b321a55b2240c5ce3c0 - -ENV HOME=/opt/ansible \ - USER_NAME=ansible \ - USER_UID=1001 - -# Ensure directory permissions are properly set -RUN echo "${USER_NAME}:x:${USER_UID}:0:${USER_NAME} user:${HOME}:/sbin/nologin" >> /etc/passwd \ - && mkdir -p ${HOME}/.ansible/tmp \ - && chown -R ${USER_UID}:0 ${HOME} \ - && chmod -R ug+rwx ${HOME} - -WORKDIR ${HOME} -USER ${USER_UID} - -COPY --from=builder /workspace/build/ansible-operator /usr/local/bin/ansible-operator - -ENTRYPOINT ["/tini", "--", "/usr/local/bin/ansible-operator", "run", "--watches-file=./watches.yaml"] diff --git a/images/ansible-operator/Pipfile b/images/ansible-operator/Pipfile deleted file mode 100644 index 9d1d25491e..0000000000 --- a/images/ansible-operator/Pipfile +++ /dev/null @@ -1,16 +0,0 @@ -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[packages] -ansible-runner = "~=2.3.3" -ansible-runner-http = "~=1.0.0" -ansible-core = "~=2.15.0" -urllib3 = "<2" -kubernetes = "==26.1.0" - -[dev-packages] - -[requires] -python_version = "3.9" diff --git a/images/ansible-operator/Pipfile.lock b/images/ansible-operator/Pipfile.lock deleted file mode 100644 index 9f17547553..0000000000 --- a/images/ansible-operator/Pipfile.lock +++ /dev/null @@ -1,553 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "f38af862999f7404c088a58040eb5b5f57078c9da07ab3dd5c1c9e4cded529b1" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.9" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "ansible-core": { - "hashes": [ - "sha256:07a8f0476904876aaaf51401c617519cdd9cfb6e1e3ac46597c35699b0b83d52", - "sha256:84251b001f2f9c0914beedffcf19529e745a13108159d1fe27de9e3a6a63ac5a" - ], - "index": "pypi", - "version": "==2.15.2" - }, - "ansible-runner": { - "hashes": [ - "sha256:38ff635e4b94791de2956c81e265836ec4965b30e9ee35d72fcf3271dc46b98b", - "sha256:c57ae0d096760d66b2897b0f9009856c7b83fd5428dcb831f470cba348346396" - ], - "index": "pypi", - "version": "==2.3.3" - }, - "ansible-runner-http": { - "hashes": [ - "sha256:97da445b7d5c6663b0cceaf6bd5e9b0b0dff9a4c36eae43c8c916c6208aee915", - "sha256:e2f34880531d4088a5e04967fd5eae602eb400cc4eb541b22c8c6853e342587f" - ], - "index": "pypi", - "version": "==1.0.0" - }, - "cachetools": { - "hashes": [ - "sha256:95ef631eeaea14ba2e36f06437f36463aac3a096799e876ee55e5cdccb102590", - "sha256:dce83f2d9b4e1f732a8cd44af8e8fab2dbe46201467fc98b3ef8f269092bf62b" - ], - "markers": "python_version >= '3.7'", - "version": "==5.3.1" - }, - "certifi": { - "hashes": [ - "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082", - "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9" - ], - "markers": "python_version >= '3.6'", - "version": "==2023.7.22" - }, - "cffi": { - "hashes": [ - "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5", - "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef", - "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104", - "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426", - "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405", - "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375", - "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a", - "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e", - "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc", - "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf", - "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185", - "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497", - "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3", - "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35", - "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c", - "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83", - "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21", - "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca", - "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984", - "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac", - "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd", - "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee", - "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a", - "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2", - "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192", - "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7", - "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585", - "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f", - "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e", - "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27", - "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b", - "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e", - "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e", - "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d", - "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c", - "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415", - "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82", - "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02", - "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314", - "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325", - "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c", - "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3", - "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914", - "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045", - "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d", - "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9", - "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5", - "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2", - "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c", - "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3", - "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2", - "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8", - "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d", - "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d", - "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9", - "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162", - "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76", - "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4", - "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e", - "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9", - "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6", - "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b", - "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01", - "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0" - ], - "version": "==1.15.1" - }, - "charset-normalizer": { - "hashes": [ - "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96", - "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c", - "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710", - "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706", - "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020", - "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252", - "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad", - "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329", - "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a", - "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f", - "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6", - "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4", - "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a", - "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46", - "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2", - "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23", - "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace", - "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd", - "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982", - "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10", - "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2", - "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea", - "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09", - "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5", - "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149", - "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489", - "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9", - "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80", - "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592", - "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3", - "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6", - "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed", - "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c", - "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200", - "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a", - "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e", - "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d", - "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6", - "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623", - "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669", - "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3", - "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa", - "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9", - "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2", - "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f", - "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1", - "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4", - "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a", - "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8", - "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3", - "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029", - "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f", - "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959", - "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22", - "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7", - "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952", - "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346", - "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e", - "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d", - "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299", - "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd", - "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a", - "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3", - "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037", - "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94", - "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c", - "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858", - "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a", - "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449", - "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c", - "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918", - "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1", - "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c", - "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac", - "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa" - ], - "markers": "python_full_version >= '3.7.0'", - "version": "==3.2.0" - }, - "cryptography": { - "hashes": [ - "sha256:01f1d9e537f9a15b037d5d9ee442b8c22e3ae11ce65ea1f3316a41c78756b711", - "sha256:079347de771f9282fbfe0e0236c716686950c19dee1b76240ab09ce1624d76d7", - "sha256:182be4171f9332b6741ee818ec27daff9fb00349f706629f5cbf417bd50e66fd", - "sha256:192255f539d7a89f2102d07d7375b1e0a81f7478925b3bc2e0549ebf739dae0e", - "sha256:2a034bf7d9ca894720f2ec1d8b7b5832d7e363571828037f9e0c4f18c1b58a58", - "sha256:342f3767e25876751e14f8459ad85e77e660537ca0a066e10e75df9c9e9099f0", - "sha256:439c3cc4c0d42fa999b83ded80a9a1fb54d53c58d6e59234cfe97f241e6c781d", - "sha256:49c3222bb8f8e800aead2e376cbef687bc9e3cb9b58b29a261210456a7783d83", - "sha256:674b669d5daa64206c38e507808aae49904c988fa0a71c935e7006a3e1e83831", - "sha256:7a9a3bced53b7f09da251685224d6a260c3cb291768f54954e28f03ef14e3766", - "sha256:7af244b012711a26196450d34f483357e42aeddb04128885d95a69bd8b14b69b", - "sha256:7d230bf856164de164ecb615ccc14c7fc6de6906ddd5b491f3af90d3514c925c", - "sha256:84609ade00a6ec59a89729e87a503c6e36af98ddcd566d5f3be52e29ba993182", - "sha256:9a6673c1828db6270b76b22cc696f40cde9043eb90373da5c2f8f2158957f42f", - "sha256:9b6d717393dbae53d4e52684ef4f022444fc1cce3c48c38cb74fca29e1f08eaa", - "sha256:9c3fe6534d59d071ee82081ca3d71eed3210f76ebd0361798c74abc2bcf347d4", - "sha256:a719399b99377b218dac6cf547b6ec54e6ef20207b6165126a280b0ce97e0d2a", - "sha256:b332cba64d99a70c1e0836902720887fb4529ea49ea7f5462cf6640e095e11d2", - "sha256:d124682c7a23c9764e54ca9ab5b308b14b18eba02722b8659fb238546de83a76", - "sha256:d73f419a56d74fef257955f51b18d046f3506270a5fd2ac5febbfa259d6c0fa5", - "sha256:f0dc40e6f7aa37af01aba07277d3d64d5a03dc66d682097541ec4da03cc140ee", - "sha256:f14ad275364c8b4e525d018f6716537ae7b6d369c094805cae45300847e0894f", - "sha256:f772610fe364372de33d76edcd313636a25684edb94cee53fd790195f5989d14" - ], - "markers": "python_version >= '3.7'", - "version": "==41.0.2" - }, - "docutils": { - "hashes": [ - "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6", - "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b" - ], - "markers": "python_version >= '3.7'", - "version": "==0.20.1" - }, - "google-auth": { - "hashes": [ - "sha256:164cba9af4e6e4e40c3a4f90a1a6c12ee56f14c0b4868d1ca91b32826ab334ce", - "sha256:d61d1b40897407b574da67da1a833bdc10d5a11642566e506565d1b1a46ba873" - ], - "markers": "python_version >= '3.6'", - "version": "==2.22.0" - }, - "idna": { - "hashes": [ - "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", - "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" - ], - "markers": "python_version >= '3.5'", - "version": "==3.4" - }, - "importlib-metadata": { - "hashes": [ - "sha256:5a66966b39ff1c14ef5b2d60c1d842b0141fefff0f4cc6365b4bc9446c652807", - "sha256:f65e478a7c2177bd19517a3a15dac094d253446d8690c5f3e71e735a04312374" - ], - "markers": "python_version < '3.10'", - "version": "==6.2.1" - }, - "importlib-resources": { - "hashes": [ - "sha256:2238159eb743bd85304a16e0536048b3e991c531d1cd51c4a834d1ccf2829057", - "sha256:4df460394562b4581bb4e4087ad9447bd433148fba44241754ec3152499f1d1b" - ], - "markers": "python_version < '3.10'", - "version": "==5.0.7" - }, - "jinja2": { - "hashes": [ - "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", - "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" - ], - "markers": "python_version >= '3.7'", - "version": "==3.1.2" - }, - "kubernetes": { - "hashes": [ - "sha256:5854b0c508e8d217ca205591384ab58389abdae608576f9c9afc35a3c76a366c", - "sha256:e3db6800abf7e36c38d2629b5cb6b74d10988ee0cba6fba45595a7cbe60c0042" - ], - "index": "pypi", - "version": "==26.1.0" - }, - "lockfile": { - "hashes": [ - "sha256:6aed02de03cba24efabcd600b30540140634fc06cfa603822d508d5361e9f799", - "sha256:6c3cb24f344923d30b2785d5ad75182c8ea7ac1b6171b08657258ec7429d50fa" - ], - "version": "==0.12.2" - }, - "markupsafe": { - "hashes": [ - "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e", - "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e", - "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431", - "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686", - "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559", - "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc", - "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c", - "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0", - "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4", - "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9", - "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575", - "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba", - "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d", - "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3", - "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00", - "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155", - "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac", - "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52", - "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f", - "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8", - "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b", - "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24", - "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea", - "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198", - "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0", - "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee", - "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be", - "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2", - "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707", - "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6", - "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58", - "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779", - "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636", - "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c", - "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad", - "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee", - "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc", - "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2", - "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48", - "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7", - "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e", - "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b", - "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa", - "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5", - "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e", - "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb", - "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9", - "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57", - "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc", - "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2" - ], - "markers": "python_version >= '3.7'", - "version": "==2.1.3" - }, - "oauthlib": { - "hashes": [ - "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca", - "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918" - ], - "markers": "python_version >= '3.6'", - "version": "==3.2.2" - }, - "packaging": { - "hashes": [ - "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61", - "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f" - ], - "markers": "python_version >= '3.7'", - "version": "==23.1" - }, - "pexpect": { - "hashes": [ - "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937", - "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c" - ], - "version": "==4.8.0" - }, - "ptyprocess": { - "hashes": [ - "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", - "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220" - ], - "version": "==0.7.0" - }, - "pyasn1": { - "hashes": [ - "sha256:87a2121042a1ac9358cabcaf1d07680ff97ee6404333bacca15f76aa8ad01a57", - "sha256:97b7290ca68e62a832558ec3976f15cbf911bf5d7c7039d8b861c2a0ece69fde" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==0.5.0" - }, - "pyasn1-modules": { - "hashes": [ - "sha256:5bd01446b736eb9d31512a30d46c1ac3395d676c6f3cafa4c03eb54b9925631c", - "sha256:d3ccd6ed470d9ffbc716be08bd90efbd44d0734bc9303818f7336070984a162d" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==0.3.0" - }, - "pycparser": { - "hashes": [ - "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", - "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.21" - }, - "python-daemon": { - "hashes": [ - "sha256:42bb848a3260a027fa71ad47ecd959e471327cb34da5965962edd5926229f341", - "sha256:6c57452372f7eaff40934a1c03ad1826bf5e793558e87fef49131e6464b4dae5" - ], - "markers": "python_version >= '3'", - "version": "==3.0.1" - }, - "python-dateutil": { - "hashes": [ - "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", - "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.8.2" - }, - "pyyaml": { - "hashes": [ - "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", - "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", - "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206", - "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27", - "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595", - "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62", - "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98", - "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", - "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", - "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867", - "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47", - "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", - "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", - "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3", - "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", - "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", - "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c", - "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", - "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", - "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba", - "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", - "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", - "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd", - "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3", - "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", - "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", - "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c", - "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", - "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", - "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", - "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", - "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", - "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", - "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", - "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", - "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa", - "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", - "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585", - "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", - "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f" - ], - "markers": "python_version >= '3.6'", - "version": "==6.0.1" - }, - "requests": { - "hashes": [ - "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", - "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" - ], - "markers": "python_version >= '3.7'", - "version": "==2.31.0" - }, - "requests-oauthlib": { - "hashes": [ - "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5", - "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.3.1" - }, - "requests-unixsocket": { - "hashes": [ - "sha256:28304283ea9357d45fff58ad5b11e47708cfbf5806817aa59b2a363228ee971e", - "sha256:c685c680f0809e1b2955339b1e5afc3c0022b3066f4f7eb343f43a6065fc0e5d" - ], - "version": "==0.3.0" - }, - "resolvelib": { - "hashes": [ - "sha256:04ce76cbd63fded2078ce224785da6ecd42b9564b1390793f64ddecbe997b309", - "sha256:d2da45d1a8dfee81bdd591647783e340ef3bcb104b54c383f70d422ef5cc7dbf" - ], - "version": "==1.0.1" - }, - "rsa": { - "hashes": [ - "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7", - "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21" - ], - "markers": "python_version >= '3.6' and python_version < '4'", - "version": "==4.9" - }, - "setuptools": { - "hashes": [ - "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f", - "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235" - ], - "markers": "python_version >= '3.7'", - "version": "==68.0.0" - }, - "six": { - "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" - }, - "urllib3": { - "hashes": [ - "sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f", - "sha256:8f135f6502756bde6b2a9b28989df5fbe87c9970cecaa69041edcce7f0589b14" - ], - "index": "pypi", - "version": "==1.26.16" - }, - "websocket-client": { - "hashes": [ - "sha256:c951af98631d24f8df89ab1019fc365f2227c0892f12fd150e935607c79dd0dd", - "sha256:f1f9f2ad5291f0225a49efad77abf9e700b6fef553900623060dad6e26503b9d" - ], - "markers": "python_version >= '3.7'", - "version": "==1.6.1" - }, - "zipp": { - "hashes": [ - "sha256:679e51dd4403591b2d6838a48de3d283f3d188412a9782faadf845f298736ba0", - "sha256:ebc15946aa78bd63458992fc81ec3b6f7b1e92d51c35e6de1c3804e73b799147" - ], - "markers": "python_version >= '3.8'", - "version": "==3.16.2" - } - }, - "develop": {} -} diff --git a/images/ansible-operator/base.Dockerfile b/images/ansible-operator/base.Dockerfile deleted file mode 100644 index 88be8d7f00..0000000000 --- a/images/ansible-operator/base.Dockerfile +++ /dev/null @@ -1,67 +0,0 @@ -# This Dockerfile defines the base image for the ansible-operator image. -# It is built with dependencies that take a while to download, thus speeding -# up ansible deploy jobs. - -FROM registry.access.redhat.com/ubi8/ubi:8.8 AS builder - -# Install Rust so that we can ensure backwards compatibility with installing/building the cryptography wheel across all platforms -RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -ENV PATH="/root/.cargo/bin:${PATH}" -RUN rustc --version - -# When cross-compiling the container, cargo uncontrollably consumes memory and -# gets killed by the OOM Killer when it fetches dependencies. The workaround is -# to use the git executable. -# See https://github.com/rust-lang/cargo/issues/10583 for details. -ENV CARGO_NET_GIT_FETCH_WITH_CLI=true - -# Copy python dependencies (including ansible) to be installed using Pipenv -COPY Pipfile* ./ -# Instruct pip(env) not to keep a cache of installed packages, -# to install into the global site-packages and -# to clear the pipenv cache as well -ENV PIP_NO_CACHE_DIR=1 \ - PIPENV_SYSTEM=1 \ - PIPENV_CLEAR=1 -# Ensure fresh metadata rather than cached metadata, install system and pip python deps, -# and remove those not needed at runtime. -# pip3~=21.1 fixes a vulnerability described in https://github.com/pypa/pip/pull/9827. -RUN set -e && yum clean all && rm -rf /var/cache/yum/* \ - && yum update -y \ - && yum install -y libffi-devel openssl-devel python39-devel gcc python39-pip python39-setuptools python39-wheel \ - && pip3 install --upgrade pip~=23.1.2 \ - && pip3 install pipenv==2023.6.26 \ - && pipenv install --deploy \ - && pipenv check \ - && yum remove -y gcc libffi-devel openssl-devel python39-devel \ - && yum clean all \ - && rm -rf /var/cache/yum - -FROM registry.access.redhat.com/ubi8/ubi:8.8 -ARG TARGETARCH - -# Label this image with the repo and commit that built it, for freshmaking purposes. -ARG GIT_COMMIT=devel -LABEL git_commit=$GIT_COMMIT - -RUN mkdir -p /etc/ansible \ - && echo "localhost ansible_connection=local" > /etc/ansible/hosts \ - && echo '[defaults]' > /etc/ansible/ansible.cfg \ - && echo 'roles_path = /opt/ansible/roles' >> /etc/ansible/ansible.cfg \ - && echo 'library = /usr/share/ansible/openshift' >> /etc/ansible/ansible.cfg - -RUN set -e && yum clean all && rm -rf /var/cache/yum/* \ - && yum update -y \ - && yum install -y python39-pip python39-setuptools \ - && pip3 install --upgrade pip~=23.1.2 \ - && pip3 install pipenv==2023.6.26 \ - && yum clean all \ - && rm -rf /var/cache/yum - -COPY --from=builder /usr/local/lib64/python3.9/site-packages /usr/local/lib64/python3.9/site-packages -COPY --from=builder /usr/local/lib/python3.9/site-packages /usr/local/lib/python3.9/site-packages -COPY --from=builder /usr/local/bin /usr/local/bin - -ENV TINI_VERSION=v0.19.0 -RUN curl -L -o /tini https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-${TARGETARCH} \ - && chmod +x /tini && /tini --version \ No newline at end of file diff --git a/internal/ansible/apiserver/apiserver.go b/internal/ansible/apiserver/apiserver.go deleted file mode 100644 index 0570fa001d..0000000000 --- a/internal/ansible/apiserver/apiserver.go +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2022 The Operator-SDK Authors -// -// 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 apiserver - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "time" - - logf "sigs.k8s.io/controller-runtime/pkg/log" - crmetrics "sigs.k8s.io/controller-runtime/pkg/metrics" - - "github.com/operator-framework/operator-sdk/internal/ansible/metrics" -) - -var log = logf.Log.WithName("apiserver") - -type Options struct { - Address string - Port int -} - -func Run(options Options) error { - mux := http.NewServeMux() - mux.HandleFunc("/metrics", metricsHandler) - - server := http.Server{ - Addr: fmt.Sprintf("%s:%d", options.Address, options.Port), - Handler: mux, - ReadHeaderTimeout: 5 * time.Second, - } - log.Info("Starting to serve metrics listener", "Address", server.Addr) - return server.ListenAndServe() -} - -func metricsHandler(w http.ResponseWriter, r *http.Request) { - defer func() { - _, _ = io.Copy(io.Discard, r.Body) - r.Body.Close() - }() - log.V(3).Info(fmt.Sprintf("%s %s", r.Method, r.URL)) - - var userMetric metrics.UserMetric - - switch r.Method { - case http.MethodPost: - log.V(3).Info("The apiserver has received a POST") - err := json.NewDecoder(r.Body).Decode(&userMetric) - if err != nil { - log.Info(err.Error()) - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - err = metrics.HandleUserMetric(crmetrics.Registry, userMetric) - if err != nil { - log.Info(err.Error()) - http.Error(w, err.Error(), http.StatusBadRequest) - } - default: - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - } - -} diff --git a/internal/ansible/controller/controller.go b/internal/ansible/controller/controller.go deleted file mode 100644 index 8fcc4d582a..0000000000 --- a/internal/ansible/controller/controller.go +++ /dev/null @@ -1,149 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// 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 controller - -import ( - "fmt" - "os" - "reflect" - "strings" - "time" - - libpredicate "github.com/operator-framework/operator-lib/predicate" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "sigs.k8s.io/controller-runtime/pkg/controller" - logf "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/manager" - ctrlpredicate "sigs.k8s.io/controller-runtime/pkg/predicate" - "sigs.k8s.io/controller-runtime/pkg/source" - - "github.com/operator-framework/operator-sdk/internal/ansible/events" - "github.com/operator-framework/operator-sdk/internal/ansible/handler" - "github.com/operator-framework/operator-sdk/internal/ansible/runner" -) - -var log = logf.Log.WithName("ansible-controller") - -// Options - options for your controller -type Options struct { - EventHandlers []events.EventHandler - LoggingLevel events.LogLevel - Runner runner.Runner - GVK schema.GroupVersionKind - ReconcilePeriod time.Duration - ManageStatus bool - AnsibleDebugLogs bool - WatchDependentResources bool - WatchClusterScopedResources bool - WatchAnnotationsChanges bool - MaxConcurrentReconciles int - Selector metav1.LabelSelector -} - -// Add - Creates a new ansible operator controller and adds it to the manager -func Add(mgr manager.Manager, options Options) *controller.Controller { - log.Info("Watching resource", "Options.Group", options.GVK.Group, "Options.Version", - options.GVK.Version, "Options.Kind", options.GVK.Kind) - if options.EventHandlers == nil { - options.EventHandlers = []events.EventHandler{} - } - eventHandlers := append(options.EventHandlers, events.NewLoggingEventHandler(options.LoggingLevel)) - - aor := &AnsibleOperatorReconciler{ - Client: mgr.GetClient(), - GVK: options.GVK, - Runner: options.Runner, - EventHandlers: eventHandlers, - ReconcilePeriod: options.ReconcilePeriod, - ManageStatus: options.ManageStatus, - AnsibleDebugLogs: options.AnsibleDebugLogs, - APIReader: mgr.GetAPIReader(), - WatchAnnotationsChanges: options.WatchAnnotationsChanges, - } - - scheme := mgr.GetScheme() - _, err := scheme.New(options.GVK) - if runtime.IsNotRegisteredError(err) { - // Register the GVK with the schema - scheme.AddKnownTypeWithName(options.GVK, &unstructured.Unstructured{}) - metav1.AddToGroupVersion(mgr.GetScheme(), schema.GroupVersion{ - Group: options.GVK.Group, - Version: options.GVK.Version, - }) - } else if err != nil { - log.Error(err, "") - os.Exit(1) - } - - //Create new controller runtime controller and set the controller to watch GVK. - c, err := controller.New(fmt.Sprintf("%v-controller", strings.ToLower(options.GVK.Kind)), mgr, - controller.Options{ - Reconciler: aor, - MaxConcurrentReconciles: options.MaxConcurrentReconciles, - }) - if err != nil { - log.Error(err, "") - os.Exit(1) - } - - // Set up predicates. - predicates := []ctrlpredicate.Predicate{ - ctrlpredicate.Or(ctrlpredicate.GenerationChangedPredicate{}, libpredicate.NoGenerationPredicate{}), - } - - if options.WatchAnnotationsChanges { - predicates = []ctrlpredicate.Predicate{ - ctrlpredicate.Or(ctrlpredicate.AnnotationChangedPredicate{}, predicates[0]), - } - } - - p, err := parsePredicateSelector(options.Selector) - - if err != nil { - log.Error(err, "") - os.Exit(1) - } - - if p != nil { - predicates = append(predicates, p) - } - - u := &unstructured.Unstructured{} - u.SetGroupVersionKind(options.GVK) - err = c.Watch(&source.Kind{Type: u}, &handler.LoggingEnqueueRequestForObject{}, predicates...) - if err != nil { - log.Error(err, "") - os.Exit(1) - } - - return &c -} - -// parsePredicateSelector parses the selector in the WatchOptions and creates a predicate -// that is used to filter resources based on the specified selector -func parsePredicateSelector(selector metav1.LabelSelector) (ctrlpredicate.Predicate, error) { - // If a selector has been specified in watches.yaml, add it to the watch's predicates. - if !reflect.ValueOf(selector).IsZero() { - p, err := ctrlpredicate.LabelSelectorPredicate(selector) - if err != nil { - return nil, fmt.Errorf("error constructing predicate from watches selector: %v", err) - } - return p, nil - } - return nil, nil -} diff --git a/internal/ansible/controller/controller_test.go b/internal/ansible/controller/controller_test.go deleted file mode 100644 index 968fdef114..0000000000 --- a/internal/ansible/controller/controller_test.go +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2021 The Operator-SDK Authors -// -// 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 controller - -import ( - "testing" - - "github.com/stretchr/testify/assert" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func TestFilterPredicate(t *testing.T) { - matchLabelPass := make(map[string]string) - matchLabelPass["testKey"] = "testValue" - selectorPass := metav1.LabelSelector{ - MatchLabels: matchLabelPass, - } - noSelector := metav1.LabelSelector{} - - passPredicate, err := parsePredicateSelector(selectorPass) - assert.Equal(t, nil, err, "Verify that no error is thrown on a valid populated selector") - assert.NotEqual(t, nil, passPredicate, "Verify that a predicate is constructed using a valid selector") - - nilPredicate, err := parsePredicateSelector(noSelector) - assert.Equal(t, nil, err, "Verify that no error is thrown on a valid unpopulated selector") - assert.Equal(t, nil, nilPredicate, "Verify correct parsing of an unpopulated selector") -} diff --git a/internal/ansible/controller/reconcile.go b/internal/ansible/controller/reconcile.go deleted file mode 100644 index 362a5d7178..0000000000 --- a/internal/ansible/controller/reconcile.go +++ /dev/null @@ -1,457 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// 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 controller - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "math/rand" - "os" - "strconv" - "strings" - "time" - - v1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - logf "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - - ansiblestatus "github.com/operator-framework/operator-sdk/internal/ansible/controller/status" - "github.com/operator-framework/operator-sdk/internal/ansible/events" - "github.com/operator-framework/operator-sdk/internal/ansible/metrics" - "github.com/operator-framework/operator-sdk/internal/ansible/proxy/kubeconfig" - "github.com/operator-framework/operator-sdk/internal/ansible/runner" - "github.com/operator-framework/operator-sdk/internal/ansible/runner/eventapi" -) - -const ( - // ReconcilePeriodAnnotation - annotation used by a user to specify the reconciliation interval for the CR. - // To use create a CR with an annotation "ansible.sdk.operatorframework.io/reconcile-period: 30s" or some other valid - // Duration. This will override the operators/or controllers reconcile period for that particular CR. - ReconcilePeriodAnnotation = "ansible.sdk.operatorframework.io/reconcile-period" -) - -// AnsibleOperatorReconciler - object to reconcile runner requests -type AnsibleOperatorReconciler struct { - GVK schema.GroupVersionKind - Runner runner.Runner - Client client.Client - APIReader client.Reader - EventHandlers []events.EventHandler - ReconcilePeriod time.Duration - ManageStatus bool - AnsibleDebugLogs bool - WatchAnnotationsChanges bool -} - -// Reconcile - handle the event. -func (r *AnsibleOperatorReconciler) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) { //nolint:gocyclo - // TODO: Try to reduce the complexity of this last measured at 42 (failing at > 30) and remove the // nolint:gocyclo - u := &unstructured.Unstructured{} - u.SetGroupVersionKind(r.GVK) - err := r.Client.Get(ctx, request.NamespacedName, u) - if apierrors.IsNotFound(err) { - return reconcile.Result{}, nil - } - if err != nil { - return reconcile.Result{}, err - } - - ident := strconv.Itoa(rand.Int()) - logger := logf.Log.WithName("reconciler").WithValues( - "job", ident, - "name", u.GetName(), - "namespace", u.GetNamespace(), - ) - - reconcileResult := reconcile.Result{RequeueAfter: r.ReconcilePeriod} - if ds, ok := u.GetAnnotations()[ReconcilePeriodAnnotation]; ok { - duration, err := time.ParseDuration(ds) - if err != nil { - // Should attempt to update to a failed condition - errmark := r.markError(ctx, request.NamespacedName, u, - fmt.Sprintf("Unable to parse reconcile period annotation: %v", err)) - if errmark != nil { - logger.Error(errmark, "Unable to mark error annotation") - } - logger.Error(err, "Unable to parse reconcile period annotation") - return reconcileResult, err - } - reconcileResult.RequeueAfter = duration - } - - deleted := u.GetDeletionTimestamp() != nil - finalizer, finalizerExists := r.Runner.GetFinalizer() - if !controllerutil.ContainsFinalizer(u, finalizer) { - if deleted { - // If the resource is being deleted we don't want to add the finalizer again - logger.Info("Resource is terminated, skipping reconciliation") - return reconcile.Result{}, nil - } else if finalizerExists { - logger.V(1).Info("Adding finalizer to resource", "Finalizer", finalizer) - controllerutil.AddFinalizer(u, finalizer) - err := r.Client.Update(ctx, u) - if err != nil { - logger.Error(err, "Unable to update cr with finalizer") - return reconcileResult, err - } - } - } - - spec := u.Object["spec"] - _, ok := spec.(map[string]interface{}) - // Need to handle cases where there is no spec. - // We can add the spec to the object, which will allow - // everything to work, and will not get updated. - // Therefore we can now deal with the case of secrets and configmaps. - if !ok { - logger.V(1).Info("Spec was not found") - u.Object["spec"] = map[string]interface{}{} - } - - if r.ManageStatus { - errmark := r.markRunning(ctx, request.NamespacedName, u) - if errmark != nil { - logger.Error(errmark, "Unable to update the status to mark cr as running") - return reconcileResult, errmark - } - } - - ownerRef := metav1.OwnerReference{ - APIVersion: u.GetAPIVersion(), - Kind: u.GetKind(), - Name: u.GetName(), - UID: u.GetUID(), - } - - kc, err := kubeconfig.Create(ownerRef, "http://localhost:8888", u.GetNamespace()) - if err != nil { - errmark := r.markError(ctx, request.NamespacedName, u, "Unable to run reconciliation") - if errmark != nil { - logger.Error(errmark, "Unable to mark error to run reconciliation") - } - logger.Error(err, "Unable to generate kubeconfig") - return reconcileResult, err - } - defer func() { - if err := os.Remove(kc.Name()); err != nil { - logger.Error(err, "Failed to remove generated kubeconfig file") - } - }() - result, err := r.Runner.Run(ident, u, kc.Name()) - if err != nil { - errmark := r.markError(ctx, request.NamespacedName, u, "Unable to run reconciliation") - if errmark != nil { - logger.Error(errmark, "Unable to mark error to run reconciliation") - } - logger.Error(err, "Unable to run ansible runner") - return reconcileResult, err - } - - // iterate events from ansible, looking for the final one - statusEvent := eventapi.StatusJobEvent{} - failureMessages := eventapi.FailureMessages{} - for event := range result.Events() { - for _, eHandler := range r.EventHandlers { - go eHandler.Handle(ident, u, event) - } - if event.Event == eventapi.EventPlaybookOnStats { - // convert to StatusJobEvent; would love a better way to do this - data, err := json.Marshal(event) - if err != nil { - printEventStats(statusEvent, u) - return reconcile.Result{}, err - } - err = json.Unmarshal(data, &statusEvent) - if err != nil { - printEventStats(statusEvent, u) - return reconcile.Result{}, err - } - } - if module, found := event.EventData["task_action"]; found { - if module == "operator_sdk.util.requeue_after" || module == "requeue_after" && event.Event != eventapi.EventRunnerOnFailed { - if data, exists := event.EventData["res"]; exists { - if fields, check := data.(map[string]interface{}); check { - requeueDuration, err := time.ParseDuration(fields["period"].(string)) - if err != nil { - logger.Error(err, "Unable to parse time input") - return reconcileResult, err - } - reconcileResult.RequeueAfter = requeueDuration - logger.Info(fmt.Sprintf("Set the reconciliation to occur after %s", requeueDuration)) - return reconcileResult, nil - } - } - } - } - if event.Event == eventapi.EventRunnerOnFailed && !event.IgnoreError() && !event.Rescued() { - failureMessages = append(failureMessages, event.GetFailedPlaybookMessage()) - } - } - - // To print the stats of the task - printEventStats(statusEvent, u) - - // To print the full ansible result - r.printAnsibleResult(result, u) - - if statusEvent.Event == "" { - eventErr := errors.New("did not receive playbook_on_stats event") - stdout, err := result.Stdout() - if err != nil { - errmark := r.markError(ctx, request.NamespacedName, u, "Failed to get ansible-runner stdout") - if errmark != nil { - logger.Error(errmark, "Unable to mark error to run reconciliation") - } - logger.Error(err, "Failed to get ansible-runner stdout") - return reconcileResult, err - } - logger.Error(eventErr, stdout) - return reconcileResult, eventErr - } - - // Need to get the unstructured object after the Ansible runner finishes. - // This needs to hit the API server to retrieve updates. - err = r.APIReader.Get(ctx, request.NamespacedName, u) - if err != nil { - if apierrors.IsNotFound(err) { - return reconcile.Result{}, nil - } - return reconcile.Result{}, err - } - - // We only want to update the CustomResource once, so we'll track changes - // and do it at the end - runSuccessful := len(failureMessages) == 0 - - recentlyDeleted := u.GetDeletionTimestamp() != nil - - // The finalizer has run successfully, time to remove it - if deleted && finalizerExists && runSuccessful { - controllerutil.RemoveFinalizer(u, finalizer) - err := r.Client.Update(ctx, u) - if err != nil { - logger.Error(err, "Failed to remove finalizer") - return reconcileResult, err - } - } else if recentlyDeleted && finalizerExists { - // If the CR was deleted after the reconcile began, we need to requeue for the finalizer. - reconcileResult.Requeue = true - } - if r.ManageStatus { - errmark := r.markDone(ctx, request.NamespacedName, u, statusEvent, failureMessages) - if errmark != nil { - logger.Error(errmark, "Failed to mark status done") - } - // re-trigger reconcile because of failures - if !runSuccessful { - return reconcileResult, errors.New("event runner on failed") - } - return reconcileResult, errmark - } - - // re-trigger reconcile because of failures - if !runSuccessful { - return reconcileResult, errors.New("received failed task event") - } - return reconcileResult, nil -} - -func printEventStats(statusEvent eventapi.StatusJobEvent, u *unstructured.Unstructured) { - if len(statusEvent.StdOut) > 0 { - str := fmt.Sprintf("Ansible Task Status Event StdOut (%s, %s/%s)", u.GroupVersionKind(), u.GetName(), u.GetNamespace()) - fmt.Printf("\n----- %70s -----\n\n%s\n\n----------\n", str, statusEvent.StdOut) - } -} - -func (r *AnsibleOperatorReconciler) printAnsibleResult(result runner.RunResult, u *unstructured.Unstructured) { - if r.AnsibleDebugLogs { - if res, err := result.Stdout(); err == nil && len(res) > 0 { - str := fmt.Sprintf("Ansible Debug Result (%s, %s/%s)", u.GroupVersionKind(), u.GetName(), u.GetNamespace()) - fmt.Printf("\n----- %70s -----\n\n%s\n\n----------\n", str, res) - } - } -} - -func (r *AnsibleOperatorReconciler) markRunning(ctx context.Context, nn types.NamespacedName, u *unstructured.Unstructured) error { - - // Get the latest resource to prevent updating a stale status. - if err := r.APIReader.Get(ctx, nn, u); err != nil { - return err - } - crStatus := getStatus(u) - - // If there is no current status add that we are working on this resource. - errCond := ansiblestatus.GetCondition(crStatus, ansiblestatus.FailureConditionType) - if errCond != nil { - errCond.Status = v1.ConditionFalse - ansiblestatus.SetCondition(&crStatus, *errCond) - } - successCond := ansiblestatus.GetCondition(crStatus, ansiblestatus.SuccessfulConditionType) - if successCond != nil { - successCond.Status = v1.ConditionFalse - ansiblestatus.SetCondition(&crStatus, *successCond) - } - // If the condition is currently running, making sure that the values are correct. - // If they are the same a no-op, if they are different then it is a good thing we - // are updating it. - c := ansiblestatus.NewCondition( - ansiblestatus.RunningConditionType, - v1.ConditionTrue, - nil, - ansiblestatus.RunningReason, - ansiblestatus.RunningMessage, - ) - ansiblestatus.SetCondition(&crStatus, *c) - u.Object["status"] = crStatus.GetJSONMap() - - return r.Client.Status().Update(ctx, u) -} - -// markError - used to alert the user to the issues during the validation of a reconcile run. -// i.e Annotations that could be incorrect -func (r *AnsibleOperatorReconciler) markError(ctx context.Context, nn types.NamespacedName, u *unstructured.Unstructured, - failureMessage string) error { - - logger := logf.Log.WithName("markError") - // Immediately update metrics with failed reconciliation, since Get() - // may fail. - metrics.ReconcileFailed(r.GVK.String()) - // Get the latest resource to prevent updating a stale status. - if err := r.APIReader.Get(ctx, nn, u); err != nil { - if apierrors.IsNotFound(err) { - logger.Info("Resource not found, assuming it was deleted") - return nil - } - return err - } - crStatus := getStatus(u) - - rc := ansiblestatus.GetCondition(crStatus, ansiblestatus.RunningConditionType) - if rc != nil { - rc.Status = v1.ConditionFalse - ansiblestatus.SetCondition(&crStatus, *rc) - } - sc := ansiblestatus.GetCondition(crStatus, ansiblestatus.SuccessfulConditionType) - if sc != nil { - sc.Status = v1.ConditionFalse - ansiblestatus.SetCondition(&crStatus, *sc) - } - - c := ansiblestatus.NewCondition( - ansiblestatus.FailureConditionType, - v1.ConditionTrue, - nil, - ansiblestatus.FailedReason, - failureMessage, - ) - ansiblestatus.SetCondition(&crStatus, *c) - // This needs the status subresource to be enabled by default. - u.Object["status"] = crStatus.GetJSONMap() - - return r.Client.Status().Update(ctx, u) -} - -func (r *AnsibleOperatorReconciler) markDone(ctx context.Context, nn types.NamespacedName, u *unstructured.Unstructured, - statusEvent eventapi.StatusJobEvent, failureMessages eventapi.FailureMessages) error { - - logger := logf.Log.WithName("markDone") - // Get the latest resource to prevent updating a stale status. - if err := r.APIReader.Get(ctx, nn, u); err != nil { - if apierrors.IsNotFound(err) { - logger.Info("Resource not found, assuming it was deleted") - return nil - } - return err - } - crStatus := getStatus(u) - - runSuccessful := len(failureMessages) == 0 - ansibleStatus := ansiblestatus.NewAnsibleResultFromStatusJobEvent(statusEvent) - - if runSuccessful { - metrics.ReconcileSucceeded(r.GVK.String()) - deprecatedRunningCondition := ansiblestatus.NewCondition( - ansiblestatus.RunningConditionType, - v1.ConditionTrue, - ansibleStatus, - ansiblestatus.SuccessfulReason, - ansiblestatus.AwaitingMessage, - ) - failureCondition := ansiblestatus.NewCondition( - ansiblestatus.FailureConditionType, - v1.ConditionFalse, - nil, - "", - "", - ) - successfulCondition := ansiblestatus.NewCondition( - ansiblestatus.SuccessfulConditionType, - v1.ConditionTrue, - nil, - ansiblestatus.SuccessfulReason, - ansiblestatus.SuccessfulMessage, - ) - ansiblestatus.SetCondition(&crStatus, *deprecatedRunningCondition) - ansiblestatus.SetCondition(&crStatus, *successfulCondition) - ansiblestatus.SetCondition(&crStatus, *failureCondition) - } else { - metrics.ReconcileFailed(r.GVK.String()) - sc := ansiblestatus.GetCondition(crStatus, ansiblestatus.RunningConditionType) - if sc != nil { - sc.Status = v1.ConditionFalse - ansiblestatus.SetCondition(&crStatus, *sc) - } - failureCondition := ansiblestatus.NewCondition( - ansiblestatus.FailureConditionType, - v1.ConditionTrue, - ansibleStatus, - ansiblestatus.FailedReason, - strings.Join(failureMessages, "\n"), - ) - successfulCondition := ansiblestatus.NewCondition( - ansiblestatus.SuccessfulConditionType, - v1.ConditionFalse, - nil, - "", - "", - ) - ansiblestatus.SetCondition(&crStatus, *failureCondition) - ansiblestatus.SetCondition(&crStatus, *successfulCondition) - } - // This needs the status subresource to be enabled by default. - u.Object["status"] = crStatus.GetJSONMap() - - return r.Client.Status().Update(ctx, u) -} - -// getStatus returns u's "status" block as a status.Status. -func getStatus(u *unstructured.Unstructured) ansiblestatus.Status { - statusInterface := u.Object["status"] - statusMap, ok := statusInterface.(map[string]interface{}) - // If the map is not available create one. - if !ok { - statusMap = map[string]interface{}{} - } - return ansiblestatus.CreateFromMap(statusMap) -} diff --git a/internal/ansible/controller/reconcile_test.go b/internal/ansible/controller/reconcile_test.go deleted file mode 100644 index fc6f7ea3aa..0000000000 --- a/internal/ansible/controller/reconcile_test.go +++ /dev/null @@ -1,598 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// 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 controller_test - -import ( - "context" - "reflect" - "testing" - "time" - - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" - fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - - "github.com/operator-framework/operator-sdk/internal/ansible/controller" - ansiblestatus "github.com/operator-framework/operator-sdk/internal/ansible/controller/status" - "github.com/operator-framework/operator-sdk/internal/ansible/events" - "github.com/operator-framework/operator-sdk/internal/ansible/runner" - "github.com/operator-framework/operator-sdk/internal/ansible/runner/eventapi" - "github.com/operator-framework/operator-sdk/internal/ansible/runner/fake" -) - -func TestReconcile(t *testing.T) { - gvk := schema.GroupVersionKind{ - Kind: "Testing", - Group: "operator-sdk", - Version: "v1beta1", - } - eventTime := time.Now() - testCases := []struct { - Name string - GVK schema.GroupVersionKind - ReconcilePeriod time.Duration - Runner runner.Runner - EventHandlers []events.EventHandler - Client client.Client - ExpectedObject *unstructured.Unstructured - Result reconcile.Result - Request reconcile.Request - ShouldError bool - ManageStatus bool - }{ - { - Name: "cr not found", - GVK: gvk, - ReconcilePeriod: 5 * time.Second, - Runner: &fake.Runner{ - JobEvents: []eventapi.JobEvent{}, - }, - Client: fakeclient.NewClientBuilder().Build(), - Result: reconcile.Result{}, - Request: reconcile.Request{ - NamespacedName: types.NamespacedName{ - Name: "not_found", - Namespace: "default", - }, - }, - }, - { - Name: "completed reconcile", - GVK: gvk, - ReconcilePeriod: 5 * time.Second, - ManageStatus: true, - Runner: &fake.Runner{ - JobEvents: []eventapi.JobEvent{ - eventapi.JobEvent{ - Event: eventapi.EventPlaybookOnStats, - Created: eventapi.EventTime{Time: eventTime}, - }, - }, - }, - Client: fakeclient.NewClientBuilder().WithObjects(&unstructured.Unstructured{ - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ - "name": "reconcile", - "namespace": "default", - }, - "apiVersion": "operator-sdk/v1beta1", - "kind": "Testing", - }, - }).Build(), - Result: reconcile.Result{ - RequeueAfter: 5 * time.Second, - }, - Request: reconcile.Request{ - NamespacedName: types.NamespacedName{ - Name: "reconcile", - Namespace: "default", - }, - }, - ExpectedObject: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ - "name": "reconcile", - "namespace": "default", - }, - "apiVersion": "operator-sdk/v1beta1", - "kind": "Testing", - "spec": map[string]interface{}{}, - "status": map[string]interface{}{ - "conditions": []interface{}{ - map[string]interface{}{ - "status": "True", - "type": "Running", - "ansibleResult": map[string]interface{}{ - "changed": int64(0), - "failures": int64(0), - "ok": int64(0), - "skipped": int64(0), - "completion": eventTime.Format("2006-01-02T15:04:05.99999999"), - }, - "message": "Awaiting next reconciliation", - "reason": "Successful", - }, - map[string]interface{}{ - "status": "True", - "type": "Successful", - "message": "Last reconciliation succeeded", - "reason": "Successful", - }, - map[string]interface{}{ - "status": "False", - "type": "Failure", - }, - }, - }, - }, - }, - }, - { - Name: "Failure event runner on failed with manageStatus == true", - GVK: gvk, - ManageStatus: true, - Runner: &fake.Runner{ - JobEvents: []eventapi.JobEvent{ - eventapi.JobEvent{ - Event: eventapi.EventRunnerOnFailed, - Created: eventapi.EventTime{Time: eventTime}, - EventData: map[string]interface{}{ - "res": map[string]interface{}{ - "msg": "new failure message", - }, - }, - }, - eventapi.JobEvent{ - Event: eventapi.EventPlaybookOnStats, - Created: eventapi.EventTime{Time: eventTime}, - }, - }, - }, - Client: fakeclient.NewClientBuilder().WithObjects(&unstructured.Unstructured{ - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ - "name": "reconcile", - "namespace": "default", - }, - "apiVersion": "operator-sdk/v1beta1", - "kind": "Testing", - "spec": map[string]interface{}{}, - }, - }).Build(), - Request: reconcile.Request{ - NamespacedName: types.NamespacedName{ - Name: "reconcile", - Namespace: "default", - }, - }, - ExpectedObject: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ - "name": "reconcile", - "namespace": "default", - }, - "apiVersion": "operator-sdk/v1beta1", - "kind": "Testing", - "spec": map[string]interface{}{}, - "status": map[string]interface{}{ - "conditions": []interface{}{ - map[string]interface{}{ - "status": "False", - "type": "Running", - "message": "Running reconciliation", - "reason": "Running", - }, - map[string]interface{}{ - "status": "True", - "type": "Failure", - "ansibleResult": map[string]interface{}{ - "changed": int64(0), - "failures": int64(0), - "ok": int64(0), - "skipped": int64(0), - "completion": eventTime.Format("2006-01-02T15:04:05.99999999"), - }, - "message": "new failure message", - "reason": "Failed", - }, - map[string]interface{}{ - "status": "False", - "type": "Successful", - }, - }, - }, - }, - }, - ShouldError: true, - }, - { - Name: "Failure event runner on failed", - GVK: gvk, - ManageStatus: false, - Runner: &fake.Runner{ - JobEvents: []eventapi.JobEvent{ - eventapi.JobEvent{ - Event: eventapi.EventRunnerOnFailed, - Created: eventapi.EventTime{Time: eventTime}, - EventData: map[string]interface{}{ - "res": map[string]interface{}{ - "msg": "new failure message", - }, - }, - }, - eventapi.JobEvent{ - Event: eventapi.EventPlaybookOnStats, - Created: eventapi.EventTime{Time: eventTime}, - }, - }, - }, - Client: fakeclient.NewClientBuilder().WithObjects(&unstructured.Unstructured{ - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ - "name": "reconcile", - "namespace": "default", - }, - "apiVersion": "operator-sdk/v1beta1", - "kind": "Testing", - "spec": map[string]interface{}{}, - }, - }).Build(), - Request: reconcile.Request{ - NamespacedName: types.NamespacedName{ - Name: "reconcile", - Namespace: "default", - }, - }, - ShouldError: true, - }, - { - Name: "Finalizer successful reconcile", - GVK: gvk, - ReconcilePeriod: 5 * time.Second, - ManageStatus: true, - Runner: &fake.Runner{ - JobEvents: []eventapi.JobEvent{ - eventapi.JobEvent{ - Event: eventapi.EventPlaybookOnStats, - Created: eventapi.EventTime{Time: eventTime}, - }, - }, - Finalizer: "testing.io/finalizer", - }, - Client: fakeclient.NewClientBuilder().WithObjects(&unstructured.Unstructured{ - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ - "name": "reconcile", - "namespace": "default", - "annotations": map[string]interface{}{ - controller.ReconcilePeriodAnnotation: "3s", - }, - }, - "apiVersion": "operator-sdk/v1beta1", - "kind": "Testing", - "spec": map[string]interface{}{}, - }, - }).Build(), - Result: reconcile.Result{ - RequeueAfter: 3 * time.Second, - }, - Request: reconcile.Request{ - NamespacedName: types.NamespacedName{ - Name: "reconcile", - Namespace: "default", - }, - }, - ExpectedObject: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ - "name": "reconcile", - "namespace": "default", - "annotations": map[string]interface{}{ - controller.ReconcilePeriodAnnotation: "3s", - }, - "finalizers": []interface{}{ - "testing.io/finalizer", - }, - }, - "apiVersion": "operator-sdk/v1beta1", - "kind": "Testing", - "spec": map[string]interface{}{}, - "status": map[string]interface{}{ - "conditions": []interface{}{ - map[string]interface{}{ - "status": "True", - "type": "Running", - "ansibleResult": map[string]interface{}{ - "changed": int64(0), - "failures": int64(0), - "ok": int64(0), - "skipped": int64(0), - "completion": eventTime.Format("2006-01-02T15:04:05.99999999"), - }, - "message": "Awaiting next reconciliation", - "reason": "Successful", - }, - map[string]interface{}{ - "status": "True", - "type": "Successful", - "message": "Last reconciliation succeeded", - "reason": "Successful", - }, - map[string]interface{}{ - "status": "False", - "type": "Failure", - }, - }, - }, - }, - }, - }, - { - Name: "reconcile deletetion", - GVK: gvk, - ReconcilePeriod: 5 * time.Second, - Runner: &fake.Runner{ - JobEvents: []eventapi.JobEvent{ - eventapi.JobEvent{ - Event: eventapi.EventPlaybookOnStats, - Created: eventapi.EventTime{Time: eventTime}, - }, - }, - Finalizer: "testing.io/finalizer", - }, - Client: fakeclient.NewClientBuilder().WithObjects(&unstructured.Unstructured{ - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ - "name": "reconcile", - "namespace": "default", - "annotations": map[string]interface{}{ - controller.ReconcilePeriodAnnotation: "3s", - }, - "deletionTimestamp": eventTime.Format(time.RFC3339), - }, - "apiVersion": "operator-sdk/v1beta1", - "kind": "Testing", - "spec": map[string]interface{}{}, - }, - }).Build(), - Result: reconcile.Result{}, - Request: reconcile.Request{ - NamespacedName: types.NamespacedName{ - Name: "reconcile", - Namespace: "default", - }, - }, - }, - { - Name: "Finalizer successful deletion reconcile", - GVK: gvk, - ReconcilePeriod: 5 * time.Second, - ManageStatus: true, - Runner: &fake.Runner{ - JobEvents: []eventapi.JobEvent{ - eventapi.JobEvent{ - Event: eventapi.EventPlaybookOnStats, - Created: eventapi.EventTime{Time: eventTime}, - }, - }, - Finalizer: "testing.io/finalizer", - }, - Client: fakeclient.NewClientBuilder().WithObjects(&unstructured.Unstructured{ - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ - "name": "reconcile", - "namespace": "default", - "finalizers": []interface{}{ - "testing.io/finalizer", - }, - "deletionTimestamp": eventTime.Format(time.RFC3339), - }, - "apiVersion": "operator-sdk/v1beta1", - "kind": "Testing", - "spec": map[string]interface{}{}, - "status": map[string]interface{}{ - "conditions": []interface{}{ - map[string]interface{}{ - "status": "True", - "type": "Running", - "ansibleResult": map[string]interface{}{ - "changed": int64(0), - "failures": int64(0), - "ok": int64(0), - "skipped": int64(0), - "completion": eventTime.Format("2006-01-02T15:04:05.99999999"), - }, - "message": "Awaiting next reconciliation", - "reason": "Successful", - }, - }, - }, - }, - }).Build(), - Result: reconcile.Result{ - RequeueAfter: 5 * time.Second, - }, - Request: reconcile.Request{ - NamespacedName: types.NamespacedName{ - Name: "reconcile", - Namespace: "default", - }, - }, - }, - { - Name: "No status event", - GVK: gvk, - ReconcilePeriod: 5 * time.Second, - Runner: &fake.Runner{ - JobEvents: []eventapi.JobEvent{ - eventapi.JobEvent{ - Created: eventapi.EventTime{Time: eventTime}, - }, - }, - }, - Client: fakeclient.NewClientBuilder().WithObjects(&unstructured.Unstructured{ - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ - "name": "reconcile", - "namespace": "default", - }, - "apiVersion": "operator-sdk/v1beta1", - "kind": "Testing", - "spec": map[string]interface{}{}, - "status": map[string]interface{}{ - "conditions": []interface{}{ - map[string]interface{}{ - "status": "True", - "type": "Running", - "ansibleResult": map[string]interface{}{ - "changed": int64(0), - "failures": int64(0), - "ok": int64(0), - "skipped": int64(0), - "completion": eventTime.Format("2006-01-02T15:04:05.99999999"), - }, - "message": "Failed to get ansible-runner stdout", - }, - }, - }, - }, - }).Build(), - Result: reconcile.Result{ - RequeueAfter: 5 * time.Second, - }, - Request: reconcile.Request{ - NamespacedName: types.NamespacedName{ - Name: "reconcile", - Namespace: "default", - }, - }, - ShouldError: true, - }, - { - Name: "no manage status", - GVK: gvk, - ReconcilePeriod: 5 * time.Second, - ManageStatus: false, - Runner: &fake.Runner{ - JobEvents: []eventapi.JobEvent{ - eventapi.JobEvent{ - Event: eventapi.EventPlaybookOnStats, - Created: eventapi.EventTime{Time: eventTime}, - }, - }, - }, - Client: fakeclient.NewClientBuilder().WithObjects(&unstructured.Unstructured{ - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ - "name": "reconcile", - "namespace": "default", - }, - "apiVersion": "operator-sdk/v1beta1", - "kind": "Testing", - }, - }).Build(), - Result: reconcile.Result{ - RequeueAfter: 5 * time.Second, - }, - Request: reconcile.Request{ - NamespacedName: types.NamespacedName{ - Name: "reconcile", - Namespace: "default", - }, - }, - ExpectedObject: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ - "name": "reconcile", - "namespace": "default", - }, - "apiVersion": "operator-sdk/v1beta1", - "kind": "Testing", - "spec": map[string]interface{}{}, - "status": map[string]interface{}{}, - }, - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.Name, func(t *testing.T) { - var aor reconcile.Reconciler = &controller.AnsibleOperatorReconciler{ - GVK: tc.GVK, - Runner: tc.Runner, - Client: tc.Client, - APIReader: tc.Client, - EventHandlers: tc.EventHandlers, - ReconcilePeriod: tc.ReconcilePeriod, - ManageStatus: tc.ManageStatus, - } - result, err := aor.Reconcile(context.TODO(), tc.Request) - if err != nil && !tc.ShouldError { - t.Fatalf("Unexpected error: %v", err) - } - if !reflect.DeepEqual(result, tc.Result) { - t.Fatalf("Reconcile result does not equal\nexpected: %#v\nactual: %#v", tc.Result, result) - } - if tc.ExpectedObject != nil { - actualObject := &unstructured.Unstructured{} - actualObject.SetGroupVersionKind(tc.ExpectedObject.GroupVersionKind()) - err := tc.Client.Get(context.TODO(), types.NamespacedName{ - Name: tc.ExpectedObject.GetName(), - Namespace: tc.ExpectedObject.GetNamespace(), - }, actualObject) - if err != nil { - t.Fatalf("Failed to get object: (%v)", err) - } - if !reflect.DeepEqual(actualObject.GetAnnotations(), tc.ExpectedObject.GetAnnotations()) { - t.Fatalf("Annotations are not the same\nexpected: %v\nactual: %v", - tc.ExpectedObject.GetAnnotations(), actualObject.GetAnnotations()) - } - if !reflect.DeepEqual(actualObject.GetFinalizers(), tc.ExpectedObject.GetFinalizers()) && - len(actualObject.GetFinalizers()) != 0 && len(tc.ExpectedObject.GetFinalizers()) != 0 { - t.Fatalf("Finalizers are not the same\nexpected: %#v\nactual: %#v", - tc.ExpectedObject.GetFinalizers(), actualObject.GetFinalizers()) - } - sMap, _ := tc.ExpectedObject.Object["status"].(map[string]interface{}) - expectedStatus := ansiblestatus.CreateFromMap(sMap) - sMap, _ = actualObject.Object["status"].(map[string]interface{}) - actualStatus := ansiblestatus.CreateFromMap(sMap) - if len(expectedStatus.Conditions) != len(actualStatus.Conditions) { - t.Fatalf("Status conditions not the same\nexpected: %v\nactual: %v", expectedStatus, - actualStatus) - } - for _, c := range expectedStatus.Conditions { - actualCond := ansiblestatus.GetCondition(actualStatus, c.Type) - if c.Reason != actualCond.Reason || c.Message != actualCond.Message || c.Status != - actualCond.Status { - t.Fatalf("Message or reason did not match\nexpected: %+v\nactual: %+v", c, actualCond) - } - if c.AnsibleResult == nil && actualCond.AnsibleResult != nil { - t.Fatalf("Ansible result did not match\nexpected: %+v\nactual: %+v", c.AnsibleResult, - actualCond.AnsibleResult) - } - if c.AnsibleResult != nil { - if !reflect.DeepEqual(c.AnsibleResult, actualCond.AnsibleResult) { - t.Fatalf("Ansible result did not match\nexpected: %+v\nactual: %+v", c.AnsibleResult, - actualCond.AnsibleResult) - } - } - } - } - }) - } -} diff --git a/internal/ansible/controller/status/types.go b/internal/ansible/controller/status/types.go deleted file mode 100644 index f03eda7487..0000000000 --- a/internal/ansible/controller/status/types.go +++ /dev/null @@ -1,200 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// 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 status - -import ( - "encoding/json" - "time" - - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - logf "sigs.k8s.io/controller-runtime/pkg/log" - - "github.com/operator-framework/operator-sdk/internal/ansible/runner/eventapi" -) - -var log = logf.Log.WithName("controller.status") - -const ( - host = "localhost" -) - -// AnsibleResult - encapsulation of the ansible result. -type AnsibleResult struct { - Ok int `json:"ok"` - Changed int `json:"changed"` - Skipped int `json:"skipped"` - Failures int `json:"failures"` - TimeOfCompletion eventapi.EventTime `json:"completion"` -} - -// NewAnsibleResultFromStatusJobEvent - creates a Ansible status from job event. -func NewAnsibleResultFromStatusJobEvent(je eventapi.StatusJobEvent) *AnsibleResult { - // ok events. - a := &AnsibleResult{TimeOfCompletion: je.Created} - if v, ok := je.EventData.Changed[host]; ok { - a.Changed = v - } - if v, ok := je.EventData.Ok[host]; ok { - a.Ok = v - } - if v, ok := je.EventData.Skipped[host]; ok { - a.Skipped = v - } - if v, ok := je.EventData.Failures[host]; ok { - a.Failures = v - } - return a -} - -// NewAnsibleResultFromMap - creates a Ansible status from a job event. -func NewAnsibleResultFromMap(sm map[string]interface{}) *AnsibleResult { - //Create Old top level status - // ok events. - a := &AnsibleResult{} - if v, ok := sm["changed"]; ok { - a.Changed = int(v.(int64)) - } - if v, ok := sm["ok"]; ok { - a.Ok = int(v.(int64)) - } - if v, ok := sm["skipped"]; ok { - a.Skipped = int(v.(int64)) - } - if v, ok := sm["failures"]; ok { - a.Failures = int(v.(int64)) - } - if v, ok := sm["completion"]; ok { - s := v.(string) - if err := a.TimeOfCompletion.UnmarshalJSON([]byte(s)); err != nil { - log.Error(err, "Failed to unmarshal time of completion for ansible result") - } - } - return a -} - -// ConditionType - type of condition -type ConditionType string - -const ( - // RunningConditionType - condition type of running. - RunningConditionType ConditionType = "Running" - // FailureConditionType - condition type of failure. - FailureConditionType ConditionType = "Failure" - // SuccessfulConditionType - condition type of success. - SuccessfulConditionType ConditionType = "Successful" -) - -// Condition - the condition for the ansible operator. -type Condition struct { - Type ConditionType `json:"type"` - Status v1.ConditionStatus `json:"status"` - LastTransitionTime metav1.Time `json:"lastTransitionTime"` - AnsibleResult *AnsibleResult `json:"ansibleResult,omitempty"` - Reason string `json:"reason"` - Message string `json:"message"` -} - -func createConditionFromMap(cm map[string]interface{}) Condition { - ct, ok := cm["type"].(string) - if !ok { - //If we do not find the string we are defaulting - // to make sure we can at least update the status. - ct = string(RunningConditionType) - } - status, ok := cm["status"].(string) - if !ok { - status = string(v1.ConditionTrue) - } - reason, ok := cm["reason"].(string) - if !ok { - reason = "" - } - message, ok := cm["message"].(string) - if !ok { - message = "" - } - asm, ok := cm["ansibleResult"].(map[string]interface{}) - var ansibleResult *AnsibleResult - if ok { - ansibleResult = NewAnsibleResultFromMap(asm) - } - ltts, ok := cm["lastTransitionTime"].(string) - ltt := metav1.Now() - if ok { - t, err := time.Parse("2006-01-02T15:04:05Z", ltts) - if err != nil { - log.Info("Unable to parse time for status condition", "Time", ltts) - } else { - ltt = metav1.NewTime(t) - } - } - return Condition{ - Type: ConditionType(ct), - Status: v1.ConditionStatus(status), - LastTransitionTime: ltt, - Reason: reason, - Message: message, - AnsibleResult: ansibleResult, - } -} - -// Status - The status for custom resources managed by the operator-sdk. -type Status struct { - Conditions []Condition `json:"conditions"` - CustomStatus map[string]interface{} `json:"-"` -} - -// CreateFromMap - create a status from the map -func CreateFromMap(statusMap map[string]interface{}) Status { - customStatus := make(map[string]interface{}) - for key, value := range statusMap { - if key != "conditions" { - customStatus[key] = value - } - } - conditionsInterface, ok := statusMap["conditions"].([]interface{}) - if !ok { - return Status{Conditions: []Condition{}, CustomStatus: customStatus} - } - conditions := []Condition{} - for _, ci := range conditionsInterface { - cm, ok := ci.(map[string]interface{}) - if !ok { - log.Info("Unknown condition, removing condition", "ConditionInterface", ci) - continue - } - conditions = append(conditions, createConditionFromMap(cm)) - } - return Status{Conditions: conditions, CustomStatus: customStatus} -} - -// GetJSONMap - gets the map value for the status object. -// This is used to set the status on the CR. -// This is needed because the unstructured type has special rules around DeepCopy. -// If you do not convert the status to the map, then DeepCopy for the -// unstructured will fail and throw runtime exceptions. -// Please note that this will return an empty map on error. -func (status *Status) GetJSONMap() map[string]interface{} { - b, err := json.Marshal(status) - if err != nil { - log.Error(err, "Unable to marshal json") - return status.CustomStatus - } - if err := json.Unmarshal(b, &status.CustomStatus); err != nil { - log.Error(err, "Unable to unmarshal json") - } - return status.CustomStatus -} diff --git a/internal/ansible/controller/status/utils.go b/internal/ansible/controller/status/utils.go deleted file mode 100644 index cbf2608221..0000000000 --- a/internal/ansible/controller/status/utils.go +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// 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 status - -import ( - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -const ( - // RunningReason - Condition is running - RunningReason = "Running" - // SuccessfulReason - Condition is running due to reconcile being successful - SuccessfulReason = "Successful" - // FailedReason - Condition is failed due to ansible failure - FailedReason = "Failed" - // UnknownFailedReason - Condition is unknown - UnknownFailedReason = "Unknown" -) - -const ( - // RunningMessage - message for running reason. - RunningMessage = "Running reconciliation" - // SuccessfulMessage - message for successful reason. - AwaitingMessage = "Awaiting next reconciliation" - // SuccessfulMessage - message for successful condition. - SuccessfulMessage = "Last reconciliation succeeded" -) - -// NewCondition - condition -func NewCondition(condType ConditionType, status v1.ConditionStatus, ansibleResult *AnsibleResult, reason, - message string) *Condition { - return &Condition{ - Type: condType, - Status: status, - LastTransitionTime: metav1.Now(), - Reason: reason, - Message: message, - AnsibleResult: ansibleResult, - } -} - -// GetCondition returns the condition with the provided type. -func GetCondition(status Status, condType ConditionType) *Condition { - for i := range status.Conditions { - c := status.Conditions[i] - if c.Type == condType { - return &c - } - } - return nil -} - -// SetCondition updates the scheduledReport to include the provided condition. If the condition that -// we are about to add already exists and has the same status and reason then we are not going to update. -func SetCondition(status *Status, condition Condition) { - currentCond := GetCondition(*status, condition.Type) - if currentCond != nil && currentCond.Status == condition.Status && currentCond.Reason == condition.Reason { - return - } - // Do not update lastTransitionTime if the status of the condition doesn't change. - if currentCond != nil && currentCond.Status == condition.Status { - condition.LastTransitionTime = currentCond.LastTransitionTime - } - newConditions := filterOutCondition(status.Conditions, condition.Type) - status.Conditions = append(newConditions, condition) -} - -// RemoveCondition removes the scheduledReport condition with the provided type. -func RemoveCondition(status *Status, condType ConditionType) { - status.Conditions = filterOutCondition(status.Conditions, condType) -} - -// filterOutCondition returns a new slice of scheduledReport conditions without conditions with the provided type. -func filterOutCondition(conditions []Condition, condType ConditionType) []Condition { - var newConditions []Condition - for _, c := range conditions { - if c.Type == condType { - continue - } - newConditions = append(newConditions, c) - } - return newConditions -} diff --git a/internal/ansible/controller/status/utils_test.go b/internal/ansible/controller/status/utils_test.go deleted file mode 100644 index 1671dc156d..0000000000 --- a/internal/ansible/controller/status/utils_test.go +++ /dev/null @@ -1,302 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// 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 status - -import ( - "reflect" - "testing" - - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func TestNewCondition(t *testing.T) { - testCases := []struct { - name string - condType ConditionType - status v1.ConditionStatus - ansibleResult *AnsibleResult - reason string - message string - expectedCondtion Condition - }{ - { - name: "running condition creating", - condType: RunningConditionType, - status: v1.ConditionTrue, - ansibleResult: nil, - reason: RunningReason, - message: RunningMessage, - expectedCondtion: Condition{ - Type: RunningConditionType, - Status: v1.ConditionTrue, - Reason: RunningReason, - Message: RunningMessage, - }, - }, - { - name: "failure condition creating", - condType: FailureConditionType, - status: v1.ConditionFalse, - ansibleResult: &AnsibleResult{ - Changed: 0, - Failures: 1, - Ok: 10, - Skipped: 1, - }, - reason: FailedReason, - message: "invalid parameter", - expectedCondtion: Condition{ - Type: FailureConditionType, - Status: v1.ConditionFalse, - Reason: FailedReason, - Message: "invalid parameter", - AnsibleResult: &AnsibleResult{ - Changed: 0, - Failures: 1, - Ok: 10, - Skipped: 1, - }, - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - ac := NewCondition(tc.condType, tc.status, tc.ansibleResult, tc.reason, tc.message) - tc.expectedCondtion.LastTransitionTime = ac.LastTransitionTime - if !reflect.DeepEqual(*ac, tc.expectedCondtion) { - t.Fatalf("Condition did no match expected:\nActual: %#v\nExpected: %#v", *ac, tc.expectedCondtion) - } - }) - } -} - -func TestGetCondition(t *testing.T) { - testCases := []struct { - name string - condType ConditionType - status Status - expectedCondition *Condition - }{ - { - name: "find RunningCondition", - condType: RunningConditionType, - status: Status{ - Conditions: []Condition{ - Condition{ - Type: RunningConditionType, - }, - }, - }, - expectedCondition: &Condition{ - Type: RunningConditionType, - }, - }, - { - name: "did not find RunningCondition", - condType: RunningConditionType, - status: Status{ - Conditions: []Condition{ - Condition{ - Type: FailureConditionType, - }, - }, - }, - expectedCondition: nil, - }, - { - name: "find FailureCondition", - condType: FailureConditionType, - status: Status{ - Conditions: []Condition{ - Condition{ - Type: FailureConditionType, - }, - }, - }, - expectedCondition: &Condition{ - Type: FailureConditionType, - }, - }, - { - name: "did not find FailureCondition", - condType: FailureConditionType, - status: Status{ - Conditions: []Condition{ - Condition{ - Type: RunningConditionType, - }, - }, - }, - expectedCondition: nil, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - ac := GetCondition(tc.status, tc.condType) - if !reflect.DeepEqual(ac, tc.expectedCondition) { - t.Fatalf("Condition did no match expected:\nActual: %#v\nExpected: %#v", ac, tc.expectedCondition) - } - }) - } -} - -func TestRemoveCondition(t *testing.T) { - testCases := []struct { - name string - condType ConditionType - status Status - expectedSize int - }{ - { - name: "remove RunningCondition", - condType: RunningConditionType, - status: Status{ - Conditions: []Condition{ - Condition{ - Type: RunningConditionType, - }, - }, - }, - expectedSize: 0, - }, - { - name: "did not find RunningCondition", - condType: RunningConditionType, - status: Status{ - Conditions: []Condition{ - Condition{ - Type: FailureConditionType, - }, - }, - }, - expectedSize: 1, - }, - { - name: "remove FailureCondition", - condType: FailureConditionType, - status: Status{ - Conditions: []Condition{ - Condition{ - Type: FailureConditionType, - }, - }, - }, - expectedSize: 0, - }, - { - name: "did not find FailureCondition", - condType: FailureConditionType, - status: Status{ - Conditions: []Condition{ - Condition{ - Type: RunningConditionType, - }, - }, - }, - expectedSize: 1, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - RemoveCondition(&tc.status, tc.condType) - if tc.expectedSize != len(tc.status.Conditions) { - t.Fatalf("Conditions did no match expected size:\nActual: %#v\nExpected: %#v", - len(tc.status.Conditions), tc.expectedSize) - } - }) - } -} - -func TestSetCondition(t *testing.T) { - lastTransitionTime := metav1.Now() - keeptMessage := SuccessfulMessage - testCases := []struct { - name string - status *Status - condition *Condition - expectedNewSize int - keepLastTransitionTime bool - keepMessage bool - }{ - { - name: "add new condition", - status: &Status{ - Conditions: []Condition{}, - }, - condition: NewCondition(RunningConditionType, v1.ConditionTrue, nil, RunningReason, RunningMessage), - expectedNewSize: 1, - keepLastTransitionTime: false, - }, - { - name: "update running condition", - status: &Status{ - Conditions: []Condition{ - Condition{ - Type: RunningConditionType, - Status: v1.ConditionTrue, - Reason: SuccessfulReason, - Message: SuccessfulMessage, - LastTransitionTime: lastTransitionTime, - }, - }, - }, - condition: NewCondition(RunningConditionType, v1.ConditionTrue, nil, RunningReason, RunningMessage), - expectedNewSize: 1, - keepLastTransitionTime: true, - }, - { - name: "do not update running condition", - status: &Status{ - Conditions: []Condition{ - Condition{ - Type: RunningConditionType, - Status: v1.ConditionTrue, - Reason: RunningReason, - Message: SuccessfulMessage, - LastTransitionTime: lastTransitionTime, - }, - }, - }, - condition: NewCondition(RunningConditionType, v1.ConditionTrue, nil, RunningReason, RunningMessage), - expectedNewSize: 1, - keepLastTransitionTime: true, - keepMessage: true, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - SetCondition(tc.status, *tc.condition) - if tc.expectedNewSize != len(tc.status.Conditions) { - t.Fatalf("New size of conditions did not match expected\nActual: %v\nExpected: %v", - len(tc.status.Conditions), tc.expectedNewSize) - } - if tc.keepLastTransitionTime { - tc.condition.LastTransitionTime = lastTransitionTime - } - if tc.keepMessage { - tc.condition.Message = keeptMessage - } - ac := GetCondition(*tc.status, tc.condition.Type) - if !reflect.DeepEqual(ac, tc.condition) { - t.Fatalf("Condition did not match expected:\nActual: %#v\nExpected: %#v", ac, tc.condition) - } - }) - } -} diff --git a/internal/ansible/events/log_events.go b/internal/ansible/events/log_events.go deleted file mode 100644 index f764a62e3e..0000000000 --- a/internal/ansible/events/log_events.go +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// 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 events - -import ( - "errors" - "fmt" - "os" - "strconv" - "sync" - - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - logf "sigs.k8s.io/controller-runtime/pkg/log" - - "github.com/operator-framework/operator-sdk/internal/ansible/runner/eventapi" -) - -// LogLevel - Levelt for the logging to take place. -type LogLevel int - -const ( - // Tasks - only log the high level tasks. - Tasks LogLevel = iota - - // Everything - log every event. - Everything - - // Nothing - this will log nothing. - Nothing -) - -// EventHandler - knows how to handle job events. -type EventHandler interface { - Handle(string, *unstructured.Unstructured, eventapi.JobEvent) -} - -type loggingEventHandler struct { - LogLevel LogLevel - mux *sync.Mutex -} - -func (l loggingEventHandler) Handle(ident string, u *unstructured.Unstructured, e eventapi.JobEvent) { - if l.LogLevel == Nothing { - return - } - - logger := logf.Log.WithName("logging_event_handler").WithValues( - "name", u.GetName(), - "namespace", u.GetNamespace(), - "gvk", u.GroupVersionKind().String(), - "event_type", e.Event, - "job", ident, - ) - - verbosity := GetVerbosity(u, e, ident) - - // logger only the following for the 'Tasks' LogLevel - if l.LogLevel == Tasks { - t, ok := e.EventData["task"] - if ok { - setFactAction := e.EventData["task_action"] == eventapi.TaskActionSetFact - debugAction := e.EventData["task_action"] == eventapi.TaskActionDebug - - if verbosity > 0 { - l.mux.Lock() - fmt.Println(e.StdOut) - l.mux.Unlock() - return - } - if e.Event == eventapi.EventPlaybookOnTaskStart && !setFactAction && !debugAction { - l.mux.Lock() - logger.Info("[playbook task start]", "EventData.Name", e.EventData["name"]) - l.logAnsibleStdOut(e) - l.mux.Unlock() - return - } - if e.Event == eventapi.EventRunnerOnOk && debugAction { - l.mux.Lock() - logger.Info("[playbook debug]", "EventData.TaskArgs", e.EventData["task_args"]) - l.logAnsibleStdOut(e) - l.mux.Unlock() - return - } - if e.Event == eventapi.EventRunnerItemOnOk { - l.mux.Lock() - l.logAnsibleStdOut(e) - l.mux.Unlock() - return - } - if e.Event == eventapi.EventRunnerOnFailed { - errKVs := []interface{}{ - "EventData.Task", t, - "EventData.TaskArgs", e.EventData["task_args"], - } - if taskPath, ok := e.EventData["task_path"]; ok { - errKVs = append(errKVs, "EventData.FailedTaskPath", taskPath) - } - l.mux.Lock() - logger.Error(errors.New("[playbook task failed]"), "", errKVs...) - l.logAnsibleStdOut(e) - l.mux.Unlock() - return - } - } - } - - // log everything else for the 'Everything' LogLevel - if l.LogLevel == Everything { - l.mux.Lock() - logger.Info("", "EventData", e.EventData) - l.logAnsibleStdOut(e) - l.mux.Unlock() - } -} - -// logAnsibleStdOut will print in the logs the Ansible Task Output formatted -func (l loggingEventHandler) logAnsibleStdOut(e eventapi.JobEvent) { - if len(e.StdOut) > 0 { - fmt.Printf("\n--------------------------- Ansible Task StdOut -------------------------------\n") - if e.Event != eventapi.EventPlaybookOnTaskStart { - fmt.Printf("\n TASK [%v] ******************************** \n", e.EventData["task"]) - } - fmt.Println(e.StdOut) - fmt.Printf("\n-------------------------------------------------------------------------------\n") - } -} - -// NewLoggingEventHandler - Creates a Logging Event Handler to log events. -func NewLoggingEventHandler(l LogLevel) EventHandler { - return loggingEventHandler{ - LogLevel: l, - mux: &sync.Mutex{}, - } -} - -// GetVerbosity - Parses the verbsoity from CR and environment variables -func GetVerbosity(u *unstructured.Unstructured, e eventapi.JobEvent, ident string) int { - logger := logf.Log.WithName("logging_event_handler").WithValues( - "name", u.GetName(), - "namespace", u.GetNamespace(), - "gvk", u.GroupVersionKind().String(), - "event_type", e.Event, - "job", ident, - ) - - // Parse verbosity from CR - verbosityAnnotation := 0 - if annot, exists := u.UnstructuredContent()["metadata"].(map[string]interface{})["annotations"]; exists { - if verbosityField, present := annot.(map[string]interface{})["ansible.sdk.operatorframework.io/verbosity"]; present { - var err error - verbosityAnnotation, err = strconv.Atoi(verbosityField.(string)) - if err != nil { - logger.Error(err, "Unable to parse verbosity value from CR.") - } - } - } - - // Parse verbosity from environment variable - verbosityEnvVar := 0 - everb := os.Getenv("ANSIBLE_VERBOSITY") - if everb != "" { - var err error - verbosityEnvVar, err = strconv.Atoi(everb) - if err != nil { - logger.Error(err, "Unable to parse verbosity value from environment variable.") - } - } - - // Return in order of precedence - if verbosityAnnotation > 0 { - return verbosityAnnotation - } else if verbosityEnvVar > 0 { - return verbosityEnvVar - } else { - return 0 // Default - } -} diff --git a/internal/ansible/flags/flag.go b/internal/ansible/flags/flag.go deleted file mode 100644 index c78ebb28b6..0000000000 --- a/internal/ansible/flags/flag.go +++ /dev/null @@ -1,243 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// 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 flags - -import ( - "runtime" - "time" - - "github.com/spf13/pflag" - "sigs.k8s.io/controller-runtime/pkg/manager" -) - -// Flags - Options to be used by an ansible operator -type Flags struct { - ReconcilePeriod time.Duration - WatchesFile string - InjectOwnerRef bool - LeaderElection bool - MaxConcurrentReconciles int - AnsibleVerbosity int - AnsibleRolesPath string - AnsibleCollectionsPath string - MetricsBindAddress string - ProbeAddr string - LeaderElectionResourceLock string - LeaderElectionID string - LeaderElectionNamespace string - LeaseDuration time.Duration - RenewDeadline time.Duration - GracefulShutdownTimeout time.Duration - AnsibleArgs string - AnsibleLogEvents string - ProxyPort int - - // Path to a controller-runtime componentconfig file. - // If this is empty, use default values. - ManagerConfigPath string - - // If not nil, used to deduce which flags were set in the CLI. - flagSet *pflag.FlagSet -} - -const ( - AnsibleRolesPathEnvVar = "ANSIBLE_ROLES_PATH" - AnsibleCollectionsPathEnvVar = "ANSIBLE_COLLECTIONS_PATH" -) - -// AddTo - Add the ansible operator flags to the the flagset -func (f *Flags) AddTo(flagSet *pflag.FlagSet) { - // Store flagset internally to be used for lookups later. - f.flagSet = flagSet - - // Ansible flags. - flagSet.StringVar(&f.WatchesFile, - "watches-file", - "./watches.yaml", - "Path to the watches file to use", - ) - flagSet.BoolVar(&f.InjectOwnerRef, - "inject-owner-ref", - true, - "The ansible operator will inject owner references unless this flag is false", - ) - flagSet.IntVar(&f.AnsibleVerbosity, - "ansible-verbosity", - 2, - "Ansible verbosity. Overridden by environment variable.", - ) - flagSet.StringVar(&f.AnsibleRolesPath, - "ansible-roles-path", - "", - "Ansible Roles Path. If unset, roles are assumed to be in {{CWD}}/roles.", - ) - flagSet.StringVar(&f.AnsibleCollectionsPath, - "ansible-collections-path", - "", - "Path to installed Ansible Collections. If set, collections should be located in {{value}}/ansible_collections/. "+ - "If unset, collections are assumed to be in ~/.ansible/collections or /usr/share/ansible/collections.", - ) - flagSet.StringVar(&f.AnsibleArgs, - "ansible-args", - "", - "Ansible args. Allows user to specify arbitrary arguments for ansible-based operators.", - ) - - // Controller flags. - flagSet.DurationVar(&f.ReconcilePeriod, - "reconcile-period", - 10*time.Hour, - "Default reconcile period for controllers", - ) - flagSet.IntVar(&f.MaxConcurrentReconciles, - "max-concurrent-reconciles", - runtime.NumCPU(), - "Maximum number of concurrent reconciles for controllers. Overridden by environment variable.", - ) - - // Controller manager flags. - flagSet.StringVar(&f.ManagerConfigPath, - "config", - "", - "The controller will load its initial configuration from this file. "+ - "Omit this flag to use the default configuration values. "+ - "Command-line flags override configuration from this file.", - ) - // TODO(2.0.0): remove - flagSet.StringVar(&f.MetricsBindAddress, - "metrics-addr", - ":8080", - "The address the metric endpoint binds to", - ) - _ = flagSet.MarkDeprecated("metrics-addr", "use --metrics-bind-address instead") - flagSet.StringVar(&f.MetricsBindAddress, - "metrics-bind-address", - ":8080", - "The address the metric endpoint binds to", - ) - // TODO(2.0.0): for Go/Helm the port used is: 8081 - // update it to keep the project aligned to the other - flagSet.StringVar(&f.ProbeAddr, - "health-probe-bind-address", - ":6789", - "The address the probe endpoint binds to.", - ) - // TODO(2.0.0): remove - flagSet.BoolVar(&f.LeaderElection, - "enable-leader-election", - false, - "Enable leader election for controller manager. Enabling this will"+ - " ensure there is only one active controller manager.", - ) - _ = flagSet.MarkDeprecated("enable-leader-election", "use --leader-elect instead") - flagSet.BoolVar(&f.LeaderElection, - "leader-elect", - false, - "Enable leader election for controller manager. Enabling this will"+ - " ensure there is only one active controller manager.", - ) - flagSet.StringVar(&f.LeaderElectionID, - "leader-election-id", - "", - "Name of the configmap that is used for holding the leader lock.", - ) - flagSet.StringVar(&f.LeaderElectionNamespace, - "leader-election-namespace", - "", - "Namespace in which to create the leader election configmap for"+ - " holding the leader lock (required if running locally with leader"+ - " election enabled).", - ) - flagSet.StringVar(&f.LeaderElectionResourceLock, - "leader-elect-resource-lock", - "configmapsleases", - "The type of resource object that is used for locking during leader election."+ - " Supported options are 'leases', 'endpointsleases' and 'configmapsleases'. Default is configmapsleases.", - ) - flagSet.DurationVar(&f.LeaseDuration, - "leader-elect-lease-duration", - 15*time.Second, - "LeaseDuration is the duration that non-leader candidates will wait"+ - " to force acquire leadership. This is measured against time of last observed ack. Default is 15 seconds.", - ) - flagSet.DurationVar(&f.RenewDeadline, - "leader-elect-renew-deadline", - 10*time.Second, - "RenewDeadline is the duration that the acting controlplane will retry"+ - " refreshing leadership before giving up. Default is 10 seconds.", - ) - flagSet.DurationVar(&f.GracefulShutdownTimeout, - "graceful-shutdown-timeout", - 30*time.Second, - "The amount of time that will be spent waiting"+ - " for runners to gracefully exit.", - ) - flagSet.StringVar(&f.AnsibleLogEvents, - "ansible-log-events", - "tasks", - "Ansible log events. The log level for console logging."+ - " This flag can be set to either Nothing, Tasks, or Everything.", - ) - flagSet.IntVar(&f.ProxyPort, - "proxy-port", - 8888, - "Ansible proxy server port. Defaults to 8888.", - ) -} - -// ToManagerOptions uses the flag set in f to configure options. -// Values of options take precedence over flag defaults, -// as values are assume to have been explicitly set. -func (f *Flags) ToManagerOptions(options manager.Options) manager.Options { - // Alias FlagSet.Changed so options are still updated when fields are empty. - changed := func(flagName string) bool { - return f.flagSet.Changed(flagName) - } - if f.flagSet == nil { - changed = func(flagName string) bool { return false } - } - - // TODO(2.0.0): remove metrics-addr - if changed("metrics-bind-address") || changed("metrics-addr") || options.MetricsBindAddress == "" { - options.MetricsBindAddress = f.MetricsBindAddress - } - if changed("health-probe-bind-address") || options.HealthProbeBindAddress == "" { - options.HealthProbeBindAddress = f.ProbeAddr - } - // TODO(2.0.0): remove enable-leader-election - if changed("leader-elect") || changed("enable-leader-election") || !options.LeaderElection { - options.LeaderElection = f.LeaderElection - } - if changed("leader-election-id") || options.LeaderElectionID == "" { - options.LeaderElectionID = f.LeaderElectionID - } - if changed("leader-election-namespace") || options.LeaderElectionNamespace == "" { - options.LeaderElectionNamespace = f.LeaderElectionNamespace - } - if changed("leader-elect-lease-duration") || options.LeaseDuration == nil { - options.LeaseDuration = &f.LeaseDuration - } - if changed("leader-elect-renew-deadline") || options.RenewDeadline == nil { - options.RenewDeadline = &f.RenewDeadline - } - if changed("leader-elect-resource-lock") || options.LeaderElectionResourceLock == "" { - options.LeaderElectionResourceLock = f.LeaderElectionResourceLock - } - if changed("graceful-shutdown-timeout") || options.GracefulShutdownTimeout == nil { - options.GracefulShutdownTimeout = &f.GracefulShutdownTimeout - } - - return options -} diff --git a/internal/ansible/flags/flag_test.go b/internal/ansible/flags/flag_test.go deleted file mode 100644 index 5253d65f13..0000000000 --- a/internal/ansible/flags/flag_test.go +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2021 The Operator-SDK Authors -// -// 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 flags_test - -import ( - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "github.com/spf13/pflag" - "sigs.k8s.io/controller-runtime/pkg/manager" - - "github.com/operator-framework/operator-sdk/internal/ansible/flags" -) - -var _ = Describe("Flags", func() { - Describe("ToManagerOptions", func() { - var ( - f *flags.Flags - flagSet *pflag.FlagSet - options manager.Options - ) - BeforeEach(func() { - f = &flags.Flags{} - flagSet = pflag.NewFlagSet("test", pflag.ExitOnError) - f.AddTo(flagSet) - }) - - When("the flag is set", func() { - It("uses the flag value when corresponding option value is empty", func() { - expOptionValue := ":5678" - options.MetricsBindAddress = "" - parseArgs(flagSet, "--metrics-bind-address", expOptionValue) - Expect(f.ToManagerOptions(options).MetricsBindAddress).To(Equal(expOptionValue)) - }) - It("uses the flag value when corresponding option value is not empty", func() { - expOptionValue := ":5678" - options.MetricsBindAddress = ":1234" - parseArgs(flagSet, "--metrics-bind-address", expOptionValue) - Expect(f.ToManagerOptions(options).MetricsBindAddress).To(Equal(expOptionValue)) - }) - }) - When("the flag is not set", func() { - It("uses the default flag value when corresponding option value is empty", func() { - expOptionValue := ":8080" - options.MetricsBindAddress = "" - parseArgs(flagSet) - Expect(f.ToManagerOptions(options).MetricsBindAddress).To(Equal(expOptionValue)) - }) - It("uses the option value when corresponding option value is not empty", func() { - expOptionValue := ":1234" - options.MetricsBindAddress = expOptionValue - parseArgs(flagSet) - Expect(f.ToManagerOptions(options).MetricsBindAddress).To(Equal(expOptionValue)) - }) - }) - }) -}) - -func parseArgs(fs *pflag.FlagSet, extraArgs ...string) { - Expect(fs.Parse(extraArgs)).To(Succeed()) -} diff --git a/internal/ansible/flags/suite_test.go b/internal/ansible/flags/suite_test.go deleted file mode 100644 index 8850cb768c..0000000000 --- a/internal/ansible/flags/suite_test.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2021 The Operator-SDK Authors -// -// 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 flags_test - -import ( - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -func TestFlags(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Flags Suite") -} diff --git a/internal/ansible/handler/handler_suite_test.go b/internal/ansible/handler/handler_suite_test.go deleted file mode 100644 index ae169bf7d9..0000000000 --- a/internal/ansible/handler/handler_suite_test.go +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2021 The Operator-SDK Authors -// -// 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 handler - -import ( - "bytes" - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - logf "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/log/zap" -) - -var logBuffer bytes.Buffer - -func TestEventhandler(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Handler Suite") -} - -var _ = BeforeSuite(func() { - logf.SetLogger(zap.New(zap.WriteTo(&logBuffer), zap.UseDevMode(true))) -}) diff --git a/internal/ansible/handler/logging_enqueue_annotation.go b/internal/ansible/handler/logging_enqueue_annotation.go deleted file mode 100644 index 5728119a74..0000000000 --- a/internal/ansible/handler/logging_enqueue_annotation.go +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright 2021 The Operator-SDK Authors -// -// 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 handler - -import ( - "strings" - - "github.com/operator-framework/operator-lib/handler" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/util/workqueue" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/event" -) - -// LoggingEnqueueRequestForAnnotation wraps operator-lib handler for -// "InstrumentedEnqueueRequestForObject", and logs the events as they occur -// -// &handler.LoggingEnqueueRequestForAnnotation{} -type LoggingEnqueueRequestForAnnotation struct { - handler.EnqueueRequestForAnnotation -} - -// Create implements EventHandler, and emits a log message. -func (h LoggingEnqueueRequestForAnnotation) Create(e event.CreateEvent, q workqueue.RateLimitingInterface) { - h.logEvent("Create", e.Object, nil) - h.EnqueueRequestForAnnotation.Create(e, q) -} - -// Update implements EventHandler, and emits a log message. -func (h LoggingEnqueueRequestForAnnotation) Update(e event.UpdateEvent, q workqueue.RateLimitingInterface) { - h.logEvent("Update", e.ObjectOld, e.ObjectNew) - h.EnqueueRequestForAnnotation.Update(e, q) -} - -// Delete implements EventHandler, and emits a log message. -func (h LoggingEnqueueRequestForAnnotation) Delete(e event.DeleteEvent, q workqueue.RateLimitingInterface) { - h.logEvent("Delete", e.Object, nil) - h.EnqueueRequestForAnnotation.Delete(e, q) -} - -// Generic implements EventHandler, and emits a log message. -func (h LoggingEnqueueRequestForAnnotation) Generic(e event.GenericEvent, q workqueue.RateLimitingInterface) { - h.logEvent("Generic", e.Object, nil) - h.EnqueueRequestForAnnotation.Generic(e, q) -} - -func (h LoggingEnqueueRequestForAnnotation) logEvent(eventType string, object, newObject client.Object) { - typeString, name, namespace := extractTypedOwnerAnnotations(h.EnqueueRequestForAnnotation.Type, object) - if newObject != nil && typeString == "" { - typeString, name, namespace = extractTypedOwnerAnnotations(h.EnqueueRequestForAnnotation.Type, newObject) - } - - if name != "" && typeString != "" { - kvs := []interface{}{ - "Event type", eventType, - "GroupVersionKind", object.GetObjectKind().GroupVersionKind().String(), - "Name", object.GetName(), - } - if objectNs := object.GetNamespace(); objectNs != "" { - kvs = append(kvs, "Namespace", objectNs) - } - - kvs = append(kvs, - "Owner GroupKind", typeString, - "Owner Name", name, - ) - if namespace != "" { - kvs = append(kvs, "Owner Namespace", namespace) - } - - log.V(1).Info("Annotation handler event", kvs...) - } -} - -func extractTypedOwnerAnnotations(ownerGK schema.GroupKind, object metav1.Object) (string, string, string) { - annotations := object.GetAnnotations() - if len(annotations) == 0 { - return "", "", "" - } - if typeString, ok := annotations[handler.TypeAnnotation]; ok && typeString == ownerGK.String() { - if namespacedNameString, ok := annotations[handler.NamespacedNameAnnotation]; ok { - parsed := parseNamespacedName(namespacedNameString) - return typeString, parsed.Name, parsed.Namespace - } - } - return "", "", "" -} - -// parseNamespacedName parses the provided string to extract the namespace and name into a -// types.NamespacedName. The edge case of empty string is handled prior to calling this function. -func parseNamespacedName(namespacedNameString string) types.NamespacedName { - values := strings.SplitN(namespacedNameString, "/", 2) - - switch len(values) { - case 1: - return types.NamespacedName{Name: values[0]} - default: - return types.NamespacedName{Namespace: values[0], Name: values[1]} - } -} diff --git a/internal/ansible/handler/logging_enqueue_annotation_test.go b/internal/ansible/handler/logging_enqueue_annotation_test.go deleted file mode 100644 index 4d7944a64b..0000000000 --- a/internal/ansible/handler/logging_enqueue_annotation_test.go +++ /dev/null @@ -1,442 +0,0 @@ -// Copyright 2021 The Operator-SDK Authors -// -// 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 handler - -import ( - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "github.com/operator-framework/operator-lib/handler" - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/controller/controllertest" - "sigs.k8s.io/controller-runtime/pkg/event" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - - "k8s.io/client-go/util/workqueue" -) - -var _ = Describe("LoggingEnqueueRequestForAnnotation", func() { - var q workqueue.RateLimitingInterface - var instance LoggingEnqueueRequestForAnnotation - var pod *corev1.Pod - var podOwner *corev1.Pod - - BeforeEach(func() { - q = controllertest.Queue{Interface: workqueue.New()} - pod = &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "biz", - Name: "biz", - }, - } - podOwner = &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "podOwnerNs", - Name: "podOwnerName", - }, - } - - pod.SetGroupVersionKind(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}) - podOwner.SetGroupVersionKind(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}) - - Expect(handler.SetOwnerAnnotations(podOwner, pod)).To(Succeed()) - instance = LoggingEnqueueRequestForAnnotation{ - handler.EnqueueRequestForAnnotation{ - Type: schema.GroupKind{ - Group: "", - Kind: "Pod", - }}} - }) - - Describe("Create", func() { - It("should emit a log and enqueue a Request with the annotations of the object in case of CreateEvent", func() { - evt := event.CreateEvent{ - Object: pod, - } - - logBuffer.Reset() - instance.Create(evt, q) - Expect(logBuffer.String()).To(MatchRegexp( - `ansible.handler.*Create.*/v1.*Pod.*biz.*biz.*Pod.*podOwnerName.*podOwnerNs`, - )) - Expect(q.Len()).To(Equal(1)) - - i, _ := q.Get() - Expect(i).To(Equal(reconcile.Request{ - NamespacedName: types.NamespacedName{ - Namespace: podOwner.Namespace, - Name: podOwner.Name, - }, - })) - }) - - It("should enqueue a Request to the owner resource when the annotations are applied in child object"+ - " in the Create Event", func() { - repl := &appsv1.ReplicaSet{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "foo", - Name: "faz", - }, - } - repl.SetGroupVersionKind(schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "ReplicaSet"}) - - Expect(handler.SetOwnerAnnotations(podOwner, repl)).To(Succeed()) - - evt := event.CreateEvent{ - Object: repl, - } - logBuffer.Reset() - instance.Create(evt, q) - Expect(logBuffer.String()).To(MatchRegexp( - `ansible.handler.*Create.*apps/v1.*ReplicaSet.*faz.*foo.*Pod.*podOwnerName.*podOwnerNs`, - )) - Expect(q.Len()).To(Equal(1)) - - i, _ := q.Get() - Expect(i).To(Equal(reconcile.Request{ - NamespacedName: types.NamespacedName{ - Namespace: podOwner.Namespace, - Name: podOwner.Name, - }, - })) - }) - It("should not emit a log or enqueue a request if there are no annotations matching with the object", func() { - repl := &appsv1.ReplicaSet{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "foo", - Name: "faz", - }, - } - repl.SetGroupVersionKind(schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "ReplicaSet"}) - - evt := event.CreateEvent{ - Object: repl, - } - - logBuffer.Reset() - instance.Create(evt, q) - Expect(logBuffer.String()).To(Not(ContainSubstring("ansible.handler"))) - Expect(q.Len()).To(Equal(0)) - }) - It("should not emit a log or enqueue a Request if there is no Namespace and name annotation matching the specified object are found", func() { - repl := &appsv1.ReplicaSet{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "foo", - Name: "faz", - Annotations: map[string]string{ - handler.TypeAnnotation: schema.GroupKind{Group: "", Kind: "Pod"}.String(), - }, - }, - } - repl.SetGroupVersionKind(schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "ReplicaSet"}) - - evt := event.CreateEvent{ - Object: repl, - } - - logBuffer.Reset() - instance.Create(evt, q) - Expect(logBuffer.String()).To(Not(ContainSubstring("ansible.handler"))) - Expect(q.Len()).To(Equal(0)) - }) - It("should not emit a log or enqueue a Request if there is no TypeAnnotation matching the specified Group and Kind", func() { - repl := &appsv1.ReplicaSet{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "foo", - Name: "faz", - - Annotations: map[string]string{ - handler.NamespacedNameAnnotation: "AppService", - }, - }, - } - repl.SetGroupVersionKind(schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "ReplicaSet"}) - - evt := event.CreateEvent{ - Object: repl, - } - - logBuffer.Reset() - instance.Create(evt, q) - Expect(logBuffer.String()).To(Not(ContainSubstring("ansible.handler"))) - Expect(q.Len()).To(Equal(0)) - }) - It("should emit a log and enqueue a Request if there are no Namespace annotation matching the object", func() { - repl := &appsv1.ReplicaSet{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "foo", - Name: "faz", - Annotations: map[string]string{ - handler.NamespacedNameAnnotation: "AppService", - handler.TypeAnnotation: schema.GroupKind{Group: "", Kind: "Pod"}.String(), - }, - }, - } - repl.SetGroupVersionKind(schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "ReplicaSet"}) - - evt := event.CreateEvent{ - Object: repl, - } - - logBuffer.Reset() - instance.Create(evt, q) - Expect(logBuffer.String()).To(MatchRegexp( - `ansible.handler.*Create.*apps/v1.*ReplicaSet.*faz.*foo.*Pod.*AppService`, - )) - Expect(q.Len()).To(Equal(1)) - - i, _ := q.Get() - Expect(i).To(Equal(reconcile.Request{ - NamespacedName: types.NamespacedName{Namespace: "", Name: "AppService"}})) - }) - It("should emit a log and enqueue a Request for an object that is cluster scoped which has the annotations", func() { - nd := &corev1.Node{ - ObjectMeta: metav1.ObjectMeta{ - Name: "node-1", - Annotations: map[string]string{ - handler.NamespacedNameAnnotation: "myapp", - handler.TypeAnnotation: schema.GroupKind{Group: "apps", Kind: "ReplicaSet"}.String(), - }, - }, - } - nd.SetGroupVersionKind(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Node"}) - - instance = LoggingEnqueueRequestForAnnotation{handler.EnqueueRequestForAnnotation{Type: schema.GroupKind{Group: "apps", Kind: "ReplicaSet"}}} - - evt := event.CreateEvent{ - Object: nd, - } - - logBuffer.Reset() - instance.Create(evt, q) - Expect(logBuffer.String()).To(MatchRegexp( - `ansible.handler.*Create.*/v1.*Node.*node-1.*ReplicaSet.apps.*myapp.*`, - )) - Expect(q.Len()).To(Equal(1)) - - i, _ := q.Get() - Expect(i).To(Equal(reconcile.Request{ - NamespacedName: types.NamespacedName{Namespace: "", Name: "myapp"}})) - }) - It("should not emit a log or enqueue a Request for an object that is cluster scoped which does not have annotations", func() { - nd := &corev1.Node{ - ObjectMeta: metav1.ObjectMeta{Name: "node-1"}, - } - nd.SetGroupVersionKind(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Node"}) - - instance = LoggingEnqueueRequestForAnnotation{handler.EnqueueRequestForAnnotation{Type: nd.GetObjectKind().GroupVersionKind().GroupKind()}} - evt := event.CreateEvent{ - Object: nd, - } - - logBuffer.Reset() - instance.Create(evt, q) - Expect(logBuffer.String()).To(Not(ContainSubstring("ansible.handler"))) - Expect(q.Len()).To(Equal(0)) - }) - }) - - Describe("Delete", func() { - It("should emit a log and enqueue a Request with the annotations of the object in case of DeleteEvent", func() { - evt := event.DeleteEvent{ - Object: pod, - } - logBuffer.Reset() - instance.Delete(evt, q) - Expect(logBuffer.String()).To(MatchRegexp( - `ansible.handler.*Delete.*/v1.*Pod.*biz.*biz.*Pod.*podOwnerName.*podOwnerNs`, - )) - Expect(q.Len()).To(Equal(1)) - - i, _ := q.Get() - Expect(i).To(Equal(reconcile.Request{ - NamespacedName: types.NamespacedName{ - Namespace: podOwner.Namespace, - Name: podOwner.Name, - }, - })) - }) - }) - - Describe("Update", func() { - It("should emit a log and enqueue a Request with annotations applied to both objects in UpdateEvent", func() { - newPod := pod.DeepCopy() - newPod.Name = pod.Name + "2" - newPod.Namespace = pod.Namespace + "2" - - Expect(handler.SetOwnerAnnotations(podOwner, pod)).To(Succeed()) - - evt := event.UpdateEvent{ - ObjectOld: pod, - ObjectNew: newPod, - } - - logBuffer.Reset() - instance.Update(evt, q) - Expect(logBuffer.String()).To(MatchRegexp( - `ansible.handler.*Update.*/v1.*Pod.*biz.*biz.*Pod.*podOwnerName.*podOwnerNs`, - )) - Expect(q.Len()).To(Equal(1)) - - i, _ := q.Get() - Expect(i).To(Equal(reconcile.Request{ - NamespacedName: types.NamespacedName{ - Namespace: podOwner.Namespace, - Name: podOwner.Name, - }, - })) - }) - It("should emit a log and enqueue a Request with the annotations applied in one of the objects in case of UpdateEvent", func() { - newPod := pod.DeepCopy() - newPod.Name = pod.Name + "2" - newPod.Namespace = pod.Namespace + "2" - newPod.Annotations = map[string]string{} - - evt := event.UpdateEvent{ - ObjectOld: pod, - ObjectNew: newPod, - } - - logBuffer.Reset() - instance.Update(evt, q) - Expect(logBuffer.String()).To(MatchRegexp( - `ansible.handler.*Update.*/v1.*Pod.*biz.*biz.*Pod.*podOwnerName.*podOwnerNs`, - )) - Expect(q.Len()).To(Equal(1)) - i, _ := q.Get() - - Expect(i).To(Equal(reconcile.Request{ - NamespacedName: types.NamespacedName{ - Namespace: podOwner.Namespace, - Name: podOwner.Name, - }, - })) - }) - It("should emit a log and enqueue a Request when the annotations are applied in a different resource in case of UpdateEvent", func() { - repl := &appsv1.ReplicaSet{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "foo", - Name: "faz", - }, - } - repl.SetGroupVersionKind(schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "ReplicaSet"}) - - instance = LoggingEnqueueRequestForAnnotation{ - handler.EnqueueRequestForAnnotation{ - Type: schema.GroupKind{ - Group: "apps", - Kind: "ReplicaSet", - }}} - - evt := event.CreateEvent{ - Object: repl, - } - - logBuffer.Reset() - instance.Create(evt, q) - Expect(logBuffer.String()).To(Not(ContainSubstring("ansible.handler"))) - Expect(q.Len()).To(Equal(0)) - - newRepl := repl.DeepCopy() - newRepl.Name = pod.Name + "2" - newRepl.Namespace = pod.Namespace + "2" - - newRepl.Annotations = map[string]string{ - handler.TypeAnnotation: schema.GroupKind{Group: "apps", Kind: "ReplicaSet"}.String(), - handler.NamespacedNameAnnotation: "foo/faz", - } - - instance2 := LoggingEnqueueRequestForAnnotation{ - handler.EnqueueRequestForAnnotation{ - Type: schema.GroupKind{ - Group: "apps", - Kind: "ReplicaSet", - }}} - - evt2 := event.UpdateEvent{ - ObjectOld: repl, - ObjectNew: newRepl, - } - - logBuffer.Reset() - instance2.Update(evt2, q) - Expect(logBuffer.String()).To(MatchRegexp( - `ansible.handler.*Update.*apps/v1.*ReplicaSet.*faz.*foo.*ReplicaSet.apps.*faz.*foo`, - )) - Expect(q.Len()).To(Equal(1)) - - i, _ := q.Get() - Expect(i).To(Equal(reconcile.Request{ - NamespacedName: types.NamespacedName{ - Namespace: "foo", - Name: "faz", - }, - })) - }) - It("should emit a log and enqueue multiple Update Requests when different annotations are applied to multiple objects", func() { - newPod := pod.DeepCopy() - newPod.Name = pod.Name + "2" - newPod.Namespace = pod.Namespace + "2" - - Expect(handler.SetOwnerAnnotations(podOwner, pod)).To(Succeed()) - - var podOwner2 = &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "podOwnerNsTest", - Name: "podOwnerNameTest", - }, - } - podOwner2.SetGroupVersionKind(schema.GroupVersionKind{Group: "", Kind: "Pod"}) - - Expect(handler.SetOwnerAnnotations(podOwner2, newPod)).To(Succeed()) - - evt := event.UpdateEvent{ - ObjectOld: pod, - ObjectNew: newPod, - } - logBuffer.Reset() - instance.Update(evt, q) - Expect(logBuffer.String()).To(MatchRegexp( - `ansible.handler.*Update.*/v1.*Pod.*biz.*biz.*Pod.*podOwnerName.*podOwnerNs`, - )) - Expect(q.Len()).To(Equal(2)) - }) - }) - - Describe("Generic", func() { - It("should enqueue a Request with the annotations of the object in case of GenericEvent", func() { - evt := event.GenericEvent{ - Object: pod, - } - logBuffer.Reset() - instance.Generic(evt, q) - Expect(logBuffer.String()).To(MatchRegexp( - `ansible.handler.*Generic.*/v1.*Pod.*biz.*biz.*Pod.*podOwnerName.*podOwnerNs`, - )) - Expect(q.Len()).To(Equal(1)) - - i, _ := q.Get() - Expect(i).To(Equal(reconcile.Request{ - NamespacedName: types.NamespacedName{ - Namespace: podOwner.Namespace, - Name: podOwner.Name, - }, - })) - }) - }) -}) diff --git a/internal/ansible/handler/logging_enqueue_object.go b/internal/ansible/handler/logging_enqueue_object.go deleted file mode 100644 index 0aeb8b4f69..0000000000 --- a/internal/ansible/handler/logging_enqueue_object.go +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright 2021 The Operator-SDK Authors -// -// 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 handler - -import ( - "github.com/operator-framework/operator-lib/handler" - "k8s.io/client-go/util/workqueue" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/event" - logf "sigs.k8s.io/controller-runtime/pkg/log" -) - -var log = logf.Log.WithName("ansible").WithName("handler") - -// LoggingEnqueueRequestForObject wraps operator-lib handler for -// "InstrumentedEnqueueRequestForObject", and logs the events as they occur -// -// &handler.LoggingEnqueueRequestForObject{} -type LoggingEnqueueRequestForObject struct { - handler.InstrumentedEnqueueRequestForObject -} - -// Create implements EventHandler, and emits a log message. -func (h LoggingEnqueueRequestForObject) Create(e event.CreateEvent, q workqueue.RateLimitingInterface) { - h.logEvent("Create", e.Object) - h.InstrumentedEnqueueRequestForObject.Create(e, q) -} - -// Update implements EventHandler, and emits a log message. -func (h LoggingEnqueueRequestForObject) Update(e event.UpdateEvent, q workqueue.RateLimitingInterface) { - h.logEvent("Update", e.ObjectOld) - h.InstrumentedEnqueueRequestForObject.Update(e, q) -} - -// Delete implements EventHandler, and emits a log message. -func (h LoggingEnqueueRequestForObject) Delete(e event.DeleteEvent, q workqueue.RateLimitingInterface) { - h.logEvent("Delete", e.Object) - h.InstrumentedEnqueueRequestForObject.Delete(e, q) -} - -// Generic implements EventHandler, and emits a log message. -func (h LoggingEnqueueRequestForObject) Generic(e event.GenericEvent, q workqueue.RateLimitingInterface) { - h.logEvent("Generic", e.Object) - h.EnqueueRequestForObject.Generic(e, q) -} - -func (h LoggingEnqueueRequestForObject) logEvent(eventType string, object client.Object) { - kvs := []interface{}{ - "Event type", eventType, - "GroupVersionKind", object.GetObjectKind().GroupVersionKind().String(), - "Name", object.GetName(), - } - if objectNs := object.GetNamespace(); objectNs != "" { - kvs = append(kvs, "Namespace", objectNs) - } - - log.V(1).Info("Metrics handler event", kvs...) -} diff --git a/internal/ansible/handler/logging_enqueue_object_test.go b/internal/ansible/handler/logging_enqueue_object_test.go deleted file mode 100644 index 00420d6ddb..0000000000 --- a/internal/ansible/handler/logging_enqueue_object_test.go +++ /dev/null @@ -1,218 +0,0 @@ -// Copyright 2021 The Operator-SDK Authors -// -// 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 handler - -import ( - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - dto "github.com/prometheus/client_model/go" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/controller/controllertest" - "sigs.k8s.io/controller-runtime/pkg/event" - "sigs.k8s.io/controller-runtime/pkg/metrics" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - - "k8s.io/client-go/util/workqueue" -) - -var _ = Describe("LoggingEnqueueRequestForObject", func() { - var q workqueue.RateLimitingInterface - var instance LoggingEnqueueRequestForObject - var pod *corev1.Pod - - BeforeEach(func() { - logBuffer.Reset() - q = controllertest.Queue{Interface: workqueue.New()} - instance = LoggingEnqueueRequestForObject{} - pod = &corev1.Pod{ - TypeMeta: metav1.TypeMeta{ - Kind: "Pod", - APIVersion: "v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Namespace: "biznamespace", - Name: "bizname", - CreationTimestamp: metav1.Now(), - }, - } - }) - Describe("Create", func() { - It("should emit a log, enqueue a request & emit a metric on a CreateEvent", func() { - evt := event.CreateEvent{ - Object: pod, - } - - // test the create - logBuffer.Reset() - instance.Create(evt, q) - Expect(logBuffer.String()).To(MatchRegexp( - `ansible.handler.*Create.*/v1.*Pod.*bizname.*biznamespace`, - )) - - // verify workqueue - Expect(q.Len()).To(Equal(1)) - i, _ := q.Get() - Expect(i).To(Equal(reconcile.Request{ - NamespacedName: types.NamespacedName{ - Namespace: pod.Namespace, - Name: pod.Name, - }, - })) - - // verify metrics - gauges, err := metrics.Registry.Gather() - Expect(err).NotTo(HaveOccurred()) - Expect(gauges).To(HaveLen(1)) - assertMetrics(gauges[0], 1, []*corev1.Pod{pod}) - }) - }) - - Describe("Delete", func() { - Context("when a gauge already exists", func() { - BeforeEach(func() { - evt := event.CreateEvent{ - Object: pod, - } - logBuffer.Reset() - instance.Create(evt, q) - Expect(logBuffer.String()).To(MatchRegexp( - `ansible.handler.*Create.*/v1.*Pod.*bizname.*biznamespace`, - )) - Expect(q.Len()).To(Equal(1)) - }) - It("should emit a log, enqueue a request & remove the metric on a DeleteEvent", func() { - evt := event.DeleteEvent{ - Object: pod, - } - - logBuffer.Reset() - // test the delete - instance.Delete(evt, q) - Expect(logBuffer.String()).To(MatchRegexp( - `ansible.handler.*Delete.*/v1.*Pod.*bizname.*biznamespace`, - )) - - // verify workqueue - Expect(q.Len()).To(Equal(1)) - i, _ := q.Get() - Expect(i).To(Equal(reconcile.Request{ - NamespacedName: types.NamespacedName{ - Namespace: pod.Namespace, - Name: pod.Name, - }, - })) - - // verify metrics - gauges, err := metrics.Registry.Gather() - Expect(err).NotTo(HaveOccurred()) - Expect(gauges).To(BeEmpty()) - }) - }) - Context("when a gauge does not exist", func() { - It("should emit a log, enqueue a request & there should be no new metric on a DeleteEvent", func() { - evt := event.DeleteEvent{ - Object: pod, - } - - logBuffer.Reset() - // test the delete - instance.Delete(evt, q) - Expect(logBuffer.String()).To(MatchRegexp( - `ansible.handler.*Delete.*/v1.*Pod.*bizname.*biznamespace`, - )) - - // verify workqueue - Expect(q.Len()).To(Equal(1)) - i, _ := q.Get() - Expect(i).To(Equal(reconcile.Request{ - NamespacedName: types.NamespacedName{ - Namespace: pod.Namespace, - Name: pod.Name, - }, - })) - - // verify metrics - gauges, err := metrics.Registry.Gather() - Expect(err).NotTo(HaveOccurred()) - Expect(gauges).To(BeEmpty()) - }) - }) - - }) - - Describe("Update", func() { - It("should emit a log and enqueue a request in case of UpdateEvent", func() { - newpod := &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "baznamespace", - Name: "bazname", - }, - } - evt := event.UpdateEvent{ - ObjectOld: pod, - ObjectNew: newpod, - } - - logBuffer.Reset() - // test the update - instance.Update(evt, q) - Expect(logBuffer.String()).To(MatchRegexp( - `ansible.handler.*Update.*/v1.*Pod.*bizname.*biznamespace`, - )) - - // verify workqueue - Expect(q.Len()).To(Equal(1)) - i, _ := q.Get() - Expect(i).To(Equal(reconcile.Request{ - NamespacedName: types.NamespacedName{ - Namespace: newpod.Namespace, - Name: newpod.Name, - }, - })) - - // verify metrics - gauges, err := metrics.Registry.Gather() - Expect(err).NotTo(HaveOccurred()) - Expect(gauges).To(HaveLen(1)) - assertMetrics(gauges[0], 2, []*corev1.Pod{newpod, pod}) - }) - }) -}) - -func assertMetrics(gauge *dto.MetricFamily, count int, pods []*corev1.Pod) { - Expect(gauge.Metric).To(HaveLen(count)) - for i := 0; i < count; i++ { - Expect(*gauge.Metric[i].Gauge.Value).To(Equal(float64(pods[i].GetObjectMeta().GetCreationTimestamp().UTC().Unix()))) - - for _, l := range gauge.Metric[i].Label { - if l.Name != nil { - switch *l.Name { - case "name": - Expect(l.Value).To(HaveValue(Equal(pods[i].GetObjectMeta().GetName()))) - case "namespace": - Expect(l.Value).To(HaveValue(Equal(pods[i].GetObjectMeta().GetNamespace()))) - case "group": - Expect(l.Value).To(HaveValue(Equal(pods[i].GetObjectKind().GroupVersionKind().Group))) - case "version": - Expect(l.Value).To(HaveValue(Equal(pods[i].GetObjectKind().GroupVersionKind().Version))) - case "kind": - Expect(l.Value).To(HaveValue(Equal(pods[i].GetObjectKind().GroupVersionKind().Kind))) - } - } - } - } -} diff --git a/internal/ansible/handler/logging_enqueue_owner.go b/internal/ansible/handler/logging_enqueue_owner.go deleted file mode 100644 index 8331a102c3..0000000000 --- a/internal/ansible/handler/logging_enqueue_owner.go +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2021 The Operator-SDK Authors -// -// 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 handler - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/util/workqueue" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/event" - crHandler "sigs.k8s.io/controller-runtime/pkg/handler" -) - -// LoggingEnqueueRequestForOwner wraps operator-lib handler for -// "InstrumentedEnqueueRequestForObject", and logs the events as they occur -// -// &handler.LoggingEnqueueRequestForOwner{} -type LoggingEnqueueRequestForOwner struct { - crHandler.EnqueueRequestForOwner -} - -// Create implements EventHandler, and emits a log message. -func (h LoggingEnqueueRequestForOwner) Create(e event.CreateEvent, q workqueue.RateLimitingInterface) { - h.logEvent("Create", e.Object, nil) - h.EnqueueRequestForOwner.Create(e, q) -} - -// Update implements EventHandler, and emits a log message. -func (h LoggingEnqueueRequestForOwner) Update(e event.UpdateEvent, q workqueue.RateLimitingInterface) { - h.logEvent("Update", e.ObjectOld, e.ObjectNew) - h.EnqueueRequestForOwner.Update(e, q) -} - -// Delete implements EventHandler, and emits a log message. -func (h LoggingEnqueueRequestForOwner) Delete(e event.DeleteEvent, q workqueue.RateLimitingInterface) { - h.logEvent("Delete", e.Object, nil) - h.EnqueueRequestForOwner.Delete(e, q) -} - -// Generic implements EventHandler, and emits a log message. -func (h LoggingEnqueueRequestForOwner) Generic(e event.GenericEvent, q workqueue.RateLimitingInterface) { - h.logEvent("Generic", e.Object, nil) - h.EnqueueRequestForOwner.Generic(e, q) -} - -func (h LoggingEnqueueRequestForOwner) logEvent(eventType string, object, newObject client.Object) { - ownerReference := extractTypedOwnerReference(h.EnqueueRequestForOwner.OwnerType.GetObjectKind().GroupVersionKind(), object.GetOwnerReferences()) - if ownerReference == nil && newObject != nil { - ownerReference = extractTypedOwnerReference(h.EnqueueRequestForOwner.OwnerType.GetObjectKind().GroupVersionKind(), newObject.GetOwnerReferences()) - } - - // If no ownerReference was found then it's probably not an event we care about - if ownerReference != nil { - kvs := []interface{}{ - "Event type", eventType, - "GroupVersionKind", object.GetObjectKind().GroupVersionKind().String(), - "Name", object.GetName(), - } - if objectNs := object.GetNamespace(); objectNs != "" { - kvs = append(kvs, "Namespace", objectNs) - } - kvs = append(kvs, - "Owner APIVersion", ownerReference.APIVersion, - "Owner Kind", ownerReference.Kind, - "Owner Name", ownerReference.Name, - ) - - log.V(1).Info("OwnerReference handler event", kvs...) - } -} - -func extractTypedOwnerReference(ownerGVK schema.GroupVersionKind, ownerReferences []metav1.OwnerReference) *metav1.OwnerReference { - for _, ownerRef := range ownerReferences { - refGV, err := schema.ParseGroupVersion(ownerRef.APIVersion) - if err != nil { - log.Error(err, "Could not parse OwnerReference APIVersion", - "api version", ownerRef.APIVersion) - } - - if ownerGVK.Group == refGV.Group && - ownerGVK.Kind == ownerRef.Kind { - return &ownerRef - } - } - return nil -} diff --git a/internal/ansible/handler/logging_enqueue_owner_test.go b/internal/ansible/handler/logging_enqueue_owner_test.go deleted file mode 100644 index d2f7331a95..0000000000 --- a/internal/ansible/handler/logging_enqueue_owner_test.go +++ /dev/null @@ -1,269 +0,0 @@ -// Copyright 2021 The Operator-SDK Authors -// -// 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 handler - -import ( - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" - "sigs.k8s.io/controller-runtime/pkg/controller/controllertest" - "sigs.k8s.io/controller-runtime/pkg/event" - crHandler "sigs.k8s.io/controller-runtime/pkg/handler" - - "k8s.io/client-go/util/workqueue" -) - -var _ = Describe("LoggingEnqueueRequestForOwner", func() { - var q workqueue.RateLimitingInterface - var instance LoggingEnqueueRequestForOwner - var pod *corev1.Pod - var podOwner *metav1.OwnerReference - - BeforeEach(func() { - q = controllertest.Queue{Interface: workqueue.New()} - podOwner = &metav1.OwnerReference{ - Kind: "Pod", - APIVersion: "v1", - Name: "podOwnerName", - } - - pod = &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "biz", - Name: "biz", - OwnerReferences: []metav1.OwnerReference{*podOwner}, - }, - } - - pod.SetGroupVersionKind(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}) - - instance = LoggingEnqueueRequestForOwner{ - crHandler.EnqueueRequestForOwner{ - OwnerType: pod, - }} - }) - - Describe("Create", func() { - It("should emit a log with the ownerReference of the object in case of CreateEvent", func() { - evt := event.CreateEvent{ - Object: pod, - } - - logBuffer.Reset() - instance.Create(evt, q) - Expect(logBuffer.String()).To(MatchRegexp( - `ansible.handler.*Create.*/v1.*Pod.*biz.*biz.*v1.*Pod.*podOwnerName`, - )) - }) - - It("emit a log when the ownerReferences are applied in child object"+ - " in the Create Event", func() { - repl := &appsv1.ReplicaSet{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "foo", - Name: "faz", - OwnerReferences: []metav1.OwnerReference{*podOwner}, - }, - } - repl.SetGroupVersionKind(schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "ReplicaSet"}) - - evt := event.CreateEvent{ - Object: repl, - } - logBuffer.Reset() - instance.Create(evt, q) - Expect(logBuffer.String()).To(MatchRegexp( - `ansible.handler.*Create.*apps/v1.*ReplicaSet.*faz.*foo.*Pod.*podOwnerName`, - )) - }) - It("should not emit a log or there are no ownerReferences matching with the object", func() { - repl := &appsv1.ReplicaSet{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "foo", - Name: "faz", - }, - } - repl.SetGroupVersionKind(schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "ReplicaSet"}) - - evt := event.CreateEvent{ - Object: repl, - } - - logBuffer.Reset() - instance.Create(evt, q) - Expect(logBuffer.String()).To(Not(ContainSubstring("ansible.handler"))) - Expect(q.Len()).To(Equal(0)) - }) - It("should not emit a log if the ownerReference does not match the OwnerType", func() { - repl := &appsv1.ReplicaSet{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "foo", - Name: "faz", - OwnerReferences: []metav1.OwnerReference{{ - APIVersion: "v1", - Kind: "ConfigMap", - Name: "podOwnerName", - }}, - }, - } - repl.SetGroupVersionKind(schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "ReplicaSet"}) - - evt := event.CreateEvent{ - Object: repl, - } - - logBuffer.Reset() - instance.Create(evt, q) - Expect(logBuffer.String()).To(Not(ContainSubstring("ansible.handler"))) - }) - - It("should not emit a log for an object which does not have ownerReferences", func() { - repl := &appsv1.ReplicaSet{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "foo", - Name: "faz", - }, - } - repl.SetGroupVersionKind(schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "ReplicaSet"}) - - evt := event.CreateEvent{ - Object: repl, - } - - logBuffer.Reset() - instance.Create(evt, q) - Expect(logBuffer.String()).To(Not(ContainSubstring("ansible.handler"))) - }) - }) - - Describe("Delete", func() { - It("should emit a log with the ownerReferenc of the object in case of DeleteEvent", func() { - evt := event.DeleteEvent{ - Object: pod, - } - logBuffer.Reset() - instance.Delete(evt, q) - Expect(logBuffer.String()).To(MatchRegexp( - `ansible.handler.*Delete.*/v1.*Pod.*biz.*biz.*Pod.*podOwnerName`, - )) - }) - }) - - Describe("Update", func() { - It("should emit a log and enqueue a Request with annotations applied to both objects in UpdateEvent", func() { - newPod := pod.DeepCopy() - newPod.Name = pod.Name + "2" - newPod.Namespace = pod.Namespace + "2" - - evt := event.UpdateEvent{ - ObjectOld: pod, - ObjectNew: newPod, - } - - logBuffer.Reset() - instance.Update(evt, q) - Expect(logBuffer.String()).To(MatchRegexp( - `ansible.handler.*Update.*/v1.*Pod.*biz.*biz.*Pod.*podOwnerName`, - )) - }) - It("should emit a log with the ownerReferences applied in one of the objects in case of UpdateEvent", func() { - noOwnerPod := pod.DeepCopy() - noOwnerPod.Name = pod.Name + "2" - noOwnerPod.Namespace = pod.Namespace + "2" - noOwnerPod.OwnerReferences = []metav1.OwnerReference{} - - evt := event.UpdateEvent{ - ObjectOld: pod, - ObjectNew: noOwnerPod, - } - - logBuffer.Reset() - instance.Update(evt, q) - Expect(logBuffer.String()).To(MatchRegexp( - `ansible.handler.*Update.*/v1.*Pod.*biz.*biz.*Pod.*podOwnerName`, - )) - - evt = event.UpdateEvent{ - ObjectOld: noOwnerPod, - ObjectNew: pod, - } - - logBuffer.Reset() - instance.Update(evt, q) - Expect(logBuffer.String()).To(MatchRegexp( - `ansible.handler.*Update.*/v1.*Pod.*biz.*biz.*Pod.*podOwnerName`, - )) - }) - It("should emit a log when the OwnerReference is applied after creation in case of UpdateEvent", func() { - repl := &appsv1.ReplicaSet{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "foo", - Name: "faz", - }, - } - repl.SetGroupVersionKind(schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "ReplicaSet"}) - - instance = LoggingEnqueueRequestForOwner{ - crHandler.EnqueueRequestForOwner{ - OwnerType: repl, - }} - - evt := event.CreateEvent{ - Object: repl, - } - - logBuffer.Reset() - instance.Create(evt, q) - Expect(logBuffer.String()).To(Not(ContainSubstring("ansible.handler"))) - - newRepl := repl.DeepCopy() - newRepl.Name = pod.Name + "2" - newRepl.Namespace = pod.Namespace + "2" - - newRepl.OwnerReferences = []metav1.OwnerReference{{ - APIVersion: "apps/v1", - Kind: "ReplicaSet", - Name: "faz", - }} - - evt2 := event.UpdateEvent{ - ObjectOld: repl, - ObjectNew: newRepl, - } - - logBuffer.Reset() - instance.Update(evt2, q) - Expect(logBuffer.String()).To(MatchRegexp( - `ansible.handler.*Update.*apps/v1.*ReplicaSet.*faz.*foo.*apps/v1.*ReplicaSet.*faz`, - )) - }) - }) - - Describe("Generic", func() { - It("should emit a log with the OwnerReference of the object in case of GenericEvent", func() { - evt := event.GenericEvent{ - Object: pod, - } - logBuffer.Reset() - instance.Generic(evt, q) - Expect(logBuffer.String()).To(MatchRegexp( - `ansible.handler.*Generic.*/v1.*Pod.*biz.*biz.*Pod.*podOwnerName`, - )) - }) - }) -}) diff --git a/internal/ansible/metrics/metrics.go b/internal/ansible/metrics/metrics.go deleted file mode 100644 index 9dd460410e..0000000000 --- a/internal/ansible/metrics/metrics.go +++ /dev/null @@ -1,263 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// 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 metrics - -import ( - "errors" - "fmt" - - "github.com/prometheus/client_golang/prometheus" - logf "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/metrics" - - sdkVersion "github.com/operator-framework/operator-sdk/internal/version" -) - -const ( - subsystem = "ansible_operator" -) - -var ( - buildInfo = prometheus.NewGauge( - prometheus.GaugeOpts{ - Subsystem: subsystem, - Name: "build_info", - Help: "Build information for the ansible-operator binary", - ConstLabels: map[string]string{ - "commit": sdkVersion.GitCommit, - "version": sdkVersion.Version, - }, - }) - - reconcileResults = prometheus.NewGaugeVec( - prometheus.GaugeOpts{ - Subsystem: subsystem, - Name: "reconcile_result", - Help: "Gauge of reconciles and their results.", - }, - []string{ - "GVK", - "result", - }) - - reconciles = prometheus.NewHistogramVec( - prometheus.HistogramOpts{ - Subsystem: subsystem, - Name: "reconciles", - Help: "How long in seconds a reconcile takes.", - }, - []string{ - "GVK", - }) - - userMetrics = map[string]prometheus.Collector{} -) - -func init() { - metrics.Registry.MustRegister(reconcileResults) - metrics.Registry.MustRegister(reconciles) -} - -// We will never want to panic our app because of metric saving. -// Therefore, we will recover our panics here and error log them -// for later diagnosis but will never fail the app. -func recoverMetricPanic() { - if r := recover(); r != nil { - logf.Log.WithName("metrics").Error(fmt.Errorf("%v", r), - "Recovering from metric function") - } -} - -func RegisterBuildInfo(r prometheus.Registerer) { - buildInfo.Set(1) - r.MustRegister(buildInfo) -} - -type UserMetric struct { - Name string `json:"name" yaml:"name"` - Help string `json:"description" yaml:"description"` - Counter *UserMetricCounter `json:"counter,omitempty" yaml:"counter,omitempty"` - Gauge *UserMetricGauge `json:"gauge,omitempty" yaml:"gauge,omitempty"` - Histogram *UserMetricHistogram `json:"histogram,omitempty" yaml:"histogram,omitempty"` - Summary *UserMetricSummary `json:"summary,omitempty" yaml:"summary,omitempty"` -} - -type UserMetricCounter struct { - Inc bool `json:"increment,omitempty" yaml:"increment,omitempty"` - Add float64 `json:"add,omitempty" yaml:"add,omitempty"` -} - -type UserMetricGauge struct { - Set float64 `json:"set,omitempty" yaml:"set,omitempty"` - Inc bool `json:"increment,omitempty" yaml:"increment,omitempty"` - Dec bool `json:"decrement,omitempty" yaml:"decrement,omitempty"` - SetToCurrentTime bool `json:"set_to_current_time,omitempty" yaml:"set_to_current_time,omitempty"` - Add float64 `json:"add,omitempty" yaml:"add,omitempty"` - Sub float64 `json:"subtract,omitempty" yaml:"subtract,omitempty"` -} - -type UserMetricHistogram struct { - Observe float64 `json:"observe,omitempty" yaml:"observe,omitempty"` -} - -type UserMetricSummary struct { - Observe float64 `json:"observe,omitempty" yaml:"observe,omitempty"` -} - -func validateMetricSpec(metricSpec UserMetric) error { - var metricConfigs int - if metricSpec.Counter != nil { - metricConfigs++ - } - if metricSpec.Gauge != nil { - metricConfigs++ - } - if metricSpec.Summary != nil { - metricConfigs++ - } - if metricSpec.Histogram != nil { - metricConfigs++ - } - if metricConfigs > 1 { - return errors.New("only one metric can be processed at a time") - } else if metricConfigs == 0 { - return errors.New("a request should contain at least one metric") - } - return nil -} - -func handleCounter(metricSpec UserMetric, counter prometheus.Counter) error { - if metricSpec.Counter == nil { - return fmt.Errorf("cannot change metric type of %s, which is a counter", metricSpec.Name) - } - if metricSpec.Counter.Inc { - counter.Inc() - } else if metricSpec.Counter.Add != 0.0 { - if metricSpec.Counter.Add < 0 { - return errors.New("counter metrics cannot decrease in value") - } - counter.Add(metricSpec.Counter.Add) - } - return nil -} - -func handleGauge(metricSpec UserMetric, gauge prometheus.Gauge) error { - if metricSpec.Gauge == nil { - return fmt.Errorf("cannot change metric type of %s, which is a gauge", metricSpec.Name) - } - if metricSpec.Gauge.Inc { - gauge.Inc() - } else if metricSpec.Gauge.Dec { - gauge.Dec() - } else if metricSpec.Gauge.Add != 0.0 { - gauge.Add(metricSpec.Gauge.Add) - } else if metricSpec.Gauge.Sub != 0.0 { - gauge.Sub(metricSpec.Gauge.Sub) - } else if metricSpec.Gauge.Set != 0.0 { - gauge.Set(metricSpec.Gauge.Set) - } else if metricSpec.Gauge.SetToCurrentTime { - gauge.SetToCurrentTime() - } - return nil -} - -func handleSummaryOrHistogram(metricSpec UserMetric, summary prometheus.Summary) error { - if metricSpec.Histogram == nil && metricSpec.Summary == nil { - return fmt.Errorf("cannot change metric type of %s, which is a histogram or summary", metricSpec.Name) - } - if metricSpec.Histogram != nil { - summary.Observe(metricSpec.Histogram.Observe) - } else if metricSpec.Summary != nil { - summary.Observe(metricSpec.Summary.Observe) - } - return nil -} - -func ensureMetric(r prometheus.Registerer, metricSpec UserMetric) { - if _, ok := userMetrics[metricSpec.Name]; !ok { - // This is the first time we've seen this metric - logf.Log.WithName("metrics").Info("Registering", "metric", metricSpec.Name) - if metricSpec.Counter != nil { - userMetrics[metricSpec.Name] = prometheus.NewCounter(prometheus.CounterOpts{ - Name: metricSpec.Name, - Help: metricSpec.Help, - }) - } - if metricSpec.Gauge != nil { - userMetrics[metricSpec.Name] = prometheus.NewGauge(prometheus.GaugeOpts{ - Name: metricSpec.Name, - Help: metricSpec.Help, - }) - } - if metricSpec.Histogram != nil { - userMetrics[metricSpec.Name] = prometheus.NewHistogram(prometheus.HistogramOpts{ - Name: metricSpec.Name, - Help: metricSpec.Help, - }) - } - if metricSpec.Summary != nil { - userMetrics[metricSpec.Name] = prometheus.NewSummary(prometheus.SummaryOpts{ - Name: metricSpec.Name, - Help: metricSpec.Help, - }) - } - if err := r.Register(userMetrics[metricSpec.Name]); err != nil { - logf.Log.WithName("metrics").Info("Unable to register %s metric with prometheus.", metricSpec.Name) - } - } -} - -func HandleUserMetric(r prometheus.Registerer, metricSpec UserMetric) error { - if err := validateMetricSpec(metricSpec); err != nil { - return err - } - ensureMetric(r, metricSpec) - collector := userMetrics[metricSpec.Name] - switch v := collector.(type) { - // Gauge must be first, because a Counter is a Gauge, but a Gauge is not a Counter. - case prometheus.Gauge: - if err := handleGauge(metricSpec, v); err != nil { - return err - } - case prometheus.Counter: - if err := handleCounter(metricSpec, v); err != nil { - return err - } - // Histogram and Summary interfaces are identical, so we accept either case. - case prometheus.Histogram: - if err := handleSummaryOrHistogram(metricSpec, v); err != nil { - return err - } - } - return nil -} - -func ReconcileSucceeded(gvk string) { - defer recoverMetricPanic() - reconcileResults.WithLabelValues(gvk, "succeeded").Inc() -} - -func ReconcileFailed(gvk string) { - // TODO: consider taking in a failure reason - defer recoverMetricPanic() - reconcileResults.WithLabelValues(gvk, "failed").Inc() -} - -func ReconcileTimer(gvk string) *prometheus.Timer { - defer recoverMetricPanic() - return prometheus.NewTimer(prometheus.ObserverFunc(func(duration float64) { - reconciles.WithLabelValues(gvk).Observe(duration) - })) -} diff --git a/internal/ansible/paramconv/paramconv.go b/internal/ansible/paramconv/paramconv.go deleted file mode 100644 index 4e283830f6..0000000000 --- a/internal/ansible/paramconv/paramconv.go +++ /dev/null @@ -1,209 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// 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. - -// Based on https://github.com/iancoleman/strcase - -package paramconv - -import ( - "regexp" - "strings" - - "golang.org/x/text/cases" - "golang.org/x/text/language" -) - -var ( - numberSequence = regexp.MustCompile(`([a-zA-Z])(\d+)([a-zA-Z](\d+))`) - numberReplacement = []byte(`$1 $2 $3`) - wordMapping = map[string]string{ - "http": "HTTP", - "url": "URL", - "ip": "IP", - } -) - -func addWordBoundariesToNumbers(s string) string { - b := []byte(s) - b = numberSequence.ReplaceAll(b, numberReplacement) - return string(b) -} - -func translateWord(word string, initCase bool) string { - if val, ok := wordMapping[word]; ok { - return val - } - if initCase { - caser := cases.Title(language.AmericanEnglish) - return caser.String(word) - } - return word -} - -// Converts a string to CamelCase -func ToCamel(s string) string { - s = addWordBoundariesToNumbers(s) - s = strings.Trim(s, " ") - n := "" - bits := []string{} - for _, v := range s { - if v == '_' || v == ' ' || v == '-' { - bits = append(bits, n) - n = "" - } else { - n += string(v) - } - } - bits = append(bits, n) - - ret := "" - for i, substr := range bits { - ret += translateWord(substr, i != 0) - } - return ret -} - -// preprocessWordMapping() will check if value contains special words mapped or its plural in -// wordMapping, then processes it such that ToSnake() can convert it to snake case. -// If value contains special word, the character "_" is appended as a prefix and postfix -// to the special word found. For example, if the input string is "egressIP", -// which contains is a special word "IP", the function will return "egress_IP". -// If the last character of the special word is an "s" (i.e plural of the word -// found in wordMapping), it is considered a part of that word and will be capitalized. -func preprocessWordMapping(value string) string { - - for _, word := range wordMapping { - idx := strings.Index(value, word) - if idx >= 0 { - // The special non-plural word appears at the end of the string. - if (idx + len(word) - 1) == len(value)-1 { - value = value[:idx] + "_" + value[idx:] - } else if value[idx+len(word)] == 's' { - // The special plural word occurs at the end, start, or somewhere in the middle of value. - if idx+len(word) == len(value)-1 { - value = value[:idx] + "_" + value[idx:(idx+len(word))] + "S" - } else if idx == 0 { - value = value[:(idx+len(word))] + "S" + "_" + value[(idx+len(word)+1):] - } else { - value = value[:idx] + "_" + value[idx:(idx+len(word))] + "S" + "_" + value[(idx+len(word)+1):] - } - } else if idx == 0 { - // The special non-plural word occurs at the start or somewhere in the middle of value. - value = value[:(idx+len(word))] + "_" + value[(idx+len(word)):] - } else { - value = value[:idx] + "_" + value[idx:(idx+len(word))] + "_" + value[(idx+len(word)):] - } - } - } - - return value -} - -// Converts a string to snake_case -func ToSnake(s string) string { - s = addWordBoundariesToNumbers(s) - s = strings.Trim(s, " ") - var prefix string - char1 := []rune(s)[0] - if char1 >= 'A' && char1 <= 'Z' { - prefix = "_" - } else { - prefix = "" - } - bits := []string{} - n := "" - iReal := -1 - - // append underscore (_) as prefix and postfix to isolate special words defined in the wordMapping - s = preprocessWordMapping(s) - - for i, v := range s { - iReal++ - // treat acronyms as words, eg for JSONData -> JSON is a whole word - nextCaseIsChanged := false - if i+1 < len(s) { - next := s[i+1] - if (v >= 'A' && v <= 'Z' && next >= 'a' && next <= 'z') || (v >= 'a' && v <= 'z' && next >= 'A' && next <= 'Z') { - nextCaseIsChanged = true - } - } - - if iReal > 0 && n[len(n)-1] != '_' && nextCaseIsChanged { - // add underscore if next letter case type is changed - if v >= 'A' && v <= 'Z' { - bits = append(bits, strings.ToLower(n)) - n = string(v) - iReal = 0 - } else if v >= 'a' && v <= 'z' { - bits = append(bits, strings.ToLower(n+string(v))) - n = "" - iReal = -1 - } - } else if v == ' ' || v == '_' || v == '-' { - // replace spaces/underscores with delimiters - bits = append(bits, strings.ToLower(n)) - n = "" - iReal = -1 - } else { - n = n + string(v) - } - } - bits = append(bits, strings.ToLower(n)) - joined := strings.Join(bits, "_") - - // prepending an underscore (_) if the word begins with a Capital Letter - if _, ok := wordMapping[bits[0]]; !ok { - return prefix + joined - } - return joined -} - -func convertParameter(fn func(string) string, v interface{}) interface{} { - switch v := v.(type) { - case map[string]interface{}: - ret := map[string]interface{}{} - for key, val := range v { - ret[fn(key)] = convertParameter(fn, val) - } - return ret - case []interface{}: - return convertArray(fn, v) - default: - return v - } -} - -func convertArray(fn func(string) string, in []interface{}) []interface{} { - res := make([]interface{}, len(in)) - for i, v := range in { - res[i] = convertParameter(fn, v) - } - return res -} - -func convertMapKeys(fn func(string) string, in map[string]interface{}) map[string]interface{} { - converted := map[string]interface{}{} - for key, val := range in { - converted[fn(key)] = convertParameter(fn, val) - } - return converted -} - -func MapToSnake(in map[string]interface{}) map[string]interface{} { - return convertMapKeys(ToSnake, in) -} - -func MapToCamel(in map[string]interface{}) map[string]interface{} { - return convertMapKeys(ToCamel, in) -} diff --git a/internal/ansible/paramconv/paramconv_test.go b/internal/ansible/paramconv/paramconv_test.go deleted file mode 100644 index feac3a0d9a..0000000000 --- a/internal/ansible/paramconv/paramconv_test.go +++ /dev/null @@ -1,242 +0,0 @@ -// Copyright 2020 The Operator-SDK Authors -// -// 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. - -// Based on https://github.com/iancoleman/strcase - -package paramconv - -import ( - "reflect" - "testing" -) - -func TestMapToCamel(t *testing.T) { - type args struct { - in map[string]interface{} - } - tests := []struct { - name string - args args - want map[string]interface{} - }{ - { - name: "should convert the Map to Camel", - args: args{map[string]interface{}{ - "var": "value", - "appService": "value", - "app_8sk_": "value", - "_app_8sk_test": "value", - }}, - want: map[string]interface{}{ - "var": "value", - "appService": "value", - "app8Sk": "value", - "App8SkTest": "value", - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := MapToCamel(tt.args.in); !reflect.DeepEqual(got, tt.want) { - t.Errorf("MapToCamel() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestMapToSnake(t *testing.T) { - type args struct { - in map[string]interface{} - } - tests := []struct { - name string - args args - want map[string]interface{} - }{ - { - name: "should convert the Map to Snake", - args: args{map[string]interface{}{ - "var": "value", - "var_var": "value", - "size_k8s_test": "value", - "888": "value", - }}, - want: map[string]interface{}{ - "var": "value", - "var_var": "value", - "size_k8s_test": "value", - "888": "value", - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := MapToSnake(tt.args.in); !reflect.DeepEqual(got, tt.want) { - t.Errorf("MapToSnake() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestToCamel(t *testing.T) { - type args struct { - s string - } - tests := []struct { - name string - args args - want string - }{ - { - name: "should convert to Camel", - args: args{"app_test"}, - want: "appTest", - }, - { - name: "should convert to Camel when start with _", - args: args{"_app_test"}, - want: "AppTest", - }, - { - name: "should convert to Camel when has numbers", - args: args{"_app_test_k8s"}, - want: "AppTestK8s", - }, { - name: "should convert to Camel when has numbers and _", - args: args{"var_k8s"}, - want: "varK8s", - }, - { - name: "should handle special words", - args: args{"egressIPs"}, - want: "egressIPs", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := ToCamel(tt.args.s); got != tt.want { - t.Errorf("ToCamel() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestToSnake(t *testing.T) { - type args struct { - s string - } - tests := []struct { - name string - args args - want string - }{ - { - name: "should keep the same", - args: args{"var"}, - want: "var", - }, - { - name: "should convert to Snake when is only numbers", - args: args{"888"}, - want: "888", - }, - { - name: "should convert to Snake when has numbers and _", - args: args{"k8s_var"}, - want: "k8s_var", - }, - { - name: "should convert to Snake when start with _", - args: args{"_k8s_var"}, - want: "_k8s_var", - }, - { - name: "should convert to Snake and replace the space for _", - args: args{"k8s var"}, - want: "k8s_var", - }, - { - name: "should handle Camel and add _ prefix when starts with", - args: args{"ThisShouldHaveUnderscores"}, - want: "_this_should_have_underscores", - }, - { - name: "should convert to snake when has Camel and numbers", - args: args{"sizeK8sBuckets"}, - want: "size_k8s_buckets", - }, - { - name: "should be able to handle mixed vars", - args: args{"_CanYou_Handle_mixedVars"}, - want: "_can_you_handle_mixed_vars", - }, - { - name: "should be a noop", - args: args{"this_should_be_a_noop"}, - want: "this_should_be_a_noop", - }, - { - name: "should handle special plural word at end", - args: args{"egressIPs"}, - want: "egress_ips", - }, - { - name: "should handle special plural word in middle", - args: args{"egressIPsEgress"}, - want: "egress_ips_egress", - }, - { - name: "should handle special plural word in middle followed by lowercase letter", - args: args{"egressIPsegress"}, - want: "egress_ips_egress", - }, - { - name: "should handle special plural word at the start", - args: args{"IPsegress"}, - want: "_ips_egress", - }, - { - name: "should handle special word at the end", - args: args{"egressIP"}, - want: "egress_ip", - }, - { - name: "should handle special word in the middle", - args: args{"egressIPEgress"}, - want: "egress_ip_egress", - }, - { - name: "should handle special word in the middle followed by lowercase", - args: args{"egressIPegress"}, - want: "egress_ip_egress", - }, - { - name: "should handle multiple special words", - args: args{"URLegressIPEgressHTTP"}, - want: "url_egress_ip_egress_http", - }, - { - name: "should handle multiple plural special words", - args: args{"URLsegressIPsEgressHTTPs"}, - want: "_urls_egress_ips_egress_https", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := ToSnake(tt.args.s); got != tt.want { - t.Errorf("ToSnake() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/internal/ansible/proxy/cache_response.go b/internal/ansible/proxy/cache_response.go deleted file mode 100644 index b0fb145803..0000000000 --- a/internal/ansible/proxy/cache_response.go +++ /dev/null @@ -1,305 +0,0 @@ -// Copyright 2019 The Operator-SDK Authors -// -// 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 proxy - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "net/http" - "regexp" - "strings" - - libhandler "github.com/operator-framework/operator-lib/handler" - "k8s.io/apimachinery/pkg/api/meta" - metainternalscheme "k8s.io/apimachinery/pkg/apis/meta/internalversion/scheme" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/fields" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/utils/set" - "sigs.k8s.io/controller-runtime/pkg/cache" - "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/operator-framework/operator-sdk/internal/ansible/proxy/controllermap" - k8sRequest "github.com/operator-framework/operator-sdk/internal/ansible/proxy/requestfactory" -) - -type marshaler interface { - MarshalJSON() ([]byte, error) -} - -type cacheResponseHandler struct { - next http.Handler - informerCache cache.Cache - restMapper meta.RESTMapper - watchedNamespaces map[string]interface{} - cMap *controllermap.ControllerMap - injectOwnerRef bool - apiResources *apiResources - skipPathRegexp []*regexp.Regexp -} - -func (c *cacheResponseHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { - switch req.Method { - case http.MethodGet: - // GET request means we need to check the cache - rf := k8sRequest.RequestInfoFactory{APIPrefixes: set.New("api", "apis"), - GrouplessAPIPrefixes: set.New("api")} - r, err := rf.NewRequestInfo(req) - if err != nil { - log.Error(err, "Failed to convert request") - break - } - - // Skip cache for non-cacheable requests, not a part of skipCacheLookup for performance. - if !r.IsResourceRequest || !(r.Subresource == "" || r.Subresource == "status") { - log.V(2).Info("Skipping cache lookup", "resource", r) - break - } - - if c.restMapper == nil { - c.restMapper = meta.NewDefaultRESTMapper([]schema.GroupVersion{schema.GroupVersion{ - Group: r.APIGroup, - Version: r.APIVersion, - }}) - } - k, err := getGVKFromRequestInfo(r, c.restMapper) - if err != nil { - // break here in case resource doesn't exist in cache - log.Error(err, "Cache miss, can not find in rest mapper") - break - } - - if c.skipCacheLookup(r, k, req) { - log.V(2).Info("Skipping cache lookup", "resource", r) - break - } - - // Determine if the resource is virtual. If it is then we should not attempt to use cache - isVR, err := c.apiResources.IsVirtualResource(k) - if err != nil { - // break here in case we can not understand if virtual resource or not - log.Error(err, "Unable to determine if virtual resource", "gvk", k) - break - } - - if isVR { - log.V(2).Info("Virtual resource, must ask the cluster API", "gvk", k) - break - } - - var m marshaler - - log.V(2).Info("Get resource in our cache", "r", r) - if r.Verb == "list" { - m, err = c.getListFromCache(r, req, k) - if err != nil { - break - } - } else { - m, err = c.getObjectFromCache(r, req, k) - if err != nil { - break - } - } - - i := bytes.Buffer{} - resp, err := m.MarshalJSON() - if err != nil { - // return will give a 500 - log.Error(err, "Failed to marshal data") - http.Error(w, "", http.StatusInternalServerError) - return - } - - // Set Content-Type header - w.Header().Set("Content-Type", "application/json") - // Set X-Cache header to signal that response is served from Cache - w.Header().Set("X-Cache", "HIT") - if err := json.Indent(&i, resp, "", " "); err != nil { - log.Error(err, "Failed to indent json") - } - _, err = w.Write(i.Bytes()) - if err != nil { - log.Error(err, "Failed to write response") - http.Error(w, "", http.StatusInternalServerError) - return - } - - // Return so that request isn't passed along to APIserver - log.Info("Read object from cache", "resource", r) - return - } - c.next.ServeHTTP(w, req) -} - -// skipCacheLookup - determine if we should skip the cache lookup -func (c *cacheResponseHandler) skipCacheLookup(r *k8sRequest.RequestInfo, gvk schema.GroupVersionKind, - req *http.Request) bool { - - skip := matchesRegexp(req.URL.String(), c.skipPathRegexp) - if skip { - return true - } - - owner, err := getRequestOwnerRef(req) - if err != nil { - log.Error(err, "Could not get owner reference from proxy.") - return false - } - if owner != nil { - ownerGV, err := schema.ParseGroupVersion(owner.APIVersion) - if err != nil { - m := fmt.Sprintf("Could not get group version for: %v.", owner) - log.Error(err, m) - return false - } - ownerGVK := schema.GroupVersionKind{ - Group: ownerGV.Group, - Version: ownerGV.Version, - Kind: owner.Kind, - } - - relatedController, ok := c.cMap.Get(ownerGVK) - if !ok { - log.Info("Could not find controller for gvk.", "ownerGVK:", ownerGVK) - return false - } - if relatedController.Blacklist[gvk] { - log.Info("Skipping, because gvk is blacklisted", "GVK", gvk) - return true - } - } - // check if resource doesn't exist in watched namespaces - // if watchedNamespaces[""] exists then we are watching all namespaces - // and want to continue - _, allNsPresent := c.watchedNamespaces[metav1.NamespaceAll] - _, reqNsPresent := c.watchedNamespaces[r.Namespace] - if !allNsPresent && !reqNsPresent { - return true - } - - if strings.HasPrefix(r.Path, "/version") { - // Temporarily pass along to API server - // Ideally we cache this response as well - return true - } - - return false -} - -func (c *cacheResponseHandler) recoverDependentWatches(req *http.Request, un *unstructured.Unstructured) { - ownerRef, err := getRequestOwnerRef(req) - if err != nil { - log.Error(err, "Could not get ownerRef from proxy") - return - } - // This happens when a request unrelated to reconciliation hits the proxy - if ownerRef == nil { - return - } - - for _, oRef := range un.GetOwnerReferences() { - if oRef.APIVersion == ownerRef.APIVersion && oRef.Kind == ownerRef.Kind { - err := addWatchToController(*ownerRef, c.cMap, un, c.restMapper, true) - if err != nil { - log.Error(err, "Could not recover dependent resource watch", "owner", ownerRef) - return - } - } - } - if typeString, ok := un.GetAnnotations()[libhandler.TypeAnnotation]; ok { - ownerGV, err := schema.ParseGroupVersion(ownerRef.APIVersion) - if err != nil { - m := fmt.Sprintf("could not get group version for: %v", ownerGV) - log.Error(err, m) - return - } - if typeString == fmt.Sprintf("%v.%v", ownerRef.Kind, ownerGV.Group) { - err := addWatchToController(*ownerRef, c.cMap, un, c.restMapper, false) - if err != nil { - log.Error(err, "Could not recover dependent resource watch", "owner", ownerRef) - return - } - } - } -} - -func (c *cacheResponseHandler) getListFromCache(r *k8sRequest.RequestInfo, req *http.Request, - k schema.GroupVersionKind) (marshaler, error) { - k8sListOpts := &metav1.ListOptions{} - if err := metainternalscheme.ParameterCodec.DecodeParameters(req.URL.Query(), metav1.SchemeGroupVersion, - k8sListOpts); err != nil { - log.Error(err, "Unable to decode list options from request") - return nil, err - } - clientListOpts := []client.ListOption{ - client.InNamespace(r.Namespace), - } - if k8sListOpts.LabelSelector != "" { - sel, err := labels.ConvertSelectorToLabelsMap(k8sListOpts.LabelSelector) - if err != nil { - log.Error(err, "Unable to convert label selectors for the client") - return nil, err - } - clientListOpts = append(clientListOpts, client.MatchingLabels(sel)) - } - if k8sListOpts.FieldSelector != "" { - sel, err := fields.ParseSelector(k8sListOpts.FieldSelector) - if err != nil { - log.Error(err, "Unable to parse field selectors for the client") - return nil, err - } - clientListOpts = append(clientListOpts, client.MatchingFieldsSelector{Selector: sel}) - } - k.Kind = k.Kind + "List" - un := unstructured.UnstructuredList{} - un.SetGroupVersionKind(k) - ctx, cancel := context.WithTimeout(context.Background(), cacheEstablishmentTimeout) - defer cancel() - err := c.informerCache.List(ctx, &un, clientListOpts...) - if err != nil { - // break here in case resource doesn't exist in cache but exists on APIserver - // This is very unlikely but provides user with expected 404 - log.Info(fmt.Sprintf("cache miss: %v err-%v", k, err)) - return nil, err - } - return &un, nil -} - -func (c *cacheResponseHandler) getObjectFromCache(r *k8sRequest.RequestInfo, req *http.Request, - k schema.GroupVersionKind) (marshaler, error) { - un := &unstructured.Unstructured{} - un.SetGroupVersionKind(k) - obj := client.ObjectKey{Namespace: r.Namespace, Name: r.Name} - ctx, cancel := context.WithTimeout(context.Background(), cacheEstablishmentTimeout) - defer cancel() - err := c.informerCache.Get(ctx, obj, un) - if err != nil { - // break here in case resource doesn't exist in cache but exists on APIserver - // This is very unlikely but provides user with expected 404 - log.Info(fmt.Sprintf("Cache miss: %v, %v", k, obj)) - return nil, err - } - // Once we get the resource, we are going to attempt to recover the dependent watches here, - // This will happen in the background, and log errors. - if c.injectOwnerRef { - go c.recoverDependentWatches(req, un) - } - return un, nil -} diff --git a/internal/ansible/proxy/controllermap/controllermap.go b/internal/ansible/proxy/controllermap/controllermap.go deleted file mode 100644 index fe2380b520..0000000000 --- a/internal/ansible/proxy/controllermap/controllermap.go +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// 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 controllermap - -import ( - "sync" - - "k8s.io/apimachinery/pkg/runtime/schema" - "sigs.k8s.io/controller-runtime/pkg/controller" -) - -// ControllerMap - map of GVK to ControllerMapContents -type ControllerMap struct { - mutex sync.RWMutex - internal map[schema.GroupVersionKind]*Contents -} - -// WatchMap - map of GVK to interface. Determines if resource is being watched already -type WatchMap struct { - mutex sync.RWMutex - internal map[schema.GroupVersionKind]interface{} -} - -// Contents - Contains internal data associated with each controller -type Contents struct { - Controller controller.Controller - WatchDependentResources bool - WatchClusterScopedResources bool - OwnerWatchMap *WatchMap - AnnotationWatchMap *WatchMap - Blacklist map[schema.GroupVersionKind]bool -} - -// NewControllerMap returns a new object that contains a mapping between GVK -// and ControllerMapContents object -func NewControllerMap() *ControllerMap { - return &ControllerMap{ - internal: make(map[schema.GroupVersionKind]*Contents), - } -} - -// NewWatchMap - returns a new object that maps GVK to interface to determine -// if resource is being watched -func NewWatchMap() *WatchMap { - return &WatchMap{ - internal: make(map[schema.GroupVersionKind]interface{}), - } -} - -// Get - Returns a ControllerMapContents given a GVK as the key. `ok` -// determines if the key exists -func (cm *ControllerMap) Get(key schema.GroupVersionKind) (value *Contents, ok bool) { - cm.mutex.RLock() - defer cm.mutex.RUnlock() - value, ok = cm.internal[key] - return value, ok -} - -// Delete - Deletes associated GVK to controller mapping from the ControllerMap -func (cm *ControllerMap) Delete(key schema.GroupVersionKind) { - cm.mutex.Lock() - defer cm.mutex.Unlock() - delete(cm.internal, key) -} - -// Store - Adds a new GVK to controller mapping -func (cm *ControllerMap) Store(key schema.GroupVersionKind, value *Contents, blacklist []schema.GroupVersionKind) { - cm.mutex.Lock() - defer cm.mutex.Unlock() - cm.internal[key] = value - // watches.go Blacklist is []schema.GroupVersionKind, which we convert to a map (better performance) - // for the controller. - value.Blacklist = map[schema.GroupVersionKind]bool{} - for _, blacklistGVK := range blacklist { - cm.internal[key].Blacklist[blacklistGVK] = true - } -} - -// Get - Checks if GVK is already watched -func (wm *WatchMap) Get(key schema.GroupVersionKind) (value interface{}, ok bool) { - wm.mutex.RLock() - defer wm.mutex.RUnlock() - value, ok = wm.internal[key] - return value, ok -} - -// Delete - Deletes associated watches for a specific GVK -func (wm *WatchMap) Delete(key schema.GroupVersionKind) { - wm.mutex.Lock() - defer wm.mutex.Unlock() - delete(wm.internal, key) -} - -// Store - Adds a new GVK to be watched -func (wm *WatchMap) Store(key schema.GroupVersionKind) { - wm.mutex.Lock() - defer wm.mutex.Unlock() - wm.internal[key] = nil -} diff --git a/internal/ansible/proxy/inject_owner.go b/internal/ansible/proxy/inject_owner.go deleted file mode 100644 index 22dec36327..0000000000 --- a/internal/ansible/proxy/inject_owner.go +++ /dev/null @@ -1,188 +0,0 @@ -// Copyright 2019 The Operator-SDK Authors -// -// 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 proxy - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - "net/http/httputil" - - "github.com/operator-framework/operator-lib/handler" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/utils/set" - - "github.com/operator-framework/operator-sdk/internal/ansible/proxy/controllermap" - k8sRequest "github.com/operator-framework/operator-sdk/internal/ansible/proxy/requestfactory" - "github.com/operator-framework/operator-sdk/internal/util/k8sutil" -) - -// injectOwnerReferenceHandler will handle proxied requests and inject the -// owner reference found in the authorization header. The Authorization is -// then deleted so that the proxy can re-set with the correct authorization. -type injectOwnerReferenceHandler struct { - next http.Handler - cMap *controllermap.ControllerMap - restMapper meta.RESTMapper - watchedNamespaces map[string]interface{} - apiResources *apiResources -} - -func (i *injectOwnerReferenceHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { - switch req.Method { - case http.MethodPost: - dump, _ := httputil.DumpRequest(req, false) - log.V(2).Info("Dumping request", "RequestDump", string(dump)) - rf := k8sRequest.RequestInfoFactory{APIPrefixes: set.New("api", "apis"), - GrouplessAPIPrefixes: set.New("api")} - r, err := rf.NewRequestInfo(req) - if err != nil { - m := "Could not convert request" - log.Error(err, m) - http.Error(w, m, http.StatusBadRequest) - return - } - if r.Subresource != "" { - // Don't inject owner ref if we are POSTing to a subresource - break - } - - if i.restMapper == nil { - i.restMapper = meta.NewDefaultRESTMapper([]schema.GroupVersion{schema.GroupVersion{ - Group: r.APIGroup, - Version: r.APIVersion, - }}) - } - - k, err := getGVKFromRequestInfo(r, i.restMapper) - if err != nil { - // break here in case resource doesn't exist in cache - log.Error(err, "Cache miss, can not find in rest mapper") - break - } - - // Determine if the resource is virtual. If it is then we should not attempt to use cache - isVR, err := i.apiResources.IsVirtualResource(k) - if err != nil { - // Fail if we can't determine whether it's a virtual resource or not. - // Otherwise we might create a resource without an ownerReference, which will prevent - // dependentWatches from being re-established and garbage collection from deleting the - // resource, unless a user manually adds the ownerReference. - m := "Unable to determine if virtual resource" - log.Error(err, m, "gvk", k) - http.Error(w, m, http.StatusInternalServerError) - return - } - - if isVR { - log.V(2).Info("Virtual resource, must ask the cluster API", "gvk", k) - break - } - - log.Info("Injecting owner reference") - owner, err := getRequestOwnerRef(req) - if err != nil { - m := "Could not get owner reference" - log.Error(err, m) - http.Error(w, m, http.StatusInternalServerError) - return - } - if owner != nil { - body, err := io.ReadAll(req.Body) - if err != nil { - m := "Could not read request body" - log.Error(err, m) - http.Error(w, m, http.StatusInternalServerError) - return - } - data := &unstructured.Unstructured{} - err = json.Unmarshal(body, data) - if err != nil { - m := "Could not deserialize request body" - log.Error(err, m) - http.Error(w, m, http.StatusBadRequest) - return - } - ownerGV, err := schema.ParseGroupVersion(owner.APIVersion) - if err != nil { - m := fmt.Sprintf("could not get group version for: %v", owner) - log.Error(err, m) - http.Error(w, m, http.StatusBadRequest) - return - } - ownerGVK := schema.GroupVersionKind{ - Group: ownerGV.Group, - Version: ownerGV.Version, - Kind: owner.Kind, - } - ownerObject := &unstructured.Unstructured{} - ownerObject.SetGroupVersionKind(ownerGVK) - ownerObject.SetNamespace(owner.Namespace) - ownerObject.SetName(owner.Name) - addOwnerRef, err := k8sutil.SupportsOwnerReference(i.restMapper, ownerObject, data, r.Namespace) - if err != nil { - m := "Could not determine if we should add owner ref" - log.Error(err, m) - http.Error(w, m, http.StatusBadRequest) - return - } - if addOwnerRef { - data.SetOwnerReferences(append(data.GetOwnerReferences(), owner.OwnerReference)) - } else { - err := handler.SetOwnerAnnotations(ownerObject, data) - if err != nil { - m := "Could not set owner annotations" - log.Error(err, m) - http.Error(w, m, http.StatusBadRequest) - return - } - } - newBody, err := json.Marshal(data.Object) - if err != nil { - m := "Could not serialize body" - log.Error(err, m) - http.Error(w, m, http.StatusInternalServerError) - return - } - log.V(2).Info("Serialized body", "Body", string(newBody)) - req.Body = io.NopCloser(bytes.NewBuffer(newBody)) - req.ContentLength = int64(len(newBody)) - - // add watch for resource - // check if resource doesn't exist in watched namespaces - // if watchedNamespaces[""] exists then we are watching all namespaces - // and want to continue - // This is making sure we are not attempting to watch a resource outside of the - // namespaces that the cache can watch. - _, allNsPresent := i.watchedNamespaces[metav1.NamespaceAll] - _, reqNsPresent := i.watchedNamespaces[r.Namespace] - if allNsPresent || reqNsPresent { - err = addWatchToController(*owner, i.cMap, data, i.restMapper, addOwnerRef) - if err != nil { - m := "could not add watch to controller" - log.Error(err, m) - http.Error(w, m, http.StatusInternalServerError) - return - } - } - } - } - i.next.ServeHTTP(w, req) -} diff --git a/internal/ansible/proxy/inject_owner_test.go b/internal/ansible/proxy/inject_owner_test.go deleted file mode 100644 index 741a816f34..0000000000 --- a/internal/ansible/proxy/inject_owner_test.go +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright 2021 The Operator-SDK Authors -// -// 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 proxy - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/operator-framework/operator-sdk/internal/ansible/proxy/kubeconfig" -) - -var _ = Describe("injectOwnerReferenceHandler", func() { - - Describe("ServeHTTP", func() { - It("Should inject ownerReferences even when namespace is not explicitly set", func() { - if testing.Short() { - Skip("skipping ansible owner reference injection testing in short mode") - } - cm := corev1.ConfigMap{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "v1", - Kind: "ConfigMap", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "test-owner-ref-injection", - }, - Data: map[string]string{ - "hello": "world", - }, - } - - body, err := json.Marshal(cm) - if err != nil { - Fail("Failed to marshal body") - } - - po, err := createTestPod("test-injection", "default", testClient) - if err != nil { - Fail(fmt.Sprintf("Failed to create pod: %v", err)) - } - defer func() { - if err := testClient.Delete(context.Background(), po); err != nil { - Fail(fmt.Sprintf("Failed to delete the pod: %v", err)) - } - }() - - req, err := http.NewRequest("POST", "http://localhost:8888/api/v1/namespaces/default/configmaps", bytes.NewReader(body)) - if err != nil { - Fail(fmt.Sprintf("Failed to create http request: %v", err)) - } - - username, err := kubeconfig.EncodeOwnerRef( - metav1.OwnerReference{ - APIVersion: "v1", - Kind: "Pod", - Name: po.GetName(), - UID: po.GetUID(), - }, "default") - if err != nil { - Fail("Failed to encode owner reference") - } - req.SetBasicAuth(username, "unused") - - httpClient := http.Client{} - - defer func() { - cleanupReq, err := http.NewRequest("DELETE", "http://localhost:8888/api/v1/namespaces/default/configmaps/test-owner-ref-injection", bytes.NewReader([]byte{})) - if err != nil { - Fail(fmt.Sprintf("Failed to delete configmap: %v", err)) - } - _, err = httpClient.Do(cleanupReq) - if err != nil { - Fail(fmt.Sprintf("Failed to delete configmap: %v", err)) - } - }() - - resp, err := httpClient.Do(req) - if err != nil { - Fail(fmt.Sprintf("Failed to create configmap: %v", err)) - } - respBody, err := io.ReadAll(resp.Body) - if err != nil { - Fail(fmt.Sprintf("Failed to read response body: %v", err)) - } - var modifiedCM corev1.ConfigMap - err = json.Unmarshal(respBody, &modifiedCM) - if err != nil { - Fail(fmt.Sprintf("Failed to unmarshal configmap: %v", err)) - } - ownerRefs := modifiedCM.ObjectMeta.OwnerReferences - - Expect(ownerRefs).To(HaveLen(1)) - - ownerRef := ownerRefs[0] - - Expect(ownerRef.APIVersion).To(Equal("v1")) - Expect(ownerRef.Kind).To(Equal("Pod")) - Expect(ownerRef.Name).To(Equal(po.GetName())) - Expect(ownerRef.UID).To(Equal(po.GetUID())) - }) - }) -}) diff --git a/internal/ansible/proxy/kubeconfig/kubeconfig.go b/internal/ansible/proxy/kubeconfig/kubeconfig.go deleted file mode 100644 index 3c5d0506a2..0000000000 --- a/internal/ansible/proxy/kubeconfig/kubeconfig.go +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// 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 kubeconfig - -import ( - "bytes" - "encoding/base64" - "encoding/json" - "errors" - "html/template" - "net/url" - "os" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - logf "sigs.k8s.io/controller-runtime/pkg/log" -) - -var log = logf.Log.WithName("kubeconfig") - -// kubectl, as of 1.10.5, only does basic auth if the username is present in -// the URL. The python client used by ansible, as of 6.0.0, only does basic -// auth if the username and password are provided under the "user" key within -// "users". -const kubeConfigTemplate = `--- -apiVersion: v1 -kind: Config -clusters: -- cluster: - insecure-skip-tls-verify: true - server: {{.ProxyURL}} - name: proxy-server -contexts: -- context: - cluster: proxy-server - user: admin/proxy-server - name: {{.Namespace}}/proxy-server -current-context: {{.Namespace}}/proxy-server -preferences: {} -users: -- name: admin/proxy-server - user: - username: {{.Username}} - password: unused -` - -// values holds the data used to render the template -type values struct { - Username string - ProxyURL string - Namespace string -} - -type NamespacedOwnerReference struct { - metav1.OwnerReference - Namespace string -} - -// EncodeOwnerRef takes an ownerReference and a namespace and returns a base64 encoded -// string that can be used in the username field of a request to associate the -// owner with the request being made. -func EncodeOwnerRef(ownerRef metav1.OwnerReference, namespace string) (string, error) { - nsOwnerRef := NamespacedOwnerReference{OwnerReference: ownerRef, Namespace: namespace} - ownerRefJSON, err := json.Marshal(nsOwnerRef) - if err != nil { - return "", err - } - return base64.URLEncoding.EncodeToString(ownerRefJSON), nil -} - -// Create renders a kubeconfig template and writes it to disk -func Create(ownerRef metav1.OwnerReference, proxyURL string, namespace string) (*os.File, error) { - parsedURL, err := url.Parse(proxyURL) - if err != nil { - return nil, err - } - username, err := EncodeOwnerRef(ownerRef, namespace) - if err != nil { - return nil, err - } - parsedURL.User = url.User(username) - v := values{ - Username: username, - ProxyURL: parsedURL.String(), - Namespace: namespace, - } - - var parsed bytes.Buffer - - t := template.Must(template.New("kubeconfig").Parse(kubeConfigTemplate)) - if err := t.Execute(&parsed, v); err != nil { - return nil, err - } - - file, err := os.CreateTemp("", "kubeconfig") - if err != nil { - return nil, err - } - // multiple calls to close file will not hurt anything, - // but we don't want to lose the error because we are - // writing to the file, so we will call close twice. - defer func() { - if err := file.Close(); err != nil && !errors.Is(err, os.ErrClosed) { - log.Error(err, "Failed to close generated kubeconfig file") - } - }() - - if _, err := file.WriteString(parsed.String()); err != nil { - return nil, err - } - if err := file.Close(); err != nil { - return nil, err - } - return file, nil -} diff --git a/internal/ansible/proxy/kubectl.go b/internal/ansible/proxy/kubectl.go deleted file mode 100644 index 165100806f..0000000000 --- a/internal/ansible/proxy/kubectl.go +++ /dev/null @@ -1,278 +0,0 @@ -/* -Copyright 2014 The Kubernetes Authors. - -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. -*/ - -// This code was retrieved from -// https://github.com/kubernetes/kubernetes/blob/204d994/pkg/kubectl/proxy/proxy_server.go -// and modified for use in this project. - -package proxy - -import ( - "fmt" - "net" - "net/http" - "net/url" - "os" - "regexp" - "strings" - "time" - - utilnet "k8s.io/apimachinery/pkg/util/net" - k8sproxy "k8s.io/apimachinery/pkg/util/proxy" - "k8s.io/client-go/rest" - "k8s.io/client-go/transport" - "k8s.io/kubectl/pkg/util" - logf "sigs.k8s.io/controller-runtime/pkg/log" -) - -var log = logf.Log.WithName("proxy") - -const ( - // DefaultHostAcceptRE is the default value for which hosts to accept. - DefaultHostAcceptRE = "^localhost$,^127\\.0\\.0\\.1$,^\\[::1\\]$" - // DefaultPathAcceptRE is the default path to accept. - DefaultPathAcceptRE = "^.*" - // DefaultPathRejectRE is the default set of paths to reject. - DefaultPathRejectRE = "^$" - // DefaultMethodRejectRE is the set of HTTP methods to reject by default. - DefaultMethodRejectRE = "^$" -) - -var ( - // ReverseProxyFlushInterval is the frequency to flush the reverse proxy. - // Only matters for long poll connections like the one used to watch. With an - // interval of 0 the reverse proxy will buffer content sent on any connection - // with transfer-encoding=chunked. - // TODO: Flush after each chunk so the client doesn't suffer a 100ms latency per - // watch event. - ReverseProxyFlushInterval = 100 * time.Millisecond -) - -// FilterServer rejects requests which don't match one of the specified regular expressions -type FilterServer struct { - // Only paths that match this regexp will be accepted - AcceptPaths []*regexp.Regexp - // Paths that match this regexp will be rejected, even if they match the above - RejectPaths []*regexp.Regexp - // Hosts are required to match this list of regexp - AcceptHosts []*regexp.Regexp - // Methods that match this regexp are rejected - RejectMethods []*regexp.Regexp - // The delegate to call to handle accepted requests. - delegate http.Handler -} - -// MakeRegexpArray splits a comma separated list of regexps into an array of Regexp objects. -func MakeRegexpArray(str string) ([]*regexp.Regexp, error) { - parts := strings.Split(str, ",") - result := make([]*regexp.Regexp, len(parts)) - for ix := range parts { - re, err := regexp.Compile(parts[ix]) - if err != nil { - return nil, err - } - result[ix] = re - } - return result, nil -} - -// MakeRegexpArrayOrDie creates an array of regular expression objects from a string or exits. -func MakeRegexpArrayOrDie(str string) []*regexp.Regexp { - result, err := MakeRegexpArray(str) - if err != nil { - log.Error(err, "Error compiling re") - os.Exit(1) - } - return result -} - -func matchesRegexp(str string, regexps []*regexp.Regexp) bool { - for _, re := range regexps { - if re.MatchString(str) { - log.Info("Matched found", "MatchString", str, "Regexp", re) - return true - } - } - return false -} - -func (f *FilterServer) accept(method, path, host string) bool { - if matchesRegexp(path, f.RejectPaths) { - return false - } - if matchesRegexp(method, f.RejectMethods) { - return false - } - if matchesRegexp(path, f.AcceptPaths) && matchesRegexp(host, f.AcceptHosts) { - return true - } - return false -} - -// HandlerFor makes a shallow copy of f which passes its requests along to the -// new delegate. -func (f *FilterServer) HandlerFor(delegate http.Handler) *FilterServer { - f2 := *f - f2.delegate = delegate - return &f2 -} - -// Get host from a host header value like "localhost" or "localhost:8080" -func extractHost(header string) (host string) { - host, _, err := net.SplitHostPort(header) - if err != nil { - host = header - } - return host -} - -func (f *FilterServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) { - host := extractHost(req.Host) - if f.accept(req.Method, req.URL.Path, host) { - log.Info("Filter acception", "Request.Method", req.Method, "Request.URL", req.URL.Path, "Host", host) - f.delegate.ServeHTTP(rw, req) - return - } - log.Info("Filter rejection", "Request.Method", req.Method, "Request.URL", req.URL.Path, "Host", host) - rw.WriteHeader(http.StatusForbidden) - if _, err := rw.Write([]byte("

Unauthorized

")); err != nil { - log.Error(err, "Failed to write response body") - } -} - -// Server is a http.Handler which proxies Kubernetes APIs to remote API server. -type server struct { - Handler http.Handler -} - -type responder struct{} - -func (r *responder) Error(w http.ResponseWriter, req *http.Request, err error) { - log.Error(err, "Error while proxying request") - http.Error(w, err.Error(), http.StatusInternalServerError) -} - -// makeUpgradeTransport creates a transport that explicitly bypasses HTTP2 support -// for proxy connections that must upgrade. -func makeUpgradeTransport(config *rest.Config) (k8sproxy.UpgradeRequestRoundTripper, error) { - transportConfig, err := config.TransportConfig() - if err != nil { - return nil, err - } - tlsConfig, err := transport.TLSConfigFor(transportConfig) - if err != nil { - return nil, err - } - rt := utilnet.SetOldTransportDefaults(&http.Transport{ - TLSClientConfig: tlsConfig, - DialContext: (&net.Dialer{ - Timeout: 30 * time.Second, - }).DialContext, - }) - - upgrader, err := transport.HTTPWrappersForConfig(transportConfig, k8sproxy.MirrorRequest) - if err != nil { - return nil, err - } - return k8sproxy.NewUpgradeRequestRoundTripper(rt, upgrader), nil -} - -// NewServer creates and installs a new Server. -func newServer(apiProxyPrefix string, cfg *rest.Config) (*server, error) { - host := cfg.Host - if !strings.HasSuffix(host, "/") { - host = host + "/" - } - target, err := url.Parse(host) - if err != nil { - return nil, err - } - - responder := &responder{} - transport, err := rest.TransportFor(cfg) - if err != nil { - return nil, err - } - upgradeTransport, err := makeUpgradeTransport(cfg) - if err != nil { - return nil, err - } - proxy := k8sproxy.NewUpgradeAwareHandler(target, transport, false, false, responder) - proxy.UpgradeTransport = upgradeTransport - proxy.UseRequestLocation = true - - proxyServer := http.Handler(proxy) - - if !strings.HasPrefix(apiProxyPrefix, "/api") { - proxyServer = stripLeaveSlash(apiProxyPrefix, proxyServer) - } - - mux := http.NewServeMux() - mux.Handle(apiProxyPrefix, proxyServer) - return &server{Handler: mux}, nil -} - -// Listen is a simple wrapper around net.Listen. -func (s *server) Listen(address string, port int) (net.Listener, error) { - return net.Listen("tcp", fmt.Sprintf("%s:%d", address, port)) -} - -// ListenUnix does net.Listen for a unix socket -func (s *server) ListenUnix(path string) (net.Listener, error) { - // Remove any socket, stale or not, but fall through for other files - fi, err := os.Stat(path) - if err == nil && (fi.Mode()&os.ModeSocket) != 0 { - if err := os.Remove(path); err != nil { - return nil, err - } - } - // Default to only user accessible socket, caller can open up later if desired - oldmask, _ := util.Umask(0077) - l, err := net.Listen("unix", path) - if err != nil { - return l, err - } - if _, err = util.Umask(oldmask); err != nil { - return l, err - } - return l, err -} - -// ServeOnListener starts the server using given listener, loops forever. -func (s *server) ServeOnListener(l net.Listener) error { - server := http.Server{ - Handler: s.Handler, - ReadHeaderTimeout: 5 * time.Second, - } - return server.Serve(l) -} - -// like http.StripPrefix, but always leaves an initial slash. (so that our -// regexps will work.) -func stripLeaveSlash(prefix string, h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - p := strings.TrimPrefix(req.URL.Path, prefix) - if len(p) >= len(req.URL.Path) { - http.NotFound(w, req) - return - } - if len(p) > 0 && p[:1] != "/" { - p = "/" + p - } - req.URL.Path = p - h.ServeHTTP(w, req) - }) -} diff --git a/internal/ansible/proxy/proxy.go b/internal/ansible/proxy/proxy.go deleted file mode 100644 index f6865c0cd5..0000000000 --- a/internal/ansible/proxy/proxy.go +++ /dev/null @@ -1,393 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// 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 proxy - -import ( - "bytes" - "context" - "encoding/base64" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "strings" - "sync" - "time" - - libhandler "github.com/operator-framework/operator-lib/handler" - "github.com/operator-framework/operator-lib/predicate" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/discovery" - "k8s.io/client-go/rest" - "sigs.k8s.io/controller-runtime/pkg/cache" - crHandler "sigs.k8s.io/controller-runtime/pkg/handler" - "sigs.k8s.io/controller-runtime/pkg/source" - - "github.com/operator-framework/operator-sdk/internal/ansible/handler" - "github.com/operator-framework/operator-sdk/internal/ansible/proxy/controllermap" - "github.com/operator-framework/operator-sdk/internal/ansible/proxy/kubeconfig" - k8sRequest "github.com/operator-framework/operator-sdk/internal/ansible/proxy/requestfactory" -) - -// This is the default timeout to wait for the cache to respond -// todo(shawn-hurley): Eventually this should be configurable -const cacheEstablishmentTimeout = 6 * time.Second -const AutoSkipCacheREList = "^/api/.*/pods/.*/exec,^/api/.*/pods/.*/attach" - -// RequestLogHandler - log the requests that come through the proxy. -func RequestLogHandler(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - // read body - body, err := io.ReadAll(req.Body) - if err != nil { - log.Error(err, "Could not read request body") - } - // fix body - req.Body = io.NopCloser(bytes.NewBuffer(body)) - log.Info("Request Info", "method", req.Method, "uri", req.RequestURI, "body", string(body)) - // Removing the authorization so that the proxy can set the correct authorization. - req.Header.Del("Authorization") - h.ServeHTTP(w, req) - }) -} - -// HandlerChain will be used for users to pass defined handlers to the proxy. -// The hander chain will be run after InjectingOwnerReference if it is added -// and before the proxy handler. -type HandlerChain func(http.Handler) http.Handler - -// Options will be used by the user to specify the desired details -// for the proxy. -type Options struct { - Address string - Port int - Handler HandlerChain - KubeConfig *rest.Config - Cache cache.Cache - RESTMapper meta.RESTMapper - ControllerMap *controllermap.ControllerMap - WatchedNamespaces []string - DisableCache bool - OwnerInjection bool - LogRequests bool -} - -// Run will start a proxy server in a go routine that returns on the error -// channel if something is not correct on startup. Run will not return until -// the network socket is listening. -func Run(done chan error, o Options) error { - server, err := newServer("/", o.KubeConfig) - if err != nil { - return err - } - if o.Handler != nil { - server.Handler = o.Handler(server.Handler) - } - if o.ControllerMap == nil { - return fmt.Errorf("failed to get controller map from options") - } - if o.WatchedNamespaces == nil { - return fmt.Errorf("failed to get list of watched namespaces from options") - } - - watchedNamespaceMap := make(map[string]interface{}) - // Convert string list to map - for _, ns := range o.WatchedNamespaces { - watchedNamespaceMap[ns] = nil - } - - // Create apiResources and - discoveryClient, err := discovery.NewDiscoveryClientForConfig(o.KubeConfig) - if err != nil { - return err - } - resources := &apiResources{ - mu: &sync.RWMutex{}, - gvkToAPIResource: map[string]metav1.APIResource{}, - discoveryClient: discoveryClient, - } - - if o.Cache == nil && !o.DisableCache { - // Need to initialize cache since we don't have one - log.Info("Initializing and starting informer cache...") - informerCache, err := cache.New(o.KubeConfig, cache.Options{}) - if err != nil { - return err - } - ctx, cancel := context.WithCancel(context.TODO()) - go func() { - if err := informerCache.Start(ctx); err != nil { - log.Error(err, "Failed to start informer cache") - } - defer cancel() - }() - log.Info("Waiting for cache to sync...") - synced := informerCache.WaitForCacheSync(context.TODO()) - if !synced { - return fmt.Errorf("failed to sync cache") - } - log.Info("Cache sync was successful") - o.Cache = informerCache - } - - // Remove the authorization header so the proxy can correctly inject the header. - server.Handler = removeAuthorizationHeader(server.Handler) - - if o.OwnerInjection { - server.Handler = &injectOwnerReferenceHandler{ - next: server.Handler, - cMap: o.ControllerMap, - restMapper: o.RESTMapper, - watchedNamespaces: watchedNamespaceMap, - apiResources: resources, - } - } else { - log.Info("Warning: injection of owner references and dependent watches is turned off") - } - if o.LogRequests { - server.Handler = RequestLogHandler(server.Handler) - } - if !o.DisableCache { - autoSkipCacheRegexp, err := MakeRegexpArray(AutoSkipCacheREList) - if err != nil { - log.Error(err, "Failed to parse cache skip regular expression") - } - server.Handler = &cacheResponseHandler{ - next: server.Handler, - informerCache: o.Cache, - restMapper: o.RESTMapper, - watchedNamespaces: watchedNamespaceMap, - cMap: o.ControllerMap, - injectOwnerRef: o.OwnerInjection, - apiResources: resources, - skipPathRegexp: autoSkipCacheRegexp, - } - } - - l, err := server.Listen(o.Address, o.Port) - if err != nil { - return err - } - go func() { - log.Info("Starting to serve", "Address", l.Addr().String()) - done <- server.ServeOnListener(l) - }() - return nil -} - -// Helper function used by cache response and owner injection -func addWatchToController(owner kubeconfig.NamespacedOwnerReference, cMap *controllermap.ControllerMap, - resource *unstructured.Unstructured, restMapper meta.RESTMapper, useOwnerRef bool) error { - dataMapping, err := restMapper.RESTMapping(resource.GroupVersionKind().GroupKind(), - resource.GroupVersionKind().Version) - if err != nil { - m := fmt.Sprintf("Could not get rest mapping for: %v", resource.GroupVersionKind()) - log.Error(err, m) - return err - - } - ownerGV, err := schema.ParseGroupVersion(owner.APIVersion) - if err != nil { - m := fmt.Sprintf("could not get group version for: %v", owner) - log.Error(err, m) - return err - } - ownerMapping, err := restMapper.RESTMapping(schema.GroupKind{Kind: owner.Kind, Group: ownerGV.Group}, - ownerGV.Version) - if err != nil { - m := fmt.Sprintf("could not get rest mapping for: %v", owner) - log.Error(err, m) - return err - } - - dataNamespaceScoped := dataMapping.Scope.Name() != meta.RESTScopeNameRoot - contents, ok := cMap.Get(ownerMapping.GroupVersionKind) - if !ok { - return errors.New("failed to find controller in map") - } - owMap := contents.OwnerWatchMap - awMap := contents.AnnotationWatchMap - u := &unstructured.Unstructured{} - u.SetGroupVersionKind(ownerMapping.GroupVersionKind) - - // Add a watch to controller - if contents.WatchDependentResources && !contents.Blacklist[resource.GroupVersionKind()] { - // Store watch in map - // Use EnqueueRequestForOwner unless user has configured watching cluster scoped resources and we have to - switch { - case useOwnerRef: - _, exists := owMap.Get(resource.GroupVersionKind()) - // If already watching resource no need to add a new watch - if exists { - return nil - } - - owMap.Store(resource.GroupVersionKind()) - log.Info("Watching child resource", "kind", resource.GroupVersionKind(), - "enqueue_kind", u.GroupVersionKind()) - err := contents.Controller.Watch(&source.Kind{Type: resource}, - &handler.LoggingEnqueueRequestForOwner{ - EnqueueRequestForOwner: crHandler.EnqueueRequestForOwner{OwnerType: u}, - }, predicate.DependentPredicate{}) - // Store watch in map - if err != nil { - log.Error(err, "Failed to watch child resource", - "kind", resource.GroupVersionKind(), "enqueue_kind", u.GroupVersionKind()) - return err - } - case (!useOwnerRef && dataNamespaceScoped) || contents.WatchClusterScopedResources: - _, exists := awMap.Get(resource.GroupVersionKind()) - // If already watching resource no need to add a new watch - if exists { - return nil - } - awMap.Store(resource.GroupVersionKind()) - ownerGK := schema.GroupKind{ - Kind: owner.Kind, - Group: ownerGV.Group, - } - log.Info("Watching child resource", "kind", resource.GroupVersionKind(), - "enqueue_annotation_type", ownerGK.String()) - err = contents.Controller.Watch(&source.Kind{Type: resource}, - &handler.LoggingEnqueueRequestForAnnotation{ - EnqueueRequestForAnnotation: libhandler.EnqueueRequestForAnnotation{Type: ownerGK}, - }, predicate.DependentPredicate{}) - if err != nil { - log.Error(err, "Failed to watch child resource", - "kind", resource.GroupVersionKind(), "enqueue_kind", u.GroupVersionKind()) - return err - } - } - } else { - log.Info("Resource will not be watched/cached.", "GVK", resource.GroupVersionKind()) - } - return nil -} - -func removeAuthorizationHeader(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - req.Header.Del("Authorization") - h.ServeHTTP(w, req) - }) -} - -// Helper function used by recovering dependent watches and owner ref injection. -func getRequestOwnerRef(req *http.Request) (*kubeconfig.NamespacedOwnerReference, error) { - owner := kubeconfig.NamespacedOwnerReference{} - user, _, ok := req.BasicAuth() - if !ok { - return nil, nil - } - authString, err := base64.StdEncoding.DecodeString(user) - if err != nil { - m := "Could not base64 decode username" - log.Error(err, m) - return &owner, err - } - // Set owner to NamespacedOwnerReference, which has metav1.OwnerReference - // as a subset along with the Namespace of the owner. Please see the - // kubeconfig.NamespacedOwnerReference type for more information. The - // namespace is required when creating the reconcile requests. - if err := json.Unmarshal(authString, &owner); err != nil { - m := "Could not unmarshal auth string" - log.Error(err, m) - return &owner, err - } - return &owner, err -} - -func getGVKFromRequestInfo(r *k8sRequest.RequestInfo, restMapper meta.RESTMapper) (schema.GroupVersionKind, error) { - gvr := schema.GroupVersionResource{ - Group: r.APIGroup, - Version: r.APIVersion, - Resource: r.Resource, - } - return restMapper.KindFor(gvr) -} - -type apiResources struct { - mu *sync.RWMutex - gvkToAPIResource map[string]metav1.APIResource - discoveryClient discovery.DiscoveryInterface -} - -func (a *apiResources) resetResources() error { - a.mu.Lock() - defer a.mu.Unlock() - - _, apisResourceList, err := a.discoveryClient.ServerGroupsAndResources() - if err != nil { - return err - } - - a.gvkToAPIResource = map[string]metav1.APIResource{} - - for _, apiResource := range apisResourceList { - gv, err := schema.ParseGroupVersion(apiResource.GroupVersion) - if err != nil { - return err - } - for _, resource := range apiResource.APIResources { - // Names containing a "/" are subresources and should be ignored - if strings.Contains(resource.Name, "/") { - continue - } - gvk := schema.GroupVersionKind{ - Group: gv.Group, - Version: gv.Version, - Kind: resource.Kind, - } - - a.gvkToAPIResource[gvk.String()] = resource - } - } - - return nil -} - -func (a *apiResources) IsVirtualResource(gvk schema.GroupVersionKind) (bool, error) { - a.mu.RLock() - apiResource, ok := a.gvkToAPIResource[gvk.String()] - a.mu.RUnlock() - - if !ok { - //reset the resources - err := a.resetResources() - if err != nil { - return false, err - } - // retry to get the resource - a.mu.RLock() - apiResource, ok = a.gvkToAPIResource[gvk.String()] - a.mu.RUnlock() - if !ok { - return false, fmt.Errorf("unable to get api resource for gvk: %v", gvk) - } - } - - allVerbs := discovery.SupportsAllVerbs{ - Verbs: []string{"watch", "get", "list"}, - } - - if !allVerbs.Match(gvk.GroupVersion().String(), &apiResource) { - return true, nil - } - - return false, nil -} diff --git a/internal/ansible/proxy/proxy_suite_test.go b/internal/ansible/proxy/proxy_suite_test.go deleted file mode 100644 index 150dabad73..0000000000 --- a/internal/ansible/proxy/proxy_suite_test.go +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright 2021 The Operator-SDK Authors -// -// 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 proxy - -import ( - "context" - "fmt" - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "github.com/operator-framework/operator-sdk/internal/ansible/proxy/controllermap" - kcorev1 "k8s.io/api/core/v1" - kmetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/client/config" - "sigs.k8s.io/controller-runtime/pkg/manager" -) - -var testMgr manager.Manager - -var testClient client.Client - -func TestProxy(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Proxy Test Suite") -} - -var _ = BeforeSuite(func() { - if testing.Short() { - return - } - var err error - testMgr, err = manager.New(config.GetConfigOrDie(), manager.Options{Namespace: "default"}) - if err != nil { - Fail(fmt.Sprintf("Failed to instantiate manager: %v", err)) - } - done := make(chan error) - cMap := controllermap.NewControllerMap() - err = Run(done, Options{ - Address: "localhost", - Port: 8888, - KubeConfig: testMgr.GetConfig(), - Cache: nil, - RESTMapper: testMgr.GetRESTMapper(), - ControllerMap: cMap, - WatchedNamespaces: []string{"test-watched-namespace"}, - OwnerInjection: true, - }) - if err != nil { - Fail(fmt.Sprintf("Error starting proxy: %v", err)) - } - testClient, err = client.New(testMgr.GetConfig(), client.Options{}) - if err != nil { - Fail(fmt.Sprintf("Failed to create the client: %v", err)) - } - _, err = createTestNamespace("test-watched-namespace", testClient) - if err != nil { - Fail(fmt.Sprintf("Failed to create watched namespace: %v", err)) - } -}) - -var _ = AfterSuite(func() { - if testing.Short() { - return - } - err := testClient.Delete(context.Background(), &kcorev1.Namespace{ - ObjectMeta: kmetav1.ObjectMeta{ - Name: "test-watched-namespace", - Labels: map[string]string{ - "test-label": "test-watched-namespace", - }, - }, - }) - - if err != nil { - Fail(fmt.Sprintf("Failed to clean up namespace: %v:", err)) - } -}) - -func createTestNamespace(name string, cl client.Client) (client.Object, error) { - ns := &kcorev1.Namespace{ - ObjectMeta: kmetav1.ObjectMeta{ - Name: name, - Labels: map[string]string{ - "test-label": name, - }, - }, - } - if err := cl.Create(context.Background(), ns); err != nil { - return nil, err - } - return ns, nil -} - -func createTestPod(name, namespace string, cl client.Client) (client.Object, error) { - three := int64(3) - pod := &kcorev1.Pod{ - ObjectMeta: kmetav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Labels: map[string]string{ - "test-label": name, - }, - }, - Spec: kcorev1.PodSpec{ - Containers: []kcorev1.Container{{Name: "nginx", Image: "nginx"}}, - RestartPolicy: "Always", - ActiveDeadlineSeconds: &three, - }, - } - if err := cl.Create(context.Background(), pod); err != nil { - return nil, err - } - return pod, nil -} diff --git a/internal/ansible/proxy/proxy_test.go b/internal/ansible/proxy/proxy_test.go deleted file mode 100644 index e28d1d08c6..0000000000 --- a/internal/ansible/proxy/proxy_test.go +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// 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 proxy - -import ( - "context" - "encoding/json" - "errors" - "io" - "net/http" - "os" - "testing" - - . "github.com/onsi/ginkgo/v2" - - kcorev1 "k8s.io/api/core/v1" -) - -var _ = Describe("proxyTests", func() { - t := GinkgoT() - - It("should retrieve resources from the cache", func() { - if testing.Short() { - Skip("skipping ansible proxy testing in short mode") - } - po, err := createTestPod("test", "test-watched-namespace", testClient) - if err != nil { - t.Fatalf("Failed to create the pod: %v", err) - } - defer func() { - if err := testClient.Delete(context.Background(), po); err != nil { - t.Fatalf("Failed to delete the pod: %v", err) - } - }() - - resp, err := http.Get("http://localhost:8888/api/v1/namespaces/test-watched-namespace/pods/test") - if err != nil { - t.Fatalf("Error getting pod from proxy: %v", err) - } - defer func() { - if err := resp.Body.Close(); err != nil && !errors.Is(err, os.ErrClosed) { - t.Errorf("Failed to close response body: (%v)", err) - } - }() - body, err := io.ReadAll(resp.Body) - if err != nil { - t.Fatalf("Error reading response body: %v", err) - } - // Should only be one string from 'X-Cache' header (explicitly set to HIT in proxy) - if resp.Header["X-Cache"] == nil { - t.Fatalf("Object was not retrieved from cache") - } - if resp.Header["X-Cache"][0] != "HIT" { - t.Fatalf("Cache response header found but got [%v], expected [HIT]", resp.Header["X-Cache"][0]) - } - data := kcorev1.Pod{} - err = json.Unmarshal(body, &data) - if err != nil { - t.Fatalf("Error parsing response: %v", err) - } - if data.Name != "test" { - t.Fatalf("Got unexpected pod name: %#v", data.Name) - } - }) -}) diff --git a/internal/ansible/proxy/requestfactory/requestinfo.go b/internal/ansible/proxy/requestfactory/requestinfo.go deleted file mode 100644 index 5d3d73b810..0000000000 --- a/internal/ansible/proxy/requestfactory/requestinfo.go +++ /dev/null @@ -1,278 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors. - -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. -*/ - -// This code was retrieved from -// https://github.com/kubernetes/apiserver/blob/master/pkg/endpoints/request/requestinfo.go -// and slightly modified for use in this project - -package requestfactory - -import ( - "fmt" - "net/http" - "strings" - - "k8s.io/apimachinery/pkg/api/validation/path" - metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" - metainternalscheme "k8s.io/apimachinery/pkg/apis/meta/internalversion/scheme" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/utils/set" - - logf "sigs.k8s.io/controller-runtime/pkg/log" -) - -var log = logf.Log.WithName("requestfactory") - -// RequestInfo holds information parsed from the http.Request -type RequestInfo struct { - // IsResourceRequest indicates whether or not the request is for an API - // resource or subresource - IsResourceRequest bool - // Path is the URL path of the request - Path string - // Verb is the kube verb associated with the request for API requests, not - // the http verb. This includes things like list and watch. for - // non-resource requests, this is the lowercase http verb - Verb string - - APIPrefix string - APIGroup string - APIVersion string - Namespace string - // Resource is the name of the resource being requested. This is not the - // kind. For example: pods - Resource string - // Subresource is the name of the subresource being requested. This is a - // different resource, scoped to the parent resource, but it may have a - // different kind. For instance, /pods has the resource "pods" and the kind - // "Pod", while /pods/foo/status has the resource "pods", the sub resource - // "status", and the kind "Pod" (because status operates on pods). The - // binding resource for a pod though may be /pods/foo/binding, which has - // resource "pods", subresource "binding", and kind "Binding". - Subresource string - // Name is empty for some verbs, but if the request directly indicates a name - // (not in body content) then this field is filled in. - Name string - // Parts are the path parts for the request, always starting with - // /{resource}/{name} - Parts []string -} - -// specialVerbs contains just strings which are used in REST paths for special -// actions that don't fall under the normal CRUDdy GET/POST/PUT/DELETE actions -// on REST objects. TODO: find a way to keep this up to date automatically. -// Maybe dynamically populate list as handlers added to master's Mux. -var specialVerbs = set.New("proxy", "watch") - -// specialVerbsNoSubresources contains root verbs which do not allow -// subresources -var specialVerbsNoSubresources = set.New("proxy") - -// namespaceSubresources contains subresources of namespace this list allows -// the parser to distinguish between a namespace subresource, and a namespaced -// resource -var namespaceSubresources = set.New("status", "finalize") - -// NamespaceSubResourcesForTest exports namespaceSubresources for testing in -// pkg/master/master_test.go, so we never drift -var NamespaceSubResourcesForTest = set.New(namespaceSubresources.SortedList()...) - -type RequestInfoFactory struct { - APIPrefixes set.Set[string] // without leading and trailing slashes - GrouplessAPIPrefixes set.Set[string] // without leading and trailing slashes -} - -// TODO write an integration test against the swagger doc to test the -// RequestInfo and match up behavior to responses NewRequestInfo returns the -// information from the http request. If error is not nil, RequestInfo holds -// the information as best it is known before the failure It handles both -// resource and non-resource requests and fills in all the pertinent -// information for each. -// Valid Inputs: -// Resource paths -// /apis/{api-group}/{version}/namespaces -// /api/{version}/namespaces -// /api/{version}/namespaces/{namespace} -// /api/{version}/namespaces/{namespace}/{resource} -// /api/{version}/namespaces/{namespace}/{resource}/{resourceName} -// /api/{version}/{resource} -// /api/{version}/{resource}/{resourceName} -// -// Special verbs without subresources: -// /api/{version}/proxy/{resource}/{resourceName} -// /api/{version}/proxy/namespaces/{namespace}/{resource}/{resourceName} -// -// Special verbs with subresources: -// /api/{version}/watch/{resource} -// /api/{version}/watch/namespaces/{namespace}/{resource} -// -// NonResource paths -// /apis/{api-group}/{version} -// /apis/{api-group} -// /apis -// /api/{version} -// /api -// /healthz - -func (r *RequestInfoFactory) NewRequestInfo(req *http.Request) (*RequestInfo, error) { //nolint:gocyclo - // TODO: Try to reduce the complexity of this last measured at 33 (failing at > 30) and remove the // nolint:gocyclo - // start with a non-resource request until proven otherwise - requestInfo := RequestInfo{ - IsResourceRequest: false, - Path: req.URL.Path, - Verb: strings.ToLower(req.Method), - } - - currentParts := splitPath(req.URL.Path) - if len(currentParts) < 3 { - // return a non-resource request - return &requestInfo, nil - } - - if !r.APIPrefixes.Has(currentParts[0]) { - // return a non-resource request - return &requestInfo, nil - } - requestInfo.APIPrefix = currentParts[0] - currentParts = currentParts[1:] - - if !r.GrouplessAPIPrefixes.Has(requestInfo.APIPrefix) { - // one part (APIPrefix) has already been consumed, so this is actually "do - // we have four parts?" - if len(currentParts) < 3 { - // return a non-resource request - return &requestInfo, nil - } - - requestInfo.APIGroup = currentParts[0] - currentParts = currentParts[1:] - } - - requestInfo.IsResourceRequest = true - requestInfo.APIVersion = currentParts[0] - currentParts = currentParts[1:] - - // handle input of form /{specialVerb}/* - if specialVerbs.Has(currentParts[0]) { - if len(currentParts) < 2 { - return &requestInfo, fmt.Errorf("unable to determine kind and namespace from url, %v", req.URL) - } - - requestInfo.Verb = currentParts[0] - currentParts = currentParts[1:] - - } else { - switch req.Method { - case "POST": - requestInfo.Verb = "create" - case "GET", "HEAD": - requestInfo.Verb = "get" - case "PUT": - requestInfo.Verb = "update" - case "PATCH": - requestInfo.Verb = "patch" - case "DELETE": - requestInfo.Verb = "delete" - default: - requestInfo.Verb = "" - } - } - - // URL forms: /namespaces/{namespace}/{kind}/*, where parts are adjusted to - // be relative to kind - if currentParts[0] == "namespaces" { - if len(currentParts) > 1 { - requestInfo.Namespace = currentParts[1] - - // if there is another step after the namespace name and it is not a - // known namespace subresource move currentParts to include it as a - // resource in its own right - if len(currentParts) > 2 && !namespaceSubresources.Has(currentParts[2]) { - currentParts = currentParts[2:] - } - } - } else { - requestInfo.Namespace = metav1.NamespaceNone - } - - // parsing successful, so we now know the proper value for .Parts - requestInfo.Parts = currentParts - - // parts look like: - // resource/resourceName/subresource/other/stuff/we/don't/interpret - switch { - case len(requestInfo.Parts) >= 3 && !specialVerbsNoSubresources.Has(requestInfo.Verb): - requestInfo.Subresource = requestInfo.Parts[2] - fallthrough - case len(requestInfo.Parts) >= 2: - requestInfo.Name = requestInfo.Parts[1] - fallthrough - case len(requestInfo.Parts) >= 1: - requestInfo.Resource = requestInfo.Parts[0] - } - - // if there's no name on the request and we thought it was a get before, then - // the actual verb is a list or a watch - if len(requestInfo.Name) == 0 && requestInfo.Verb == "get" { - opts := metainternalversion.ListOptions{} - if err := metainternalscheme.ParameterCodec.DecodeParameters(req.URL.Query(), metav1.SchemeGroupVersion, - &opts); err != nil { - // An error in parsing request will result in default to "list" and not - // setting "name" field. - log.Error(err, "Could not parse request") - // Reset opts to not rely on partial results from parsing. - // However, if watch is set, let's report it. - opts = metainternalversion.ListOptions{} - if values := req.URL.Query()["watch"]; len(values) > 0 { - switch strings.ToLower(values[0]) { - case "false", "0": - default: - opts.Watch = true - } - } - } - - if opts.Watch { - requestInfo.Verb = "watch" - } else { - requestInfo.Verb = "list" - } - - if opts.FieldSelector != nil { - if name, ok := opts.FieldSelector.RequiresExactMatch("metadata.name"); ok { - if len(path.IsValidPathSegmentName(name)) == 0 { - requestInfo.Name = name - } - } - } - } - // if there's no name on the request and we thought it was a delete before, - // then the actual verb is deletecollection - if len(requestInfo.Name) == 0 && requestInfo.Verb == "delete" { - requestInfo.Verb = "deletecollection" - } - - return &requestInfo, nil -} - -// splitPath returns the segments for a URL path. -func splitPath(path string) []string { - path = strings.Trim(path, "/") - if path == "" { - return []string{} - } - return strings.Split(path, "/") -} diff --git a/internal/ansible/runner/eventapi/eventapi.go b/internal/ansible/runner/eventapi/eventapi.go deleted file mode 100644 index cb75aa0b30..0000000000 --- a/internal/ansible/runner/eventapi/eventapi.go +++ /dev/null @@ -1,177 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// 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 eventapi - -import ( - "encoding/json" - "errors" - "fmt" - "io" - "net" - "net/http" - "os" - "strings" - "sync" - "time" - - "github.com/go-logr/logr" - logf "sigs.k8s.io/controller-runtime/pkg/log" -) - -// EventReceiver serves the event API -type EventReceiver struct { - // Events is the channel used by the event API handler to send JobEvents - // back to the runner, or whatever code is using this receiver. - Events chan JobEvent - - // SocketPath is the path on the filesystem to a unix streaming socket - SocketPath string - - // URLPath is the path portion of the url at which events should be - // received. For example, "/events/" - URLPath string - - // server is the http.Server instance that serves the event API. It must be - // closed. - server io.Closer - - // stopped indicates if this receiver has permanently stopped receiving - // events. When true, requests to POST an event will receive a "410 Gone" - // response, and the body will be ignored. - stopped bool - - // mutex controls access to the "stopped" bool above, ensuring that writes - // are goroutine-safe. - mutex sync.RWMutex - - // ident is the unique identifier for a particular run of ansible-runner - ident string - - // logger holds a logger that has some fields already set - logger logr.Logger -} - -func New(ident string, errChan chan<- error) (*EventReceiver, error) { - sockPath := fmt.Sprintf("/tmp/ansibleoperator-%s", ident) - listener, err := net.Listen("unix", sockPath) - if err != nil { - return nil, err - } - - rec := EventReceiver{ - Events: make(chan JobEvent, 1000), - SocketPath: sockPath, - URLPath: "/events/", - ident: ident, - logger: logf.Log.WithName("eventapi").WithValues("job", ident), - } - - mux := http.NewServeMux() - mux.HandleFunc(rec.URLPath, rec.handleEvents) - srv := http.Server{Handler: mux, ReadHeaderTimeout: 5 * time.Second} - rec.server = &srv - - go func() { - errChan <- srv.Serve(listener) - }() - return &rec, nil -} - -// Close ensures that appropriate resources are cleaned up, such as any unix -// streaming socket that may be in use. Close must be called. -func (e *EventReceiver) Close() { - e.mutex.Lock() - e.stopped = true - e.mutex.Unlock() - e.logger.V(1).Info("Event API stopped") - if err := e.server.Close(); err != nil && !errors.Is(err, os.ErrClosed) { - e.logger.Error(err, "Failed to close event receiver") - } - os.Remove(e.SocketPath) - close(e.Events) -} - -func (e *EventReceiver) handleEvents(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != e.URLPath { - http.NotFound(w, r) - e.logger.Info("Path not found", "code", "404", "Request.Path", r.URL.Path) - return - } - - if r.Method != http.MethodPost { - e.logger.Info("Method not allowed", "code", "405", "Request.Method", r.Method) - w.WriteHeader(http.StatusMethodNotAllowed) - return - } - - ct := r.Header.Get("content-type") - if strings.Split(ct, ";")[0] != "application/json" { - e.logger.Info("Wrong content type", "code", "415", "Request.Content-Type", ct) - w.WriteHeader(http.StatusUnsupportedMediaType) - if _, err := w.Write([]byte("The content-type must be \"application/json\"")); err != nil { - e.logger.Error(err, "Failed to write response body") - } - return - } - - body, err := io.ReadAll(r.Body) - if err != nil { - e.logger.Error(err, "Could not read request body", "code", "500") - w.WriteHeader(http.StatusInternalServerError) - return - } - - event := JobEvent{} - err = json.Unmarshal(body, &event) - if err != nil { - e.logger.Info("Could not deserialize body.", "code", "400", "Error", err) - w.WriteHeader(http.StatusBadRequest) - if _, err := w.Write([]byte("Could not deserialize body as JSON")); err != nil { - e.logger.Error(err, "Failed to write response body") - } - return - } - - // Guarantee that the Events channel will not be written to if stopped == - // true, because in that case the channel has been closed. - e.mutex.RLock() - defer e.mutex.RUnlock() - if e.stopped { - e.mutex.RUnlock() - w.WriteHeader(http.StatusGone) - e.logger.Info("Stopped and not accepting additional events for this job", "code", "410") - return - } - // ansible-runner sends "status events" and "ansible events". The "status - // events" signify a change in the state of ansible-runner itself, which - // we're not currently interested in. - // https://ansible-runner.readthedocs.io/en/latest/external_interface.html#event-structure - if event.UUID == "" { - e.logger.V(1).Info("Dropping event that is not a JobEvent") - e.logger.V(2).Info("Dropped event", "event", event, "request", string(body)) - } else { - // timeout if the channel blocks for too long - timeout := time.NewTimer(10 * time.Second) - select { - case e.Events <- event: - case <-timeout.C: - e.logger.Info("Timed out writing event to channel", "code", "500") - w.WriteHeader(http.StatusInternalServerError) - return - } - _ = timeout.Stop() - } - w.WriteHeader(http.StatusNoContent) -} diff --git a/internal/ansible/runner/eventapi/types.go b/internal/ansible/runner/eventapi/types.go deleted file mode 100644 index 669a67b698..0000000000 --- a/internal/ansible/runner/eventapi/types.go +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// 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 eventapi - -import ( - "fmt" - "strings" - "time" -) - -const ( - // Ansible Events - - // EventPlaybookOnTaskStart - playbook is starting to run a task. - EventPlaybookOnTaskStart = "playbook_on_task_start" - // EventRunnerOnOk - task finished with ok status. - EventRunnerOnOk = "runner_on_ok" - // EventRunnerOnFailed - task finished with failed status. - EventRunnerOnFailed = "runner_on_failed" - // EventPlaybookOnStats - playbook has finished running. - EventPlaybookOnStats = "playbook_on_stats" - // EventRunnerItemOnOk - item finished with ok status. - EventRunnerItemOnOk = "runner_item_on_ok" - - // Ansible Task Actions - - // TaskActionSetFact - task action of setting a fact. - TaskActionSetFact = "set_fact" - // TaskActionDebug - task action of printing a debug message. - TaskActionDebug = "debug" - - // defaultFailedMessage - Default failed playbook message - defaultFailedMessage = "unknown playbook failure" -) - -// EventTime - time to unmarshal nano time. -type EventTime struct { - time.Time -} - -// UnmarshalJSON - override unmarshal json. -func (e *EventTime) UnmarshalJSON(b []byte) (err error) { - e.Time, err = time.Parse("2006-01-02T15:04:05.999999999", strings.Trim(string(b[:]), "\"\\")) - return -} - -// MarshalJSON - override the marshal json. -func (e EventTime) MarshalJSON() ([]byte, error) { - return []byte(fmt.Sprintf("\"%s\"", e.Time.Format("2006-01-02T15:04:05.99999999"))), nil -} - -// JobEvent - event of an ansible run. -type JobEvent struct { - UUID string `json:"uuid"` - Counter int `json:"counter"` - StdOut string `json:"stdout"` - StartLine int `json:"start_line"` - EndLine int `json:"EndLine"` - Event string `json:"event"` - EventData map[string]interface{} `json:"event_data"` - PID int `json:"pid"` - Created EventTime `json:"created"` -} - -// StatusJobEvent - event of an ansible run. -type StatusJobEvent struct { - UUID string `json:"uuid"` - Counter int `json:"counter"` - StdOut string `json:"stdout"` - StartLine int `json:"start_line"` - EndLine int `json:"EndLine"` - Event string `json:"event"` - EventData StatsEventData `json:"event_data"` - PID int `json:"pid"` - Created EventTime `json:"created"` -} - -// StatsEventData - data for a the status event. -type StatsEventData struct { - Playbook string `json:"playbook"` - PlaybookUUID string `json:"playbook_uuid"` - Changed map[string]int `json:"changed"` - Ok map[string]int `json:"ok"` - Failures map[string]int `json:"failures"` - Skipped map[string]int `json:"skipped"` -} - -// FailureMessages - failure messages from the event api -type FailureMessages []string - -// GetFailedPlaybookMessage - get the failure message from res.msg -func (je JobEvent) GetFailedPlaybookMessage() string { - message := defaultFailedMessage - result, ok := je.EventData["res"].(map[string]interface{}) - if !ok { - return message - } - if m, ok := result["msg"].(string); ok { - message = m - } - return message -} - -// IgnoreError - Does the job event contain the ignore_error ansible flag -func (je JobEvent) IgnoreError() bool { - ignoreErrors, ok := je.EventData["ignore_errors"] - if !ok { - return false - } - if b, ok := ignoreErrors.(bool); ok && b { - return b - } - return false -} - -// Rescued - Detects whether or not a task was rescued -func (je JobEvent) Rescued() bool { - if rescued, contains := je.EventData["rescued"]; contains { - for _, v := range rescued.(map[string]interface{}) { - if int(v.(float64)) == 1 { - return true - } - } - } - return false -} diff --git a/internal/ansible/runner/fake/runner.go b/internal/ansible/runner/fake/runner.go deleted file mode 100644 index d105076e94..0000000000 --- a/internal/ansible/runner/fake/runner.go +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// 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 fake - -import ( - "fmt" - "time" - - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - - "github.com/operator-framework/operator-sdk/internal/ansible/runner" - "github.com/operator-framework/operator-sdk/internal/ansible/runner/eventapi" -) - -// Runner - implements the Runner interface for a GVK that's being watched. -type Runner struct { - Finalizer string - ReconcilePeriod time.Duration - ManageStatus bool - WatchDependentResources bool - WatchClusterScopedResources bool - // Used to send error if Run should fail. - Error error - // Job Events that will be sent back from the runs channel - JobEvents []eventapi.JobEvent - //Stdout standard out to reply if failure occurs. - Stdout string -} - -type runResult struct { - events <-chan eventapi.JobEvent - stdout string -} - -func (r *runResult) Events() <-chan eventapi.JobEvent { - return r.events -} - -func (r *runResult) Stdout() (string, error) { - if r.stdout != "" { - return r.stdout, nil - } - return r.stdout, fmt.Errorf("unable to find standard out") -} - -// Run - runs the fake runner. -func (r *Runner) Run(_ string, u *unstructured.Unstructured, _ string) (runner.RunResult, error) { - if r.Error != nil { - return nil, r.Error - } - c := make(chan eventapi.JobEvent) - go func() { - for _, je := range r.JobEvents { - c <- je - } - close(c) - }() - return &runResult{events: c, stdout: r.Stdout}, nil -} - -// GetReconcilePeriod - new reconcile period. -func (r *Runner) GetReconcilePeriod() (time.Duration, bool) { - return r.ReconcilePeriod, r.ReconcilePeriod != time.Duration(0) -} - -// GetManageStatus - get managestatus. -func (r *Runner) GetManageStatus() bool { - return r.ManageStatus -} - -// GetWatchDependentResources - get watchDependentResources. -func (r *Runner) GetWatchDependentResources() bool { - return r.WatchDependentResources -} - -// GetWatchClusterScopedResources - get watchClusterScopedResources. -func (r *Runner) GetWatchClusterScopedResources() bool { - return r.WatchClusterScopedResources -} - -// GetFinalizer - gets the fake finalizer. -func (r *Runner) GetFinalizer() (string, bool) { - return r.Finalizer, r.Finalizer != "" -} diff --git a/internal/ansible/runner/internal/inputdir/inputdir.go b/internal/ansible/runner/internal/inputdir/inputdir.go deleted file mode 100644 index 48490a465b..0000000000 --- a/internal/ansible/runner/internal/inputdir/inputdir.go +++ /dev/null @@ -1,208 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// 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 inputdir - -import ( - "encoding/json" - "errors" - "fmt" - "io" - "os" - "path/filepath" - "strings" - - "github.com/spf13/afero" - logf "sigs.k8s.io/controller-runtime/pkg/log" -) - -var log = logf.Log.WithName("inputdir") - -// InputDir represents an input directory for ansible-runner. -type InputDir struct { - Path string - PlaybookPath string - Parameters map[string]interface{} - EnvVars map[string]string - Settings map[string]string - CmdLine string -} - -// makeDirs creates the required directory structure. -func (i *InputDir) makeDirs() error { - for _, path := range []string{"env", "project", "inventory"} { - fullPath := filepath.Join(i.Path, path) - err := os.MkdirAll(fullPath, os.ModePerm) - if err != nil { - log.Error(err, "Unable to create directory", "Path", fullPath) - return err - } - } - return nil -} - -// addFile adds a file to the given relative path within the input directory. -func (i *InputDir) addFile(path string, content []byte) error { - fullPath := filepath.Join(i.Path, path) - err := os.WriteFile(fullPath, content, 0644) - if err != nil { - log.Error(err, "Unable to write file", "Path", fullPath) - } - return err -} - -// copyInventory copies a file or directory from src to dst -func (i *InputDir) copyInventory(src string, dst string) error { - fs := afero.NewOsFs() - return afero.Walk(fs, src, - func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - fullDst := strings.Replace(path, src, dst, 1) - if info.IsDir() { - if err = fs.MkdirAll(fullDst, info.Mode()); err != nil { - return err - } - } else { - f, err := fs.Open(path) - if err != nil { - return err - } - if err = afero.WriteReader(fs, fullDst, f); err != nil { - return err - } - if err = fs.Chmod(fullDst, info.Mode()); err != nil { - return err - } - } - return nil - }) -} - -// Stdout reads the stdout from the ansible artifact that corresponds to the -// given ident and returns it as a string. -func (i *InputDir) Stdout(ident string) (string, error) { - errorPath := filepath.Join(i.Path, "artifacts", ident, "stdout") - errorText, err := os.ReadFile(errorPath) - return string(errorText), err -} - -// Write commits the object's state to the filesystem at i.Path. -func (i *InputDir) Write() error { - paramBytes, err := json.Marshal(i.Parameters) - if err != nil { - return err - } - envVarBytes, err := json.Marshal(i.EnvVars) - if err != nil { - return err - } - settingsBytes, err := json.Marshal(i.Settings) - if err != nil { - return err - } - - err = i.makeDirs() - if err != nil { - return err - } - - err = i.addFile("env/envvars", envVarBytes) - if err != nil { - return err - } - err = i.addFile("env/extravars", paramBytes) - if err != nil { - return err - } - err = i.addFile("env/settings", settingsBytes) - if err != nil { - return err - } - - // Trimming off the first and last characters if the command is wrapped by single quotations - if strings.HasPrefix(i.CmdLine, string("'")) && i.CmdLine[0] == i.CmdLine[len(i.CmdLine)-1] { - i.CmdLine = i.CmdLine[1 : len(i.CmdLine)-1] - } - - cmdLineBytes := []byte(i.CmdLine) - if len(cmdLineBytes) > 0 { - err = i.addFile("env/cmdline", cmdLineBytes) - if err != nil { - return err - } - } - - // ANSIBLE_INVENTORY takes precedence over our generated hosts file - // so if the envvar is set we don't bother making it, we just copy - // the inventory into our runner directory - ansibleInventory := os.Getenv("ANSIBLE_INVENTORY") - if ansibleInventory == "" { - // If ansible-runner is running in a python virtual environment, propagate - // that to ansible. - venv := os.Getenv("VIRTUAL_ENV") - hosts := "localhost ansible_connection=local" - if venv != "" { - hosts = fmt.Sprintf("%s ansible_python_interpreter=%s", hosts, filepath.Join(venv, "bin", "python3")) - } else { - hosts = fmt.Sprintf("%s ansible_python_interpreter=%s", hosts, "{{ansible_playbook_python}}") - } - err = i.addFile("inventory/hosts", []byte(hosts)) - if err != nil { - return err - } - } else { - fi, err := os.Stat(ansibleInventory) - if err != nil { - return err - } - switch mode := fi.Mode(); { - case mode.IsDir(): - err = i.copyInventory(ansibleInventory, filepath.Join(i.Path, "inventory")) - if err != nil { - return err - } - case mode.IsRegular(): - err = i.copyInventory(ansibleInventory, filepath.Join(i.Path, "inventory/hosts")) - if err != nil { - return err - } - } - } - - if i.PlaybookPath != "" { - f, err := os.Open(i.PlaybookPath) - if err != nil { - log.Error(err, "Failed to open playbook file", "Path", i.PlaybookPath) - return err - } - defer func() { - if err := f.Close(); err != nil && !errors.Is(err, os.ErrClosed) { - log.Error(err, "Failed to close playbook file") - } - }() - - playbookBytes, err := io.ReadAll(f) - if err != nil { - return err - } - - err = i.addFile("project/playbook.yaml", playbookBytes) - if err != nil { - return err - } - } - return nil -} diff --git a/internal/ansible/runner/runner.go b/internal/ansible/runner/runner.go deleted file mode 100644 index 1f2db21117..0000000000 --- a/internal/ansible/runner/runner.go +++ /dev/null @@ -1,458 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// 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 runner - -import ( - "errors" - "fmt" - "net/http" - "os" - "os/exec" - "path/filepath" - "strconv" - "strings" - - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - logf "sigs.k8s.io/controller-runtime/pkg/log" - - "github.com/operator-framework/operator-sdk/internal/ansible/metrics" - "github.com/operator-framework/operator-sdk/internal/ansible/paramconv" - "github.com/operator-framework/operator-sdk/internal/ansible/runner/eventapi" - "github.com/operator-framework/operator-sdk/internal/ansible/runner/internal/inputdir" - "github.com/operator-framework/operator-sdk/internal/ansible/watches" -) - -var log = logf.Log.WithName("runner") - -const ( - // MaxRunnerArtifactsAnnotation - annotation used by a user to specify the max artifacts to keep - // in the runner directory. This will override the value provided by the watches file for a - // particular CR. Setting this to zero will cause all artifact directories to be kept. - // Example usage "ansible.sdk.operatorframework.io/max-runner-artifacts: 100" - MaxRunnerArtifactsAnnotation = "ansible.sdk.operatorframework.io/max-runner-artifacts" - - // AnsibleVerbosityAnnotation - annotation used by a user to specify the verbosity given - // to the ansible-runner command. This will override the value for a particular CR. - // Example usage "ansible.sdk.operatorframework.io/verbosity: 5" - AnsibleVerbosityAnnotation = "ansible.sdk.operatorframework.io/verbosity" - - ansibleRunnerBin = "ansible-runner" -) - -// Runner - a runnable that should take the parameters and name and namespace -// and run the correct code. -type Runner interface { - Run(string, *unstructured.Unstructured, string) (RunResult, error) - GetFinalizer() (string, bool) -} - -// ansibleVerbosityString will return the string with the -v* levels -func ansibleVerbosityString(verbosity int) string { - if verbosity > 0 { - // the default verbosity is 0 - // more info: https://docs.ansible.com/ansible/latest/reference_appendices/config.html#default-verbosity - return fmt.Sprintf("-%v", strings.Repeat("v", verbosity)) - } - // Return default verbosity - return "" -} - -type cmdFuncType func(ident, inputDirPath string, maxArtifacts, verbosity int) *exec.Cmd - -func playbookCmdFunc(path string) cmdFuncType { - return func(ident, inputDirPath string, maxArtifacts, verbosity int) *exec.Cmd { - cmdArgs := []string{"run", inputDirPath} - cmdOptions := []string{ - "--rotate-artifacts", fmt.Sprintf("%v", maxArtifacts), - "-p", path, - "-i", ident, - } - - // check the verbosity since the exec.Command will fail if an arg as "" or " " be informed - if verbosity > 0 { - cmdOptions = append(cmdOptions, ansibleVerbosityString(verbosity)) - } - return exec.Command("ansible-runner", append(cmdArgs, cmdOptions...)...) - } -} - -func roleCmdFunc(path string) cmdFuncType { - rolePath, roleName := filepath.Split(path) - return func(ident, inputDirPath string, maxArtifacts, verbosity int) *exec.Cmd { - // check the verbosity since the exec.Command will fail if an arg as "" or " " be informed - - cmdOptions := []string{ - "--rotate-artifacts", fmt.Sprintf("%v", maxArtifacts), - "--role", roleName, - "--roles-path", rolePath, - "--hosts", "localhost", - "-i", ident, - } - cmdArgs := []string{"run", inputDirPath} - - if verbosity > 0 { - cmdOptions = append(cmdOptions, ansibleVerbosityString(verbosity)) - } - ansibleGathering := os.Getenv("ANSIBLE_GATHERING") - - // When running a role directly, ansible-runner does not respect the ANSIBLE_GATHERING - // environment variable, so we need to skip fact collection manually - if ansibleGathering == "explicit" { - cmdOptions = append(cmdOptions, "--role-skip-facts") - } - return exec.Command("ansible-runner", append(cmdArgs, cmdOptions...)...) - } -} - -// New - creates a Runner from a Watch struct -func New(watch watches.Watch, runnerArgs string) (Runner, error) { - var path string - var cmdFunc, finalizerCmdFunc cmdFuncType - - err := watch.Validate() - if err != nil { - log.Error(err, "Failed to validate watch") - return nil, err - } - - switch { - case watch.Playbook != "": - path = watch.Playbook - cmdFunc = playbookCmdFunc(path) - case watch.Role != "": - path = watch.Role - cmdFunc = roleCmdFunc(path) - } - - // handle finalizer - switch { - case watch.Finalizer == nil: - finalizerCmdFunc = nil - case watch.Finalizer.Playbook != "": - finalizerCmdFunc = playbookCmdFunc(watch.Finalizer.Playbook) - case watch.Finalizer.Role != "": - finalizerCmdFunc = roleCmdFunc(watch.Finalizer.Role) - default: - finalizerCmdFunc = cmdFunc - } - - return &runner{ - Path: path, - cmdFunc: cmdFunc, - Vars: watch.Vars, - Finalizer: watch.Finalizer, - finalizerCmdFunc: finalizerCmdFunc, - GVK: watch.GroupVersionKind, - maxRunnerArtifacts: watch.MaxRunnerArtifacts, - ansibleVerbosity: watch.AnsibleVerbosity, - ansibleArgs: runnerArgs, - snakeCaseParameters: watch.SnakeCaseParameters, - markUnsafe: watch.MarkUnsafe, - }, nil -} - -// runner - implements the Runner interface for a GVK that's being watched. -type runner struct { - Path string // path on disk to a playbook or role depending on what cmdFunc expects - GVK schema.GroupVersionKind // GVK being watched that corresponds to the Path - Finalizer *watches.Finalizer - Vars map[string]interface{} - cmdFunc cmdFuncType // returns a Cmd that runs ansible-runner - finalizerCmdFunc cmdFuncType - maxRunnerArtifacts int - ansibleVerbosity int - snakeCaseParameters bool - markUnsafe bool - ansibleArgs string -} - -func (r *runner) Run(ident string, u *unstructured.Unstructured, kubeconfig string) (RunResult, error) { - if _, err := exec.LookPath(ansibleRunnerBin); err != nil { - return nil, err - } - - timer := metrics.ReconcileTimer(r.GVK.String()) - defer timer.ObserveDuration() - - if u.GetDeletionTimestamp() != nil && !r.isFinalizerRun(u) { - return nil, errors.New("resource has been deleted, but no finalizer was matched, skipping reconciliation") - } - logger := log.WithValues( - "job", ident, - "name", u.GetName(), - "namespace", u.GetNamespace(), - ) - - // start the event receiver. We'll check errChan for an error after - // ansible-runner exits. - errChan := make(chan error, 1) - receiver, err := eventapi.New(ident, errChan) - if err != nil { - return nil, err - } - inputDir := inputdir.InputDir{ - Path: filepath.Join("/tmp/ansible-operator/runner/", r.GVK.Group, r.GVK.Version, r.GVK.Kind, - u.GetNamespace(), u.GetName()), - Parameters: r.makeParameters(u), - EnvVars: map[string]string{ - "K8S_AUTH_KUBECONFIG": kubeconfig, - "KUBECONFIG": kubeconfig, - }, - Settings: map[string]string{ - "runner_http_url": receiver.SocketPath, - "runner_http_path": receiver.URLPath, - }, - CmdLine: r.ansibleArgs, - } - // If Path is a dir, assume it is a role path. Otherwise assume it's a - // playbook path - fi, err := os.Lstat(r.Path) - if err != nil { - return nil, err - } - if !fi.IsDir() { - inputDir.PlaybookPath = r.Path - } - err = inputDir.Write() - if err != nil { - return nil, err - } - maxArtifacts := r.maxRunnerArtifacts - if ma, ok := u.GetAnnotations()[MaxRunnerArtifactsAnnotation]; ok { - i, err := strconv.Atoi(ma) - if err != nil { - log.Info("Invalid max runner artifact annotation", "err", err, "value", ma) - } else { - maxArtifacts = i - } - } - - verbosity := r.ansibleVerbosity - if av, ok := u.GetAnnotations()[AnsibleVerbosityAnnotation]; ok { - i, err := strconv.Atoi(av) - if err != nil { - log.Info("Invalid ansible verbosity annotation", "err", err, "value", av) - } else { - verbosity = i - } - } - - go func() { - var dc *exec.Cmd - if r.isFinalizerRun(u) { - logger.V(1).Info("Resource is marked for deletion, running finalizer", - "Finalizer", r.Finalizer.Name) - dc = r.finalizerCmdFunc(ident, inputDir.Path, maxArtifacts, verbosity) - } else { - dc = r.cmdFunc(ident, inputDir.Path, maxArtifacts, verbosity) - } - // Append current environment since setting dc.Env to anything other than nil overwrites current env - dc.Env = append(dc.Env, os.Environ()...) - dc.Env = append(dc.Env, fmt.Sprintf("K8S_AUTH_KUBECONFIG=%s", kubeconfig), - fmt.Sprintf("KUBECONFIG=%s", kubeconfig)) - - output, err := dc.CombinedOutput() - if err != nil { - logger.Error(err, string(output)) - } else { - logger.Info("Ansible-runner exited successfully") - } - - receiver.Close() - err = <-errChan - // http.Server returns this in the case of being closed cleanly - if err != nil && err != http.ErrServerClosed { - logger.Error(err, "Error from event API") - } - - // link the current run to the `latest` directory under artifacts - currentRun := filepath.Join(inputDir.Path, "artifacts", ident) - latestArtifacts := filepath.Join(inputDir.Path, "artifacts", "latest") - if _, err = os.Lstat(latestArtifacts); err != nil { - if !errors.Is(err, os.ErrNotExist) { - logger.Error(err, "Latest artifacts dir has error") - return - } - } else if err = os.Remove(latestArtifacts); err != nil { - logger.Error(err, "Error removing the latest artifacts symlink") - return - } - - if err = os.Symlink(currentRun, latestArtifacts); err != nil { - logger.Error(err, "Error symlinking latest artifacts") - } - - }() - - return &runResult{ - events: receiver.Events, - inputDir: &inputDir, - ident: ident, - }, nil -} - -func (r *runner) isFinalizerRun(u *unstructured.Unstructured) bool { - finalizersSet := r.Finalizer != nil && u.GetFinalizers() != nil - // The resource is deleted and our finalizer is present, we need to run the finalizer - if finalizersSet && u.GetDeletionTimestamp() != nil { - for _, f := range u.GetFinalizers() { - if f == r.Finalizer.Name { - return true - } - } - } - return false -} - -// makeParameters - creates the extravars parameters for ansible -// The resulting structure in json is: -// -// { "ansible_operator_meta": { -// "name": , -// "namespace": , -// }, -// , -// , -// , -// __: { -// as is -// } -// ___spec: { -// as is -// } -// } -func (r *runner) makeParameters(u *unstructured.Unstructured) map[string]interface{} { - s := u.Object["spec"] - spec, ok := s.(map[string]interface{}) - if !ok { - log.Info("Spec was not found for CR", "GroupVersionKind", u.GroupVersionKind(), - "Namespace", u.GetNamespace(), "Name", u.GetName()) - spec = map[string]interface{}{} - } - - parameters := map[string]interface{}{} - - if r.snakeCaseParameters { - parameters = paramconv.MapToSnake(spec) - } else { - for k, v := range spec { - parameters[k] = v - } - } - - if r.markUnsafe { - for key, val := range parameters { - parameters[key] = markUnsafe(val) - } - } - - parameters["ansible_operator_meta"] = map[string]string{"namespace": u.GetNamespace(), "name": u.GetName()} - - objKey := escapeAnsibleKey(fmt.Sprintf("_%v_%v", r.GVK.Group, strings.ToLower(r.GVK.Kind))) - parameters[objKey] = u.Object - - specKey := fmt.Sprintf("%s_spec", objKey) - parameters[specKey] = spec - if r.markUnsafe { - parameters[specKey] = markUnsafe(spec) - } - - for k, v := range r.Vars { - parameters[k] = v - } - if r.isFinalizerRun(u) { - for k, v := range r.Finalizer.Vars { - parameters[k] = v - } - } - return parameters -} - -// markUnsafe recursively checks for string values and marks them unsafe. -// for eg: -// -// spec: -// key: "val" -// -// would be marked unsafe in JSON format as: -// -// spec: -// key: map{__ansible_unsafe:"val"} -func markUnsafe(values interface{}) interface{} { - switch v := values.(type) { - case []interface{}: - p := make([]interface{}, 0) - for _, n := range v { - p = append(p, markUnsafe(n)) - } - return p - case map[string]interface{}: - m := make(map[string]interface{}) - for k, v := range v { - m[k] = markUnsafe(v) - } - return m - case string: - return map[string]interface{}{"__ansible_unsafe": values} - default: - return values - } -} - -// escapeAnsibleKey - replaces characters that would result in an inaccessible Ansible parameter with underscores -// ie, _cert-manager.k8s.io would be converted to _cert_manager_k8s_io -func escapeAnsibleKey(key string) string { - disallowed := []string{".", "-"} - for _, c := range disallowed { - key = strings.ReplaceAll(key, c, "_") - } - return key -} - -func (r *runner) GetFinalizer() (string, bool) { - if r.Finalizer != nil { - return r.Finalizer.Name, true - } - return "", false -} - -// RunResult - result of a ansible run -type RunResult interface { - // Stdout returns the stdout from ansible-runner if it is available, else an error. - Stdout() (string, error) - // Events returns the events from ansible-runner if it is available, else an error. - Events() <-chan eventapi.JobEvent -} - -// RunResult facilitates access to information about a run of ansible. -type runResult struct { - // Events is a channel of events from ansible that contain state related - // to a run of ansible. - events <-chan eventapi.JobEvent - - ident string - inputDir *inputdir.InputDir -} - -// Stdout returns the stdout from ansible-runner if it is available, else an error. -func (r *runResult) Stdout() (string, error) { - return r.inputDir.Stdout(r.ident) -} - -// Events returns the events from ansible-runner if it is available, else an error. -func (r *runResult) Events() <-chan eventapi.JobEvent { - return r.events -} diff --git a/internal/ansible/runner/runner_test.go b/internal/ansible/runner/runner_test.go deleted file mode 100644 index 92c9700fdd..0000000000 --- a/internal/ansible/runner/runner_test.go +++ /dev/null @@ -1,373 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// 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 runner - -import ( - "os" - "os/exec" - "path/filepath" - "reflect" - "testing" - - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - - "github.com/operator-framework/operator-sdk/internal/ansible/watches" -) - -func checkCmdFunc(t *testing.T, cmdFunc cmdFuncType, playbook, role string, verbosity int) { - ident := "test" - inputDirPath := "/test/path" - maxArtifacts := 1 - var expectedCmd, gotCmd *exec.Cmd - switch { - case playbook != "": - expectedCmd = playbookCmdFunc(playbook)(ident, inputDirPath, maxArtifacts, verbosity) - case role != "": - expectedCmd = roleCmdFunc(role)(ident, inputDirPath, maxArtifacts, verbosity) - } - - gotCmd = cmdFunc(ident, inputDirPath, maxArtifacts, verbosity) - - if expectedCmd.Path != gotCmd.Path { - t.Fatalf("Unexpected cmd path %v expected cmd path %v", gotCmd.Path, expectedCmd.Path) - } - - if !reflect.DeepEqual(expectedCmd.Args, gotCmd.Args) { - t.Fatalf("Unexpected cmd args %v expected cmd args %v", gotCmd.Args, expectedCmd.Args) - } -} - -func TestNew(t *testing.T) { - cwd, err := os.Getwd() - if err != nil { - t.Fatalf("Unable to get working director: %v", err) - } - validPlaybook := filepath.Join(cwd, "testdata", "playbook.yml") - validRole := filepath.Join(cwd, "testdata", "roles", "role") - testCases := []struct { - name string - gvk schema.GroupVersionKind - playbook string - role string - vars map[string]interface{} - finalizer *watches.Finalizer - desiredObjectKey string - }{ - { - name: "basic runner with playbook", - gvk: schema.GroupVersionKind{ - Group: "operator.example.com", - Version: "v1alpha1", - Kind: "Example", - }, - playbook: validPlaybook, - }, - { - name: "basic runner with role", - gvk: schema.GroupVersionKind{ - Group: "operator.example.com", - Version: "v1alpha1", - Kind: "Example", - }, - role: validRole, - }, - { - name: "basic runner with playbook + finalizer playbook", - gvk: schema.GroupVersionKind{ - Group: "operator.example.com", - Version: "v1alpha1", - Kind: "Example", - }, - playbook: validPlaybook, - finalizer: &watches.Finalizer{ - Name: "operator.example.com/finalizer", - Playbook: validPlaybook, - }, - }, - { - name: "basic runner with role + finalizer role", - gvk: schema.GroupVersionKind{ - Group: "operator.example.com", - Version: "v1alpha1", - Kind: "Example", - }, - role: validRole, - finalizer: &watches.Finalizer{ - Name: "operator.example.com/finalizer", - Role: validRole, - }, - }, - { - name: "basic runner with playbook + finalizer vars", - gvk: schema.GroupVersionKind{ - Group: "operator.example.com", - Version: "v1alpha1", - Kind: "Example", - }, - playbook: validPlaybook, - finalizer: &watches.Finalizer{ - Name: "operator.example.com/finalizer", - Vars: map[string]interface{}{ - "state": "absent", - }, - }, - }, - { - name: "basic runner with playbook, vars + finalizer vars", - gvk: schema.GroupVersionKind{ - Group: "operator.example.com", - Version: "v1alpha1", - Kind: "Example", - }, - playbook: validPlaybook, - vars: map[string]interface{}{ - "type": "this", - }, - finalizer: &watches.Finalizer{ - Name: "operator.example.com/finalizer", - Vars: map[string]interface{}{ - "state": "absent", - }, - }, - }, - { - name: "basic runner with a dash in the group name", - gvk: schema.GroupVersionKind{ - Group: "operator-with-dash.example.com", - Version: "v1alpha1", - Kind: "Example", - }, - playbook: validPlaybook, - desiredObjectKey: "_operator_with_dash_example_com_example", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - testWatch := watches.New(tc.gvk, tc.role, tc.playbook, tc.vars, tc.finalizer) - - testRunner, err := New(*testWatch, "") - if err != nil { - t.Fatalf("Error occurred unexpectedly: %v", err) - } - testRunnerStruct, ok := testRunner.(*runner) - if !ok { - t.Fatalf("Error occurred unexpectedly: %v", err) - } - - switch { - case testWatch.Playbook != "": - if testRunnerStruct.Path != testWatch.Playbook { - t.Fatalf("Unexpected path %v expected path %v", testRunnerStruct.Path, testWatch.Playbook) - } - case testWatch.Role != "": - if testRunnerStruct.Path != testWatch.Role { - t.Fatalf("Unexpected path %v expected path %v", testRunnerStruct.Path, testWatch.Role) - } - } - - // check that the group + kind are properly formatted into a parameter - if tc.desiredObjectKey != "" { - parameters := testRunnerStruct.makeParameters(&unstructured.Unstructured{}) - if _, ok := parameters[tc.desiredObjectKey]; !ok { - t.Fatalf("Did not find expected objKey %v in parameters %+v", tc.desiredObjectKey, parameters) - } - - } - - if testRunnerStruct.GVK != testWatch.GroupVersionKind { - t.Fatalf("Unexpected GVK %v expected GVK %v", testRunnerStruct.GVK, testWatch.GroupVersionKind) - } - - if testRunnerStruct.maxRunnerArtifacts != testWatch.MaxRunnerArtifacts { - t.Fatalf("Unexpected maxRunnerArtifacts %v expected maxRunnerArtifacts %v", - testRunnerStruct.maxRunnerArtifacts, testWatch.MaxRunnerArtifacts) - } - - // Check the cmdFunc - checkCmdFunc(t, testRunnerStruct.cmdFunc, testWatch.Playbook, testWatch.Role, testWatch.AnsibleVerbosity) - - // Check finalizer - if testRunnerStruct.Finalizer != testWatch.Finalizer { - t.Fatalf("Unexpected finalizer %v expected finalizer %v", testRunnerStruct.Finalizer, - testWatch.Finalizer) - } - - if testWatch.Finalizer != nil { - if testRunnerStruct.Finalizer.Name != testWatch.Finalizer.Name { - t.Fatalf("Unexpected finalizer name %v expected finalizer name %v", - testRunnerStruct.Finalizer.Name, testWatch.Finalizer.Name) - } - - if len(testWatch.Finalizer.Vars) == 0 { - checkCmdFunc(t, testRunnerStruct.cmdFunc, testWatch.Finalizer.Playbook, testWatch.Finalizer.Role, - testWatch.AnsibleVerbosity) - } else { - // when finalizer vars is set the finalizerCmdFunc should be the same as the cmdFunc - checkCmdFunc(t, testRunnerStruct.finalizerCmdFunc, testWatch.Playbook, testWatch.Role, - testWatch.AnsibleVerbosity) - } - } - }) - } -} - -func TestAnsibleVerbosityString(t *testing.T) { - testCases := []struct { - verbosity int - expectedString string - }{ - {verbosity: -1, expectedString: ""}, - {verbosity: 0, expectedString: ""}, - {verbosity: 1, expectedString: "-v"}, - {verbosity: 2, expectedString: "-vv"}, - {verbosity: 7, expectedString: "-vvvvvvv"}, - } - - for _, tc := range testCases { - gotString := ansibleVerbosityString(tc.verbosity) - if tc.expectedString != gotString { - t.Fatalf("Unexpected string %v for expected %v from verbosity %v", gotString, tc.expectedString, tc.verbosity) - } - } -} - -func TestMakeParameters(t *testing.T) { - var ( - inputSpec = "testKey" - ) - - testCases := []struct { - name string - inputParams unstructured.Unstructured - expectedSafeParams interface{} - }{ - { - name: "should mark values passed as string unsafe", - inputParams: unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ - inputSpec: "testVal", - }, - }, - }, - expectedSafeParams: map[string]interface{}{ - "__ansible_unsafe": "testVal", - }, - }, - { - name: "should not mark integers unsafe", - inputParams: unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ - inputSpec: 3, - }, - }, - }, - expectedSafeParams: 3, - }, - { - name: "should recursively mark values in dictionary as unsafe", - inputParams: unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ - inputSpec: map[string]interface{}{ - "testsubKey1": "val1", - "testsubKey2": "val2", - }, - }, - }, - }, - expectedSafeParams: map[string]interface{}{ - "testsubKey1": map[string]interface{}{ - "__ansible_unsafe": "val1", - }, - "testsubKey2": map[string]interface{}{ - "__ansible_unsafe": "val2", - }, - }, - }, - { - name: "should recursively mark values in list as unsafe", - inputParams: unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ - inputSpec: []interface{}{ - "testVal1", - "testVal2", - }, - }, - }, - }, - expectedSafeParams: []interface{}{ - map[string]interface{}{ - "__ansible_unsafe": "testVal1", - }, - map[string]interface{}{ - "__ansible_unsafe": "testVal2", - }, - }, - }, - { - name: "should recursively mark values in list/dict as unsafe", - inputParams: unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ - inputSpec: []interface{}{ - "testVal1", - "testVal2", - map[string]interface{}{ - "testVal3": 3, - "testVal4": "__^&{__)", - }, - }, - }, - }, - }, - expectedSafeParams: []interface{}{ - map[string]interface{}{ - "__ansible_unsafe": "testVal1", - }, - map[string]interface{}{ - "__ansible_unsafe": "testVal2", - }, - map[string]interface{}{ - "testVal3": 3, - "testVal4": map[string]interface{}{ - "__ansible_unsafe": "__^&{__)", - }, - }, - }, - }, - } - - for _, tc := range testCases { - testRunner := runner{ - markUnsafe: true, - } - parameters := testRunner.makeParameters(&tc.inputParams) - - val, ok := parameters[inputSpec] - if !ok { - t.Fatalf("Error occurred, value %s in spec is missing", inputSpec) - } else { - eq := reflect.DeepEqual(val, tc.expectedSafeParams) - if !eq { - t.Errorf("Error occurred, parameters %v are not marked unsafe", val) - } - } - } -} diff --git a/internal/ansible/runner/testdata/playbook.yml b/internal/ansible/runner/testdata/playbook.yml deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/internal/ansible/runner/testdata/roles/role/tasks.yaml b/internal/ansible/runner/testdata/roles/role/tasks.yaml deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/internal/ansible/watches/testdata/ansible_collections/nameSpace/collection/roles/someRole/empty_file b/internal/ansible/watches/testdata/ansible_collections/nameSpace/collection/roles/someRole/empty_file deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/internal/ansible/watches/testdata/duplicate_gvk.yaml b/internal/ansible/watches/testdata/duplicate_gvk.yaml deleted file mode 100644 index ce345bf6dc..0000000000 --- a/internal/ansible/watches/testdata/duplicate_gvk.yaml +++ /dev/null @@ -1,17 +0,0 @@ ---- -- version: v1alpha1 - group: app.example.com - kind: Database - playbook: playbook.yaml - finalizer: - name: app.example.com/finalizer - vars: - sentinel: finalizer_running -- version: v1alpha1 - group: app.example.com - kind: Database - playbook: playbook.yaml - finalizer: - name: app.example.com/finalizer - vars: - sentinel: finalizer_running diff --git a/internal/ansible/watches/testdata/invalid.yaml b/internal/ansible/watches/testdata/invalid.yaml deleted file mode 100644 index 110371c4fa..0000000000 --- a/internal/ansible/watches/testdata/invalid.yaml +++ /dev/null @@ -1,9 +0,0 @@ ---- -version: v1alpha1 - group: app.example.com - kind: Database - playbook: playbook.yaml - finalizer: - name: app.example.com/finalizer - vars: - sentinel: finalizer_running diff --git a/internal/ansible/watches/testdata/invalid_collection.yaml b/internal/ansible/watches/testdata/invalid_collection.yaml deleted file mode 100644 index b172198702..0000000000 --- a/internal/ansible/watches/testdata/invalid_collection.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -- version: v1alpha1 - group: app.example.com - kind: SanityUnconfirmed - role: nameSpace.collection.someRole diff --git a/internal/ansible/watches/testdata/invalid_duration.yaml b/internal/ansible/watches/testdata/invalid_duration.yaml deleted file mode 100644 index f5b7ab14e7..0000000000 --- a/internal/ansible/watches/testdata/invalid_duration.yaml +++ /dev/null @@ -1,6 +0,0 @@ ---- -- version: v1alpha1 - group: app.example.com - kind: Database - playbook: playbook.yaml - reconcilePeriod: invalid diff --git a/internal/ansible/watches/testdata/invalid_finalizer_no_vars.yaml b/internal/ansible/watches/testdata/invalid_finalizer_no_vars.yaml deleted file mode 100644 index bade2a38ed..0000000000 --- a/internal/ansible/watches/testdata/invalid_finalizer_no_vars.yaml +++ /dev/null @@ -1,7 +0,0 @@ ---- -- version: v1alpha1 - group: app.example.com - kind: Database - playbook: playbook.yaml - finalizer: - name: foo.app.example.com/finalizer diff --git a/internal/ansible/watches/testdata/invalid_finalizer_playbook_path.yaml b/internal/ansible/watches/testdata/invalid_finalizer_playbook_path.yaml deleted file mode 100644 index 66d877570f..0000000000 --- a/internal/ansible/watches/testdata/invalid_finalizer_playbook_path.yaml +++ /dev/null @@ -1,10 +0,0 @@ ---- -- version: v1alpha1 - group: app.example.com - kind: Database - playbook: playbook.yaml - finalizer: - name: app.example.com/finalizer - playbook: playbook.yaml - vars: - sentinel: finalizer_running diff --git a/internal/ansible/watches/testdata/invalid_finalizer_role_path.yaml b/internal/ansible/watches/testdata/invalid_finalizer_role_path.yaml deleted file mode 100644 index a060604a58..0000000000 --- a/internal/ansible/watches/testdata/invalid_finalizer_role_path.yaml +++ /dev/null @@ -1,10 +0,0 @@ ---- -- version: v1alpha1 - group: app.example.com - kind: Database - playbook: playbook.yaml - finalizer: - name: app.example.com/finalizer - role: ansible/role - vars: - sentinel: finalizer_running diff --git a/internal/ansible/watches/testdata/invalid_finalizer_whithout_name.yaml b/internal/ansible/watches/testdata/invalid_finalizer_whithout_name.yaml deleted file mode 100644 index 0728af66f7..0000000000 --- a/internal/ansible/watches/testdata/invalid_finalizer_whithout_name.yaml +++ /dev/null @@ -1,8 +0,0 @@ ---- -- version: v1alpha1 - group: app.example.com - kind: Database - playbook: playbook.yaml - finalizer: - name: - diff --git a/internal/ansible/watches/testdata/invalid_playbook_path.yaml b/internal/ansible/watches/testdata/invalid_playbook_path.yaml deleted file mode 100644 index 9966a3ab8c..0000000000 --- a/internal/ansible/watches/testdata/invalid_playbook_path.yaml +++ /dev/null @@ -1,9 +0,0 @@ ---- -- version: v1alpha1 - group: app.example.com - kind: Database - playbook: invalid/playbook.yaml - finalizer: - name: app.example.com/finalizer - vars: - sentinel: finalizer_running diff --git a/internal/ansible/watches/testdata/invalid_role_path.yaml b/internal/ansible/watches/testdata/invalid_role_path.yaml deleted file mode 100644 index 8fb5d34559..0000000000 --- a/internal/ansible/watches/testdata/invalid_role_path.yaml +++ /dev/null @@ -1,9 +0,0 @@ ---- -- version: v1alpha1 - group: app.example.com - kind: Database - role: opt/ansible/playbook.yaml - finalizer: - name: app.example.com/finalizer - vars: - sentinel: finalizer_running diff --git a/internal/ansible/watches/testdata/invalid_status.yaml b/internal/ansible/watches/testdata/invalid_status.yaml deleted file mode 100644 index 3ba502ca9b..0000000000 --- a/internal/ansible/watches/testdata/invalid_status.yaml +++ /dev/null @@ -1,6 +0,0 @@ ---- -- version: v1alpha1 - group: app.example.com - kind: Database - playbook: playbook.yaml - watches: invalid diff --git a/internal/ansible/watches/testdata/invalid_yaml_file.yaml b/internal/ansible/watches/testdata/invalid_yaml_file.yaml deleted file mode 100644 index 9759ac29a3..0000000000 --- a/internal/ansible/watches/testdata/invalid_yaml_file.yaml +++ /dev/null @@ -1,3 +0,0 @@ ---- -invalid file layout - diff --git a/internal/ansible/watches/testdata/playbook.yml b/internal/ansible/watches/testdata/playbook.yml deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/internal/ansible/watches/testdata/roles/role/tasks.yaml b/internal/ansible/watches/testdata/roles/role/tasks.yaml deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/internal/ansible/watches/testdata/valid.yaml.tmpl b/internal/ansible/watches/testdata/valid.yaml.tmpl deleted file mode 100644 index 273dbc6584..0000000000 --- a/internal/ansible/watches/testdata/valid.yaml.tmpl +++ /dev/null @@ -1,124 +0,0 @@ ---- -- version: v1alpha1 - group: app.example.com - kind: NoFinalizer - playbook: {{ .ValidPlaybook }} - reconcilePeriod: 2s -- version: v1alpha1 - group: app.example.com - kind: WithUnsafeMarked - playbook: {{ .ValidPlaybook }} - reconcilePeriod: 2s - markUnsafe: True -- version: v1alpha1 - group: app.example.com - kind: Playbook - playbook: {{ .ValidPlaybook }} - finalizer: - name: app.example.com/finalizer - role: {{ .ValidRole }} - vars: - sentinel: finalizer_running -- version: v1alpha1 - group: app.example.com - kind: WatchClusterScoped - playbook: {{ .ValidPlaybook }} - reconcilePeriod: 2s - watchClusterScopedResources: true -- version: v1alpha1 - group: app.example.com - kind: NoReconcile - playbook: {{ .ValidPlaybook }} - reconcilePeriod: 0s -- version: v1alpha1 - group: app.example.com - kind: DefaultStatus - playbook: {{ .ValidPlaybook }} -- version: v1alpha1 - group: app.example.com - kind: DisableStatus - playbook: {{ .ValidPlaybook }} - manageStatus: False -- version: v1alpha1 - group: app.example.com - kind: EnableStatus - playbook: {{ .ValidPlaybook }} - manageStatus: True -- version: v1alpha1 - group: app.example.com - kind: Role - role: {{ .ValidRole }} - finalizer: - name: app.example.com/finalizer - playbook: {{ .ValidPlaybook }} - vars: - sentinel: finalizer_running -- version: v1alpha1 - group: app.example.com - kind: FinalizerRole - role: {{ .ValidRole }} - finalizer: - name: app.example.com/finalizer - vars: - sentinel: finalizer_running -- version: v1alpha1 - group: app.example.com - kind: MaxConcurrentReconcilesDefault - role: {{ .ValidRole }} -- version: v1alpha1 - group: app.example.com - kind: MaxConcurrentReconcilesIgnored - role: {{ .ValidRole }} - maxWorkers: 5 -- version: v1alpha1 - group: app.example.com - kind: MaxConcurrentReconcilesEnv - role: {{ .ValidRole }} -- version: v1alpha1 - group: app.example.com - kind: AnsibleVerbosityDefault - role: {{ .ValidRole }} -- version: v1alpha1 - group: app.example.com - kind: AnsibleVerbosityIgnored - role: {{ .ValidRole }} - ansibleVerbosity: 5 -- version: v1alpha1 - group: app.example.com - kind: AnsibleVerbosityEnv - role: {{ .ValidRole }} -- version: v1alpha1 - group: app.example.com - kind: WatchWithVars - role: {{ .ValidRole }} - vars: - sentinel: reconciling -- version: v1alpha1 - group: app.example.com - kind: AnsibleCollectionEnvTest - role: nameSpace.collection.someRole -- version: v1alpha1 - group: app.example.com - kind: AnsibleBlacklistTest - manageStatus: True - role: {{ .ValidRole }} - blacklist: - - version: "v1alpha1.1" - group: "app.example.com/1" - kind: "AnsibleBlacklistTest_1" - - version: "v1alpha1.2" - group: "app.example.com/2" - kind: "AnsibleBlacklistTest_2" - - version: "v1alpha1.3" - group: "app.example.com/3" - kind: "AnsibleBlacklistTest_3" -- version: "v1alpha1" - group: "app.example.com" - kind: "AnsibleSelectorTest" - manageStatus: True - role: {{ .ValidRole }} - selector: - matchLabels: - matchLabel_1: matchLabel_1 - matchExpressions: - - {key: matchexpression_key, operator: matchexpression_operator, values: [value1,value2]} diff --git a/internal/ansible/watches/watches.go b/internal/ansible/watches/watches.go deleted file mode 100644 index a73842115d..0000000000 --- a/internal/ansible/watches/watches.go +++ /dev/null @@ -1,484 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// 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 watches provides the structures and functions for mapping a -// GroupVersionKind to an Ansible playbook or role. -package watches - -import ( - "errors" - "fmt" - "os" - "path/filepath" - "runtime" - "strconv" - "strings" - "time" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" - logf "sigs.k8s.io/controller-runtime/pkg/log" - yaml "sigs.k8s.io/yaml" - - "github.com/operator-framework/operator-sdk/internal/ansible/flags" -) - -var log = logf.Log.WithName("watches") - -// Watch - holds data used to create a mapping of GVK to ansible playbook or role. -// The mapping is used to compose an ansible operator. -type Watch struct { - GroupVersionKind schema.GroupVersionKind `yaml:",inline"` - Blacklist []schema.GroupVersionKind `yaml:"blacklist"` - Playbook string `yaml:"playbook"` - Role string `yaml:"role"` - Vars map[string]interface{} `yaml:"vars"` - MaxRunnerArtifacts int `yaml:"maxRunnerArtifacts"` - ReconcilePeriod metav1.Duration `yaml:"reconcilePeriod"` - Finalizer *Finalizer `yaml:"finalizer"` - ManageStatus bool `yaml:"manageStatus"` - WatchDependentResources bool `yaml:"watchDependentResources"` - WatchClusterScopedResources bool `yaml:"watchClusterScopedResources"` - SnakeCaseParameters bool `yaml:"snakeCaseParameters"` - WatchAnnotationsChanges bool `yaml:"watchAnnotationsChanges"` - MarkUnsafe bool `yaml:"markUnsafe"` - Selector metav1.LabelSelector `yaml:"selector"` - - // Not configurable via watches.yaml - MaxConcurrentReconciles int `yaml:"-"` - AnsibleVerbosity int `yaml:"-"` -} - -// Finalizer - Expose finalizer to be used by a user. -type Finalizer struct { - Name string `yaml:"name"` - Playbook string `yaml:"playbook"` - Role string `yaml:"role"` - Vars map[string]interface{} `yaml:"vars"` -} - -// Default values for optional fields on Watch -var ( - blacklistDefault = []schema.GroupVersionKind{} - maxRunnerArtifactsDefault = 20 - reconcilePeriodDefault = metav1.Duration{Duration: time.Duration(0)} - manageStatusDefault = true - watchDependentResourcesDefault = true - watchClusterScopedResourcesDefault = false - snakeCaseParametersDefault = true - watchAnnotationsChangesDefault = false - markUnsafeDefault = false - selectorDefault = metav1.LabelSelector{} - - // these are overridden by cmdline flags - maxConcurrentReconcilesDefault = runtime.NumCPU() - ansibleVerbosityDefault = 2 -) - -// Use an alias struct to handle complex types -type alias struct { - Group string `yaml:"group"` - Version string `yaml:"version"` - Kind string `yaml:"kind"` - Playbook string `yaml:"playbook"` - Role string `yaml:"role"` - Vars map[string]interface{} `yaml:"vars"` - MaxRunnerArtifacts int `yaml:"maxRunnerArtifacts"` - ReconcilePeriod *metav1.Duration `yaml:"reconcilePeriod,omitempty"` - ManageStatus *bool `yaml:"manageStatus,omitempty"` - WatchDependentResources *bool `yaml:"watchDependentResources,omitempty"` - WatchClusterScopedResources *bool `yaml:"watchClusterScopedResources,omitempty"` - SnakeCaseParameters *bool `yaml:"snakeCaseParameters"` - WatchAnnotationsChanges *bool `yaml:"watchAnnotationsChanges"` - MarkUnsafe *bool `yaml:"markUnsafe"` - Blacklist []schema.GroupVersionKind `yaml:"blacklist,omitempty"` - Finalizer *Finalizer `yaml:"finalizer"` - Selector metav1.LabelSelector `yaml:"selector"` -} - -// buildWatch will build Watch based on the values parsed from alias -func (w *Watch) setValuesFromAlias(tmp alias) error { - // by default, the operator will manage status and watch dependent resources - if tmp.ManageStatus == nil { - tmp.ManageStatus = &manageStatusDefault - } - // the operator will not manage cluster scoped resources by default. - if tmp.WatchDependentResources == nil { - tmp.WatchDependentResources = &watchDependentResourcesDefault - } - if tmp.MaxRunnerArtifacts == 0 { - tmp.MaxRunnerArtifacts = maxRunnerArtifactsDefault - } - - if tmp.ReconcilePeriod == nil { - tmp.ReconcilePeriod = &reconcilePeriodDefault - } - - if tmp.WatchClusterScopedResources == nil { - tmp.WatchClusterScopedResources = &watchClusterScopedResourcesDefault - } - - if tmp.Blacklist == nil { - tmp.Blacklist = blacklistDefault - } - - if tmp.SnakeCaseParameters == nil { - tmp.SnakeCaseParameters = &snakeCaseParametersDefault - } - - if tmp.WatchAnnotationsChanges == nil { - tmp.WatchAnnotationsChanges = &watchAnnotationsChangesDefault - } - - if tmp.MarkUnsafe == nil { - tmp.MarkUnsafe = &markUnsafeDefault - } - - gvk := schema.GroupVersionKind{ - Group: tmp.Group, - Version: tmp.Version, - Kind: tmp.Kind, - } - err := verifyGVK(gvk) - if err != nil { - return fmt.Errorf("invalid GVK: %s: %w", gvk, err) - } - - // Rewrite values to struct being unmarshalled - w.GroupVersionKind = gvk - w.Playbook = tmp.Playbook - w.Role = tmp.Role - w.Vars = tmp.Vars - w.MaxRunnerArtifacts = tmp.MaxRunnerArtifacts - w.MaxConcurrentReconciles = getMaxConcurrentReconciles(gvk, maxConcurrentReconcilesDefault) - w.ReconcilePeriod = *tmp.ReconcilePeriod - w.ManageStatus = *tmp.ManageStatus - w.WatchDependentResources = *tmp.WatchDependentResources - w.SnakeCaseParameters = *tmp.SnakeCaseParameters - w.WatchAnnotationsChanges = *tmp.WatchAnnotationsChanges - w.MarkUnsafe = *tmp.MarkUnsafe - w.WatchClusterScopedResources = *tmp.WatchClusterScopedResources - w.Finalizer = tmp.Finalizer - w.AnsibleVerbosity = getAnsibleVerbosity(gvk, ansibleVerbosityDefault) - w.Blacklist = tmp.Blacklist - - wd, err := os.Getwd() - if err != nil { - return err - } - w.addRolePlaybookPaths(wd) - w.Selector = tmp.Selector - - return nil -} - -// addRolePlaybookPaths will add the full path based on the current dir -func (w *Watch) addRolePlaybookPaths(rootDir string) { - if len(w.Playbook) > 0 { - w.Playbook = getFullPath(rootDir, w.Playbook) - } - - if len(w.Role) > 0 { - possibleRolePaths := getPossibleRolePaths(rootDir, w.Role) - for _, possiblePath := range possibleRolePaths { - if _, err := os.Stat(possiblePath); err == nil { - w.Role = possiblePath - break - } - } - } - if w.Finalizer != nil && len(w.Finalizer.Role) > 0 { - possibleRolePaths := getPossibleRolePaths(rootDir, w.Finalizer.Role) - for _, possiblePath := range possibleRolePaths { - if _, err := os.Stat(possiblePath); err == nil { - w.Finalizer.Role = possiblePath - break - } - } - } - if w.Finalizer != nil && len(w.Finalizer.Playbook) > 0 { - w.Finalizer.Playbook = getFullPath(rootDir, w.Finalizer.Playbook) - } -} - -// getFullPath returns an absolute path for the playbook -func getFullPath(rootDir, path string) string { - if len(path) > 0 && !filepath.IsAbs(path) { - return filepath.Join(rootDir, path) - } - return path -} - -// getPossibleRolePaths returns list of possible absolute paths derived from a user provided value. -func getPossibleRolePaths(rootDir, path string) []string { - possibleRolePaths := []string{} - if filepath.IsAbs(path) || len(path) == 0 { - return append(possibleRolePaths, path) - } - fqcn := strings.Split(path, ".") - // If fqcn is a valid fully qualified collection name, it is .. - if len(fqcn) == 3 { - ansibleCollectionsPathEnv, ok := os.LookupEnv(flags.AnsibleCollectionsPathEnvVar) - if !ok || len(ansibleCollectionsPathEnv) == 0 { - ansibleCollectionsPathEnv = "/usr/share/ansible/collections" - home, err := os.UserHomeDir() - if err == nil { - homeCollections := filepath.Join(home, ".ansible/collections") - ansibleCollectionsPathEnv = ansibleCollectionsPathEnv + ":" + homeCollections - } - } - for _, possiblePathParent := range strings.Split(ansibleCollectionsPathEnv, ":") { - possiblePath := filepath.Join(possiblePathParent, "ansible_collections", fqcn[0], fqcn[1], "roles", fqcn[2]) - possibleRolePaths = append(possibleRolePaths, possiblePath) - } - } - - // Check for the role where Ansible would. If it exists, use it. - ansibleRolesPathEnv, ok := os.LookupEnv(flags.AnsibleRolesPathEnvVar) - if ok && len(ansibleRolesPathEnv) > 0 { - for _, possiblePathParent := range strings.Split(ansibleRolesPathEnv, ":") { - // "roles" is optionally a part of the path. Check with, and without. - possibleRolePaths = append(possibleRolePaths, filepath.Join(possiblePathParent, path)) - possibleRolePaths = append(possibleRolePaths, filepath.Join(possiblePathParent, "roles", path)) - } - } - // Roles can also live in the current working directory. - return append(possibleRolePaths, getFullPath(rootDir, filepath.Join("roles", path))) -} - -// Validate - ensures that a Watch is valid -// A Watch is considered valid if it: -// - Specifies a valid path to a Role||Playbook -// - If a Finalizer is non-nil, it must have a name + valid path to a Role||Playbook or Vars -func (w *Watch) Validate() error { - err := verifyAnsiblePath(w.Playbook, w.Role) - if err != nil { - log.Error(err, fmt.Sprintf("Invalid ansible path for GVK: %v", w.GroupVersionKind.String())) - return err - } - - if w.Finalizer != nil { - if w.Finalizer.Name == "" { - err = fmt.Errorf("finalizer must have name") - log.Error(err, fmt.Sprintf("Invalid finalizer for GVK: %v", w.GroupVersionKind.String())) - return err - } - // only fail if Vars not set - err = verifyAnsiblePath(w.Finalizer.Playbook, w.Finalizer.Role) - if err != nil && len(w.Finalizer.Vars) == 0 { - log.Error(err, fmt.Sprintf("Invalid ansible path on Finalizer for GVK: %v", - w.GroupVersionKind.String())) - return err - } - } - - return nil -} - -// New - returns a Watch with sensible defaults. -func New(gvk schema.GroupVersionKind, role, playbook string, vars map[string]interface{}, finalizer *Finalizer) *Watch { - return &Watch{ - Blacklist: blacklistDefault, - GroupVersionKind: gvk, - Playbook: playbook, - Role: role, - Vars: vars, - MaxRunnerArtifacts: maxRunnerArtifactsDefault, - MaxConcurrentReconciles: maxConcurrentReconcilesDefault, - ReconcilePeriod: reconcilePeriodDefault, - ManageStatus: manageStatusDefault, - WatchDependentResources: watchDependentResourcesDefault, - WatchClusterScopedResources: watchClusterScopedResourcesDefault, - SnakeCaseParameters: snakeCaseParametersDefault, - WatchAnnotationsChanges: watchAnnotationsChangesDefault, - MarkUnsafe: markUnsafeDefault, - Finalizer: finalizer, - AnsibleVerbosity: ansibleVerbosityDefault, - Selector: selectorDefault, - } -} - -// Load - loads a slice of Watches from the watches file from the CLI -func Load(path string, maxReconciler, ansibleVerbosity int) ([]Watch, error) { - maxConcurrentReconcilesDefault = maxReconciler - ansibleVerbosityDefault = ansibleVerbosity - b, err := os.ReadFile(path) - if err != nil { - log.Error(err, "Failed to get config file") - return nil, err - } - - // First unmarshal into a slice of aliases. - alias := []alias{} - err = yaml.Unmarshal(b, &alias) - if err != nil { - log.Error(err, "Failed to unmarshal config") - return nil, err - } - - // Create one Watch per alias in aliases. - - watches := []Watch{} - for _, tmp := range alias { - w := Watch{} - err = w.setValuesFromAlias(tmp) - if err != nil { - return nil, err - } - watches = append(watches, w) - } - - watchesMap := make(map[schema.GroupVersionKind]bool) - for _, watch := range watches { - // prevent dupes - if _, ok := watchesMap[watch.GroupVersionKind]; ok { - return nil, fmt.Errorf("duplicate GVK: %v", watch.GroupVersionKind.String()) - } - - watchesMap[watch.GroupVersionKind] = true - - err = watch.Validate() - if err != nil { - log.Error(err, fmt.Sprintf("Watch with GVK %v failed validation", watch.GroupVersionKind.String())) - return nil, err - } - } - - return watches, nil -} - -// verify that a given GroupVersionKind has a Version and Kind -// A GVK without a group is valid. Certain scenarios may cause a GVK -// without a group to fail in other ways later in the initialization -// process. -func verifyGVK(gvk schema.GroupVersionKind) error { - if gvk.Version == "" { - return errors.New("version must not be empty") - } - if gvk.Kind == "" { - return errors.New("kind must not be empty") - } - return nil -} - -// verify that a valid path is specified for a given role or playbook -func verifyAnsiblePath(playbook string, role string) error { - switch { - case playbook != "": - if _, err := os.Stat(playbook); err != nil { - return fmt.Errorf("playbook: %v was not found", playbook) - } - case role != "": - if _, err := os.Stat(role); err != nil { - return fmt.Errorf("role: %v was not found", role) - } - default: - return fmt.Errorf("must specify Role or Playbook") - } - return nil -} - -// if the WORKER_* environment variable is set, use that value. -// Otherwise, use defValue. This is definitely -// counter-intuitive but it allows the operator admin adjust the -// number of workers based on their cluster resources. While the -// author may use the CLI option to specify a suggested -// configuration for the operator. -func getMaxConcurrentReconciles(gvk schema.GroupVersionKind, defValue int) int { - envVarMaxWorker := strings.ToUpper(strings.ReplaceAll( - fmt.Sprintf("WORKER_%s_%s", gvk.Kind, gvk.Group), - ".", - "_", - )) - envVarMaxReconciler := strings.ToUpper(strings.ReplaceAll( - fmt.Sprintf("MAX_CONCURRENT_RECONCILES_%s_%s", gvk.Kind, gvk.Group), - ".", - "_", - )) - envVal := getIntegerEnvMaxReconcile(envVarMaxWorker, envVarMaxReconciler, defValue) - if envVal <= 0 { - log.Info("Value %v not valid. Using default %v", envVal, defValue) - return defValue - } - return envVal -} - -// if the ANSIBLE_VERBOSITY_* environment variable is set, use that value. -// Otherwise, use defValue. -func getAnsibleVerbosity(gvk schema.GroupVersionKind, defValue int) int { - envVar := strings.ToUpper(strings.Replace( - fmt.Sprintf("ANSIBLE_VERBOSITY_%s_%s", gvk.Kind, gvk.Group), - ".", - "_", - -1, - )) - ansibleVerbosity := getIntegerEnvWithDefault(envVar, defValue) - // Use default value when value doesn't make sense - if ansibleVerbosity < 0 { - log.Info("Value %v not valid. Using default %v", ansibleVerbosity, defValue) - return defValue - } - if ansibleVerbosity > 7 { - log.Info("Value %v not valid. Using default %v", ansibleVerbosity, defValue) - return defValue - } - return ansibleVerbosity -} - -// getIntegerEnvWithDefault returns value for MaxWorkers/Ansibleverbosity based on if envVar is set -// sor a defvalue is used. -func getIntegerEnvWithDefault(envVar string, defValue int) int { - val := defValue - if envVal, ok := os.LookupEnv(envVar); ok { - if i, err := strconv.Atoi(envVal); err != nil { - log.Info("Could not parse environment variable as an integer; using default value", - "envVar", envVar, "default", defValue) - } else { - val = i - } - } else if !ok { - log.Info("Environment variable not set; using default value", "envVar", envVar, - "default", defValue) - } - return val -} - -// getIntegerEnvMaxReconcile looks for global variable "MAX_CONCURRENT_RECONCILES__", -// if not present it checks for "WORKER__" and logs deprecation message -// if required. If both of them are not set, we use the default value passed on by command line -// flags. -func getIntegerEnvMaxReconcile(envVarMaxWorker, envVarMaxReconciler string, defValue int) int { - val := defValue - if envValRecon, ok := os.LookupEnv(envVarMaxReconciler); ok { - if i, err := strconv.Atoi(envValRecon); err != nil { - log.Info("Could not parse environment variable as an integer; using default value", - "envVar", envVarMaxReconciler, "default", defValue) - } else { - val = i - } - } else if !ok { - if envValWorker, ok := os.LookupEnv(envVarMaxWorker); ok { - deprecationMsg := fmt.Sprintf("Environment variable %s is deprecated, use %s instead", envVarMaxWorker, envVarMaxReconciler) - log.Info(deprecationMsg) - if i, err := strconv.Atoi(envValWorker); err != nil { - log.Info("Could not parse environment variable as an integer; using default value", - "envVar", envVarMaxWorker, "default", defValue) - } else { - val = i - } - } - } - return val - -} diff --git a/internal/ansible/watches/watches_test.go b/internal/ansible/watches/watches_test.go deleted file mode 100644 index 6b75d8aebd..0000000000 --- a/internal/ansible/watches/watches_test.go +++ /dev/null @@ -1,894 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// 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 watches - -import ( - "fmt" - "html/template" - "os" - "path/filepath" - "reflect" - "sort" - "strconv" - "testing" - "time" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" -) - -func TestNew(t *testing.T) { - basicGVK := schema.GroupVersionKind{ - Version: "v1alpha1", - Group: "app.example.com", - Kind: "Example", - } - testCases := []struct { - name string - gvk schema.GroupVersionKind - role string - playbook string - vars map[string]interface{} - finalizer *Finalizer - shouldValidate bool - }{ - { - name: "default invalid watch", - gvk: basicGVK, - shouldValidate: false, - }, - } - - expectedReconcilePeriod, _ := time.ParseDuration(reconcilePeriodDefault.String()) - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - watch := New(tc.gvk, tc.role, tc.playbook, tc.vars, tc.finalizer) - if watch.GroupVersionKind != tc.gvk { - t.Fatalf("Unexpected GVK %v expected %v", watch.GroupVersionKind, tc.gvk) - } - if watch.MaxRunnerArtifacts != maxRunnerArtifactsDefault { - t.Fatalf("Unexpected maxRunnerArtifacts %v expected %v", watch.MaxRunnerArtifacts, - maxRunnerArtifactsDefault) - } - if watch.MaxConcurrentReconciles != maxConcurrentReconcilesDefault { - t.Fatalf("Unexpected maxConcurrentReconciles %v expected %v", watch.MaxConcurrentReconciles, - maxConcurrentReconcilesDefault) - } - if watch.ReconcilePeriod.Duration != expectedReconcilePeriod { - t.Fatalf("Unexpected reconcilePeriod %v expected %v", watch.ReconcilePeriod, - expectedReconcilePeriod) - } - if watch.ManageStatus != manageStatusDefault { - t.Fatalf("Unexpected manageStatus %v expected %v", watch.ManageStatus, &manageStatusDefault) - } - if watch.WatchDependentResources != watchDependentResourcesDefault { - t.Fatalf("Unexpected watchDependentResources %v expected %v", watch.WatchDependentResources, - watchDependentResourcesDefault) - } - if watch.SnakeCaseParameters != snakeCaseParametersDefault { - t.Fatalf("Unexpected snakeCaseParameters %v expected %v", watch.SnakeCaseParameters, - snakeCaseParametersDefault) - } - if watch.MarkUnsafe != markUnsafeDefault { - t.Fatalf("Unexpected markUnsafe %v expected %v", watch.MarkUnsafe, markUnsafeDefault) - } - if watch.WatchClusterScopedResources != watchClusterScopedResourcesDefault { - t.Fatalf("Unexpected watchClusterScopedResources %v expected %v", - watch.WatchClusterScopedResources, watchClusterScopedResourcesDefault) - } - if watch.AnsibleVerbosity != ansibleVerbosityDefault { - t.Fatalf("Unexpected ansibleVerbosity %v expected %v", watch.AnsibleVerbosity, - ansibleVerbosityDefault) - } - - err := watch.Validate() - if err != nil && tc.shouldValidate { - t.Fatalf("Watch %v failed validation", watch) - } - if err == nil && !tc.shouldValidate { - t.Fatalf("Watch %v should have failed validation", watch) - } - }) - } -} - -func TestLoad(t *testing.T) { //nolint:gocyclo - cwd, err := os.Getwd() - if err != nil { - t.Fatalf("Unable to get working director: %v", err) - } - - validTemplate := struct { - ValidPlaybook string - ValidRole string - }{ - ValidPlaybook: filepath.Join(cwd, "testdata", "playbook.yml"), - ValidRole: filepath.Join(cwd, "testdata", "roles", "role"), - } - - tmpl, err := template.ParseFiles("testdata/valid.yaml.tmpl") - if err != nil { - t.Fatalf("Unable to parse template: %v", err) - } - f, err := os.Create("testdata/valid.yaml") - if err != nil { - t.Fatalf("Unable to create valid.yaml: %v", err) - } - defer os.Remove("testdata/valid.yaml") - err = tmpl.Execute(f, validTemplate) - if err != nil { - t.Fatalf("Unable to create valid.yaml: %v", err) - return - } - - zeroSeconds := metav1.Duration{Duration: time.Duration(0)} - twoSeconds := metav1.Duration{Duration: time.Second * 2} - - validWatches := []Watch{ - Watch{ - GroupVersionKind: schema.GroupVersionKind{ - Version: "v1alpha1", - Group: "app.example.com", - Kind: "NoFinalizer", - }, - Playbook: validTemplate.ValidPlaybook, - ManageStatus: true, - ReconcilePeriod: twoSeconds, - WatchDependentResources: true, - WatchClusterScopedResources: false, - SnakeCaseParameters: true, - MarkUnsafe: false, - }, - Watch{ - GroupVersionKind: schema.GroupVersionKind{ - Version: "v1alpha1", - Group: "app.example.com", - Kind: "WithUnsafeMarked", - }, - Playbook: validTemplate.ValidPlaybook, - ManageStatus: true, - ReconcilePeriod: twoSeconds, - MarkUnsafe: true, - }, - Watch{ - GroupVersionKind: schema.GroupVersionKind{ - Version: "v1alpha1", - Group: "app.example.com", - Kind: "Playbook", - }, - Playbook: validTemplate.ValidPlaybook, - ManageStatus: true, - WatchDependentResources: true, - SnakeCaseParameters: false, - WatchClusterScopedResources: false, - Finalizer: &Finalizer{ - Name: "app.example.com/finalizer", - Role: validTemplate.ValidRole, - Vars: map[string]interface{}{"sentinel": "finalizer_running"}, - }, - }, - Watch{ - GroupVersionKind: schema.GroupVersionKind{ - Version: "v1alpha1", - Group: "app.example.com", - Kind: "WatchClusterScoped", - }, - Playbook: validTemplate.ValidPlaybook, - ReconcilePeriod: twoSeconds, - ManageStatus: true, - WatchDependentResources: true, - WatchClusterScopedResources: true, - }, - Watch{ - GroupVersionKind: schema.GroupVersionKind{ - Version: "v1alpha1", - Group: "app.example.com", - Kind: "NoReconcile", - }, - Playbook: validTemplate.ValidPlaybook, - ReconcilePeriod: zeroSeconds, - ManageStatus: true, - }, - Watch{ - GroupVersionKind: schema.GroupVersionKind{ - Version: "v1alpha1", - Group: "app.example.com", - Kind: "DefaultStatus", - }, - Playbook: validTemplate.ValidPlaybook, - ManageStatus: true, - }, - Watch{ - GroupVersionKind: schema.GroupVersionKind{ - Version: "v1alpha1", - Group: "app.example.com", - Kind: "DisableStatus", - }, - Playbook: validTemplate.ValidPlaybook, - ManageStatus: false, - }, - Watch{ - GroupVersionKind: schema.GroupVersionKind{ - Version: "v1alpha1", - Group: "app.example.com", - Kind: "EnableStatus", - }, - Playbook: validTemplate.ValidPlaybook, - ManageStatus: true, - }, - Watch{ - GroupVersionKind: schema.GroupVersionKind{ - Version: "v1alpha1", - Group: "app.example.com", - Kind: "Role", - }, - Role: validTemplate.ValidRole, - ManageStatus: true, - Finalizer: &Finalizer{ - Name: "app.example.com/finalizer", - Playbook: validTemplate.ValidPlaybook, - Vars: map[string]interface{}{"sentinel": "finalizer_running"}, - }, - }, - Watch{ - GroupVersionKind: schema.GroupVersionKind{ - Version: "v1alpha1", - Group: "app.example.com", - Kind: "FinalizerRole", - }, - Role: validTemplate.ValidRole, - ManageStatus: true, - Finalizer: &Finalizer{ - Name: "app.example.com/finalizer", - Vars: map[string]interface{}{"sentinel": "finalizer_running"}, - }, - }, - Watch{ - GroupVersionKind: schema.GroupVersionKind{ - Version: "v1alpha1", - Group: "app.example.com", - Kind: "MaxConcurrentReconcilesDefault", - }, - Role: validTemplate.ValidRole, - ManageStatus: true, - MaxConcurrentReconciles: 1, - }, - Watch{ - GroupVersionKind: schema.GroupVersionKind{ - Version: "v1alpha1", - Group: "app.example.com", - Kind: "MaxConcurrentReconcilesIgnored", - }, - Role: validTemplate.ValidRole, - ManageStatus: true, - MaxConcurrentReconciles: 1, - }, - Watch{ - GroupVersionKind: schema.GroupVersionKind{ - Version: "v1alpha1", - Group: "app.example.com", - Kind: "MaxConcurrentReconcilesEnv", - }, - Role: validTemplate.ValidRole, - ManageStatus: true, - MaxConcurrentReconciles: 4, - }, - Watch{ - GroupVersionKind: schema.GroupVersionKind{ - Version: "v1alpha1", - Group: "app.example.com", - Kind: "AnsibleVerbosityDefault", - }, - Role: validTemplate.ValidRole, - ManageStatus: true, - AnsibleVerbosity: 2, - }, - Watch{ - GroupVersionKind: schema.GroupVersionKind{ - Version: "v1alpha1", - Group: "app.example.com", - Kind: "AnsibleVerbosityIgnored", - }, - Role: validTemplate.ValidRole, - ManageStatus: true, - AnsibleVerbosity: 2, - }, - Watch{ - GroupVersionKind: schema.GroupVersionKind{ - Version: "v1alpha1", - Group: "app.example.com", - Kind: "AnsibleVerbosityEnv", - }, - Role: validTemplate.ValidRole, - ManageStatus: true, - AnsibleVerbosity: 4, - }, - Watch{ - GroupVersionKind: schema.GroupVersionKind{ - Version: "v1alpha1", - Group: "app.example.com", - Kind: "WatchWithVars", - }, - Role: validTemplate.ValidRole, - ManageStatus: true, - Vars: map[string]interface{}{"sentinel": "reconciling"}, - }, - Watch{ - GroupVersionKind: schema.GroupVersionKind{ - Version: "v1alpha1", - Group: "app.example.com", - Kind: "AnsibleCollectionEnvTest", - }, - Role: filepath.Join(cwd, "testdata", "ansible_collections", "nameSpace", "collection", "roles", "someRole"), - ManageStatus: true, - }, - Watch{ - GroupVersionKind: schema.GroupVersionKind{ - Version: "v1alpha1", - Group: "app.example.com", - Kind: "AnsibleBlacklistTest", - }, - Role: validTemplate.ValidRole, - Blacklist: []schema.GroupVersionKind{ - { - Version: "v1alpha1.1", - Group: "app.example.com/1", - Kind: "AnsibleBlacklistTest_1", - }, - { - Version: "v1alpha1.2", - Group: "app.example.com/2", - Kind: "AnsibleBlacklistTest_2", - }, - { - Version: "v1alpha1.3", - Group: "app.example.com/3", - Kind: "AnsibleBlacklistTest_3", - }, - }, - ManageStatus: true, - }, - Watch{ - GroupVersionKind: schema.GroupVersionKind{ - Version: "v1alpha1", - Group: "app.example.com", - Kind: "AnsibleSelectorTest", - }, - Role: validTemplate.ValidRole, - Selector: metav1.LabelSelector{ - MatchLabels: map[string]string{ - "matchLabel_1": "matchLabel_1", - }, - MatchExpressions: []metav1.LabelSelectorRequirement{ - { - Key: "matchexpression_key", - Operator: "matchexpression_operator", - Values: []string{"value1", "value2"}, - }, - }, - }, - ManageStatus: true, - }, - } - - testCases := []struct { - name string - path string - maxConcurrentReconciles int - ansibleVerbosity int - expected []Watch - shouldError bool - shouldSetAnsibleRolePathEnvVar bool - shouldSetAnsibleCollectionPathEnvVar bool - }{ - { - name: "error duplicate GVK", - path: "testdata/duplicate_gvk.yaml", - shouldError: true, - }, - { - name: "error no file", - path: "testdata/please_don't_create_me_gvk.yaml", - shouldError: true, - }, - { - name: "error invalid yaml", - path: "testdata/invalid.yaml", - shouldError: true, - }, - { - name: "error invalid playbook path", - path: "testdata/invalid_playbook_path.yaml", - shouldError: true, - }, - { - name: "error invalid playbook finalizer path", - path: "testdata/invalid_finalizer_playbook_path.yaml", - shouldError: true, - }, - { - name: "error invalid finalizer whithout name", - path: "testdata/invalid_finalizer_whithout_name.yaml", - shouldError: true, - }, - { - name: "error invalid role path", - path: "testdata/invalid_role_path.yaml", - shouldError: true, - }, - { - name: "error invalid yaml file", - path: "testdata/invalid_yaml_file.yaml", - shouldError: true, - }, - { - name: "error invalid role path", - path: "testdata/invalid_role_path.yaml", - shouldError: true, - }, - { - name: "error invalid role finalizer path", - path: "testdata/invalid_finalizer_role_path.yaml", - shouldError: true, - }, - { - name: "error invalid finalizer no path/role/vars", - path: "testdata/invalid_finalizer_no_vars.yaml", - shouldError: true, - }, - { - name: "error invalid duration", - path: "testdata/invalid_duration.yaml", - shouldError: true, - }, - { - name: "error invalid status", - path: "testdata/invalid_status.yaml", - shouldError: true, - }, - { - name: "if collection env var is not set and collection is not installed to the default locations, fail", - path: "testdata/invalid_collection.yaml", - shouldError: true, - }, - { - name: "valid watches file", - path: "testdata/valid.yaml", - maxConcurrentReconciles: 1, - ansibleVerbosity: 2, - shouldSetAnsibleCollectionPathEnvVar: true, - expected: validWatches, - }, - { - name: "should load file successfully with ANSIBLE ROLES PATH ENV VAR set", - path: "testdata/valid.yaml", - maxConcurrentReconciles: 1, - ansibleVerbosity: 2, - shouldSetAnsibleRolePathEnvVar: true, - shouldSetAnsibleCollectionPathEnvVar: true, - expected: validWatches, - }, - } - - os.Setenv("WORKER_MAXCONCURRENTRECONCILESENV_APP_EXAMPLE_COM", "4") - defer os.Unsetenv("WORKER_MAXCONCURRENTRECONCILESENV_APP_EXAMPLE_COM") - os.Setenv("ANSIBLE_VERBOSITY_ANSIBLEVERBOSITYENV_APP_EXAMPLE_COM", "4") - defer os.Unsetenv("ANSIBLE_VERBOSITY_ANSIBLEVERBOSITYENV_APP_EXAMPLE_COM") - - wd, err := os.Getwd() - if err != nil { - t.Fatal(err) - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - - // Test Load with ANSIBLE_ROLES_PATH var - if tc.shouldSetAnsibleRolePathEnvVar { - anisbleEnvVar := "path/invalid:/path/invalid/myroles:" + wd - os.Setenv("ANSIBLE_ROLES_PATH", anisbleEnvVar) - defer os.Unsetenv("ANSIBLE_ROLES_PATH") - } - if tc.shouldSetAnsibleCollectionPathEnvVar { - - ansibleCollectionPathEnv := filepath.Join(wd, "testdata") - os.Setenv("ANSIBLE_COLLECTIONS_PATH", ansibleCollectionPathEnv) - defer os.Unsetenv("ANSIBLE_COLLECTIONS_PATH") - } - - watchSlice, err := Load(tc.path, tc.maxConcurrentReconciles, tc.ansibleVerbosity) - if err != nil && !tc.shouldError { - t.Fatalf("Error occurred unexpectedly: %v", err) - } - if err != nil && tc.shouldError { - return - } - // meant to protect from adding test to valid without corresponding check - if len(tc.expected) != len(watchSlice) { - t.Fatalf("Unexpected watches length: %v expected: %v", len(watchSlice), len(tc.expected)) - } - for idx, expectedWatch := range tc.expected { - gvk := expectedWatch.GroupVersionKind - gotWatch := watchSlice[idx] - if gotWatch.GroupVersionKind != gvk { - t.Fatalf("Unexpected GVK: \nunexpected GVK: %#v\nexpected GVK: %#v", - gotWatch.GroupVersionKind, gvk) - } - if gotWatch.Role != expectedWatch.Role { - t.Fatalf("The GVK: %v unexpected Role: %v expected Role: %v", gvk, gotWatch.Role, - expectedWatch.Role) - } - if gotWatch.Playbook != expectedWatch.Playbook { - t.Fatalf("The GVK: %v unexpected Playbook: %v expected Playbook: %v", gvk, gotWatch.Playbook, - expectedWatch.Playbook) - } - if gotWatch.ManageStatus != expectedWatch.ManageStatus { - t.Fatalf("The GVK: %v\nunexpected manageStatus:%#v\nexpected manageStatus: %#v", gvk, - gotWatch.ManageStatus, expectedWatch.ManageStatus) - } - if gotWatch.Finalizer != expectedWatch.Finalizer { - if gotWatch.Finalizer.Name != expectedWatch.Finalizer.Name || gotWatch.Finalizer.Playbook != - expectedWatch.Finalizer.Playbook || gotWatch.Finalizer.Role != - expectedWatch.Finalizer.Role || reflect.DeepEqual(gotWatch.Finalizer.Vars["sentinel"], - expectedWatch.Finalizer.Vars["sentininel"]) { - t.Fatalf("The GVK: %v\nunexpected finalizer: %#v\nexpected finalizer: %#v", gvk, - gotWatch.Finalizer, expectedWatch.Finalizer) - } - } - if gotWatch.ReconcilePeriod != expectedWatch.ReconcilePeriod { - t.Fatalf("The GVK: %v unexpected reconcile period: %v expected reconcile period: %v", gvk, - gotWatch.ReconcilePeriod, expectedWatch.ReconcilePeriod) - } - if gotWatch.MarkUnsafe != expectedWatch.MarkUnsafe { - t.Fatalf("The GVK: %v unexpected mark unsafe: %v expected mark unsafe: %v", gvk, - gotWatch.MarkUnsafe, expectedWatch.MarkUnsafe) - } - - for i, val := range expectedWatch.Blacklist { - if val != gotWatch.Blacklist[i] { - t.Fatalf("Incorrect blacklist GVK %s: got %s, expected %s", gvk, - val, gotWatch.Blacklist[i]) - } - } - - if !reflect.DeepEqual(gotWatch.Selector, expectedWatch.Selector) { - t.Fatalf("Incorrect selector GVK %s:\n\tgot %s\n\texpected %s", gvk, - gotWatch.Selector, expectedWatch.Selector) - } - - if expectedWatch.MaxConcurrentReconciles == 0 { - if gotWatch.MaxConcurrentReconciles != tc.maxConcurrentReconciles { - t.Fatalf("Unexpected max workers: %v expected workers: %v", gotWatch.MaxConcurrentReconciles, - tc.maxConcurrentReconciles) - } - } else { - if gotWatch.MaxConcurrentReconciles != expectedWatch.MaxConcurrentReconciles { - t.Fatalf("Unexpected max workers: %v expected workers: %v", gotWatch.MaxConcurrentReconciles, - expectedWatch.MaxConcurrentReconciles) - } - } - } - }) - } -} - -func TestMaxConcurrentReconciles(t *testing.T) { - testCases := []struct { - name string - gvk schema.GroupVersionKind - defValue int - expectedValue int - setEnv bool - envVarMap map[string]int - }{ - { - name: "no env, use default value", - gvk: schema.GroupVersionKind{ - Group: "cache.example.com", - Version: "v1alpha1", - Kind: "MemCacheService", - }, - defValue: 1, - expectedValue: 1, - setEnv: false, - envVarMap: map[string]int{ - "WORKER_MEMCACHESERVICE_CACHE_EXAMPLE_COM": 0, - }, - }, - { - name: "invalid env, use default value", - gvk: schema.GroupVersionKind{ - Group: "cache.example.com", - Version: "v1alpha1", - Kind: "MemCacheService", - }, - defValue: 1, - expectedValue: 1, - setEnv: true, - envVarMap: map[string]int{ - "WORKER_MEMCACHESERVICE_CACHE_EXAMPLE_COM": 0, - }, - }, - { - name: "worker_%s_%s env set to 3, expect 3", - gvk: schema.GroupVersionKind{ - Group: "cache.example.com", - Version: "v1alpha1", - Kind: "MemCacheService", - }, - defValue: 1, - expectedValue: 3, - setEnv: true, - envVarMap: map[string]int{ - "WORKER_MEMCACHESERVICE_CACHE_EXAMPLE_COM": 3, - }, - }, - { - name: "max_concurrent_reconciler_%s_%s set to 2, expect 2", - gvk: schema.GroupVersionKind{ - Group: "cache.example.com", - Version: "v1alpha1", - Kind: "MemCacheService", - }, - defValue: 1, - expectedValue: 2, - setEnv: true, - envVarMap: map[string]int{ - "MAX_CONCURRENT_RECONCILES_MEMCACHESERVICE_CACHE_EXAMPLE_COM": 2, - }, - }, - { - name: "set multiple env variables", - gvk: schema.GroupVersionKind{ - Group: "cache.example.com", - Version: "v1alpha1", - Kind: "MemCacheService", - }, - defValue: 1, - expectedValue: 3, - setEnv: true, - envVarMap: map[string]int{ - "MAX_CONCURRENT_RECONCILES_MEMCACHESERVICE_CACHE_EXAMPLE_COM": 3, - "WORKER_MEMCACHESERVICE_CACHE_EXAMPLE_COM": 1, - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - for key, val := range tc.envVarMap { - os.Unsetenv(key) - if tc.setEnv { - os.Setenv(key, strconv.Itoa(val)) - } - } - workers := getMaxConcurrentReconciles(tc.gvk, tc.defValue) - if tc.expectedValue != workers { - t.Fatalf("Unexpected MaxConcurrentReconciles: %v expected MaxConcurrentReconciles: %v", - workers, tc.expectedValue) - } - }) - } -} - -func TestAnsibleVerbosity(t *testing.T) { - testCases := []struct { - name string - gvk schema.GroupVersionKind - defValue int - expectedValue int - setEnv bool - envKey string - envValue int - }{ - { - name: "no env, use default value", - gvk: schema.GroupVersionKind{ - Group: "cache.example.com", - Version: "v1alpha1", - Kind: "MemCacheService", - }, - defValue: 1, - expectedValue: 1, - setEnv: false, - envKey: "ANSIBLE_VERBOSITY_MEMCACHESERVICE_CACHE_EXAMPLE_COM", - }, - { - name: "invalid env, lt 0, use default value", - gvk: schema.GroupVersionKind{ - Group: "cache.example.com", - Version: "v1alpha1", - Kind: "MemCacheService", - }, - defValue: 1, - expectedValue: 1, - setEnv: true, - envKey: "ANSIBLE_VERBOSITY_MEMCACHESERVICE_CACHE_EXAMPLE_COM", - envValue: -1, - }, - { - name: "invalid env, gt 7, use default value", - gvk: schema.GroupVersionKind{ - Group: "cache.example.com", - Version: "v1alpha1", - Kind: "MemCacheService", - }, - defValue: 1, - expectedValue: 1, - setEnv: true, - envKey: "ANSIBLE_VERBOSITY_MEMCACHESERVICE_CACHE_EXAMPLE_COM", - envValue: 8, - }, - { - name: "env set to 3, expect 3", - gvk: schema.GroupVersionKind{ - Group: "cache.example.com", - Version: "v1alpha1", - Kind: "MemCacheService", - }, - defValue: 1, - expectedValue: 3, - setEnv: true, - envKey: "ANSIBLE_VERBOSITY_MEMCACHESERVICE_CACHE_EXAMPLE_COM", - envValue: 3, - }, - { - name: "boundary test 0", - gvk: schema.GroupVersionKind{ - Group: "cache.example.com", - Version: "v1alpha1", - Kind: "MemCacheService", - }, - defValue: 1, - expectedValue: 0, - setEnv: true, - envKey: "ANSIBLE_VERBOSITY_MEMCACHESERVICE_CACHE_EXAMPLE_COM", - envValue: 0, - }, - { - name: "boundary test 7", - gvk: schema.GroupVersionKind{ - Group: "cache.example.com", - Version: "v1alpha1", - Kind: "MemCacheService", - }, - defValue: 1, - expectedValue: 7, - setEnv: true, - envKey: "ANSIBLE_VERBOSITY_MEMCACHESERVICE_CACHE_EXAMPLE_COM", - envValue: 7, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - os.Unsetenv(tc.envKey) - if tc.setEnv { - os.Setenv(tc.envKey, strconv.Itoa(tc.envValue)) - } - verbosity := getAnsibleVerbosity(tc.gvk, tc.defValue) - if tc.expectedValue != verbosity { - t.Fatalf("Unexpected Verbosity: %v expected Verbosity: %v", verbosity, tc.expectedValue) - } - }) - } -} - -// Test the func getPossibleRolePaths. -func TestGetPossibleRolePaths(t *testing.T) { - wd, err := os.Getwd() - if err != nil { - t.Fatal(err) - } - // Mock default Full Path based in the current directory - rolesPath := filepath.Join(wd, "roles") - home, err := os.UserHomeDir() - if err != nil { - t.Fatal(err) - } - - type args struct { - path string - rolesEnv string - collectionsEnv string - } - tests := []struct { - name string - args args - want []string - }{ - { - name: "check the current dir for a role name", - args: args{ - path: "Foo", - }, - want: []string{filepath.Join(rolesPath, "Foo")}, - }, - { - name: "check the current dir for a relative path", - args: args{ - path: "relative/Foo", - }, - want: []string{filepath.Join(rolesPath, "relative/Foo")}, - }, - { - name: "check all paths in ANSIBLE_ROLES_PATH env var", - args: args{ - rolesEnv: "relative:nested/relative:/and/abs", - path: "Foo", - }, - want: []string{ - filepath.Join(rolesPath, "Foo"), - filepath.Join("relative", "Foo"), - filepath.Join("relative", "roles", "Foo"), - filepath.Join("nested/relative", "Foo"), - filepath.Join("nested/relative", "roles", "Foo"), - filepath.Join("/and/abs", "Foo"), - filepath.Join("/and/abs", "roles", "Foo"), - }, - }, - { - name: "Check for roles inside default collection locations when given fqcn", - args: args{ - path: "myNS.myCol.myRole", - }, - want: []string{ - filepath.Join(rolesPath, "myNS.myCol.myRole"), - filepath.Join("/usr/share/ansible/collections", "ansible_collections", "myNS", "myCol", "roles", "myRole"), - filepath.Join(home, ".ansible/collections", "ansible_collections", "myNS", "myCol", "roles", "myRole"), - }, - }, - { - name: "Check for roles inside ANSIBLE_COLLECTIONS_PATH locations when set and given path is fqcn", - args: args{ - path: "myNS.myCol.myRole", - collectionsEnv: "/my/collections/", - }, - want: []string{ - filepath.Join(rolesPath, "myNS.myCol.myRole"), - filepath.Join("/my/collections/", "ansible_collections", "myNS", "myCol", "roles", "myRole"), - // Note: Defaults are not checked when the env variable is set - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - - if len(tt.args.rolesEnv) > 0 { - os.Setenv("ANSIBLE_ROLES_PATH", tt.args.rolesEnv) - defer os.Unsetenv("ANSIBLE_ROLES_PATH") - } - if len(tt.args.collectionsEnv) > 0 { - os.Setenv("ANSIBLE_COLLECTIONS_PATH", tt.args.collectionsEnv) - defer os.Unsetenv("ANSIBLE_COLLECTIONS_PATH") - } - - allPathsToCheck := getPossibleRolePaths(wd, tt.args.path) - sort.Strings(tt.want) - sort.Strings(allPathsToCheck) - if !reflect.DeepEqual(allPathsToCheck, tt.want) { - t.Errorf("Unexpected paths returned") - fmt.Println("Returned:") - for i, path := range allPathsToCheck { - fmt.Println(i, path) - } - fmt.Println("Wanted:") - for i, path := range tt.want { - fmt.Println(i, path) - } - } - }) - } -} diff --git a/internal/cmd/ansible-operator/run/cmd.go b/internal/cmd/ansible-operator/run/cmd.go deleted file mode 100644 index 9f20b96121..0000000000 --- a/internal/cmd/ansible-operator/run/cmd.go +++ /dev/null @@ -1,365 +0,0 @@ -// Copyright 2020 The Operator-SDK Authors -// -// 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 run - -import ( - "errors" - "flag" - "fmt" - "os" - "runtime" - "strconv" - "strings" - "time" - - "github.com/spf13/cobra" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/rest" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/cache" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/client/config" - "sigs.k8s.io/controller-runtime/pkg/healthz" - logf "sigs.k8s.io/controller-runtime/pkg/log" - zapf "sigs.k8s.io/controller-runtime/pkg/log/zap" - "sigs.k8s.io/controller-runtime/pkg/manager" - "sigs.k8s.io/controller-runtime/pkg/manager/signals" - crmetrics "sigs.k8s.io/controller-runtime/pkg/metrics" - - "github.com/operator-framework/operator-sdk/internal/ansible/apiserver" - "github.com/operator-framework/operator-sdk/internal/ansible/controller" - "github.com/operator-framework/operator-sdk/internal/ansible/events" - "github.com/operator-framework/operator-sdk/internal/ansible/flags" - "github.com/operator-framework/operator-sdk/internal/ansible/metrics" - "github.com/operator-framework/operator-sdk/internal/ansible/proxy" - "github.com/operator-framework/operator-sdk/internal/ansible/proxy/controllermap" - "github.com/operator-framework/operator-sdk/internal/ansible/runner" - "github.com/operator-framework/operator-sdk/internal/ansible/watches" - "github.com/operator-framework/operator-sdk/internal/util/k8sutil" - sdkVersion "github.com/operator-framework/operator-sdk/internal/version" -) - -var log = logf.Log.WithName("cmd") - -func printVersion() { - version := sdkVersion.GitVersion - if version == "unknown" { - version = sdkVersion.Version - } - log.Info("Version", - "Go Version", runtime.Version(), - "GOOS", runtime.GOOS, - "GOARCH", runtime.GOARCH, - "ansible-operator", version, - "commit", sdkVersion.GitCommit) -} - -func NewCmd() *cobra.Command { - f := &flags.Flags{} - zapfs := flag.NewFlagSet("zap", flag.ExitOnError) - opts := &zapf.Options{} - opts.BindFlags(zapfs) - - cmd := &cobra.Command{ - Use: "run", - Short: "Run the operator", - Run: func(cmd *cobra.Command, _ []string) { - logf.SetLogger(zapf.New(zapf.UseFlagOptions(opts))) - run(cmd, f) - }, - } - - f.AddTo(cmd.Flags()) - cmd.Flags().AddGoFlagSet(zapfs) - return cmd -} - -func run(cmd *cobra.Command, f *flags.Flags) { - printVersion() - metrics.RegisterBuildInfo(crmetrics.Registry) - - // Load config options from the config at f.ManagerConfigPath. - // These options will not override those set by flags. - var ( - options manager.Options - err error - ) - if f.ManagerConfigPath != "" { - cfgLoader := ctrl.ConfigFile().AtPath(f.ManagerConfigPath) - if options, err = options.AndFrom(cfgLoader); err != nil { - log.Error(err, "Unable to load the manager config file") - os.Exit(1) - } - } - exitIfUnsupported(options) - - cfg, err := config.GetConfig() - if err != nil { - log.Error(err, "Failed to get config.") - os.Exit(1) - } - - // TODO(2.0.0): remove - // Deprecated: OPERATOR_NAME environment variable is an artifact of the - // legacy operator-sdk project scaffolding. Flag `--leader-election-id` - // should be used instead. - if operatorName, found := os.LookupEnv("OPERATOR_NAME"); found { - log.Info("Environment variable OPERATOR_NAME has been deprecated, use --leader-election-id instead.") - if cmd.Flags().Changed("leader-election-id") { - log.Info("Ignoring OPERATOR_NAME environment variable since --leader-election-id is set") - } else if options.LeaderElectionID == "" { - // Only set leader election ID using OPERATOR_NAME if unset everywhere else, - // since this env var is deprecated. - options.LeaderElectionID = operatorName - } - } - - //TODO(2.0.0): remove the following checks. they are required just because of the flags deprecation - if cmd.Flags().Changed("leader-elect") && cmd.Flags().Changed("enable-leader-election") { - log.Error(errors.New("only one of --leader-elect and --enable-leader-election may be set"), "invalid flags usage") - os.Exit(1) - } - - if cmd.Flags().Changed("metrics-addr") && cmd.Flags().Changed("metrics-bind-address") { - log.Error(errors.New("only one of --metrics-addr and --metrics-bind-address may be set"), "invalid flags usage") - os.Exit(1) - } - - // Set default manager options - // TODO: probably should expose the host & port as an environment variables - options = f.ToManagerOptions(options) - if options.NewClient == nil { - options.NewClient = func(cache cache.Cache, config *rest.Config, options client.Options, uncachedObjects ...client.Object) (client.Client, error) { - // Create the Client for Write operations. - c, err := client.New(config, options) - if err != nil { - return nil, err - } - return client.NewDelegatingClient(client.NewDelegatingClientInput{ - CacheReader: cache, - Client: c, - UncachedObjects: uncachedObjects, - CacheUnstructured: true, - }) - } - } - - namespace, found := os.LookupEnv(k8sutil.WatchNamespaceEnvVar) - log = log.WithValues("Namespace", namespace) - if found { - log.V(1).Info(fmt.Sprintf("Setting namespace with value in %s", k8sutil.WatchNamespaceEnvVar)) - if namespace == metav1.NamespaceAll { - log.Info("Watching all namespaces.") - options.Namespace = metav1.NamespaceAll - } else { - if strings.Contains(namespace, ",") { - log.Info("Watching multiple namespaces.") - options.NewCache = cache.MultiNamespacedCacheBuilder(strings.Split(namespace, ",")) - } else { - log.Info("Watching single namespace.") - options.Namespace = namespace - } - } - } else if options.Namespace == "" { - log.Info(fmt.Sprintf("Watch namespaces not configured by environment variable %s or file. "+ - "Watching all namespaces.", k8sutil.WatchNamespaceEnvVar)) - options.Namespace = metav1.NamespaceAll - } - - err = setAnsibleEnvVars(f) - if err != nil { - log.Error(err, "Failed to set environment variable.") - os.Exit(1) - } - - // Create a new manager to provide shared dependencies and start components - mgr, err := manager.New(cfg, options) - if err != nil { - log.Error(err, "Failed to create a new manager.") - os.Exit(1) - } - - if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { - log.Error(err, "Unable to set up health check") - os.Exit(1) - } - if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { - log.Error(err, "Unable to set up ready check") - os.Exit(1) - } - - cMap := controllermap.NewControllerMap() - watches, err := watches.Load(f.WatchesFile, f.MaxConcurrentReconciles, f.AnsibleVerbosity) - if err != nil { - log.Error(err, "Failed to load watches.") - os.Exit(1) - } - for _, w := range watches { - reconcilePeriod := f.ReconcilePeriod - if w.ReconcilePeriod.Duration != time.Duration(0) { - // if a duration other than default was passed in through watches, - // it will take precedence over the command-line flag - reconcilePeriod = w.ReconcilePeriod.Duration - } - - runner, err := runner.New(w, f.AnsibleArgs) - if err != nil { - log.Error(err, "Failed to create runner") - os.Exit(1) - } - - ctr := controller.Add(mgr, controller.Options{ - GVK: w.GroupVersionKind, - Runner: runner, - ManageStatus: w.ManageStatus, - AnsibleDebugLogs: getAnsibleDebugLog(), - MaxConcurrentReconciles: w.MaxConcurrentReconciles, - ReconcilePeriod: reconcilePeriod, - Selector: w.Selector, - LoggingLevel: getAnsibleEventsToLog(f), - WatchAnnotationsChanges: w.WatchAnnotationsChanges, - }) - if ctr == nil { - log.Error(fmt.Errorf("failed to add controller for GVK %v", w.GroupVersionKind.String()), "") - os.Exit(1) - } - - cMap.Store(w.GroupVersionKind, &controllermap.Contents{Controller: *ctr, //nolint:staticcheck - WatchDependentResources: w.WatchDependentResources, - WatchClusterScopedResources: w.WatchClusterScopedResources, - OwnerWatchMap: controllermap.NewWatchMap(), - AnnotationWatchMap: controllermap.NewWatchMap(), - }, w.Blacklist) - } - - // TODO(2.0.0): remove - err = mgr.AddHealthzCheck("ping", healthz.Ping) - if err != nil { - log.Error(err, "Failed to add Healthz check.") - } - - done := make(chan error) - - // start the proxy - err = proxy.Run(done, proxy.Options{ - Address: "localhost", - Port: f.ProxyPort, - KubeConfig: mgr.GetConfig(), - Cache: mgr.GetCache(), - RESTMapper: mgr.GetRESTMapper(), - ControllerMap: cMap, - OwnerInjection: f.InjectOwnerRef, - WatchedNamespaces: strings.Split(namespace, ","), - }) - if err != nil { - log.Error(err, "Error starting proxy.") - os.Exit(1) - } - // start the ansible-operator api server - go func() { - err = apiserver.Run(apiserver.Options{ - Address: "localhost", - Port: 5050, - }) - done <- err - }() - - // start the operator - go func() { - done <- mgr.Start(signals.SetupSignalHandler()) - }() - - // wait for either to finish - err = <-done - if err != nil { - log.Error(err, "Proxy or operator exited with error.") - os.Exit(1) - } - log.Info("Exiting.") -} - -// exitIfUnsupported prints an error containing unsupported field names and exits -// if any of those fields are not their default values. -func exitIfUnsupported(options manager.Options) { - var keys []string - // The below options are webhook-specific, which is not supported by ansible. - if options.CertDir != "" { - keys = append(keys, "certDir") - } - if options.Host != "" { - keys = append(keys, "host") - } - if options.Port != 0 { - keys = append(keys, "port") - } - - if len(keys) > 0 { - log.Error(fmt.Errorf("%s set in manager options", strings.Join(keys, ", ")), "unsupported fields") - os.Exit(1) - } -} - -// getAnsibleDebugLog return the value from the ANSIBLE_DEBUG_LOGS it order to -// print the full Ansible logs -func getAnsibleDebugLog() bool { - const envVar = "ANSIBLE_DEBUG_LOGS" - val := false - if envVal, ok := os.LookupEnv(envVar); ok { - if i, err := strconv.ParseBool(envVal); err != nil { - log.Info("Could not parse environment variable as an boolean; using default value", - "envVar", envVar, "default", val) - } else { - val = i - } - } else if !ok { - log.Info("Environment variable not set; using default value", "envVar", envVar, - envVar, val) - } - return val -} - -// getAnsibleEventsToLog return the integer value of the log level set in the flag -func getAnsibleEventsToLog(f *flags.Flags) events.LogLevel { - if strings.ToLower(f.AnsibleLogEvents) == "everything" { - return events.Everything - } else if strings.ToLower(f.AnsibleLogEvents) == "nothing" { - return events.Nothing - } else { - if strings.ToLower(f.AnsibleLogEvents) != "tasks" && f.AnsibleLogEvents != "" { - log.Error(fmt.Errorf("--ansible-log-events flag value '%s' not recognized. Must be one of: Tasks, Everything, Nothing", f.AnsibleLogEvents), "unrecognized log level") - } - return events.Tasks // Tasks is the default - } -} - -// setAnsibleEnvVars will set environment variables based on CLI flags -func setAnsibleEnvVars(f *flags.Flags) error { - if len(f.AnsibleRolesPath) > 0 { - if err := os.Setenv(flags.AnsibleRolesPathEnvVar, f.AnsibleRolesPath); err != nil { - return fmt.Errorf("failed to set environment variable %s: %v", flags.AnsibleRolesPathEnvVar, err) - } - log.Info("Set the environment variable", "envVar", flags.AnsibleRolesPathEnvVar, - "value", f.AnsibleRolesPath) - } - - if len(f.AnsibleCollectionsPath) > 0 { - if err := os.Setenv(flags.AnsibleCollectionsPathEnvVar, f.AnsibleCollectionsPath); err != nil { - return fmt.Errorf("failed to set environment variable %s: %v", flags.AnsibleCollectionsPathEnvVar, err) - } - log.Info("Set the environment variable", "envVar", flags.AnsibleCollectionsPathEnvVar, - "value", f.AnsibleCollectionsPath) - } - return nil -} diff --git a/internal/cmd/ansible-operator/run/proxy_suite_test.go b/internal/cmd/ansible-operator/run/proxy_suite_test.go deleted file mode 100644 index 53094ef209..0000000000 --- a/internal/cmd/ansible-operator/run/proxy_suite_test.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2020 The Operator-SDK Authors -// -// 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 run - -import ( - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -func TestVersion(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "cmd suite") -} diff --git a/internal/cmd/ansible-operator/version/cmd.go b/internal/cmd/ansible-operator/version/cmd.go deleted file mode 100644 index 4c57673bbe..0000000000 --- a/internal/cmd/ansible-operator/version/cmd.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2019 The Operator-SDK Authors -// -// 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 version - -import ( - "fmt" - "runtime" - - "github.com/spf13/cobra" - - ver "github.com/operator-framework/operator-sdk/internal/version" -) - -func NewCmd() *cobra.Command { - versionCmd := &cobra.Command{ - Use: "version", - Short: "Prints the version of operator-sdk", - Run: func(cmd *cobra.Command, args []string) { - run() - }, - } - return versionCmd -} - -func run() { - version := ver.GitVersion - if version == "unknown" { - version = ver.Version - } - fmt.Printf("ansible-operator version: %q, commit: %q, kubernetes version: %q, go version: %q, GOOS: %q, GOARCH: %q\n", - version, ver.GitCommit, ver.KubernetesVersion, runtime.Version(), runtime.GOOS, runtime.GOARCH) -} diff --git a/internal/cmd/ansible-operator/version/cmd_test.go b/internal/cmd/ansible-operator/version/cmd_test.go deleted file mode 100644 index ec65551013..0000000000 --- a/internal/cmd/ansible-operator/version/cmd_test.go +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright 2020 The Operator-SDK Authors -// -// 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 version - -import ( - "fmt" - "io" - "os" - "runtime" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - ver "github.com/operator-framework/operator-sdk/internal/version" -) - -var _ = Describe("Running a version command", func() { - Describe("NewCmd", func() { - It("builds a cobra command", func() { - cmd := NewCmd() - Expect(cmd).NotTo(BeNil()) - Expect(cmd.Use).NotTo(Equal("")) - Expect(cmd.Short).NotTo(Equal("")) - }) - }) - Describe("run", func() { - It("prints the correct version info", func() { - r, w, _ := os.Pipe() - tmp := os.Stdout - defer func() { - os.Stdout = tmp - }() - os.Stdout = w - go func() { - run() - w.Close() - }() - stdout, err := io.ReadAll(r) - Expect(err).ToNot(HaveOccurred()) - stdoutString := string(stdout) - version := ver.GitVersion - if version == "unknown" { - version = ver.Version - } - Expect(stdoutString).To(ContainSubstring(fmt.Sprintf("version: %q", version))) - Expect(stdoutString).To(ContainSubstring(fmt.Sprintf("commit: %q", ver.GitCommit))) - Expect(stdoutString).To(ContainSubstring(fmt.Sprintf("kubernetes version: %q", ver.KubernetesVersion))) - Expect(stdoutString).To(ContainSubstring(fmt.Sprintf("go version: %q", runtime.Version()))) - Expect(stdoutString).To(ContainSubstring(fmt.Sprintf("GOOS: %q", runtime.GOOS))) - Expect(stdoutString).To(ContainSubstring(fmt.Sprintf("GOARCH: %q", runtime.GOARCH))) - }) - }) -}) diff --git a/internal/cmd/ansible-operator/version/version_suite_test.go b/internal/cmd/ansible-operator/version/version_suite_test.go deleted file mode 100644 index 37c17fdde1..0000000000 --- a/internal/cmd/ansible-operator/version/version_suite_test.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2020 The Operator-SDK Authors -// -// 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 version_test - -import ( - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -func TestVersion(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Version Cmd Suite") -} diff --git a/internal/plugins/ansible/v1/api.go b/internal/plugins/ansible/v1/api.go deleted file mode 100644 index 47c35b8584..0000000000 --- a/internal/plugins/ansible/v1/api.go +++ /dev/null @@ -1,167 +0,0 @@ -// Copyright 2020 The Operator-SDK Authors -// -// 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 ansible - -import ( - "errors" - "fmt" - - "github.com/sirupsen/logrus" - "github.com/spf13/pflag" - "sigs.k8s.io/kubebuilder/v3/pkg/config" - "sigs.k8s.io/kubebuilder/v3/pkg/machinery" - "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" - "sigs.k8s.io/kubebuilder/v3/pkg/plugin" - pluginutil "sigs.k8s.io/kubebuilder/v3/pkg/plugin/util" - - "github.com/operator-framework/operator-sdk/internal/plugins/ansible/v1/scaffolds" - "github.com/operator-framework/operator-sdk/internal/plugins/util" -) - -const ( - crdVersionFlag = "crd-version" - generatePlaybookFlag = "generate-playbook" - generateRoleFlag = "generate-role" - - defaultCrdVersion = "v1" - legacyCrdVersion = "v1beta1" -) - -type createAPIOptions struct { - CRDVersion string - DoRole, DoPlaybook bool -} - -func (opts createAPIOptions) UpdateResource(res *resource.Resource) { - res.API = &resource.API{ - CRDVersion: opts.CRDVersion, - Namespaced: true, - } - - // Ensure that Path is empty and Controller false as this is not a Go project - res.Path = "" - res.Controller = false -} - -var _ plugin.CreateAPISubcommand = &createAPISubcommand{} - -type createAPISubcommand struct { - config config.Config - resource *resource.Resource - options createAPIOptions -} - -func (p *createAPISubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) { - subcmdMeta.Description = `Scaffold a Kubernetes API in which the controller is an Ansible role or playbook. - - - generates a Custom Resource Definition and sample - - Updates watches.yaml - - optionally generates Ansible Role tree - - optionally generates Ansible playbook - - For the scaffolded operator to be runnable with no changes, specify either --generate-role or --generate-playbook. - -` - subcmdMeta.Examples = fmt.Sprintf(`# Create a new API, without Ansible roles or playbooks - $ %[1]s create api \ - --group=apps --version=v1alpha1 \ - --kind=AppService - - $ %[1]s create api \ - --group=apps --version=v1alpha1 \ - --kind=AppService \ - --generate-role - - $ %[1]s create api \ - --group=apps --version=v1alpha1 \ - --kind=AppService \ - --generate-playbook - - $ %[1]s create api \ - --group=apps --version=v1alpha1 \ - --kind=AppService - --generate-playbook - --generate-role -`, cliMeta.CommandName) -} - -func (p *createAPISubcommand) BindFlags(fs *pflag.FlagSet) { - fs.SortFlags = false - fs.StringVar(&p.options.CRDVersion, crdVersionFlag, defaultCrdVersion, "crd version to generate") - // (not required raise an error in this case) - // nolint:errcheck,gosec - fs.MarkDeprecated(crdVersionFlag, util.WarnMessageRemovalV1beta1) - - fs.BoolVar(&p.options.DoRole, generateRoleFlag, false, "Generate an Ansible role skeleton.") - fs.BoolVar(&p.options.DoPlaybook, generatePlaybookFlag, false, "Generate an Ansible playbook. If passed with --generate-role, the playbook will invoke the role.") -} - -func (p *createAPISubcommand) InjectConfig(c config.Config) error { - p.config = c - - return nil -} - -func (p *createAPISubcommand) PreScaffold(machinery.Filesystem) error { - if p.options.CRDVersion == legacyCrdVersion { - logrus.Warn(util.WarnMessageRemovalV1beta1) - } - return nil -} - -func (p *createAPISubcommand) InjectResource(res *resource.Resource) error { - p.resource = res - - p.options.UpdateResource(p.resource) - - if err := p.resource.Validate(); err != nil { - return err - } - - // Check that resource doesn't have the API scaffolded - if res, err := p.config.GetResource(p.resource.GVK); err == nil && res.HasAPI() { - return errors.New("the API resource already exists") - } - - // Check that the provided group can be added to the project - if !p.config.IsMultiGroup() && p.config.ResourcesLength() != 0 && !p.config.HasGroup(p.resource.Group) { - return fmt.Errorf("multiple groups are not allowed by default, to enable multi-group set 'multigroup: true' in your PROJECT file") - } - - // Selected CRD version must match existing CRD versions. - // nolint:staticcheck - if pluginutil.HasDifferentCRDVersion(p.config, p.resource.API.CRDVersion) { - return fmt.Errorf("only one CRD version can be used for all resources, cannot add %q", p.resource.API.CRDVersion) - } - - return nil -} - -func (p *createAPISubcommand) Scaffold(fs machinery.Filesystem) error { - if err := util.RemoveKustomizeCRDManifests(); err != nil { - return fmt.Errorf("error removing kustomization CRD manifests: %v", err) - } - if err := util.UpdateKustomizationsCreateAPI(); err != nil { - return fmt.Errorf("error updating kustomization.yaml files: %v", err) - } - - scaffolder := scaffolds.NewCreateAPIScaffolder(p.config, *p.resource, p.options.DoRole, p.options.DoPlaybook) - scaffolder.InjectFS(fs) - if err := scaffolder.Scaffold(); err != nil { - return err - } - - return nil -} diff --git a/internal/plugins/ansible/v1/constants/constants.go b/internal/plugins/ansible/v1/constants/constants.go deleted file mode 100644 index 91aa6a6f0c..0000000000 --- a/internal/plugins/ansible/v1/constants/constants.go +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// 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 constants - -import ( - "path/filepath" -) - -const ( - filePathSep = string(filepath.Separator) - RolesDir = "roles" - PlaybooksDir = "playbooks" - MoleculeDir = "molecule" - MoleculeDefaultDir = MoleculeDir + filePathSep + "default" - MoleculeTestLocalDir = MoleculeDir + filePathSep + "test-local" - MoleculeClusterDir = MoleculeDir + filePathSep + "cluster" - MoleculeTemplatesDir = MoleculeDir + filePathSep + "templates" -) diff --git a/internal/plugins/ansible/v1/init.go b/internal/plugins/ansible/v1/init.go deleted file mode 100644 index 2cea84be89..0000000000 --- a/internal/plugins/ansible/v1/init.go +++ /dev/null @@ -1,267 +0,0 @@ -// Copyright 2020 The Operator-SDK Authors -// -// 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 ansible - -import ( - "fmt" - "os" - "path/filepath" - - "github.com/spf13/pflag" - "sigs.k8s.io/kubebuilder/v3/pkg/config" - "sigs.k8s.io/kubebuilder/v3/pkg/machinery" - "sigs.k8s.io/kubebuilder/v3/pkg/plugin" - "sigs.k8s.io/kubebuilder/v3/pkg/plugin/util" - - "github.com/operator-framework/operator-sdk/internal/plugins/ansible/v1/scaffolds" - sdkpluginutil "github.com/operator-framework/operator-sdk/internal/plugins/util" -) - -const ( - groupFlag = "group" - versionFlag = "version" - kindFlag = "kind" -) - -var _ plugin.InitSubcommand = &initSubcommand{} - -type initSubcommand struct { - // Wrapped plugin that we will call at post-scaffold - apiSubcommand createAPISubcommand - - config config.Config - - // For help text. - commandName string - - // Flags - group string - version string - kind string -} - -// UpdateContext injects documentation for the command -func (p *initSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) { - subcmdMeta.Description = ` -Initialize a new Ansible-based operator project. - -Writes the following files -- a kubebuilder PROJECT file with the domain and project layout configuration -- a Makefile that provides an interface for building and managing the operator -- Kubernetes manifests and kustomize configuration -- a watches.yaml file that defines the mapping between APIs and Roles/Playbooks - -Optionally creates a new API, using the same flags as "create api" -` - subcmdMeta.Examples = fmt.Sprintf(` - # Scaffold a project with no API - $ %[1]s init --plugins=%[2]s --domain=my.domain \ - - # Invokes "create api" - $ %[1]s init --plugins=%[2]s \ - --domain=my.domain \ - --group=apps --version=v1alpha1 --kind=AppService - - $ %[1]s init --plugins=%[2]s \ - --domain=my.domain \ - --group=apps --version=v1alpha1 --kind=AppService \ - --generate-role - - $ %[1]s init --plugins=%[2]s \ - --domain=my.domain \ - --group=apps --version=v1alpha1 --kind=AppService \ - --generate-playbook - - $ %[1]s init --plugins=%[2]s \ - --domain=my.domain \ - --group=apps --version=v1alpha1 --kind=AppService \ - --generate-playbook \ - --generate-role -`, cliMeta.CommandName, pluginKey) - - p.commandName = cliMeta.CommandName -} - -func (p *initSubcommand) BindFlags(fs *pflag.FlagSet) { - fs.SortFlags = false - fs.StringVar(&p.group, "group", "", "resource Group") - fs.StringVar(&p.version, "version", "", "resource Version") - fs.StringVar(&p.kind, "kind", "", "resource Kind") - p.apiSubcommand.BindFlags(fs) -} - -func (p *initSubcommand) InjectConfig(c config.Config) error { - p.config = c - return nil -} - -func (p *initSubcommand) Scaffold(fs machinery.Filesystem) error { - - if err := addInitCustomizations(p.config.GetProjectName(), p.config.IsComponentConfig()); err != nil { - return fmt.Errorf("error updating init manifests: %s", err) - } - - scaffolder := scaffolds.NewInitScaffolder(p.config) - scaffolder.InjectFS(fs) - return scaffolder.Scaffold() -} - -func (p *initSubcommand) PostScaffold() error { - doAPI := p.group != "" || p.version != "" || p.kind != "" - if !doAPI { - fmt.Printf("Next: define a resource with:\n$ %s create api\n", p.commandName) - } else { - args := []string{"create", "api"} - // The following three checks should match the default values in sig.k8s.io/kubebuilder/v3/pkg/cli/resource.go - if p.group != "" { - args = append(args, fmt.Sprintf("--%s", groupFlag), p.group) - } - if p.version != "" { - args = append(args, fmt.Sprintf("--%s", versionFlag), p.version) - } - if p.kind != "" { - args = append(args, fmt.Sprintf("--%s", kindFlag), p.kind) - } - if p.apiSubcommand.options.CRDVersion != defaultCrdVersion { - args = append(args, fmt.Sprintf("--%s", crdVersionFlag), p.apiSubcommand.options.CRDVersion) - } - if p.apiSubcommand.options.DoPlaybook { - args = append(args, fmt.Sprintf("--%s", generatePlaybookFlag)) - } - if p.apiSubcommand.options.DoRole { - args = append(args, fmt.Sprintf("--%s", generateRoleFlag)) - } - if err := util.RunCmd("Creating the API", os.Args[0], args...); err != nil { - return err - } - } - - return nil -} - -// addInitCustomizations will perform the required customizations for this plugin on the common base -func addInitCustomizations(projectName string, componentConfig bool) error { - managerFile := filepath.Join("config", "manager", "manager.yaml") - managerProxyPatchFile := filepath.Join("config", "default", "manager_auth_proxy_patch.yaml") - - // todo: we ought to use afero instead. Replace this methods to insert/update - // by https://github.com/kubernetes-sigs/kubebuilder/pull/2119 - - // Add leader election - if componentConfig { - err := util.InsertCode(managerFile, - "- /manager", - fmt.Sprintf("\n args:\n - --leader-election-id=%s", projectName)) - if err != nil { - return err - } - - err = util.InsertCode(managerProxyPatchFile, - "memory: 64Mi", - fmt.Sprintf("\n - name: manager\n args:\n - \"--leader-election-id=%s\"", projectName)) - if err != nil { - return err - } - } else { - err := util.InsertCode(managerFile, - "--leader-elect", - fmt.Sprintf("\n - --leader-election-id=%s", projectName)) - if err != nil { - return err - } - err = util.InsertCode(managerProxyPatchFile, - "- \"--leader-elect\"", - fmt.Sprintf("\n - \"--leader-election-id=%s\"", projectName)) - if err != nil { - return err - } - } - - // update default resource request and limits with bigger values - const resourcesLimitsFragment = ` resources: - limits: - cpu: 500m - memory: 128Mi - requests: - cpu: 10m - memory: 64Mi - ` - - const resourcesLimitsAnsibleFragment = ` resources: - limits: - cpu: 500m - memory: 768Mi - requests: - cpu: 10m - memory: 256Mi - ` - - err := util.ReplaceInFile(managerFile, resourcesLimitsFragment, resourcesLimitsAnsibleFragment) - if err != nil { - return err - } - - // Add ANSIBLE_GATHERING env var - const envVar = ` - env: - - name: ANSIBLE_GATHERING - value: explicit` - err = util.InsertCode(managerFile, "name: manager", envVar) - if err != nil { - return err - } - - // replace the default ports because ansible has been using another one - // todo: remove it when we be able to change the port for the default one - // issue: https://github.com/operator-framework/operator-sdk/issues/4331 - err = util.ReplaceInFile(managerFile, "port: 8081", "port: 6789") - if err != nil { - return err - } - - if componentConfig { - managerConfigFile := filepath.Join("config", "manager", "controller_manager_config.yaml") - err = util.ReplaceInFile(managerConfigFile, "8081", "6789") - if err != nil { - return err - } - // Remove the webhook option for the componentConfig since webhooks are not supported by ansible - err = util.ReplaceInFile(managerConfigFile, "webhook:\n port: 9443", "") - if err != nil { - return err - } - } else { - err = util.ReplaceInFile(managerProxyPatchFile, "8081", "6789") - if err != nil { - return err - } - } - - // Remove the call to the command as manager. Helm/Ansible has not been exposing this entrypoint - // todo: provide the manager entrypoint for helm/ansible and then remove it - const command = `command: - - /manager - ` - err = util.ReplaceInFile(managerFile, command, "") - if err != nil { - return err - } - - if err := sdkpluginutil.UpdateKustomizationsInit(); err != nil { - return fmt.Errorf("error updating kustomization.yaml files: %v", err) - } - - return nil -} diff --git a/internal/plugins/ansible/v1/plugin.go b/internal/plugins/ansible/v1/plugin.go deleted file mode 100644 index 7ae087cee5..0000000000 --- a/internal/plugins/ansible/v1/plugin.go +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2020 The Operator-SDK Authors -// -// 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 ansible - -import ( - "sigs.k8s.io/kubebuilder/v3/pkg/config" - cfgv3 "sigs.k8s.io/kubebuilder/v3/pkg/config/v3" - "sigs.k8s.io/kubebuilder/v3/pkg/plugin" - - "github.com/operator-framework/operator-sdk/internal/plugins" -) - -const pluginName = "base.ansible" + plugins.DefaultNameQualifier - -var ( - pluginVersion = plugin.Version{Number: 1} - supportedProjectVersions = []config.Version{cfgv3.Version} - pluginKey = plugin.KeyFor(Plugin{}) -) - -var ( - _ plugin.Plugin = Plugin{} - _ plugin.Init = Plugin{} - _ plugin.CreateAPI = Plugin{} -) - -type Plugin struct { - initSubcommand - createAPISubcommand -} - -func (Plugin) Name() string { return pluginName } -func (Plugin) Version() plugin.Version { return pluginVersion } -func (Plugin) SupportedProjectVersions() []config.Version { return supportedProjectVersions } -func (p Plugin) GetInitSubcommand() plugin.InitSubcommand { return &p.initSubcommand } -func (p Plugin) GetCreateAPISubcommand() plugin.CreateAPISubcommand { return &p.createAPISubcommand } diff --git a/internal/plugins/ansible/v1/scaffolds/api.go b/internal/plugins/ansible/v1/scaffolds/api.go deleted file mode 100644 index f6902bf23e..0000000000 --- a/internal/plugins/ansible/v1/scaffolds/api.go +++ /dev/null @@ -1,108 +0,0 @@ -/* -Copyright 2019 The Kubernetes Authors. -Modifications copyright 2020 The Operator-SDK Authors - -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 scaffolds - -import ( - "sigs.k8s.io/kubebuilder/v3/pkg/config" - "sigs.k8s.io/kubebuilder/v3/pkg/machinery" - "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" - "sigs.k8s.io/kubebuilder/v3/pkg/plugins" - - "github.com/operator-framework/operator-sdk/internal/plugins/ansible/v1/constants" - "github.com/operator-framework/operator-sdk/internal/plugins/ansible/v1/scaffolds/internal/templates" - "github.com/operator-framework/operator-sdk/internal/plugins/ansible/v1/scaffolds/internal/templates/config/crd" - "github.com/operator-framework/operator-sdk/internal/plugins/ansible/v1/scaffolds/internal/templates/config/rbac" - "github.com/operator-framework/operator-sdk/internal/plugins/ansible/v1/scaffolds/internal/templates/molecule/mdefault" - "github.com/operator-framework/operator-sdk/internal/plugins/ansible/v1/scaffolds/internal/templates/playbooks" - ansibleroles "github.com/operator-framework/operator-sdk/internal/plugins/ansible/v1/scaffolds/internal/templates/roles" -) - -var _ plugins.Scaffolder = &apiScaffolder{} - -type apiScaffolder struct { - fs machinery.Filesystem - - config config.Config - resource resource.Resource - - doRole, doPlaybook bool -} - -// NewCreateAPIScaffolder returns a new plugins.Scaffolder for project initialization operations -func NewCreateAPIScaffolder(cfg config.Config, res resource.Resource, doRole, doPlaybook bool) plugins.Scaffolder { - return &apiScaffolder{ - config: cfg, - resource: res, - doRole: doRole, - doPlaybook: doPlaybook, - } -} - -// InjectFS implements plugins.Scaffolder -func (s *apiScaffolder) InjectFS(fs machinery.Filesystem) { - s.fs = fs -} - -// Scaffold implements plugins.Scaffolder -func (s *apiScaffolder) Scaffold() error { - if err := s.config.UpdateResource(s.resource); err != nil { - return err - } - - // Initialize the machinery.Scaffold that will write the files to disk - scaffold := machinery.NewScaffold(s.fs, - // NOTE: kubebuilder's default permissions are only for root users - machinery.WithDirectoryPermissions(0755), - machinery.WithFilePermissions(0644), - machinery.WithConfig(s.config), - machinery.WithResource(&s.resource), - ) - - createAPITemplates := []machinery.Builder{ - &rbac.ManagerRoleUpdater{}, - &crd.CRD{}, - &crd.Kustomization{}, - &templates.WatchesUpdater{ - GeneratePlaybook: s.doPlaybook, - GenerateRole: s.doRole, - PlaybooksDir: constants.PlaybooksDir, - }, - &mdefault.ResourceTest{}, - } - - if s.doRole { - createAPITemplates = append(createAPITemplates, - &ansibleroles.TasksMain{}, - &ansibleroles.DefaultsMain{}, - &ansibleroles.RoleFiles{}, - &ansibleroles.HandlersMain{}, - &ansibleroles.MetaMain{}, - &ansibleroles.RoleTemplates{}, - &ansibleroles.VarsMain{}, - &ansibleroles.Readme{}, - ) - } - - if s.doPlaybook { - createAPITemplates = append(createAPITemplates, - &playbooks.Playbook{GenerateRole: s.doRole}, - ) - } - - return scaffold.Execute(createAPITemplates...) -} diff --git a/internal/plugins/ansible/v1/scaffolds/init.go b/internal/plugins/ansible/v1/scaffolds/init.go deleted file mode 100644 index b3ab4f1c77..0000000000 --- a/internal/plugins/ansible/v1/scaffolds/init.go +++ /dev/null @@ -1,103 +0,0 @@ -/* -Copyright 2019 The Kubernetes Authors. -Modifications copyright 2020 The Operator-SDK Authors - -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 scaffolds - -import ( - "sigs.k8s.io/kubebuilder/v3/pkg/config" - "sigs.k8s.io/kubebuilder/v3/pkg/machinery" - "sigs.k8s.io/kubebuilder/v3/pkg/plugins" - kustomizev2Alpha "sigs.k8s.io/kubebuilder/v3/pkg/plugins/common/kustomize/v2-alpha" - - "github.com/operator-framework/operator-sdk/internal/plugins/ansible/v1/scaffolds/internal/templates" - "github.com/operator-framework/operator-sdk/internal/plugins/ansible/v1/scaffolds/internal/templates/config/rbac" - "github.com/operator-framework/operator-sdk/internal/plugins/ansible/v1/scaffolds/internal/templates/config/testing" - "github.com/operator-framework/operator-sdk/internal/plugins/ansible/v1/scaffolds/internal/templates/config/testing/pullpolicy" - "github.com/operator-framework/operator-sdk/internal/plugins/ansible/v1/scaffolds/internal/templates/molecule/mdefault" - "github.com/operator-framework/operator-sdk/internal/plugins/ansible/v1/scaffolds/internal/templates/molecule/mkind" - "github.com/operator-framework/operator-sdk/internal/plugins/ansible/v1/scaffolds/internal/templates/playbooks" - "github.com/operator-framework/operator-sdk/internal/plugins/ansible/v1/scaffolds/internal/templates/roles" - "github.com/operator-framework/operator-sdk/internal/version" -) - -const imageName = "controller:latest" - -// ansibleOperatorVersion is set to the version of ansible-operator at compile-time. -var ansibleOperatorVersion = version.ImageVersion - -var _ plugins.Scaffolder = &initScaffolder{} - -type initScaffolder struct { - fs machinery.Filesystem - - config config.Config -} - -// NewInitScaffolder returns a new plugins.Scaffolder for project initialization operations -func NewInitScaffolder(config config.Config) plugins.Scaffolder { - return &initScaffolder{ - config: config, - } -} - -// InjectFS implements plugins.Scaffolder -func (s *initScaffolder) InjectFS(fs machinery.Filesystem) { - s.fs = fs -} - -// Scaffold implements plugins.Scaffolder -func (s *initScaffolder) Scaffold() error { - // Initialize the machinery.Scaffold that will write the files to disk - scaffold := machinery.NewScaffold(s.fs, - // NOTE: kubebuilder's default permissions are only for root users - machinery.WithDirectoryPermissions(0755), - machinery.WithFilePermissions(0644), - machinery.WithConfig(s.config), - ) - - return scaffold.Execute( - &templates.Dockerfile{AnsibleOperatorVersion: ansibleOperatorVersion}, - &templates.Makefile{ - Image: imageName, - KustomizeVersion: kustomizev2Alpha.KustomizeVersion, - AnsibleOperatorVersion: ansibleOperatorVersion, - }, - &templates.GitIgnore{}, - &templates.RequirementsYml{}, - &templates.Watches{}, - &rbac.ManagerRole{}, - &roles.Placeholder{}, - &playbooks.Placeholder{}, - &mdefault.Converge{}, - &mdefault.Create{}, - &mdefault.Destroy{}, - &mdefault.Kustomize{}, - &mdefault.Molecule{}, - &mdefault.Prepare{}, - &mdefault.Verify{}, - &mkind.Converge{}, - &mkind.Create{}, - &mkind.Destroy{}, - &mkind.Molecule{}, - &pullpolicy.AlwaysPullPatch{}, - &pullpolicy.IfNotPresentPullPatch{}, - &pullpolicy.NeverPullPatch{}, - &testing.DebugLogsPatch{}, - &testing.Kustomization{}, - &testing.ManagerImage{}, - ) -} diff --git a/internal/plugins/ansible/v1/scaffolds/internal/templates/config/crd/crd.go b/internal/plugins/ansible/v1/scaffolds/internal/templates/config/crd/crd.go deleted file mode 100644 index 85f81da328..0000000000 --- a/internal/plugins/ansible/v1/scaffolds/internal/templates/config/crd/crd.go +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright 2020 The Operator-SDK Authors -// -// 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 crd - -import ( - "fmt" - "path/filepath" - - "github.com/kr/text" - "sigs.k8s.io/kubebuilder/v3/pkg/machinery" -) - -var _ machinery.Template = &CRD{} - -// CRD scaffolds a manifest for CRD sample. -type CRD struct { - machinery.TemplateMixin - machinery.ResourceMixin -} - -// SetTemplateDefaults implements machinery.Template -func (f *CRD) SetTemplateDefaults() error { - if f.Path == "" { - f.Path = filepath.Join("config", "crd", "bases", fmt.Sprintf("%s_%%[plural].yaml", f.Resource.QualifiedGroup())) - } - f.Path = f.Resource.Replacer().Replace(f.Path) - - f.IfExistsAction = machinery.Error - - f.TemplateBody = fmt.Sprintf(crdTemplate, - text.Indent(openAPIV3SchemaTemplate, " "), - text.Indent(openAPIV3SchemaTemplate, " "), - ) - - return nil -} - -const crdTemplate = `--- -apiVersion: apiextensions.k8s.io/{{ .Resource.API.CRDVersion }} -kind: CustomResourceDefinition -metadata: - name: {{ .Resource.Plural }}.{{ .Resource.QualifiedGroup }} -spec: - group: {{ .Resource.QualifiedGroup }} - names: - kind: {{ .Resource.Kind }} - listKind: {{ .Resource.Kind }}List - plural: {{ .Resource.Plural }} - singular: {{ .Resource.Kind | lower }} - scope: Namespaced -{{- if eq .Resource.API.CRDVersion "v1beta1" }} - subresources: - status: {} - validation: -%s -{{- end }} - versions: - - name: {{ .Resource.Version }} -{{- if eq .Resource.API.CRDVersion "v1" }} - schema: -%s -{{- end }} - served: true - storage: true -{{- if eq .Resource.API.CRDVersion "v1" }} - subresources: - status: {} -{{- end }} -` - -const openAPIV3SchemaTemplate = `openAPIV3Schema: - description: {{ .Resource.Kind }} is the Schema for the {{ .Resource.Plural }} 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: Spec defines the desired state of {{ .Resource.Kind }} - type: object - x-kubernetes-preserve-unknown-fields: true - status: - description: Status defines the observed state of {{ .Resource.Kind }} - type: object - x-kubernetes-preserve-unknown-fields: true - type: object -` diff --git a/internal/plugins/ansible/v1/scaffolds/internal/templates/config/crd/kustomization.go b/internal/plugins/ansible/v1/scaffolds/internal/templates/config/crd/kustomization.go deleted file mode 100644 index 871c00e196..0000000000 --- a/internal/plugins/ansible/v1/scaffolds/internal/templates/config/crd/kustomization.go +++ /dev/null @@ -1,80 +0,0 @@ -/* -Copyright 2019 The Kubernetes Authors. -Modifications copyright 2020 The Operator-SDK Authors - -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 crd - -import ( - "fmt" - "path/filepath" - - "sigs.k8s.io/kubebuilder/v3/pkg/machinery" -) - -var ( - _ machinery.Template = &Kustomization{} - _ machinery.Inserter = &Kustomization{} -) - -// Kustomization scaffolds the kustomization file in manager folder. -type Kustomization struct { - machinery.TemplateMixin - machinery.ResourceMixin -} - -// SetTemplateDefaults implements machinery.Template -func (f *Kustomization) SetTemplateDefaults() error { - if f.Path == "" { - f.Path = filepath.Join("config", "crd", "kustomization.yaml") - } - - f.TemplateBody = fmt.Sprintf(kustomizationTemplate, machinery.NewMarkerFor(f.Path, resourceMarker)) - - return nil -} - -const ( - resourceMarker = "crdkustomizeresource" -) - -// GetMarkers implements machinery.Inserter -func (f *Kustomization) GetMarkers() []machinery.Marker { - return []machinery.Marker{ - machinery.NewMarkerFor(f.Path, resourceMarker), - } -} - -const ( - resourceCodeFragment = `- bases/%s_%s.yaml -` -) - -// GetCodeFragments implements machinery.Inserter -func (f *Kustomization) GetCodeFragments() machinery.CodeFragmentsMap { - return machinery.CodeFragmentsMap{ - // Generate resource code fragments - machinery.NewMarkerFor(f.Path, resourceMarker): []string{ - fmt.Sprintf(resourceCodeFragment, f.Resource.QualifiedGroup(), f.Resource.Plural), - }, - } -} - -var kustomizationTemplate = `# This kustomization.yaml is not intended to be run by itself, -# since it depends on service name and namespace that are out of this kustomize package. -# It should be run by config/default -resources: -%s -` diff --git a/internal/plugins/ansible/v1/scaffolds/internal/templates/config/rbac/role.go b/internal/plugins/ansible/v1/scaffolds/internal/templates/config/rbac/role.go deleted file mode 100644 index e7cc3bea50..0000000000 --- a/internal/plugins/ansible/v1/scaffolds/internal/templates/config/rbac/role.go +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright 2019 The Operator-SDK Authors -// -// 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 rbac - -import ( - "bytes" - "fmt" - "path/filepath" - "text/template" - - "sigs.k8s.io/kubebuilder/v3/pkg/machinery" -) - -var _ machinery.Template = &ManagerRole{} - -var defaultRoleFile = filepath.Join("config", "rbac", "role.yaml") - -// ManagerRole scaffolds the role.yaml file -type ManagerRole struct { - machinery.TemplateMixin -} - -// SetTemplateDefaults implements machinery.Template -func (f *ManagerRole) SetTemplateDefaults() error { - if f.Path == "" { - f.Path = defaultRoleFile - } - - f.TemplateBody = fmt.Sprintf(roleTemplate, machinery.NewMarkerFor(f.Path, rulesMarker)) - - return nil -} - -var _ machinery.Inserter = &ManagerRoleUpdater{} - -type ManagerRoleUpdater struct { - machinery.ResourceMixin - - SkipDefaultRules bool -} - -func (*ManagerRoleUpdater) GetPath() string { - return defaultRoleFile -} - -func (*ManagerRoleUpdater) GetIfExistsAction() machinery.IfExistsAction { - return machinery.OverwriteFile -} - -const ( - rulesMarker = "rules" -) - -func (f *ManagerRoleUpdater) GetMarkers() []machinery.Marker { - return []machinery.Marker{ - machinery.NewMarkerFor(defaultRoleFile, rulesMarker), - } -} - -func (f *ManagerRoleUpdater) GetCodeFragments() machinery.CodeFragmentsMap { - fragments := make(machinery.CodeFragmentsMap, 1) - - // If resource is not being provided we are creating the file, not updating it - if f.Resource == nil { - return fragments - } - - buf := &bytes.Buffer{} - tmpl := template.Must(template.New("rules").Parse(rulesFragment)) - err := tmpl.Execute(buf, f) - if err != nil { - panic(err) - } - - // Generate rule fragment - rules := []string{buf.String()} - - if len(rules) != 0 { - fragments[machinery.NewMarkerFor(defaultRoleFile, rulesMarker)] = rules - } - return fragments -} - -const roleTemplate = `--- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: manager-role -rules: - ## - ## Base operator rules - ## - - apiGroups: - - "" - resources: - - secrets - - pods - - pods/exec - - pods/log - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - - apiGroups: - - apps - resources: - - deployments - - daemonsets - - replicasets - - statefulsets - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -%s -` - -const rulesFragment = ` ## - ## Rules for {{.Resource.QualifiedGroup}}/{{.Resource.Version}}, Kind: {{.Resource.Kind}} - ## - - apiGroups: - - {{.Resource.QualifiedGroup}} - resources: - - {{.Resource.Plural}} - - {{.Resource.Plural}}/status - - {{.Resource.Plural}}/finalizers - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -` diff --git a/internal/plugins/ansible/v1/scaffolds/internal/templates/config/testing/debug_logs_patch.go b/internal/plugins/ansible/v1/scaffolds/internal/templates/config/testing/debug_logs_patch.go deleted file mode 100644 index 6db9b26054..0000000000 --- a/internal/plugins/ansible/v1/scaffolds/internal/templates/config/testing/debug_logs_patch.go +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2020 The Operator-SDK Authors -// -// 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 testing - -import ( - "path/filepath" - - "sigs.k8s.io/kubebuilder/v3/pkg/machinery" -) - -var _ machinery.Template = &DebugLogsPatch{} - -// DebugLogsPatch scaffolds the patch file for enabling -// verbose logs during Ansible testing -type DebugLogsPatch struct { - machinery.TemplateMixin -} - -// SetTemplateDefaults implements machinery.Template -func (f *DebugLogsPatch) SetTemplateDefaults() error { - if f.Path == "" { - f.Path = filepath.Join("config", "testing", "debug_logs_patch.yaml") - } - - f.TemplateBody = debugLogsPatchTemplate - - f.IfExistsAction = machinery.Error - - return nil -} - -const debugLogsPatchTemplate = `--- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: controller-manager - namespace: system -spec: - template: - spec: - containers: - - name: manager - env: - - name: ANSIBLE_DEBUG_LOGS - value: "TRUE" -` diff --git a/internal/plugins/ansible/v1/scaffolds/internal/templates/config/testing/kustomization.go b/internal/plugins/ansible/v1/scaffolds/internal/templates/config/testing/kustomization.go deleted file mode 100644 index 95bbd099d5..0000000000 --- a/internal/plugins/ansible/v1/scaffolds/internal/templates/config/testing/kustomization.go +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2020 The Operator-SDK Authors -// -// 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 testing - -import ( - "path/filepath" - - "sigs.k8s.io/kubebuilder/v3/pkg/machinery" -) - -var _ machinery.Template = &Kustomization{} - -// Kustomization scaffolds the kustomization file for use -// during Ansible testing -type Kustomization struct { - machinery.TemplateMixin -} - -// SetTemplateDefaults implements machinery.Template -func (f *Kustomization) SetTemplateDefaults() error { - if f.Path == "" { - f.Path = filepath.Join("config", "testing", "kustomization.yaml") - } - - f.TemplateBody = KustomizationTemplate - - f.IfExistsAction = machinery.Error - - return nil -} - -const KustomizationTemplate = `# Adds namespace to all resources. -namespace: osdk-test - -namePrefix: osdk- - -# Labels to add to all resources and selectors. -#commonLabels: -# someName: someValue - -patchesStrategicMerge: -- manager_image.yaml -- debug_logs_patch.yaml -- ../default/manager_auth_proxy_patch.yaml - -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: -- ../crd -- ../rbac -- ../manager -images: -- name: testing - newName: testing-operator -` diff --git a/internal/plugins/ansible/v1/scaffolds/internal/templates/config/testing/manager_image.go b/internal/plugins/ansible/v1/scaffolds/internal/templates/config/testing/manager_image.go deleted file mode 100644 index 59406516bb..0000000000 --- a/internal/plugins/ansible/v1/scaffolds/internal/templates/config/testing/manager_image.go +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2020 The Operator-SDK Authors -// -// 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 testing - -import ( - "path/filepath" - - "sigs.k8s.io/kubebuilder/v3/pkg/machinery" -) - -var _ machinery.Template = &ManagerImage{} - -// ManagerImage scaffolds the patch file for overriding the -// default image during Ansible testing -type ManagerImage struct { - machinery.TemplateMixin -} - -// SetTemplateDefaults implements machinery.Template -func (f *ManagerImage) SetTemplateDefaults() error { - if f.Path == "" { - f.Path = filepath.Join("config", "testing", "manager_image.yaml") - } - - f.TemplateBody = managerImageTemplate - - f.IfExistsAction = machinery.Error - - return nil -} - -const managerImageTemplate = `--- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: controller-manager - namespace: system -spec: - template: - spec: - containers: - - name: manager - image: testing -` diff --git a/internal/plugins/ansible/v1/scaffolds/internal/templates/config/testing/pullpolicy/always_pull_patch.go b/internal/plugins/ansible/v1/scaffolds/internal/templates/config/testing/pullpolicy/always_pull_patch.go deleted file mode 100644 index a84c7bdd3e..0000000000 --- a/internal/plugins/ansible/v1/scaffolds/internal/templates/config/testing/pullpolicy/always_pull_patch.go +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2020 The Operator-SDK Authors -// -// 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 pullpolicy - -import ( - "path/filepath" - - "sigs.k8s.io/kubebuilder/v3/pkg/machinery" -) - -var _ machinery.Template = &AlwaysPullPatch{} - -// AlwaysPullPatch scaffolds the patch file for overriding the -// default image pull policy during Ansible testing -type AlwaysPullPatch struct { - machinery.TemplateMixin -} - -// SetTemplateDefaults implements machinery.Template -func (f *AlwaysPullPatch) SetTemplateDefaults() error { - if f.Path == "" { - f.Path = filepath.Join("config", "testing", "pull_policy", "Always.yaml") - } - - f.TemplateBody = alwaysPullPatchTemplate - - f.IfExistsAction = machinery.Error - - return nil -} - -const alwaysPullPatchTemplate = `--- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: controller-manager - namespace: system -spec: - template: - spec: - containers: - - name: manager - imagePullPolicy: Always -` diff --git a/internal/plugins/ansible/v1/scaffolds/internal/templates/config/testing/pullpolicy/ifnotpresent_pull_patch.go b/internal/plugins/ansible/v1/scaffolds/internal/templates/config/testing/pullpolicy/ifnotpresent_pull_patch.go deleted file mode 100644 index 6cee22ab27..0000000000 --- a/internal/plugins/ansible/v1/scaffolds/internal/templates/config/testing/pullpolicy/ifnotpresent_pull_patch.go +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2020 The Operator-SDK Authors -// -// 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 pullpolicy - -import ( - "path/filepath" - - "sigs.k8s.io/kubebuilder/v3/pkg/machinery" -) - -var _ machinery.Template = &IfNotPresentPullPatch{} - -// IfNotPresentPullPatch scaffolds the patch file for overriding the -// default image pull policy during Ansible testing -type IfNotPresentPullPatch struct { - machinery.TemplateMixin -} - -// SetTemplateDefaults implements machinery.Template -func (f *IfNotPresentPullPatch) SetTemplateDefaults() error { - if f.Path == "" { - f.Path = filepath.Join("config", "testing", "pull_policy", "IfNotPresent.yaml") - } - - f.TemplateBody = ifNotPresentPullPatchTemplate - - f.IfExistsAction = machinery.Error - - return nil -} - -const ifNotPresentPullPatchTemplate = `--- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: controller-manager - namespace: system -spec: - template: - spec: - containers: - - name: manager - imagePullPolicy: IfNotPresent -` diff --git a/internal/plugins/ansible/v1/scaffolds/internal/templates/config/testing/pullpolicy/never_pull_patch.go b/internal/plugins/ansible/v1/scaffolds/internal/templates/config/testing/pullpolicy/never_pull_patch.go deleted file mode 100644 index 197b661741..0000000000 --- a/internal/plugins/ansible/v1/scaffolds/internal/templates/config/testing/pullpolicy/never_pull_patch.go +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2020 The Operator-SDK Authors -// -// 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 pullpolicy - -import ( - "path/filepath" - - "sigs.k8s.io/kubebuilder/v3/pkg/machinery" -) - -var _ machinery.Template = &NeverPullPatch{} - -// NeverPullPatch scaffolds the patch file for overriding the -// default image pull policy during Ansible testing -type NeverPullPatch struct { - machinery.TemplateMixin -} - -// SetTemplateDefaults implements machinery.Template -func (f *NeverPullPatch) SetTemplateDefaults() error { - if f.Path == "" { - f.Path = filepath.Join("config", "testing", "pull_policy", "Never.yaml") - } - - f.TemplateBody = neverPullPatchTemplate - - f.IfExistsAction = machinery.Error - - return nil -} - -const neverPullPatchTemplate = `--- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: controller-manager - namespace: system -spec: - template: - spec: - containers: - - name: manager - imagePullPolicy: Never -` diff --git a/internal/plugins/ansible/v1/scaffolds/internal/templates/dockerfile.go b/internal/plugins/ansible/v1/scaffolds/internal/templates/dockerfile.go deleted file mode 100644 index 35db77ea05..0000000000 --- a/internal/plugins/ansible/v1/scaffolds/internal/templates/dockerfile.go +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// 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 templates - -import ( - "errors" - - "sigs.k8s.io/kubebuilder/v3/pkg/machinery" - - "github.com/operator-framework/operator-sdk/internal/plugins/ansible/v1/constants" -) - -var _ machinery.Template = &Dockerfile{} - -// Dockerfile scaffolds a Dockerfile for building a main -type Dockerfile struct { - machinery.TemplateMixin - - // AnsibleOperatorVersion is the version of the Dockerfile's base image. - AnsibleOperatorVersion string - - // These variables are always overwritten. - RolesDir string - PlaybooksDir string -} - -// SetTemplateDefaults implements machinery.Template -func (f *Dockerfile) SetTemplateDefaults() error { - if f.Path == "" { - f.Path = "Dockerfile" - } - - f.TemplateBody = dockerfileTemplate - - if f.AnsibleOperatorVersion == "" { - return errors.New("ansible-operator version is required in scaffold") - } - - f.RolesDir = constants.RolesDir - f.PlaybooksDir = constants.PlaybooksDir - - return nil -} - -const dockerfileTemplate = `FROM quay.io/operator-framework/ansible-operator:{{ .AnsibleOperatorVersion }} - -COPY requirements.yml ${HOME}/requirements.yml -RUN ansible-galaxy collection install -r ${HOME}/requirements.yml \ - && chmod -R ug+rwx ${HOME}/.ansible - -COPY watches.yaml ${HOME}/watches.yaml -COPY {{ .RolesDir }}/ ${HOME}/{{ .RolesDir }}/ -COPY {{ .PlaybooksDir }}/ ${HOME}/{{ .PlaybooksDir }}/ -` diff --git a/internal/plugins/ansible/v1/scaffolds/internal/templates/gitignore.go b/internal/plugins/ansible/v1/scaffolds/internal/templates/gitignore.go deleted file mode 100644 index 0d7290b58b..0000000000 --- a/internal/plugins/ansible/v1/scaffolds/internal/templates/gitignore.go +++ /dev/null @@ -1,56 +0,0 @@ -/* -Copyright 2018 The Kubernetes Authors. -Modifications copyright 2020 The Operator-SDK Authors - -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 templates - -import ( - "sigs.k8s.io/kubebuilder/v3/pkg/machinery" -) - -var _ machinery.Template = &GitIgnore{} - -// GitIgnore scaffolds the .gitignore file -type GitIgnore struct { - machinery.TemplateMixin -} - -// SetTemplateDefaults implements machinery.Template -func (f *GitIgnore) SetTemplateDefaults() error { - if f.Path == "" { - f.Path = ".gitignore" - } - - f.TemplateBody = gitignoreTemplate - - return nil -} - -const gitignoreTemplate = ` -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib -bin - -# editor and IDE paraphernalia -.idea -*.swp -*.swo -*~ -` diff --git a/internal/plugins/ansible/v1/scaffolds/internal/templates/makefile.go b/internal/plugins/ansible/v1/scaffolds/internal/templates/makefile.go deleted file mode 100644 index 7d027cddf8..0000000000 --- a/internal/plugins/ansible/v1/scaffolds/internal/templates/makefile.go +++ /dev/null @@ -1,176 +0,0 @@ -/* -Copyright 2019 The Kubernetes Authors. -Modifications copyright 2020 The Operator-SDK Authors - -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 templates - -import ( - "errors" - - "sigs.k8s.io/kubebuilder/v3/pkg/machinery" -) - -var _ machinery.Template = &Makefile{} - -// Makefile scaffolds the Makefile -type Makefile struct { - machinery.TemplateMixin - - // Image is controller manager image name - Image string - - // Kustomize version to use in the project - KustomizeVersion string - - // AnsibleOperatorVersion is the version of the ansible-operator binary downloaded by the Makefile. - AnsibleOperatorVersion string -} - -// SetTemplateDefaults implements machinery.Template -func (f *Makefile) SetTemplateDefaults() error { - if f.Path == "" { - f.Path = "Makefile" - } - - f.TemplateBody = makefileTemplate - - f.IfExistsAction = machinery.Error - - if f.Image == "" { - f.Image = "controller:latest" - } - - if f.KustomizeVersion == "" { - return errors.New("kustomize version is required in scaffold") - } - - if f.AnsibleOperatorVersion == "" { - return errors.New("ansible-operator version is required in scaffold") - } - - return nil -} - -const makefileTemplate = ` -# Image URL to use all building/pushing image targets -IMG ?= {{ .Image }} - -.PHONY: all -all: docker-build - -##@ General - -# The help target prints out all targets with their descriptions organized -# beneath their categories. The categories are represented by '##@' and the -# target descriptions by '##'. The awk commands is responsible for reading the -# entire set of makefiles included in this invocation, looking for lines of the -# file as xyz: ## something, and then pretty-format the target and help. Then, -# if there's a line with ##@ something, that gets pretty-printed as a category. -# More info on the usage of ANSI control characters for terminal formatting: -# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters -# More info on the awk command: -# http://linuxcommand.org/lc3_adv_awk.php - -.PHONY: help -help: ## Display this help. - @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) - -##@ Build - -.PHONY: run -ANSIBLE_ROLES_PATH?="$(shell pwd)/roles" -run: ansible-operator ## Run against the configured Kubernetes cluster in ~/.kube/config - $(ANSIBLE_OPERATOR) run - -.PHONY: docker-build -docker-build: ## Build docker image with the manager. - docker build -t ${IMG} . - -.PHONY: docker-push -docker-push: ## Push docker image with the manager. - docker push ${IMG} - -# PLATFORMS defines the target platforms for the manager image be build to provide support to multiple -# architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to: -# - able to use docker buildx . More info: https://docs.docker.com/build/buildx/ -# - have enable BuildKit, More info: https://docs.docker.com/develop/develop-images/build_enhancements/ -# - be able to push the image for your registry (i.e. if you do not inform a valid value via IMG=> than the export will fail) -# To properly provided solutions that supports more than one platform you should use this option. -PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le -.PHONY: docker-buildx -docker-buildx: test ## Build and push docker image for the manager for cross-platform support - # copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile - sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross - - docker buildx create --name project-v3-builder - docker buildx use project-v3-builder - - docker buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross . - - docker buildx rm project-v3-builder - rm Dockerfile.cross - -##@ Deployment - -.PHONY: install -install: kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. - $(KUSTOMIZE) build config/crd | kubectl apply -f - - -.PHONY: uninstall -uninstall: kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. - $(KUSTOMIZE) build config/crd | kubectl delete -f - - -.PHONY: deploy -deploy: 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 - - -.PHONY: undeploy -undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. - $(KUSTOMIZE) build config/default | kubectl delete -f - - -OS := $(shell uname -s | tr '[:upper:]' '[:lower:]') -ARCH := $(shell uname -m | sed 's/x86_64/amd64/' | sed 's/aarch64/arm64/') - -.PHONY: kustomize -KUSTOMIZE = $(shell pwd)/bin/kustomize -kustomize: ## Download kustomize locally if necessary. -ifeq (,$(wildcard $(KUSTOMIZE))) -ifeq (,$(shell which kustomize 2>/dev/null)) - @{ \ - set -e ;\ - mkdir -p $(dir $(KUSTOMIZE)) ;\ - curl -sSLo - https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize/{{ .KustomizeVersion }}/kustomize_{{ .KustomizeVersion }}_$(OS)_$(ARCH).tar.gz | \ - tar xzf - -C bin/ ;\ - } -else -KUSTOMIZE = $(shell which kustomize) -endif -endif - -.PHONY: ansible-operator -ANSIBLE_OPERATOR = $(shell pwd)/bin/ansible-operator -ansible-operator: ## Download ansible-operator locally if necessary, preferring the $(pwd)/bin path over global if both exist. -ifeq (,$(wildcard $(ANSIBLE_OPERATOR))) -ifeq (,$(shell which ansible-operator 2>/dev/null)) - @{ \ - set -e ;\ - mkdir -p $(dir $(ANSIBLE_OPERATOR)) ;\ - curl -sSLo $(ANSIBLE_OPERATOR) https://github.com/operator-framework/operator-sdk/releases/download/{{ .AnsibleOperatorVersion }}/ansible-operator_$(OS)_$(ARCH) ;\ - chmod +x $(ANSIBLE_OPERATOR) ;\ - } -else -ANSIBLE_OPERATOR = $(shell which ansible-operator) -endif -endif -` diff --git a/internal/plugins/ansible/v1/scaffolds/internal/templates/molecule/mdefault/converge.go b/internal/plugins/ansible/v1/scaffolds/internal/templates/molecule/mdefault/converge.go deleted file mode 100644 index fdc1961981..0000000000 --- a/internal/plugins/ansible/v1/scaffolds/internal/templates/molecule/mdefault/converge.go +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright 2020 The Operator-SDK Authors -// -// 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 mdefault - -import ( - "path/filepath" - - "sigs.k8s.io/kubebuilder/v3/pkg/machinery" -) - -var _ machinery.Template = &Converge{} - -// Converge scaffolds a Converge for building a main -type Converge struct { - machinery.TemplateMixin -} - -// SetTemplateDefaults implements machinery.Template -func (f *Converge) SetTemplateDefaults() error { - if f.Path == "" { - f.Path = filepath.Join("molecule", "default", "converge.yml") - } - f.TemplateBody = convergeTemplate - return nil -} - -const convergeTemplate = `--- -- name: Converge - hosts: localhost - connection: local - gather_facts: no - collections: - - kubernetes.core - - tasks: - - name: Create Namespace - k8s: - api_version: v1 - kind: Namespace - name: '{{ "{{ namespace }}" }}' - - - import_tasks: kustomize.yml - vars: - state: present -` diff --git a/internal/plugins/ansible/v1/scaffolds/internal/templates/molecule/mdefault/create.go b/internal/plugins/ansible/v1/scaffolds/internal/templates/molecule/mdefault/create.go deleted file mode 100644 index 03f254dc87..0000000000 --- a/internal/plugins/ansible/v1/scaffolds/internal/templates/molecule/mdefault/create.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2020 The Operator-SDK Authors -// -// 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 mdefault - -import ( - "path/filepath" - - "sigs.k8s.io/kubebuilder/v3/pkg/machinery" -) - -var _ machinery.Template = &Create{} - -// Create scaffolds a Create for building a main -type Create struct { - machinery.TemplateMixin -} - -// SetTemplateDefaults implements machinery.Template -func (f *Create) SetTemplateDefaults() error { - if f.Path == "" { - f.Path = filepath.Join("molecule", "default", "create.yml") - } - f.TemplateBody = createTemplate - return nil -} - -const createTemplate = `--- -- name: Create - hosts: localhost - connection: local - gather_facts: false - tasks: [] -` diff --git a/internal/plugins/ansible/v1/scaffolds/internal/templates/molecule/mdefault/destroy.go b/internal/plugins/ansible/v1/scaffolds/internal/templates/molecule/mdefault/destroy.go deleted file mode 100644 index 472cf2176d..0000000000 --- a/internal/plugins/ansible/v1/scaffolds/internal/templates/molecule/mdefault/destroy.go +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright 2020 The Operator-SDK Authors -// -// 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 mdefault - -import ( - "path/filepath" - - "sigs.k8s.io/kubebuilder/v3/pkg/machinery" -) - -var _ machinery.Template = &Destroy{} - -// Destroy scaffolds a Destroy for building a main -type Destroy struct { - machinery.TemplateMixin -} - -// SetTemplateDefaults implements machinery.Template -func (f *Destroy) SetTemplateDefaults() error { - if f.Path == "" { - f.Path = filepath.Join("molecule", "default", "destroy.yml") - } - f.TemplateBody = destroyTemplate - return nil -} - -const destroyTemplate = `--- -- name: Destroy - hosts: localhost - connection: local - gather_facts: false - collections: - - kubernetes.core - - tasks: - - import_tasks: kustomize.yml - vars: - state: absent - - - name: Destroy Namespace - k8s: - api_version: v1 - kind: Namespace - name: '{{ "{{ namespace }}" }}' - state: absent - - - name: Unset pull policy - command: '{{ "{{ kustomize }}" }} edit remove patch pull_policy/{{ "{{ operator_pull_policy }}" }}.yaml' - args: - chdir: '{{ "{{ config_dir }}" }}/testing' -` diff --git a/internal/plugins/ansible/v1/scaffolds/internal/templates/molecule/mdefault/kustomize.go b/internal/plugins/ansible/v1/scaffolds/internal/templates/molecule/mdefault/kustomize.go deleted file mode 100644 index 24f9c4f568..0000000000 --- a/internal/plugins/ansible/v1/scaffolds/internal/templates/molecule/mdefault/kustomize.go +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright 2020 The Operator-SDK Authors -// -// 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 mdefault - -import ( - "path/filepath" - - "sigs.k8s.io/kubebuilder/v3/pkg/machinery" -) - -var _ machinery.Template = &Kustomize{} - -// Kustomize scaffolds a Kustomize for building a main -type Kustomize struct { - machinery.TemplateMixin -} - -// SetTemplateDefaults implements machinery.Template -func (f *Kustomize) SetTemplateDefaults() error { - if f.Path == "" { - f.Path = filepath.Join("molecule", "default", "kustomize.yml") - } - f.TemplateBody = kustomizeTemplate - return nil -} - -const kustomizeTemplate = `--- -- name: Build kustomize testing overlay - # load_restrictor must be set to none so we can load patch files from the default overlay - command: '{{ "{{ kustomize }}" }} build --load-restrictor LoadRestrictionsNone' - args: - chdir: '{{ "{{ config_dir }}" }}/testing' - register: resources - changed_when: false - -- name: Set resources to {{ "{{ state }}" }} - k8s: - definition: '{{ "{{ item }}" }}' - state: '{{ "{{ state }}" }}' - wait: no - loop: '{{ "{{ resources.stdout | from_yaml_all | list }}" }}' - -- name: Wait for resources to get to {{ "{{ state }}" }} - k8s: - definition: '{{ "{{ item }}" }}' - state: '{{ "{{ state }}" }}' - wait: yes - loop: '{{ "{{ resources.stdout | from_yaml_all | list }}" }}' -` diff --git a/internal/plugins/ansible/v1/scaffolds/internal/templates/molecule/mdefault/molecule.go b/internal/plugins/ansible/v1/scaffolds/internal/templates/molecule/mdefault/molecule.go deleted file mode 100644 index e35353d9f5..0000000000 --- a/internal/plugins/ansible/v1/scaffolds/internal/templates/molecule/mdefault/molecule.go +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright 2020 The Operator-SDK Authors -// -// 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 mdefault - -import ( - "path/filepath" - - "sigs.k8s.io/kubebuilder/v3/pkg/machinery" -) - -var _ machinery.Template = &Molecule{} - -// Molecule scaffolds a Molecule for building a main -type Molecule struct { - machinery.TemplateMixin -} - -// SetTemplateDefaults implements machinery.Template -func (f *Molecule) SetTemplateDefaults() error { - if f.Path == "" { - f.Path = filepath.Join("molecule", "default", "molecule.yml") - } - f.TemplateBody = moleculeTemplate - return nil -} - -const moleculeTemplate = `--- -dependency: - name: galaxy -driver: - name: delegated -platforms: - - name: cluster - groups: - - k8s -provisioner: - name: ansible - inventory: - group_vars: - all: - namespace: ${TEST_OPERATOR_NAMESPACE:-osdk-test} - host_vars: - localhost: - ansible_python_interpreter: '{{ "{{ ansible_playbook_python }}" }}' - config_dir: ${MOLECULE_PROJECT_DIRECTORY}/config - samples_dir: ${MOLECULE_PROJECT_DIRECTORY}/config/samples - operator_image: ${OPERATOR_IMAGE:-""} - operator_pull_policy: ${OPERATOR_PULL_POLICY:-"Always"} - kustomize: ${KUSTOMIZE_PATH:-kustomize} - env: - K8S_AUTH_KUBECONFIG: ${KUBECONFIG:-"~/.kube/config"} -verifier: - name: ansible -` diff --git a/internal/plugins/ansible/v1/scaffolds/internal/templates/molecule/mdefault/prepare.go b/internal/plugins/ansible/v1/scaffolds/internal/templates/molecule/mdefault/prepare.go deleted file mode 100644 index 32ce9dea33..0000000000 --- a/internal/plugins/ansible/v1/scaffolds/internal/templates/molecule/mdefault/prepare.go +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2020 The Operator-SDK Authors -// -// 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 mdefault - -import ( - "path/filepath" - - "sigs.k8s.io/kubebuilder/v3/pkg/machinery" -) - -var _ machinery.Template = &Prepare{} - -// Prepare scaffolds a Prepare for building a main -type Prepare struct { - machinery.TemplateMixin -} - -// SetTemplateDefaults implements machinery.Template -func (f *Prepare) SetTemplateDefaults() error { - if f.Path == "" { - f.Path = filepath.Join("molecule", "default", "prepare.yml") - } - f.TemplateBody = prepareTemplate - return nil -} - -const prepareTemplate = `--- -- name: Prepare - hosts: localhost - connection: local - gather_facts: false - - tasks: - - name: Ensure operator image is set - fail: - msg: | - You must specify the OPERATOR_IMAGE environment variable in order to run the - 'default' scenario - when: not operator_image - - - name: Set testing image - command: '{{ "{{ kustomize }}" }} edit set image testing={{ "{{ operator_image }}" }}' - args: - chdir: '{{ "{{ config_dir }}" }}/testing' - - - name: Set pull policy - command: '{{ "{{ kustomize }}" }} edit add patch --path pull_policy/{{ "{{ operator_pull_policy }}" }}.yaml' - args: - chdir: '{{ "{{ config_dir }}" }}/testing' - - - name: Set testing namespace - command: '{{ "{{ kustomize }}" }} edit set namespace {{ "{{ namespace }}" }}' - args: - chdir: '{{ "{{ config_dir }}" }}/testing' -` diff --git a/internal/plugins/ansible/v1/scaffolds/internal/templates/molecule/mdefault/tasks_test_resource.go b/internal/plugins/ansible/v1/scaffolds/internal/templates/molecule/mdefault/tasks_test_resource.go deleted file mode 100644 index bfb1346189..0000000000 --- a/internal/plugins/ansible/v1/scaffolds/internal/templates/molecule/mdefault/tasks_test_resource.go +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2020 The Operator-SDK Authors -// -// 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 mdefault - -import ( - "path/filepath" - - "sigs.k8s.io/kubebuilder/v3/pkg/machinery" -) - -var _ machinery.Template = &ResourceTest{} - -// ResourceTest scaffolds a ResourceTest for building a main -type ResourceTest struct { - machinery.TemplateMixin - machinery.ResourceMixin - SampleFile string -} - -// SetTemplateDefaults implements machinery.Template -func (f *ResourceTest) SetTemplateDefaults() error { - if f.Path == "" { - f.Path = filepath.Join("molecule", "default", "tasks", "%[kind]_test.yml") - f.Path = f.Resource.Replacer().Replace(f.Path) - } - f.SampleFile = f.Resource.Replacer().Replace("%[group]_%[version]_%[kind].yaml") - - f.TemplateBody = resourceTestTemplate - return nil -} - -const resourceTestTemplate = `--- -- name: Create the {{ .Resource.QualifiedGroup }}/{{ .Resource.Version }}.{{ .Resource.Kind }} - k8s: - state: present - namespace: '{{ "{{ namespace }}" }}' - definition: "{{ "{{ lookup('template', '/'.join([samples_dir, cr_file])) | from_yaml }}" }}" - wait: yes - wait_timeout: 300 - wait_condition: - type: Successful - status: "True" - vars: - cr_file: '{{ .SampleFile }}' - -- name: Add assertions here - assert: - that: false - fail_msg: FIXME Add real assertions for your operator -` diff --git a/internal/plugins/ansible/v1/scaffolds/internal/templates/molecule/mdefault/verify.go b/internal/plugins/ansible/v1/scaffolds/internal/templates/molecule/mdefault/verify.go deleted file mode 100644 index 07ce387c4c..0000000000 --- a/internal/plugins/ansible/v1/scaffolds/internal/templates/molecule/mdefault/verify.go +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright 2020 The Operator-SDK Authors -// -// 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 mdefault - -import ( - "path/filepath" - - "sigs.k8s.io/kubebuilder/v3/pkg/machinery" -) - -var _ machinery.Template = &Verify{} - -// Verify scaffolds a Verify for building a main -type Verify struct { - machinery.TemplateMixin -} - -// SetTemplateDefaults implements machinery.Template -func (f *Verify) SetTemplateDefaults() error { - if f.Path == "" { - f.Path = filepath.Join("molecule", "default", "verify.yml") - } - f.TemplateBody = verifyTemplate - return nil -} - -const verifyTemplate = `--- -- name: Verify - hosts: localhost - connection: local - gather_facts: no - collections: - - kubernetes.core - - vars: - ctrl_label: control-plane=controller-manager - - tasks: - - block: - - name: Import all test files from tasks/ - include_tasks: '{{ "{{ item }}" }}' - with_fileglob: - - tasks/*_test.yml - rescue: - - name: Retrieve relevant resources - k8s_info: - api_version: '{{ "{{ item.api_version }}" }}' - kind: '{{ "{{ item.kind }}" }}' - namespace: '{{ "{{ namespace }}" }}' - loop: - - api_version: v1 - kind: Pod - - api_version: apps/v1 - kind: Deployment - - api_version: v1 - kind: Secret - - api_version: v1 - kind: ConfigMap - register: debug_resources - - - name: Retrieve Pod logs - k8s_log: - name: '{{ "{{ item.metadata.name }}" }}' - namespace: '{{ "{{ namespace }}" }}' - container: manager - loop: "{{ "{{ q('k8s', api_version='v1', kind='Pod', namespace=namespace, label_selector=ctrl_label) }}" }}" - register: debug_logs - - - name: Output gathered resources - debug: - var: debug_resources - - - name: Output gathered logs - debug: - var: item.log_lines - loop: '{{ "{{ debug_logs.results }}" }}' - - - name: Re-emit failure - vars: - failed_task: - result: '{{ "{{ ansible_failed_result }}" }}' - fail: - msg: '{{ "{{ failed_task }}" }}' -` diff --git a/internal/plugins/ansible/v1/scaffolds/internal/templates/molecule/mkind/converge.go b/internal/plugins/ansible/v1/scaffolds/internal/templates/molecule/mkind/converge.go deleted file mode 100644 index 3eb39800ca..0000000000 --- a/internal/plugins/ansible/v1/scaffolds/internal/templates/molecule/mkind/converge.go +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright 2020 The Operator-SDK Authors -// -// 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 mkind - -import ( - "path/filepath" - - "sigs.k8s.io/kubebuilder/v3/pkg/machinery" -) - -var _ machinery.Template = &Converge{} - -// Converge scaffolds a Converge for building a main -type Converge struct { - machinery.TemplateMixin -} - -// SetTemplateDefaults implements machinery.Template -func (f *Converge) SetTemplateDefaults() error { - if f.Path == "" { - f.Path = filepath.Join("molecule", "kind", "converge.yml") - } - f.TemplateBody = convergeTemplate - return nil -} - -const convergeTemplate = `--- -- name: Converge - hosts: localhost - connection: local - gather_facts: no - - tasks: - - name: Build operator image - docker_image: - build: - path: '{{ "{{ project_dir }}" }}' - pull: no - name: '{{ "{{ operator_image }}" }}' - tag: latest - push: no - source: build - force_source: yes - - - name: Load image into kind cluster - command: kind load docker-image --name osdk-test '{{ "{{ operator_image }}" }}' - register: result - changed_when: '"not yet present" in result.stdout' - -- import_playbook: ../default/converge.yml -` diff --git a/internal/plugins/ansible/v1/scaffolds/internal/templates/molecule/mkind/create.go b/internal/plugins/ansible/v1/scaffolds/internal/templates/molecule/mkind/create.go deleted file mode 100644 index 56fd8ba1ff..0000000000 --- a/internal/plugins/ansible/v1/scaffolds/internal/templates/molecule/mkind/create.go +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2020 The Operator-SDK Authors -// -// 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 mkind - -import ( - "path/filepath" - - "sigs.k8s.io/kubebuilder/v3/pkg/machinery" -) - -var _ machinery.Template = &Create{} - -// Create scaffolds a Create for building a main -type Create struct { - machinery.TemplateMixin -} - -// SetTemplateDefaults implements machinery.Template -func (f *Create) SetTemplateDefaults() error { - if f.Path == "" { - f.Path = filepath.Join("molecule", "kind", "create.yml") - } - f.TemplateBody = createTemplate - return nil -} - -const createTemplate = `--- -- name: Create - hosts: localhost - connection: local - gather_facts: false - tasks: - - name: Create test kind cluster - command: kind create cluster --name osdk-test --kubeconfig {{ "{{ kubeconfig }}" }} -` diff --git a/internal/plugins/ansible/v1/scaffolds/internal/templates/molecule/mkind/destroy.go b/internal/plugins/ansible/v1/scaffolds/internal/templates/molecule/mkind/destroy.go deleted file mode 100644 index 086a107b09..0000000000 --- a/internal/plugins/ansible/v1/scaffolds/internal/templates/molecule/mkind/destroy.go +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2020 The Operator-SDK Authors -// -// 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 mkind - -import ( - "path/filepath" - - "sigs.k8s.io/kubebuilder/v3/pkg/machinery" -) - -var _ machinery.Template = &Destroy{} - -// Destroy scaffolds a Destroy for building a main -type Destroy struct { - machinery.TemplateMixin -} - -// SetTemplateDefaults implements machinery.Template -func (f *Destroy) SetTemplateDefaults() error { - if f.Path == "" { - f.Path = filepath.Join("molecule", "kind", "destroy.yml") - } - f.TemplateBody = destroyTemplate - return nil -} - -const destroyTemplate = `--- -- name: Destroy - hosts: localhost - connection: local - gather_facts: false - collections: - - kubernetes.core - - tasks: - - name: Destroy test kind cluster - command: kind delete cluster --name osdk-test --kubeconfig {{ "{{ kubeconfig }}" }} - - - name: Unset pull policy - command: '{{ "{{ kustomize }}" }} edit remove patch pull_policy/{{ "{{ operator_pull_policy }}" }}.yaml' - args: - chdir: '{{ "{{ config_dir }}" }}/testing' -` diff --git a/internal/plugins/ansible/v1/scaffolds/internal/templates/molecule/mkind/molecule.go b/internal/plugins/ansible/v1/scaffolds/internal/templates/molecule/mkind/molecule.go deleted file mode 100644 index d0e6c9df9f..0000000000 --- a/internal/plugins/ansible/v1/scaffolds/internal/templates/molecule/mkind/molecule.go +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2020 The Operator-SDK Authors -// -// 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 mkind - -import ( - "path/filepath" - - "sigs.k8s.io/kubebuilder/v3/pkg/machinery" -) - -var _ machinery.Template = &Molecule{} - -// Molecule scaffolds a Molecule for building a main -type Molecule struct { - machinery.TemplateMixin -} - -// SetTemplateDefaults implements machinery.Template -func (f *Molecule) SetTemplateDefaults() error { - if f.Path == "" { - f.Path = filepath.Join("molecule", "kind", "molecule.yml") - } - f.TemplateBody = moleculeTemplate - return nil -} - -const moleculeTemplate = `--- -dependency: - name: galaxy -driver: - name: delegated -platforms: - - name: cluster - groups: - - k8s -provisioner: - name: ansible - playbooks: - prepare: ../default/prepare.yml - verify: ../default/verify.yml - inventory: - group_vars: - all: - namespace: ${TEST_OPERATOR_NAMESPACE:-osdk-test} - host_vars: - localhost: - ansible_python_interpreter: '{{ "{{ ansible_playbook_python }}" }}' - config_dir: ${MOLECULE_PROJECT_DIRECTORY}/config - samples_dir: ${MOLECULE_PROJECT_DIRECTORY}/config/samples - project_dir: ${MOLECULE_PROJECT_DIRECTORY} - operator_image: testing-operator - operator_pull_policy: "Never" - kubeconfig: "{{ "{{ lookup('env', 'KUBECONFIG') }}" }}" - kustomize: ${KUSTOMIZE_PATH:-kustomize} - env: - K8S_AUTH_KUBECONFIG: ${MOLECULE_EPHEMERAL_DIRECTORY}/kubeconfig - KUBECONFIG: ${MOLECULE_EPHEMERAL_DIRECTORY}/kubeconfig -verifier: - name: ansible -` diff --git a/internal/plugins/ansible/v1/scaffolds/internal/templates/playbooks/placeholder.go b/internal/plugins/ansible/v1/scaffolds/internal/templates/playbooks/placeholder.go deleted file mode 100644 index 20dc757e4a..0000000000 --- a/internal/plugins/ansible/v1/scaffolds/internal/templates/playbooks/placeholder.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// 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 playbooks - -import ( - "path/filepath" - - "sigs.k8s.io/kubebuilder/v3/pkg/machinery" -) - -var _ machinery.Template = &Placeholder{} - -type Placeholder struct { - machinery.TemplateMixin -} - -// SetTemplateDefaults implements machinery.Template -func (f *Placeholder) SetTemplateDefaults() error { - if f.Path == "" { - f.Path = filepath.Join("playbooks", ".placeholder") - } - f.TemplateBody = placeholderTemplate - return nil -} - -const placeholderTemplate = `` diff --git a/internal/plugins/ansible/v1/scaffolds/internal/templates/playbooks/playbook.go b/internal/plugins/ansible/v1/scaffolds/internal/templates/playbooks/playbook.go deleted file mode 100644 index 3e68b7df00..0000000000 --- a/internal/plugins/ansible/v1/scaffolds/internal/templates/playbooks/playbook.go +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright 2020 The Operator-SDK Authors -// -// 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 playbooks - -import ( - "path/filepath" - - "sigs.k8s.io/kubebuilder/v3/pkg/machinery" -) - -var _ machinery.Template = &Playbook{} - -type Playbook struct { - machinery.TemplateMixin - machinery.ResourceMixin - - GenerateRole bool -} - -func (f *Playbook) SetTemplateDefaults() error { - if f.Path == "" { - f.Path = filepath.Join("playbooks", "%[kind].yml") - } - f.Path = f.Resource.Replacer().Replace(f.Path) - - f.TemplateBody = playbookTmpl - - return nil -} - -const playbookTmpl = `--- -- hosts: localhost - gather_facts: no - collections: - - kubernetes.core - - operator_sdk.util - - {{- if .GenerateRole }} - tasks: - - import_role: - name: "{{ lower .Resource.Kind }}" - {{- else }} - tasks: [] - {{- end }} -` diff --git a/internal/plugins/ansible/v1/scaffolds/internal/templates/requirements.go b/internal/plugins/ansible/v1/scaffolds/internal/templates/requirements.go deleted file mode 100644 index 56798546d2..0000000000 --- a/internal/plugins/ansible/v1/scaffolds/internal/templates/requirements.go +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright 2020 The Operator-SDK Authors -// -// 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 templates - -import ( - "sigs.k8s.io/kubebuilder/v3/pkg/machinery" -) - -var _ machinery.Template = &RequirementsYml{} - -// RequirementsYml - A requirements file for Ansible collection dependencies -type RequirementsYml struct { - machinery.TemplateMixin -} - -func (f *RequirementsYml) SetTemplateDefaults() error { - if f.Path == "" { - f.Path = "requirements.yml" - } - f.TemplateBody = requirementsYmlTmpl - return nil -} - -const requirementsYmlTmpl = `--- -collections: - - name: operator_sdk.util - version: "0.5.0" - - name: kubernetes.core - version: "2.4.0" - - name: cloud.common - version: "2.1.1" - - name: community.docker - version: "3.4.0" -` diff --git a/internal/plugins/ansible/v1/scaffolds/internal/templates/roles/defaults_main.go b/internal/plugins/ansible/v1/scaffolds/internal/templates/roles/defaults_main.go deleted file mode 100644 index 3ce78f10bd..0000000000 --- a/internal/plugins/ansible/v1/scaffolds/internal/templates/roles/defaults_main.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// 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 roles - -import ( - "path/filepath" - - "sigs.k8s.io/kubebuilder/v3/pkg/machinery" - - "github.com/operator-framework/operator-sdk/internal/plugins/ansible/v1/constants" -) - -var _ machinery.Template = &DefaultsMain{} - -type DefaultsMain struct { - machinery.TemplateMixin - machinery.ResourceMixin -} - -// SetTemplateDefaults implements machinery.Template -func (f *DefaultsMain) SetTemplateDefaults() error { - if f.Path == "" { - f.Path = filepath.Join(constants.RolesDir, "%[kind]", "defaults", "main.yml") - f.Path = f.Resource.Replacer().Replace(f.Path) - } - f.TemplateBody = defaultsMainAnsibleTmpl - return nil -} - -const defaultsMainAnsibleTmpl = `--- -# defaults file for {{ .Resource.Kind }} -` diff --git a/internal/plugins/ansible/v1/scaffolds/internal/templates/roles/files_dir.go b/internal/plugins/ansible/v1/scaffolds/internal/templates/roles/files_dir.go deleted file mode 100644 index 736c867aab..0000000000 --- a/internal/plugins/ansible/v1/scaffolds/internal/templates/roles/files_dir.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// 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 roles - -import ( - "path/filepath" - - "sigs.k8s.io/kubebuilder/v3/pkg/machinery" - - "github.com/operator-framework/operator-sdk/internal/plugins/ansible/v1/constants" -) - -var _ machinery.Template = &RoleFiles{} - -type RoleFiles struct { - machinery.TemplateMixin - machinery.ResourceMixin -} - -// SetTemplateDefaults implements machinery.Template -func (f *RoleFiles) SetTemplateDefaults() error { - if f.Path == "" { - f.Path = filepath.Join(constants.RolesDir, "%[kind]", "files", ".placeholder") - f.Path = f.Resource.Replacer().Replace(f.Path) - } - - f.TemplateBody = rolesFilesDirPlaceholder - return nil -} - -const rolesFilesDirPlaceholder = `` diff --git a/internal/plugins/ansible/v1/scaffolds/internal/templates/roles/handlers_main.go b/internal/plugins/ansible/v1/scaffolds/internal/templates/roles/handlers_main.go deleted file mode 100644 index 52575065f8..0000000000 --- a/internal/plugins/ansible/v1/scaffolds/internal/templates/roles/handlers_main.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// 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 roles - -import ( - "path/filepath" - - "sigs.k8s.io/kubebuilder/v3/pkg/machinery" - - "github.com/operator-framework/operator-sdk/internal/plugins/ansible/v1/constants" -) - -var _ machinery.Template = &HandlersMain{} - -type HandlersMain struct { - machinery.TemplateMixin - machinery.ResourceMixin -} - -// SetTemplateDefaults implements machinery.Template -func (f *HandlersMain) SetTemplateDefaults() error { - if f.Path == "" { - f.Path = filepath.Join(constants.RolesDir, "%[kind]", "handlers", "main.yml") - f.Path = f.Resource.Replacer().Replace(f.Path) - } - - f.TemplateBody = handlersMainAnsibleTmpl - return nil -} - -const handlersMainAnsibleTmpl = `--- -# handlers file for {{ .Resource.Kind }} -` diff --git a/internal/plugins/ansible/v1/scaffolds/internal/templates/roles/meta_main.go b/internal/plugins/ansible/v1/scaffolds/internal/templates/roles/meta_main.go deleted file mode 100644 index bfc4a79f1b..0000000000 --- a/internal/plugins/ansible/v1/scaffolds/internal/templates/roles/meta_main.go +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// 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 roles - -import ( - "path/filepath" - - "sigs.k8s.io/kubebuilder/v3/pkg/machinery" - - "github.com/operator-framework/operator-sdk/internal/plugins/ansible/v1/constants" -) - -var _ machinery.Template = &MetaMain{} - -type MetaMain struct { - machinery.TemplateMixin - machinery.ResourceMixin -} - -// SetTemplateDefaults implements machinery.Template -func (f *MetaMain) SetTemplateDefaults() error { - if f.Path == "" { - f.Path = filepath.Join(constants.RolesDir, "%[kind]", "meta", "main.yml") - } - f.Path = f.Resource.Replacer().Replace(f.Path) - - f.TemplateBody = metaMainAnsibleTmpl - return nil -} - -const metaMainAnsibleTmpl = `--- -galaxy_info: - author: your name - description: your description - company: your company (optional) - - # If the issue tracker for your role is not on github, uncomment the - # next line and provide a value - # issue_tracker_url: http://example.com/issue/tracker - - # Some suggested licenses: - # - BSD (default) - # - MIT - # - GPLv2 - # - GPLv3 - # - Apache - # - CC-BY - license: license (GPLv2, CC-BY, etc) - - min_ansible_version: 2.9 - - # If this a Container Enabled role, provide the minimum Ansible Container version. - # min_ansible_container_version: - - # Optionally specify the branch Galaxy will use when accessing the GitHub - # repo for this role. During role install, if no tags are available, - # Galaxy will use this branch. During import Galaxy will access files on - # this branch. If Travis integration is configured, only notifications for this - # branch will be accepted. Otherwise, in all cases, the repo's default branch - # (usually master) will be used. - #github_branch: - - # - # Provide a list of supported platforms, and for each platform a list of versions. - # If you don't wish to enumerate all versions for a particular platform, use 'all'. - # To view available platforms and versions (or releases), visit: - # https://galaxy.ansible.com/api/v1/platforms/ - # - # platforms: - # - name: Fedora - # versions: - # - all - # - 25 - # - name: SomePlatform - # versions: - # - all - # - 1.0 - # - 7 - # - 99.99 - - galaxy_tags: [] - # List tags for your role here, one per line. A tag is a keyword that describes - # and categorizes the role. Users find roles by searching for tags. Be sure to - # remove the '[]' above, if you add tags to this list. - # - # NOTE: A tag is limited to a single word comprised of alphanumeric characters. - # Maximum 20 tags per role. - -dependencies: [] - # List your role dependencies here, one per line. Be sure to remove the '[]' above, - # if you add dependencies to this list. -collections: -- operator_sdk.util -- kubernetes.core -` diff --git a/internal/plugins/ansible/v1/scaffolds/internal/templates/roles/placeholder.go b/internal/plugins/ansible/v1/scaffolds/internal/templates/roles/placeholder.go deleted file mode 100644 index 152ffcb410..0000000000 --- a/internal/plugins/ansible/v1/scaffolds/internal/templates/roles/placeholder.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// 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 roles - -import ( - "path/filepath" - - "sigs.k8s.io/kubebuilder/v3/pkg/machinery" -) - -var _ machinery.Template = &Placeholder{} - -type Placeholder struct { - machinery.TemplateMixin -} - -// SetTemplateDefaults implements machinery.Template -func (f *Placeholder) SetTemplateDefaults() error { - if f.Path == "" { - f.Path = filepath.Join("roles", ".placeholder") - } - f.TemplateBody = placeholderTemplate - return nil -} - -const placeholderTemplate = `` diff --git a/internal/plugins/ansible/v1/scaffolds/internal/templates/roles/readme.go b/internal/plugins/ansible/v1/scaffolds/internal/templates/roles/readme.go deleted file mode 100644 index 2253824d37..0000000000 --- a/internal/plugins/ansible/v1/scaffolds/internal/templates/roles/readme.go +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// 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 roles - -import ( - "path/filepath" - - "sigs.k8s.io/kubebuilder/v3/pkg/machinery" - - "github.com/operator-framework/operator-sdk/internal/plugins/ansible/v1/constants" -) - -const ReadmePath = "README.md" - -var _ machinery.Template = &Readme{} - -type Readme struct { - machinery.TemplateMixin - machinery.ResourceMixin -} - -func (f *Readme) SetTemplateDefaults() error { - if f.Path == "" { - f.Path = filepath.Join(constants.RolesDir, "%[kind]", ReadmePath) - } - f.Path = f.Resource.Replacer().Replace(f.Path) - - f.TemplateBody = readmeAnsibleTmpl - return nil -} - -const readmeAnsibleTmpl = `Role Name -========= - -A brief description of the role goes here. - -Requirements ------------- - -Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, -if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required. - -Role Variables --------------- - -A description of the settable variables for this role should go here, including any variables that are in -defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables -that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well - -Dependencies ------------- - -A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set -for other roles, or variables that are used from other roles. - -Example Playbook ----------------- - -Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for -users too: - - - hosts: servers - roles: - - { role: username.rolename, x: 42 } - -License -------- - -BSD - -Author Information ------------------- - -An optional section for the role authors to include contact information, or a website (HTML is not allowed). -` diff --git a/internal/plugins/ansible/v1/scaffolds/internal/templates/roles/tasks_main.go b/internal/plugins/ansible/v1/scaffolds/internal/templates/roles/tasks_main.go deleted file mode 100644 index b7466683b0..0000000000 --- a/internal/plugins/ansible/v1/scaffolds/internal/templates/roles/tasks_main.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// 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 roles - -import ( - "path/filepath" - - "sigs.k8s.io/kubebuilder/v3/pkg/machinery" - - "github.com/operator-framework/operator-sdk/internal/plugins/ansible/v1/constants" -) - -var _ machinery.Template = &TasksMain{} - -type TasksMain struct { - machinery.TemplateMixin - machinery.ResourceMixin -} - -// SetTemplateDefaults implements machinery.Template -func (f *TasksMain) SetTemplateDefaults() error { - if f.Path == "" { - f.Path = filepath.Join(constants.RolesDir, "%[kind]", "tasks", "main.yml") - f.Path = f.Resource.Replacer().Replace(f.Path) - } - - f.TemplateBody = tasksMainAnsibleTmpl - return nil -} - -const tasksMainAnsibleTmpl = `--- -# tasks file for {{ .Resource.Kind }} -` diff --git a/internal/plugins/ansible/v1/scaffolds/internal/templates/roles/templates_dir.go b/internal/plugins/ansible/v1/scaffolds/internal/templates/roles/templates_dir.go deleted file mode 100644 index db92f3a780..0000000000 --- a/internal/plugins/ansible/v1/scaffolds/internal/templates/roles/templates_dir.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// 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 roles - -import ( - "path/filepath" - - "sigs.k8s.io/kubebuilder/v3/pkg/machinery" - - "github.com/operator-framework/operator-sdk/internal/plugins/ansible/v1/constants" -) - -var _ machinery.Template = &RoleTemplates{} - -type RoleTemplates struct { - machinery.TemplateMixin - machinery.ResourceMixin -} - -// SetTemplateDefaults implements machinery.Template -func (f *RoleTemplates) SetTemplateDefaults() error { - if f.Path == "" { - f.Path = filepath.Join(constants.RolesDir, "%[kind]", "templates", ".placeholder") - } - f.Path = f.Resource.Replacer().Replace(f.Path) - - f.TemplateBody = templatesDirPlaceholder - return nil -} - -const templatesDirPlaceholder = `` diff --git a/internal/plugins/ansible/v1/scaffolds/internal/templates/roles/vars_main.go b/internal/plugins/ansible/v1/scaffolds/internal/templates/roles/vars_main.go deleted file mode 100644 index ce728b0d94..0000000000 --- a/internal/plugins/ansible/v1/scaffolds/internal/templates/roles/vars_main.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// 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 roles - -import ( - "path/filepath" - - "sigs.k8s.io/kubebuilder/v3/pkg/machinery" - - "github.com/operator-framework/operator-sdk/internal/plugins/ansible/v1/constants" -) - -var _ machinery.Template = &VarsMain{} - -type VarsMain struct { - machinery.TemplateMixin - machinery.ResourceMixin -} - -// SetTemplateDefaults implements machinery.Template -func (f *VarsMain) SetTemplateDefaults() error { - if f.Path == "" { - f.Path = filepath.Join(constants.RolesDir, "%[kind]", "vars", "main.yml") - } - f.Path = f.Resource.Replacer().Replace(f.Path) - - f.TemplateBody = varsMainAnsibleTmpl - return nil -} - -const varsMainAnsibleTmpl = `--- -# vars file for {{ .Resource.Kind }} -` diff --git a/internal/plugins/ansible/v1/scaffolds/internal/templates/watches.go b/internal/plugins/ansible/v1/scaffolds/internal/templates/watches.go deleted file mode 100644 index 4fbe188b7d..0000000000 --- a/internal/plugins/ansible/v1/scaffolds/internal/templates/watches.go +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright 2020 The Operator-SDK Authors -// -// 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 templates - -import ( - "bytes" - "fmt" - "text/template" - - "sigs.k8s.io/kubebuilder/v3/pkg/machinery" -) - -var _ machinery.Template = &Watches{} - -const ( - defaultWatchesFile = "watches.yaml" - watchMarker = "watch" -) - -// Watches scaffolds the watches.yaml file -type Watches struct { - machinery.TemplateMixin -} - -// SetTemplateDefaults implements machinery.Template -func (f *Watches) SetTemplateDefaults() error { - if f.Path == "" { - f.Path = defaultWatchesFile - } - - f.TemplateBody = fmt.Sprintf(watchesTemplate, - machinery.NewMarkerFor(f.Path, watchMarker), - ) - return nil -} - -var _ machinery.Inserter = &WatchesUpdater{} - -type WatchesUpdater struct { - machinery.ResourceMixin - - GeneratePlaybook bool - GenerateRole bool - PlaybooksDir string -} - -func (*WatchesUpdater) GetPath() string { - return defaultWatchesFile -} - -func (*WatchesUpdater) GetIfExistsAction() machinery.IfExistsAction { - return machinery.OverwriteFile -} - -func (f *WatchesUpdater) GetMarkers() []machinery.Marker { - return []machinery.Marker{ - machinery.NewMarkerFor(defaultWatchesFile, watchMarker), - } -} - -func (f *WatchesUpdater) GetCodeFragments() machinery.CodeFragmentsMap { - fragments := make(machinery.CodeFragmentsMap, 1) - - // If resource is not being provided we are creating the file, not updating it - if f.Resource == nil { - return fragments - } - - // Generate watch fragments - watches := make([]string, 0) - buf := &bytes.Buffer{} - - // TODO(asmacdo) Move template execution into a function, executed by the apiScaffolder.scaffold() - // DefaultFuncMap used provide the function "lower", used in the watch fragment. - tmpl := template.Must(template.New("rules").Funcs(machinery.DefaultFuncMap()).Parse(watchFragment)) - err := tmpl.Execute(buf, f) - if err != nil { - panic(err) - } - watches = append(watches, buf.String()) - - if len(watches) != 0 { - fragments[machinery.NewMarkerFor(defaultWatchesFile, watchMarker)] = watches - } - return fragments -} - -const watchesTemplate = `--- -# Use the 'create api' subcommand to add watches to this file. -%s -` - -const watchFragment = `- version: {{ .Resource.Version }} - group: {{ .Resource.QualifiedGroup }} - kind: {{ .Resource.Kind }} - {{- if .GeneratePlaybook }} - playbook: {{ .PlaybooksDir }}/{{ lower .Resource.Kind }}.yml - {{- else if .GenerateRole}} - role: {{ lower .Resource.Kind }} - {{- else }} - # FIXME: Specify the role or playbook for this resource. - {{- end }} -`