diff --git a/.dockerignore b/.dockerignore index a3aab7af7..b584d3a1e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,4 @@ # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file # Ignore build and test binaries. bin/ +.custom-deploy diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 000000000..88ebd919e --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,2 @@ +v1.0: +- base-branch: 'release-1.0' diff --git a/.github/workflows/check-pr-labels.yaml b/.github/workflows/check-pr-labels.yaml new file mode 100644 index 000000000..a6a68e880 --- /dev/null +++ b/.github/workflows/check-pr-labels.yaml @@ -0,0 +1,23 @@ +name: Ready +on: + pull_request: + types: + - labeled + - opened + - reopened + - synchronize + - unlabeled + +permissions: {} + +jobs: + hold: + if: github.event.pull_request.merged == false + runs-on: ubuntu-latest + steps: + - if: > + contains(github.event.pull_request.labels.*.name, 'hold') + run: 'false' + - if: > + !contains(github.event.pull_request.labels.*.name, 'hold') + run: 'true' diff --git a/.github/workflows/container_image.yaml b/.github/workflows/container_image.yaml index 816fc56a2..6e9b30935 100644 --- a/.github/workflows/container_image.yaml +++ b/.github/workflows/container_image.yaml @@ -3,13 +3,12 @@ name: container image on: push: branches: - - main + - release-1.0 permissions: contents: read env: - image_tag_latest: quay.io/orc/openstack-resource-controller:latest image_tag_branch: quay.io/orc/openstack-resource-controller:branch-${GITHUB_REF_NAME} image_tag_commit: quay.io/orc/openstack-resource-controller:commit-${GITHUB_SHA::7} @@ -18,14 +17,27 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - - run: | - docker login -u="${{ secrets.QUAY_USERNAME }}" -p="${{ secrets.QUAY_TOKEN }}" quay.io - - docker build -t ${{ env.image_tag_branch }} -t ${{ env.image_tag_latest }} . - docker push ${{ env.image_tag_branch }} - docker push ${{ env.image_tag_latest }} - - docker build -t ${{ env.image_tag_commit }} --label quay.expires-after=4w . - docker push ${{ env.image_tag_commit }} + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # tag=v6.0.2 + with: + persist-credentials: false + + - name: Build and push images + run: | + docker login -u="${{ secrets.QUAY_USERNAME }}" -p="${{ secrets.QUAY_TOKEN }}" quay.io # zizmor: ignore[secrets-outside-env] + + make docker-build IMG=${{ env.image_tag_branch }} && \ + make docker-build IMG=${{ env.image_tag_commit }} + + make docker-push IMG=${{ env.image_tag_branch }} && \ + make docker-push IMG=${{ env.image_tag_commit }} + + - name: Set expiration on commit image + env: + QUAY_OAUTH_TOKEN: ${{ secrets.QUAY_OAUTH_TOKEN }} # zizmor: ignore[secrets-outside-env] + run: | + EXPIRATION=$(($(date -u +%s) + 2419200)) + curl -sf -X PUT \ + -H "Authorization: Bearer ${QUAY_OAUTH_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{\"expiration\": $EXPIRATION}" \ + "https://quay.io/api/v1/repository/orc/openstack-resource-controller/tag/commit-${GITHUB_SHA::7}" diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index b88e4202f..56f342a82 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -13,11 +13,14 @@ jobs: fail-fast: false matrix: include: - - name: "zed" - openstack_version: "stable/zed" - ubuntu_version: "20.04" - - name: "bobcat" - openstack_version: "stable/2023.2" + - name: "flamingo" + openstack_version: "stable/2025.2" + ubuntu_version: "24.04" + - name: "epoxy" + openstack_version: "stable/2025.1" + ubuntu_version: "24.04" + - name: "dalmatian" + openstack_version: "stable/2024.2" ubuntu_version: "22.04" env: image_tag: virtual-registry.k-orc.cloud/ci:commit-${GITHUB_SHA::7} @@ -25,37 +28,44 @@ jobs: runs-on: ubuntu-${{ matrix.ubuntu_version }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # tag=v6.0.2 + with: + persist-credentials: false - name: Deploy devstack - uses: EmilienM/devstack-action@40c77372dbc135a17adc877eb77fc226a134305c + uses: gophercloud/devstack-action@60ca1042045c0c9e3e001c64575d381654ffcba1 # tag=v0.19 with: enable_workaround_docker_io: 'false' branch: ${{ matrix.openstack_version }} + enabled_services: "openstack-cli-server" - name: Deploy a Kind Cluster - uses: helm/kind-action@dda0770415bac9fc20092cacbc54aa298604d140 + uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # tag=v1.14.0 with: cluster_name: orc - name: Build and push a container image to Kind run: | - docker build -t ${{ env.image_tag }} . + make docker-build IMG=${{ env.image_tag }} kind load docker-image ${{ env.image_tag }} ${{ env.image_tag }} --name orc - name: Deploy orc run: | kubectl config use-context kind-orc - make deploy IMG=${{ env.image_tag }} + make deploy IMG=${{ env.image_tag }} LOGLEVEL=5 - - name: Apply simple-server - run: | - cp /etc/openstack/clouds.yaml config/samples/simple-server/ - kubectl apply -k config/samples/simple-server - kubectl wait --timeout=10m --for=condition=ready OpenStackServer workstation + - name: Run e2e tests + run: make test-e2e - - name: Inspect the server - run: | - openstack server show "$(kubectl get openstackserver workstation -o jsonpath='{.status.resource.id}')" + - name: Generate logs on failure + run: ./hack/collectlogs + if: failure() env: OS_CLOUD: devstack + + - name: Upload logs artifacts on failure + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # tag=v7 + with: + name: e2e-${{ matrix.name }}-${{ github.run_id }} + path: /tmp/artifacts/* diff --git a/.github/workflows/generate.yaml b/.github/workflows/generate.yaml index b8397b7a0..b347c5f34 100644 --- a/.github/workflows/generate.yaml +++ b/.github/workflows/generate.yaml @@ -1,22 +1,20 @@ on: push: branches: - - main + - release-1.0 pull_request: name: generate permissions: contents: read jobs: - make-manifests-generate: + make-generate: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - - run: | - make manifests - git diff --exit-code + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # tag=v6.0.2 + with: + persist-credentials: false - run: | make generate diff --git a/.github/workflows/go-fmt.yaml b/.github/workflows/go-fmt.yaml deleted file mode 100644 index 66c2b493e..000000000 --- a/.github/workflows/go-fmt.yaml +++ /dev/null @@ -1,22 +0,0 @@ -on: - push: - branches: - - main - pull_request: -name: go fmt -permissions: - contents: read - -jobs: - go-fmt: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-go@v5 - with: - go-version: '1' - - - run: | - test -z "$(gofmt -e -d . | tee /dev/stderr)" diff --git a/.github/workflows/go-lint.yaml b/.github/workflows/go-lint.yaml new file mode 100644 index 000000000..c1dfd6ba8 --- /dev/null +++ b/.github/workflows/go-lint.yaml @@ -0,0 +1,30 @@ +on: + push: + branches: + - release-1.0 + pull_request: +name: go lint +permissions: + contents: read + +jobs: + go-lint: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # tag=v6.0.2 + with: + persist-credentials: false + + - name: Calculate go version + id: vars + run: echo "go_version=$(make go-version)" >> $GITHUB_OUTPUT + + - name: Set up Go + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # tag=v6.4.0 + with: + go-version: ${{ steps.vars.outputs.go_version }} + cache: false + + - run: | + make lint diff --git a/.github/workflows/go-mod.yaml b/.github/workflows/go-mod.yaml deleted file mode 100644 index a6877bdaa..000000000 --- a/.github/workflows/go-mod.yaml +++ /dev/null @@ -1,22 +0,0 @@ -on: - push: - branches: - - main - pull_request: -name: go mod -permissions: - contents: read - -jobs: - go-mod: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-go@v5 - with: - go-version: '1' - - - run: | - if [ $(go mod tidy && git diff | wc -l) -gt 0 ]; then git diff && exit 1; fi diff --git a/.github/workflows/go-vet.yaml b/.github/workflows/go-vet.yaml deleted file mode 100644 index 79cd15de7..000000000 --- a/.github/workflows/go-vet.yaml +++ /dev/null @@ -1,22 +0,0 @@ -on: - push: - branches: - - main - pull_request: -name: go vet -permissions: - contents: read - -jobs: - go-vet: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-go@v5 - with: - go-version: '1' - - - run: | - go vet ./... diff --git a/.github/workflows/label-pr.yaml b/.github/workflows/label-pr.yaml new file mode 100644 index 000000000..34ee526a3 --- /dev/null +++ b/.github/workflows/label-pr.yaml @@ -0,0 +1,84 @@ +name: Label PR +on: + # zizmor: ignore[dangerous-triggers] edits job only runs actions/labeler, no code checkout + pull_request_target: + types: + - opened + - synchronize + - reopened + # zizmor: ignore[dangerous-triggers] semver-label job never checks out or executes untrusted code + workflow_run: + workflows: ["Semver analysis"] + types: + - completed + +permissions: {} + +jobs: + semver-label: + if: github.event_name == 'workflow_run' + runs-on: ubuntu-latest + permissions: + actions: read + pull-requests: write + steps: + - name: Download semver results + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # tag=v8.0.1 + with: + name: semver-results + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Read PR number + id: pr + run: echo "number=$(cat pr-number)" >> $GITHUB_OUTPUT + + - name: Report failure + if: github.event.workflow_run.conclusion == 'failure' + run: | + gh pr edit "$NUMBER" --remove-label "semver:major,semver:minor,semver:patch" + gh issue comment "$NUMBER" --body "$BODY" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + NUMBER: ${{ steps.pr.outputs.number }} + BODY: > + Failed to assess the semver bump. See [logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }}) for details. + + - name: Read semver type + if: github.event.workflow_run.conclusion == 'success' + id: semver + run: echo "type=$(cat semver-type)" >> $GITHUB_OUTPUT + + - name: Add label semver:patch + if: github.event.workflow_run.conclusion == 'success' && steps.semver.outputs.type == 'patch' + run: gh pr edit "$NUMBER" --add-label "semver:patch" --remove-label "semver:major,semver:minor" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + NUMBER: ${{ steps.pr.outputs.number }} + + - name: Add label semver:minor + if: github.event.workflow_run.conclusion == 'success' && steps.semver.outputs.type == 'minor' + run: gh pr edit "$NUMBER" --add-label "semver:minor" --remove-label "semver:major,semver:patch" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + NUMBER: ${{ steps.pr.outputs.number }} + + - name: Add label semver:major + if: github.event.workflow_run.conclusion == 'success' && steps.semver.outputs.type == 'major' + run: gh pr edit "$NUMBER" --add-label "semver:major" --remove-label "semver:minor,semver:patch" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + NUMBER: ${{ steps.pr.outputs.number }} + + edits: + if: github.event_name == 'pull_request_target' + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # tag=v6 diff --git a/.github/workflows/release_image.yaml b/.github/workflows/release_image.yaml new file mode 100644 index 000000000..affdb6f3c --- /dev/null +++ b/.github/workflows/release_image.yaml @@ -0,0 +1,27 @@ +name: release image + +on: + push: + tags: + - v** + +permissions: + contents: read + +env: + image_tag: quay.io/orc/openstack-resource-controller:${GITHUB_REF_NAME} + +jobs: + push: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # tag=v6.0.2 + with: + persist-credentials: false + + - run: | + docker login -u="${{ secrets.QUAY_USERNAME }}" -p="${{ secrets.QUAY_TOKEN }}" quay.io # zizmor: ignore[secrets-outside-env] + + docker build -t ${{ env.image_tag }} . + docker push ${{ env.image_tag }} diff --git a/.github/workflows/semver.yaml b/.github/workflows/semver.yaml new file mode 100644 index 000000000..741ed138e --- /dev/null +++ b/.github/workflows/semver.yaml @@ -0,0 +1,68 @@ +name: Semver analysis +on: + pull_request: + types: + - opened + - synchronize + - reopened + +permissions: + contents: read + +jobs: + analyze: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # tag=v6.0.2 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false + + - name: Rebase the PR against base ref to ensure actual API compatibility + run: | + git config --global user.email "localrebase@k-orc.cloud" + git config --global user.name "Local rebase" + git rebase -i origin/$BASE_REF + env: + GIT_SEQUENCE_EDITOR: '/usr/bin/true' + BASE_REF: ${{ github.base_ref }} + + - name: Calculate go version + id: vars + run: echo "go_version=$(make go-version)" >> $GITHUB_OUTPUT + + - name: Set up Go + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # tag=v6.4.0 + with: + go-version: ${{ steps.vars.outputs.go_version }} + + - name: Checking Go API Compatibility + id: go-apidiff + # if semver=major, this will return RC=1, so let's ignore the failure so label + # can be set later. We check for actual errors in the next step. + continue-on-error: true + uses: joelanford/go-apidiff@60c4206be8f84348ebda2a3e0c3ac9cb54b8f685 # tag=v0.8.3 + + # go-apidiff returns RC=1 when semver=major, which makes the workflow to return + # a failure. Instead let's just return a failure if go-apidiff failed to run. + - name: Return an error if Go API Compatibility couldn't be verified + if: steps.go-apidiff.outcome != 'success' && steps.go-apidiff.outputs.semver-type != 'major' + run: exit 1 + + - name: Save semver result + if: always() + run: | + mkdir -p semver-results + echo "$SEMVER_TYPE" > semver-results/semver-type + echo "$PR_NUMBER" > semver-results/pr-number + env: + SEMVER_TYPE: ${{ steps.go-apidiff.outputs.semver-type }} + PR_NUMBER: ${{ github.event.pull_request.number }} + + - name: Upload semver results + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # tag=v7 + with: + name: semver-results + path: semver-results/ diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index 96141cb77..7716dd504 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -1,7 +1,7 @@ on: push: branches: - - main + - release-1.0 pull_request: name: go test permissions: @@ -16,11 +16,19 @@ jobs: - '1' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # tag=v6.0.2 + with: + persist-credentials: false + + - name: Calculate go version + id: vars + run: echo "go_version=$(make go-version)" >> $GITHUB_OUTPUT - - uses: actions/setup-go@v5 + - name: Set up Go + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # tag=v6.4.0 with: - go-version: ${{ matrix.go-version }} + go-version: ${{ steps.vars.outputs.go_version }} + cache: false - run: | - go test -v ./... + make test diff --git a/.github/workflows/website.yaml b/.github/workflows/website.yaml deleted file mode 100644 index 34eca4802..000000000 --- a/.github/workflows/website.yaml +++ /dev/null @@ -1,41 +0,0 @@ -on: - push: - branches: - - main - paths: - - 'website/**' - - 'api/v1alpha1/**' - - '.github/workflows/website.yaml' -name: website - -jobs: - publish: - runs-on: ubuntu-latest - permissions: - contents: read - deployments: write - name: Publish to Cloudflare Pages - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install crd docs generator - run: go install github.com/elastic/crd-ref-docs@15438a197f339a5b68be2e622725327c3e367811 - - - name: Pip install - run: pip install -Ur website/requirements.txt - - - name: Generate reference - run: /home/runner/go/bin/crd-ref-docs --config=website/crd-ref-docs-config.yaml --output-path=website/docs/reference.md --source-path=api/v1alpha1 --renderer=markdown - - - name: Build the site - run: mkdocs build --verbose --strict --config-file website/mkdocs.yml --site-dir rendered - - - name: Publish to Cloudflare Pages - uses: cloudflare/pages-action@v1 - with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - projectName: k-orc - directory: website/rendered - gitHubToken: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index cbfd6004d..7573652a2 100644 --- a/.gitignore +++ b/.gitignore @@ -22,9 +22,14 @@ Dockerfile.cross *.swp *.swo *~ +.devcontainer # website dynamic assets /venv /website/docs/reference.md /website/rendered/ __pycache__/ + +# Custom +/openapi.json +/.custom-deploy diff --git a/Dockerfile b/Dockerfile index a48973ee7..d1c138fda 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,6 @@ # Build the manager binary -FROM golang:1.22 AS builder +ARG GO_VERSION="1.24" +FROM golang:${GO_VERSION} AS builder ARG TARGETOS ARG TARGETARCH @@ -12,16 +13,17 @@ COPY go.sum go.sum RUN go mod download # Copy the go source -COPY cmd/main.go cmd/main.go +COPY cmd/ cmd/ COPY api/ api/ -COPY internal/controller/ internal/controller/ +COPY internal/ internal/ +COPY pkg/ pkg/ # Build # the GOARCH has not a default value to allow the binary be built according to the host where the command # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. -RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go +RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/manager/main.go # Use distroless as minimal base image to package the manager binary # Refer to https://github.com/GoogleContainerTools/distroless for more details diff --git a/Makefile b/Makefile index 2127365bb..8f67d29e1 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,9 @@ # Image URL to use all building/pushing image targets IMG ?= controller:latest # ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. -ENVTEST_K8S_VERSION = 1.30.0 +ENVTEST_K8S_VERSION = 1.29.0 +TRIVY_VERSION = 0.69.3 +GO_VERSION ?= 1.25.10 # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) ifeq (,$(shell go env GOBIN)) @@ -21,6 +23,9 @@ CONTAINER_TOOL ?= docker SHELL = /usr/bin/env bash -o pipefail .SHELLFLAGS = -ec +# Enables shell script tracing. Enable by running: TRACE=1 make +TRACE ?= 0 + .PHONY: all all: build @@ -87,16 +92,14 @@ vet: ## Run go vet against code. .PHONY: test TEST_PATHS ?= ./... -test: manifests generate fmt vet test-only ## Run tests. - -.PHONY: test-only -test-only: envtest +test: envtest KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test $$(go list $(TEST_PATHS) | grep -v /e2e) -coverprofile cover.out # Utilize Kind or modify the e2e tests to load the image locally, enabling compatibility with other vendors. .PHONY: test-e2e # Run the e2e tests against a Kind k8s instance that is spun up. test-e2e: - go test ./test/e2e/ -v -ginkgo.v + # go test ./test/e2e/ -v -ginkgo.v + ./hack/e2e.sh .PHONY: lint lint: golangci-lint ## Run golangci-lint linter @@ -110,18 +113,18 @@ lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes .PHONY: build build: manifests generate fmt vet ## Build manager binary. - go build -o bin/manager cmd/main.go + go build -o bin/manager cmd/manager/main.go .PHONY: run run: manifests generate fmt vet ## Run a controller from your host. - go run ./cmd/main.go + go run ./cmd/manager/main.go # If you wish to build the manager image targeting other platforms you can use the --platform flag. # (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it. # More info: https://docs.docker.com/develop/develop-images/build_enhancements/ .PHONY: docker-build docker-build: ## Build docker image with the manager. - $(CONTAINER_TOOL) build -t ${IMG} . + $(CONTAINER_TOOL) build --tag ${IMG} --build-arg "GO_VERSION=$(GO_VERSION)" . .PHONY: docker-push docker-push: ## Push docker image with the manager. @@ -140,15 +143,15 @@ docker-buildx: ## Build and push docker image for the manager for cross-platform sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross - $(CONTAINER_TOOL) buildx create --name orc-builder $(CONTAINER_TOOL) buildx use orc-builder - - $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross . + - $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} --build-arg "GO_VERSION=$(GO_VERSION)" -f Dockerfile.cross . - $(CONTAINER_TOOL) buildx rm orc-builder rm Dockerfile.cross .PHONY: build-installer build-installer: manifests generate kustomize ## Generate a consolidated YAML with CRDs and deployment. mkdir -p dist - cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} - $(KUSTOMIZE) build config/default > dist/install.yaml + $(MAKE) custom-deploy IMG=${IMG} + $(KUSTOMIZE) build $(CUSTOMDEPLOY) > dist/install.yaml ##@ Deployment @@ -156,6 +159,34 @@ ifndef ignore-not-found ignore-not-found = false endif +# custom-deploy initialises a new kustomize module in $(CUSTOMDEPLOY) (deleting +# any existing directory first). This kustomize module: +# - includes config/default as a resource +# - overrides the controller image with the value of $(IMG) +# - adds an argument to the controller to set a custom log level if $(LOGLEVEL) +# is set + +define args_patch +- op: add + path: /spec/template/spec/containers/0/args/- + value: "-zap-log-level=$(LOGLEVEL)" +endef +export args_patch + +CUSTOMDEPLOY ?= $(shell pwd)/.custom-deploy + +.PHONY: custom-deploy +custom-deploy: customdeploy_relative = $(shell realpath -m --relative-to $(CUSTOMDEPLOY) $(shell pwd)) +custom-deploy: kustomize + if [ -d $(CUSTOMDEPLOY) ]; then rm -f $(CUSTOMDEPLOY)/kustomization.yaml && rmdir $(CUSTOMDEPLOY); fi + mkdir -p $(CUSTOMDEPLOY) + cd $(CUSTOMDEPLOY); $(KUSTOMIZE) create --resources $(customdeploy_relative)/config/default + cd $(CUSTOMDEPLOY); $(KUSTOMIZE) edit set image controller=$(IMG) + if [ -n "$(LOGLEVEL)" ]; then \ + cd $(CUSTOMDEPLOY) && \ + $(KUSTOMIZE) edit add patch --kind Deployment --namespace orc-system --name orc-controller-manager --patch "$$args_patch"; \ + fi + .PHONY: install install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. $(KUSTOMIZE) build config/crd | $(KUBECTL) apply -f - @@ -166,13 +197,35 @@ uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified .PHONY: deploy deploy: manifests 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 - + $(MAKE) custom-deploy IMG=${IMG} + $(KUSTOMIZE) build $(CUSTOMDEPLOY) | $(KUBECTL) apply -f - .PHONY: undeploy undeploy: kustomize ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. $(KUSTOMIZE) build config/default | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - +##@ Security + +.PHONY: verify-container-images +verify-container-images: ## Verify container images + TRACE=$(TRACE) ./hack/verify-container-images.sh $(TRIVY_VERSION) + +.PHONY: verify-govulncheck +verify-govulncheck: govulncheck ## Verify code for vulnerabilities + $(GOVULNCHECK) ./... && R1=$$? || R1=$$?; \ + if [ "$$R1" -ne "0" ]; then \ + exit 1; \ + fi + +.PHONY: verify-security +verify-security: ## Verify code and images for vulnerabilities + $(MAKE) verify-container-images && R1=$$? || R1=$$?; \ + $(MAKE) verify-govulncheck && R2=$$? || R2=$$?; \ + if [ "$$R1" -ne "0" ] || [ "$$R2" -ne "0" ]; then \ + echo "Check for vulnerabilities failed! There are vulnerabilities to be fixed"; \ + exit 1; \ + fi + ##@ Dependencies ## Location to install dependencies to @@ -189,13 +242,15 @@ CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen ENVTEST ?= $(LOCALBIN)/setup-envtest GOLANGCI_LINT = $(LOCALBIN)/golangci-lint MOCKGEN = $(LOCALBIN)/mockgen +GOVULNCHECK = $(LOCALBIN)/govulncheck ## Tool Versions KUSTOMIZE_VERSION ?= v5.4.2 CONTROLLER_TOOLS_VERSION ?= v0.16.4 -ENVTEST_VERSION ?= release-0.18 -GOLANGCI_LINT_VERSION ?= v1.61.0 +ENVTEST_VERSION ?= release-0.22 +GOLANGCI_LINT_VERSION ?= v1.64.8 MOCKGEN_VERSION ?= v0.4.0 +GOVULNCHECK_VERSION ?= v1.1.4 .PHONY: kustomize kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. @@ -222,6 +277,11 @@ mockgen: $(MOCKGEN) ## Download mockgen locally if necessary. $(MOCKGEN): $(LOCALBIN) $(call go-install-tool,$(MOCKGEN),go.uber.org/mock/mockgen,$(MOCKGEN_VERSION)) +.PHONY: govulncheck +govulncheck: $(GOVULNCHECK) ## Download govulncheck locally if necessary. +$(GOVULNCHECK): $(LOCALBIN) + $(call go-install-tool,$(GOVULNCHECK),golang.org/x/vuln/cmd/govulncheck,$(GOVULNCHECK_VERSION)) + # go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist # $1 - target path with name of binary # $2 - package url which can be installed @@ -237,3 +297,8 @@ mv $(1) $(1)-$(3) ;\ } ;\ ln -sf $(1)-$(3) $(1) endef + +##@ helpers: + +go-version: ## Print the go version we use to compile our binaries and images + @echo $(GO_VERSION) diff --git a/api/v1alpha1/conditions.go b/api/v1alpha1/conditions.go index 83631732e..93dfe7712 100644 --- a/api/v1alpha1/conditions.go +++ b/api/v1alpha1/conditions.go @@ -1,5 +1,5 @@ /* -Copyright 2023 The Kubernetes Authors. +Copyright 2023 The ORC Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -31,34 +31,34 @@ const ( // Message. // Normal progress: continue waiting. - OpenStackConditionReasonProgressing = "Progressing" + ConditionReasonProgressing = "Progressing" // The user must fix the configuration before trying again. - OpenStackConditionReasonInvalidConfiguration = "InvalidConfiguration" + ConditionReasonInvalidConfiguration = "InvalidConfiguration" // An error occurred which we can't recover from. It must be addressed // before we can continue. - OpenStackConditionReasonUnrecoverableError = "UnrecoverableError" + ConditionReasonUnrecoverableError = "UnrecoverableError" // An error occurred which may go away eventually if we keep trying. The // user likely wants to know about this if it persists. - OpenStackConditionReasonTransientError = "TransientError" + ConditionReasonTransientError = "TransientError" // The resource is ready for use. - OpenStackConditionReasonSuccess = "Success" + ConditionReasonSuccess = "Success" ) const ( - OpenStackConditionAvailable = "Available" - OpenStackConditionProgressing = "Progressing" + ConditionAvailable = "Available" + ConditionProgressing = "Progressing" ) // IsConditionReasonTerminal returns true if the given reason represents an error which should prevent further reconciliation. func IsConditionReasonTerminal(reason string) bool { return slices.Contains( []string{ - OpenStackConditionReasonInvalidConfiguration, - OpenStackConditionReasonUnrecoverableError, + ConditionReasonInvalidConfiguration, + ConditionReasonUnrecoverableError, }, reason) } @@ -73,7 +73,7 @@ type ObjectWithConditions interface { // exists and is up to date. func getUpToDateProgressing(obj ObjectWithConditions) *metav1.Condition { conditions := obj.GetConditions() - progressing := meta.FindStatusCondition(conditions, OpenStackConditionProgressing) + progressing := meta.FindStatusCondition(conditions, ConditionProgressing) // Not complete if Progressing condition does not exist if progressing == nil { @@ -96,7 +96,7 @@ func IsReconciliationComplete(obj ObjectWithConditions) bool { } // Complete if we've either succeeded or failed terminally - return progressing.Reason == OpenStackConditionReasonSuccess || IsConditionReasonTerminal(progressing.Reason) + return progressing.Reason == ConditionReasonSuccess || IsConditionReasonTerminal(progressing.Reason) } // GetTerminalError returns an error containing a descriptive message if reconciliation has failed terminally, or nil otherwise. @@ -115,7 +115,7 @@ func GetTerminalError(obj ObjectWithConditions) error { func IsAvailable(obj ObjectWithConditions) bool { conditions := obj.GetConditions() - available := meta.FindStatusCondition(conditions, OpenStackConditionAvailable) + available := meta.FindStatusCondition(conditions, ConditionAvailable) return available != nil && available.Status == metav1.ConditionTrue } diff --git a/api/v1alpha1/controller_options.go b/api/v1alpha1/controller_options.go index a950cbf3b..0c8a59007 100644 --- a/api/v1alpha1/controller_options.go +++ b/api/v1alpha1/controller_options.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Kubernetes Authors. +Copyright 2024 The ORC Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/api/v1alpha1/image_types.go b/api/v1alpha1/image_types.go index e57029613..ebc718670 100644 --- a/api/v1alpha1/image_types.go +++ b/api/v1alpha1/image_types.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Kubernetes Authors. +Copyright 2024 The ORC Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -274,10 +274,8 @@ type ImageHash struct { type ImageResourceSpec struct { // Name will be the name of the created Glance image. If not specified, the // name of the Image object will be used. - // +kubebuilder:validation:MinLength:=1 - // +kubebuilder:validation:MaxLength:=1000 // +optional - Name string `json:"name,omitempty"` + Name *OpenStackName `json:"name,omitempty"` // Protected specifies that the image is protected from deletion. // If not specified, the default is false. diff --git a/api/v1alpha1/openstack_types.go b/api/v1alpha1/openstack_types.go new file mode 100644 index 000000000..4d1863258 --- /dev/null +++ b/api/v1alpha1/openstack_types.go @@ -0,0 +1,29 @@ +/* +Copyright 2024 The ORC 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 v1alpha1 + +// +kubebuilder:validation:Format:=uuid +// +kubebuilder:validation:MaxLength:=36 +type UUID string + +// +kubebuilder:validation:MinLength:=1 +// +kubebuilder:validation:MaxLength:=1024 +type OpenStackName string + +// +kubebuilder:validation:MinLength:=1 +// +kubebuilder:validation:MaxLength:=1024 +type OpenStackDescription string diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 3ae986c9c..93edc816b 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -297,6 +297,11 @@ func (in *ImagePropertiesHardware) DeepCopy() *ImagePropertiesHardware { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ImageResourceSpec) DeepCopyInto(out *ImageResourceSpec) { *out = *in + if in.Name != nil { + in, out := &in.Name, &out.Name + *out = new(OpenStackName) + **out = **in + } if in.Protected != nil { in, out := &in.Protected, &out.Protected *out = new(bool) diff --git a/api/v1alpha1/zz_generated.image-resource.go b/api/v1alpha1/zz_generated.image-resource.go index 21bb4fb49..87089b93d 100644 --- a/api/v1alpha1/zz_generated.image-resource.go +++ b/api/v1alpha1/zz_generated.image-resource.go @@ -1,6 +1,6 @@ // Code generated by resource-generator. DO NOT EDIT. /* -Copyright 2024 The Kubernetes Authors. +Copyright 2024 The ORC Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -145,6 +145,10 @@ type ImageList struct { Items []Image `json:"items"` } +func (l *ImageList) GetItems() []Image { + return l.Items +} + func init() { SchemeBuilder.Register(&Image{}, &ImageList{}) } diff --git a/cmd/manager/main.go b/cmd/manager/main.go index 31ccb4c85..b5d86cd20 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -17,7 +17,6 @@ limitations under the License. package main import ( - "crypto/tls" "flag" "os" @@ -25,138 +24,79 @@ import ( // to ensure that exec-entrypoint and run can make use of them. _ "k8s.io/client-go/plugin/pkg/client/auth" - "k8s.io/apimachinery/pkg/runtime" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" - clientgoscheme "k8s.io/client-go/kubernetes/scheme" ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" - "sigs.k8s.io/controller-runtime/pkg/metrics/filters" - metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" - "sigs.k8s.io/controller-runtime/pkg/webhook" - openstackv1alpha1 "github.com/k-orc/openstack-resource-controller/api/v1alpha1" + "github.com/k-orc/openstack-resource-controller/internal/controllers/export" + "github.com/k-orc/openstack-resource-controller/internal/controllers/image" + internalmanager "github.com/k-orc/openstack-resource-controller/internal/manager" + "github.com/k-orc/openstack-resource-controller/internal/scheme" + "github.com/k-orc/openstack-resource-controller/internal/scope" // +kubebuilder:scaffold:imports ) var ( - scheme = runtime.NewScheme() - setupLog = ctrl.Log.WithName("setup") + defaultCACertsPath string + namespaceList []string ) -func init() { - utilruntime.Must(clientgoscheme.AddToScheme(scheme)) - - utilruntime.Must(openstackv1alpha1.AddToScheme(scheme)) - // +kubebuilder:scaffold:scheme -} - func main() { - var metricsAddr string - var enableLeaderElection bool - var probeAddr string - var secureMetrics bool - var enableHTTP2 bool - var tlsOpts []func(*tls.Config) - flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+ + setupLog := ctrl.Log.WithName("setup") + + orcOpts := internalmanager.Options{} + flag.StringVar(&orcOpts.MetricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+ "Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.") - flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") - flag.BoolVar(&enableLeaderElection, "leader-elect", false, + flag.StringVar(&orcOpts.ProbeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") + flag.BoolVar(&orcOpts.EnableLeaderElection, "leader-elect", false, "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") - flag.BoolVar(&secureMetrics, "metrics-secure", true, + flag.BoolVar(&orcOpts.SecureMetrics, "metrics-secure", true, "If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.") - flag.BoolVar(&enableHTTP2, "enable-http2", false, + flag.BoolVar(&orcOpts.EnableHTTP2, "enable-http2", false, "If set, HTTP/2 will be enabled for the metrics and webhook servers") - opts := zap.Options{ + flag.IntVar(&orcOpts.ScopeCacheMaxSize, "scope-cache-max-size", 10, + "The maximum credentials count the operator should keep in cache. "+ + "Setting this value to 0 means no cache.") + flag.StringVar(&defaultCACertsPath, "default-ca-certs", "", + "The path to a PEM-encoded CA Certificate file to supply as default for OpenStack API requests.") + flag.Func("namespace", "A namespace that the controller watches to reconcile ORC objects. "+ + "Can be specified multiple times.", func(ns string) error { + namespaceList = append(namespaceList, ns) + return nil + }) + + zapOpts := zap.Options{ Development: true, } - opts.BindFlags(flag.CommandLine) + zapOpts.BindFlags(flag.CommandLine) flag.Parse() - ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) - - // if the enable-http2 flag is false (the default), http/2 should be disabled - // due to its vulnerabilities. More specifically, disabling http/2 will - // prevent from being vulnerable to the HTTP/2 Stream Cancellation and - // Rapid Reset CVEs. For more information see: - // - https://github.com/advisories/GHSA-qppj-fm5r-hxr3 - // - https://github.com/advisories/GHSA-4374-p667-p6c8 - disableHTTP2 := func(c *tls.Config) { - setupLog.Info("disabling http/2") - c.NextProtos = []string{"http/1.1"} - } + log := zap.New(zap.UseFlagOptions(&zapOpts)) + ctrl.SetLogger(log) - if !enableHTTP2 { - tlsOpts = append(tlsOpts, disableHTTP2) - } - - webhookServer := webhook.NewServer(webhook.Options{ - TLSOpts: tlsOpts, - }) + // Setup the context that's going to be used in controllers and for the manager. + ctx := ctrl.SetupSignalHandler() - // Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server. - // More info: - // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.18.4/pkg/metrics/server - // - https://book.kubebuilder.io/reference/metrics.html - metricsServerOptions := metricsserver.Options{ - BindAddress: metricsAddr, - SecureServing: secureMetrics, - // TODO(user): TLSOpts is used to allow configuring the TLS config used for the server. If certificates are - // not provided, self-signed certificates will be generated by default. This option is not recommended for - // production environments as self-signed certificates do not offer the same level of trust and security - // as certificates issued by a trusted Certificate Authority (CA). The primary risk is potentially allowing - // unauthorized access to sensitive metrics data. Consider replacing with CertDir, CertName, and KeyName - // to provide certificates, ensuring the server communicates using trusted and secure certificates. - TLSOpts: tlsOpts, + var caCerts []byte + if defaultCACertsPath != "" { + var err error + caCerts, err = os.ReadFile(defaultCACertsPath) + if err != nil { + setupLog.Error(err, "unable to read provided ca certificates file") + os.Exit(1) + } } + scopeFactory := scope.NewFactory(orcOpts.ScopeCacheMaxSize, caCerts) - if secureMetrics { - // FilterProvider is used to protect the metrics endpoint with authn/authz. - // These configurations ensure that only authorized users and service accounts - // can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info: - // https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.18.4/pkg/metrics/filters#WithAuthenticationAndAuthorization - metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization + controllers := []export.Controller{ + image.New(scopeFactory), } - mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ - Scheme: scheme, - Metrics: metricsServerOptions, - WebhookServer: webhookServer, - HealthProbeBindAddress: probeAddr, - LeaderElection: enableLeaderElection, - LeaderElectionID: "f35396c5.k-orc.cloud", - // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily - // when the Manager ends. This requires the binary to immediately end when the - // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly - // speeds up voluntary leader transitions as the new leader don't have to wait - // LeaseDuration time first. - // - // In the default scaffold provided, the program ends immediately after - // the manager stops, so would be fine to enable this option. However, - // if you are doing or is intended to do any operation such as perform cleanups - // after the manager stops then its usage might be unsafe. - // LeaderElectionReleaseOnCancel: true, - }) + restConfig := ctrl.GetConfigOrDie() + orcOpts.WatchNamespaces = namespaceList + err := internalmanager.Run(ctx, &orcOpts, restConfig, scheme.New(), setupLog, log, controllers) if err != nil { - setupLog.Error(err, "unable to start manager") - os.Exit(1) - } - - // +kubebuilder:scaffold:builder - - if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { - setupLog.Error(err, "unable to set up health check") - os.Exit(1) - } - if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { - setupLog.Error(err, "unable to set up ready check") - os.Exit(1) - } - - setupLog.Info("starting manager") - if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { - setupLog.Error(err, "problem running manager") + setupLog.Error(err, "Error starting manager") os.Exit(1) } } diff --git a/cmd/resource-generator/data/api.template b/cmd/resource-generator/data/api.template index 12800dd8a..90be945d0 100644 --- a/cmd/resource-generator/data/api.template +++ b/cmd/resource-generator/data/api.template @@ -1,5 +1,5 @@ /* -Copyright {{ .Year }} The Kubernetes Authors. +Copyright {{ .Year }} The ORC Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -50,6 +50,9 @@ type {{ .Name }}Import struct { // +kubebuilder:validation:XValidation:rule="{{ .Rule }}",message="{{ .Message }}" {{ end -}} type {{ .Name }}Spec struct { +{{- if .SpecExtraType }} + {{ .SpecExtraType }} `json:",inline"` +{{ end }} // Import refers to an existing OpenStack resource which will be imported instead of // creating a new one. // +optional @@ -148,6 +151,10 @@ type {{ .Name }}List struct { Items []{{ .Name }} `json:"items"` } +func (l *{{ .Name }}List) GetItems() []{{ .Name }} { + return l.Items +} + func init() { SchemeBuilder.Register(&{{ .Name }}{}, &{{ .Name }}List{}) } diff --git a/cmd/resource-generator/main.go b/cmd/resource-generator/main.go index d628a63e7..72d11a9dc 100644 --- a/cmd/resource-generator/main.go +++ b/cmd/resource-generator/main.go @@ -25,14 +25,14 @@ type templateFields struct { APIVersion string Year string Name string + SpecExtraType string StatusExtraType string SpecExtraValidations []specExtraValidation } var allResources []templateFields = []templateFields{ { - Name: "Image", - APIVersion: "v1alpha1", + Name: "Image", SpecExtraValidations: []specExtraValidation{ { Rule: "!has(self.__import__) ? has(self.resource.content) : true", diff --git a/config/crd/bases/openstack.k-orc.cloud_images.yaml b/config/crd/bases/openstack.k-orc.cloud_images.yaml index 2275f926d..7710f5333 100644 --- a/config/crd/bases/openstack.k-orc.cloud_images.yaml +++ b/config/crd/bases/openstack.k-orc.cloud_images.yaml @@ -246,7 +246,7 @@ spec: description: |- Name will be the name of the created Glance image. If not specified, the name of the Image object will be used. - maxLength: 1000 + maxLength: 1024 minLength: 1 type: string properties: diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index 3d482f5c9..e55c1efe8 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -28,35 +28,6 @@ spec: labels: control-plane: controller-manager spec: - # TODO(user): Uncomment the following code to configure the nodeAffinity expression - # according to the platforms which are supported by your solution. - # It is considered best practice to support multiple architectures. You can - # build your manager image using the makefile target docker-buildx. - # affinity: - # nodeAffinity: - # requiredDuringSchedulingIgnoredDuringExecution: - # nodeSelectorTerms: - # - matchExpressions: - # - key: kubernetes.io/arch - # operator: In - # values: - # - amd64 - # - arm64 - # - ppc64le - # - s390x - # - key: kubernetes.io/os - # operator: In - # values: - # - linux - securityContext: - runAsNonRoot: true - # TODO(user): For common cases that do not require escalating privileges - # it is recommended to ensure that all your Pods/Containers are restrictive. - # More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted - # Please uncomment the following code if your project does NOT have to work on old Kubernetes - # versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ). - # seccompProfile: - # type: RuntimeDefault containers: - command: - /manager @@ -70,6 +41,10 @@ spec: capabilities: drop: - "ALL" + privileged: false + runAsUser: 65532 + runAsGroup: 65532 + terminationMessagePolicy: FallbackToLogsOnError livenessProbe: httpGet: path: /healthz @@ -91,5 +66,9 @@ spec: requests: cpu: 10m memory: 64Mi + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault serviceAccountName: controller-manager terminationGracePeriodSeconds: 10 diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 5b1d9a62b..abbc23fe1 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -4,6 +4,14 @@ kind: ClusterRole metadata: name: manager-role rules: +- apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - watch - apiGroups: - openstack.k-orc.cloud resources: diff --git a/dist/install.yaml b/dist/install.yaml new file mode 100644 index 000000000..0d73e4186 --- /dev/null +++ b/dist/install.yaml @@ -0,0 +1,888 @@ +apiVersion: v1 +kind: Namespace +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: orc + control-plane: controller-manager + name: orc-system +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.4 + name: images.openstack.k-orc.cloud +spec: + group: openstack.k-orc.cloud + names: + kind: Image + listKind: ImageList + plural: images + singular: image + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Resource ID + jsonPath: .status.id + name: ID + type: string + - description: Availability status of resource + jsonPath: .status.conditions[?(@.type=='Available')].status + name: Available + type: string + - description: Message describing current availability status + jsonPath: .status.conditions[?(@.type=='Available')].message + name: Message + type: string + - description: Time duration since creation + jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: Image is the Schema for an ORC resource. + 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: ImageSpec defines the desired state of an ORC object. + properties: + cloudCredentialsRef: + description: CloudCredentialsRef points to a secret containing OpenStack + credentials + properties: + cloudName: + description: CloudName specifies the name of the entry in the + clouds.yaml file to use. + maxLength: 256 + minLength: 1 + type: string + secretName: + description: |- + SecretName is the name of a secret in the same namespace as the resource being provisioned. + The secret must contain a key named `clouds.yaml` which contains an OpenStack clouds.yaml file. + The secret may optionally contain a key named `cacert` containing a PEM-encoded CA certificate. + maxLength: 253 + minLength: 1 + type: string + required: + - cloudName + - secretName + type: object + import: + description: |- + Import refers to an existing OpenStack resource which will be imported instead of + creating a new one. + maxProperties: 1 + minProperties: 1 + properties: + filter: + description: |- + Filter contains a resource query which is expected to return a single + result. The controller will continue to retry if filter returns no + results. If filter returns multiple results the controller will set an + error state and will not continue to retry. + minProperties: 1 + properties: + name: + description: Name specifies the name of a Glance image + maxLength: 1000 + minLength: 1 + type: string + type: object + id: + description: |- + ID contains the unique identifier of an existing OpenStack resource. Note + that when specifying an import by ID, the resource MUST already exist. + The ORC object will enter an error state if the resource does not exist. + format: uuid + type: string + type: object + managedOptions: + description: ManagedOptions specifies options which may be applied + to managed objects. + properties: + onDelete: + default: delete + description: |- + OnDelete specifies the behaviour of the controller when the ORC + object is deleted. Options are `delete` - delete the OpenStack resource; + `detach` - do not delete the OpenStack resource. If not specified, the + default is `delete`. + enum: + - delete + - detach + type: string + type: object + managementPolicy: + default: managed + description: |- + ManagementPolicy defines how ORC will treat the object. Valid values are + `managed`: ORC will create, update, and delete the resource; `unmanaged`: + ORC will import an existing resource, and will not apply updates to it or + delete it. + enum: + - managed + - unmanaged + type: string + x-kubernetes-validations: + - message: managementPolicy is immutable + rule: self == oldSelf + resource: + description: |- + Resource specifies the desired state of the resource. + + Resource may not be specified if the management policy is `unmanaged`. + + Resource must be specified if the management policy is `managed`. + properties: + content: + description: Content specifies how to obtain the image content. + properties: + containerFormat: + default: bare + description: |- + ContainerFormat is the format of the image container. + qcow2 and raw images do not usually have a container. This is specified as "bare", which is also the default. + Permitted values are ami, ari, aki, bare, ovf, ova, and docker. + enum: + - ami + - ari + - aki + - bare + - ovf + - ova + - docker + type: string + diskFormat: + description: |- + DiskFormat is the format of the disk image. + Normal values are "qcow2", or "raw". Glance may be configured to support others. + enum: + - ami + - ari + - aki + - vhd + - vhdx + - vmdk + - raw + - qcow2 + - vdi + - ploop + - iso + type: string + download: + description: |- + Download describes how to obtain image data by downloading it from a URL. + Must be set when creating a managed image. + properties: + decompress: + description: |- + Decompress specifies that the source data must be decompressed with the + given compression algorithm before being stored. Specifying Decompress + will disable the use of Glance's web-download, as web-download cannot + currently deterministically decompress downloaded content. + enum: + - xz + - gz + - bz2 + type: string + hash: + description: |- + Hash is a hash which will be used to verify downloaded data, i.e. + before any decompression. If not specified, no hash verification will be + performed. Specifying a Hash will disable the use of Glance's + web-download, as web-download cannot currently deterministically verify + the hash of downloaded content. + properties: + algorithm: + description: Algorithm is the hash algorithm used + to generate value. + enum: + - md5 + - sha1 + - sha256 + - sha512 + type: string + value: + description: Value is the hash of the image data using + Algorithm. It must be hex encoded using lowercase + letters. + maxLength: 1024 + minLength: 1 + pattern: ^[0-9a-f]+$ + type: string + required: + - algorithm + - value + type: object + x-kubernetes-validations: + - message: hash is immutable + rule: self == oldSelf + url: + description: URL containing image data + format: uri + type: string + required: + - url + type: object + required: + - diskFormat + - download + type: object + x-kubernetes-validations: + - message: content is immutable + rule: self == oldSelf + name: + description: |- + Name will be the name of the created Glance image. If not specified, the + name of the Image object will be used. + maxLength: 1024 + minLength: 1 + type: string + properties: + description: Properties is metadata available to consumers of + the image + properties: + hardware: + description: |- + Hardware is a set of properties which control the virtual hardware + created by Nova. + properties: + cdromBus: + description: CDROMBus specifies the type of disk controller + to attach CD-ROM devices to. + enum: + - scsi + - virtio + - uml + - xen + - ide + - usb + - lxc + type: string + cpuCores: + description: CPUCores is the preferred number of cores + to expose to the guest + type: integer + cpuPolicy: + description: |- + CPUPolicy is used to pin the virtual CPUs (vCPUs) of instances to the + host's physical CPU cores (pCPUs). Host aggregates should be used to + separate these pinned instances from unpinned instances as the latter + will not respect the resourcing requirements of the former. + + Permitted values are shared (the default), and dedicated. + + shared: The guest vCPUs will be allowed to freely float across host + pCPUs, albeit potentially constrained by NUMA policy. + + dedicated: The guest vCPUs will be strictly pinned to a set of host + pCPUs. In the absence of an explicit vCPU topology request, the + drivers typically expose all vCPUs as sockets with one core and one + thread. When strict CPU pinning is in effect the guest CPU topology + will be setup to match the topology of the CPUs to which it is + pinned. This option implies an overcommit ratio of 1.0. For example, + if a two vCPU guest is pinned to a single host core with two threads, + then the guest will get a topology of one socket, one core, two + threads. + enum: + - shared + - dedicated + type: string + cpuSockets: + description: CPUSockets is the preferred number of sockets + to expose to the guest + type: integer + cpuThreadPolicy: + description: |- + CPUThreadPolicy further refines a CPUPolicy of 'dedicated' by stating + how hardware CPU threads in a simultaneous multithreading-based (SMT) + architecture be used. SMT-based architectures include Intel + processors with Hyper-Threading technology. In these architectures, + processor cores share a number of components with one or more other + cores. Cores in such architectures are commonly referred to as + hardware threads, while the cores that a given core share components + with are known as thread siblings. + + Permitted values are prefer (the default), isolate, and require. + + prefer: The host may or may not have an SMT architecture. Where an + SMT architecture is present, thread siblings are preferred. + + isolate: The host must not have an SMT architecture or must emulate a + non-SMT architecture. If the host does not have an SMT architecture, + each vCPU is placed on a different core as expected. If the host does + have an SMT architecture - that is, one or more cores have thread + siblings - then each vCPU is placed on a different physical core. No + vCPUs from other guests are placed on the same core. All but one + thread sibling on each utilized core is therefore guaranteed to be + unusable. + + require: The host must have an SMT architecture. Each vCPU is + allocated on thread siblings. If the host does not have an SMT + architecture, then it is not used. If the host has an SMT + architecture, but not enough cores with free thread siblings are + available, then scheduling fails. + enum: + - prefer + - isolate + - require + type: string + cpuThreads: + description: CPUThreads is the preferred number of threads + to expose to the guest + type: integer + diskBus: + description: DiskBus specifies the type of disk controller + to attach disk devices to. + enum: + - scsi + - virtio + - uml + - xen + - ide + - usb + - lxc + type: string + scsiModel: + description: |- + SCSIModel enables the use of VirtIO SCSI (virtio-scsi) to provide + block device access for compute instances; by default, instances use + VirtIO Block (virtio-blk). VirtIO SCSI is a para-virtualized SCSI + controller device that provides improved scalability and performance, + and supports advanced SCSI hardware. + + The only permitted value is virtio-scsi. + enum: + - virtio-scsi + type: string + vifModel: + description: |- + VIFModel specifies the model of virtual network interface device to use. + + Permitted values are e1000, e1000e, ne2k_pci, pcnet, rtl8139, virtio, + and vmxnet3. + enum: + - e1000 + - e1000e + - ne2k_pci + - pcnet + - rtl8139 + - virtio + - vmxnet3 + type: string + type: object + minDiskGB: + description: MinDisk is the minimum amount of disk space in + GB that is required to boot the image + minimum: 1 + type: integer + minMemoryMB: + description: MinMemoryMB is the minimum amount of RAM in MB + that is required to boot the image. + minimum: 1 + type: integer + type: object + protected: + description: |- + Protected specifies that the image is protected from deletion. + If not specified, the default is false. + type: boolean + tags: + description: Tags is a list of tags which will be applied to the + image. A tag has a maximum length of 255 characters. + items: + maxLength: 255 + minLength: 1 + type: string + type: array + x-kubernetes-list-type: set + visibility: + description: Visibility of the image + enum: + - public + - private + - shared + - community + type: string + x-kubernetes-validations: + - message: visibility is immutable + rule: self == oldSelf + type: object + x-kubernetes-validations: + - message: name is immutable + rule: 'has(self.name) ? self.name == oldSelf.name : !has(oldSelf.name)' + - message: name is immutable + rule: 'has(self.protected) ? self.protected == oldSelf.protected + : !has(oldSelf.protected)' + - message: tags is immutable + rule: 'has(self.tags) ? self.tags == oldSelf.tags : !has(oldSelf.tags)' + - message: visibility is immutable + rule: 'has(self.visibility) ? self.visibility == oldSelf.visibility + : !has(oldSelf.visibility)' + - message: properties is immutable + rule: 'has(self.properties) ? self.properties == oldSelf.properties + : !has(oldSelf.properties)' + required: + - cloudCredentialsRef + type: object + x-kubernetes-validations: + - message: resource must be specified when policy is managed + rule: 'self.managementPolicy == ''managed'' ? has(self.resource) : true' + - message: import may not be specified when policy is managed + rule: 'self.managementPolicy == ''managed'' ? !has(self.__import__) + : true' + - message: resource may not be specified when policy is unmanaged + rule: 'self.managementPolicy == ''unmanaged'' ? !has(self.resource) + : true' + - message: import must be specified when policy is unmanaged + rule: 'self.managementPolicy == ''unmanaged'' ? has(self.__import__) + : true' + - message: managedOptions may only be provided when policy is managed + rule: 'has(self.managedOptions) ? self.managementPolicy == ''managed'' + : true' + - message: resource content must be specified when not importing + rule: '!has(self.__import__) ? has(self.resource.content) : true' + status: + description: ImageStatus defines the observed state of an ORC resource. + properties: + conditions: + description: |- + Conditions represents the observed status of the object. + Known .status.conditions.type are: "Available", "Progressing" + + Available represents the availability of the OpenStack resource. If it is + true then the resource is ready for use. + + Progressing indicates whether the controller is still attempting to + reconcile the current state of the OpenStack resource to the desired + state. Progressing will be False either because the desired state has + been achieved, or because some terminal error prevents it from ever being + achieved and the controller is no longer attempting to reconcile. If + Progressing is True, an observer waiting on the resource should continue + to wait. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + downloadAttempts: + description: DownloadAttempts is the number of times the controller + has attempted to download the image contents + type: integer + id: + description: ID is the unique identifier of the OpenStack resource. + type: string + resource: + description: Resource contains the observed state of the OpenStack + resource. + properties: + hash: + description: |- + Hash is the hash of the image data published by Glance. Note that this is + a hash of the data stored internally by Glance, which will have been + decompressed and potentially format converted depending on server-side + configuration which is not visible to clients. It is expected that this + hash will usually differ from the download hash. + properties: + algorithm: + description: Algorithm is the hash algorithm used to generate + value. + enum: + - md5 + - sha1 + - sha256 + - sha512 + type: string + value: + description: Value is the hash of the image data using Algorithm. + It must be hex encoded using lowercase letters. + maxLength: 1024 + minLength: 1 + pattern: ^[0-9a-f]+$ + type: string + required: + - algorithm + - value + type: object + sizeB: + description: SizeB is the size of the image data, in bytes + format: int64 + type: integer + status: + description: Status is the image status as reported by Glance + type: string + virtualSizeB: + description: VirtualSizeB is the size of the disk the image data + represents, in bytes + format: int64 + type: integer + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: orc + name: orc-controller-manager + namespace: orc-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: orc + name: orc-leader-election-role + namespace: orc-system +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: orc + name: orc-image-editor-role +rules: +- apiGroups: + - openstack.k-orc.cloud + resources: + - images + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - openstack.k-orc.cloud + resources: + - images/status + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: orc + name: orc-image-viewer-role +rules: +- apiGroups: + - openstack.k-orc.cloud + resources: + - images + verbs: + - get + - list + - watch +- apiGroups: + - openstack.k-orc.cloud + resources: + - images/status + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: orc-manager-role +rules: +- apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - watch +- apiGroups: + - openstack.k-orc.cloud + resources: + - images + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - openstack.k-orc.cloud + resources: + - images/status + verbs: + - get + - patch + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: orc-metrics-auth-role +rules: +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: orc-metrics-reader +rules: +- nonResourceURLs: + - /metrics + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: orc + name: orc-leader-election-rolebinding + namespace: orc-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: orc-leader-election-role +subjects: +- kind: ServiceAccount + name: orc-controller-manager + namespace: orc-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: orc + name: orc-manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: orc-manager-role +subjects: +- kind: ServiceAccount + name: orc-controller-manager + namespace: orc-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: orc-metrics-auth-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: orc-metrics-auth-role +subjects: +- kind: ServiceAccount + name: orc-controller-manager + namespace: orc-system +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: orc + control-plane: controller-manager + name: orc-controller-manager-metrics-service + namespace: orc-system +spec: + ports: + - name: https + port: 8443 + protocol: TCP + targetPort: 8443 + selector: + control-plane: controller-manager +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: orc + control-plane: controller-manager + name: orc-controller-manager + namespace: orc-system +spec: + replicas: 1 + selector: + matchLabels: + control-plane: controller-manager + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + labels: + control-plane: controller-manager + spec: + containers: + - args: + - --metrics-bind-address=:8443 + - --leader-elect + - --health-probe-bind-address=:8081 + command: + - /manager + image: quay.io/orc/openstack-resource-controller:v1.0.2 + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + name: manager + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + privileged: false + runAsGroup: 65532 + runAsUser: 65532 + terminationMessagePolicy: FallbackToLogsOnError + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + serviceAccountName: orc-controller-manager + terminationGracePeriodSeconds: 10 diff --git a/dist/kustomization.yaml b/dist/kustomization.yaml new file mode 100644 index 000000000..4fd12e734 --- /dev/null +++ b/dist/kustomization.yaml @@ -0,0 +1,5 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: +- install.yaml diff --git a/examples/centos-stream/image.yaml b/examples/centos-stream/image.yaml new file mode 100644 index 000000000..5abe65235 --- /dev/null +++ b/examples/centos-stream/image.yaml @@ -0,0 +1,21 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Image +metadata: + name: centos-stream-9 +spec: + cloudCredentialsRef: + cloudName: openstack + secretName: dev-test-cloud-config + managementPolicy: managed + managedOptions: + onDelete: detach + resource: + name: CentOS-Stream-GenericCloud-9-20241209.0.x86_64 + content: + diskFormat: qcow2 + download: + url: https://cloud.centos.org/centos/9-stream/x86_64/images/CentOS-Stream-GenericCloud-9-20241209.0.x86_64.qcow2 + hash: + algorithm: sha256 + value: 912218b89cdcd8e62b1207f19b62f4da756b793902d34116758b5ffb02859c5c diff --git a/examples/centos-stream/kustomization.yaml b/examples/centos-stream/kustomization.yaml new file mode 100644 index 000000000..8a0f9d62b --- /dev/null +++ b/examples/centos-stream/kustomization.yaml @@ -0,0 +1,18 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +components: +- ../kustomizeconfig + +resources: +- ../credentials +- image.yaml + +patches: +- target: + kind: Secret + name: dev-test-cloud-config + patch: |- + - op: add + path: /metadata/annotations/config.kubernetes.io~1local-config + value: "true" diff --git a/examples/credentials-only/kustomization.yaml b/examples/credentials-only/kustomization.yaml new file mode 100644 index 000000000..0eaecddc5 --- /dev/null +++ b/examples/credentials-only/kustomization.yaml @@ -0,0 +1,5 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: +- ../credentials diff --git a/examples/credentials/.dockerignore b/examples/credentials/.dockerignore new file mode 100644 index 000000000..324691d20 --- /dev/null +++ b/examples/credentials/.dockerignore @@ -0,0 +1 @@ +clouds.yaml diff --git a/examples/credentials/.gitignore b/examples/credentials/.gitignore new file mode 100644 index 000000000..b52b7e9a6 --- /dev/null +++ b/examples/credentials/.gitignore @@ -0,0 +1 @@ +/clouds.yaml diff --git a/examples/credentials/kustomization.yaml b/examples/credentials/kustomization.yaml new file mode 100644 index 000000000..11e67ddd0 --- /dev/null +++ b/examples/credentials/kustomization.yaml @@ -0,0 +1,8 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +secretGenerator: +- files: + - clouds.yaml + name: dev-test-cloud-config + type: Opaque diff --git a/examples/kustomizeconfig/kustomization.yaml b/examples/kustomizeconfig/kustomization.yaml new file mode 100644 index 000000000..5fb7d56aa --- /dev/null +++ b/examples/kustomizeconfig/kustomization.yaml @@ -0,0 +1,5 @@ +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component + +configurations: +- kustomizeconfig.yaml diff --git a/examples/kustomizeconfig/kustomizeconfig.yaml b/examples/kustomizeconfig/kustomizeconfig.yaml new file mode 100644 index 000000000..4b0317e40 --- /dev/null +++ b/examples/kustomizeconfig/kustomizeconfig.yaml @@ -0,0 +1,5 @@ +nameReference: +- kind: Secret + fieldSpecs: + - path: spec/cloudCredentialsRef/secretName + kind: Image diff --git a/go.mod b/go.mod index 87f373d24..d0d5764f2 100644 --- a/go.mod +++ b/go.mod @@ -1,30 +1,31 @@ module github.com/k-orc/openstack-resource-controller -go 1.22.0 +go 1.25.0 require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc - github.com/go-logr/logr v1.4.2 - github.com/google/go-cmp v0.6.0 - github.com/gophercloud/gophercloud/v2 v2.1.1 - github.com/gophercloud/utils/v2 v2.0.0-20241008104625-7cbb8fd76bb7 - github.com/onsi/ginkgo/v2 v2.20.0 - github.com/onsi/gomega v1.34.1 - github.com/ulikunitz/xz v0.5.12 - go.uber.org/mock v0.4.0 - k8s.io/api v0.31.1 - k8s.io/apimachinery v0.31.1 - k8s.io/client-go v0.31.1 - k8s.io/code-generator v0.31.1 + github.com/go-logr/logr v1.4.3 + github.com/google/go-cmp v0.7.0 + github.com/gophercloud/gophercloud/v2 v2.3.0 + github.com/gophercloud/utils/v2 v2.0.0-20241209100706-e3a3b7c07d26 + github.com/onsi/ginkgo/v2 v2.28.3 + github.com/onsi/gomega v1.40.0 + github.com/ulikunitz/xz v0.5.15 + go.uber.org/mock v0.6.0 + k8s.io/api v0.31.14 + k8s.io/apimachinery v0.31.14 + k8s.io/client-go v0.31.14 + k8s.io/code-generator v0.31.14 k8s.io/klog/v2 v2.130.1 k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 - sigs.k8s.io/controller-runtime v0.19.1 - sigs.k8s.io/structured-merge-diff/v4 v4.4.1 - sigs.k8s.io/yaml v1.4.0 + sigs.k8s.io/controller-runtime v0.19.7 + sigs.k8s.io/structured-merge-diff/v4 v4.7.0 + sigs.k8s.io/yaml v1.6.0 ) require ( + github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -50,7 +51,7 @@ require ( github.com/google/cel-go v0.20.1 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/gofuzz v1.2.0 // indirect - github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect + github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect github.com/imdario/mergo v0.3.15 // indirect @@ -71,39 +72,45 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect - go.opentelemetry.io/otel v1.28.0 // indirect + go.opentelemetry.io/otel v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect - go.opentelemetry.io/otel/metric v1.28.0 // indirect - go.opentelemetry.io/otel/sdk v1.28.0 // indirect - go.opentelemetry.io/otel/trace v1.28.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/sdk v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.43.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect - golang.org/x/mod v0.20.0 // indirect - golang.org/x/net v0.28.0 // indirect - golang.org/x/oauth2 v0.21.0 // indirect - golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.26.0 // indirect - golang.org/x/term v0.23.0 // indirect - golang.org/x/text v0.19.0 // indirect + golang.org/x/mod v0.35.0 // indirect + golang.org/x/net v0.53.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/term v0.42.0 // indirect + golang.org/x/text v0.36.0 // indirect golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.24.0 // indirect + golang.org/x/tools v0.44.0 // indirect + golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect - google.golang.org/grpc v1.65.0 // indirect - google.golang.org/protobuf v1.34.2 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/grpc v1.79.3 // indirect + google.golang.org/protobuf v1.36.10 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apiextensions-apiserver v0.31.0 // indirect - k8s.io/apiserver v0.31.0 // indirect - k8s.io/component-base v0.31.0 // indirect + k8s.io/apiextensions-apiserver v0.31.2 // indirect + k8s.io/apiserver v0.31.2 // indirect + k8s.io/component-base v0.31.2 // indirect k8s.io/gengo/v2 v2.0.0-20240228010128-51d4e06bde70 // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect ) + +replace github.com/gophercloud/gophercloud/v2 => github.com/gophercloud/gophercloud/v2 v2.3.1-0.20241210154048-e3bceeff054e diff --git a/go.sum b/go.sum index bebc9afe1..59b6f8ad3 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA= @@ -28,9 +30,15 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= +github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= +github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= +github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= +github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE= +github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= @@ -44,6 +52,8 @@ github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogB github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/gofrs/uuid/v5 v5.3.0 h1:m0mUMr+oVYUdxpMLgSYCZiXe7PuVPnI94+OMeVBNedk= github.com/gofrs/uuid/v5 v5.3.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -57,19 +67,19 @@ github.com/google/cel-go v0.20.1/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k= -github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= +github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 h1:EwtI+Al+DeppwYX2oXJCETMO23COyaKGP6fHVpkpWpg= +github.com/google/pprof v0.0.0-20260402051712-545e8a4df936/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gophercloud/gophercloud/v2 v2.1.1 h1:KUeVTUoq6um/CijR+hl1JRZ35SXRY62LiYUbgp4qqKw= -github.com/gophercloud/gophercloud/v2 v2.1.1/go.mod h1:f2hMRC7Kakbv5vM7wSGHrIPZh6JZR60GVHryJlF/K44= -github.com/gophercloud/utils/v2 v2.0.0-20241008104625-7cbb8fd76bb7 h1:RDFC3+cVfeCQ8zi/PgaKrPzJyUbwzg3Mi2xCWnAW+g4= -github.com/gophercloud/utils/v2 v2.0.0-20241008104625-7cbb8fd76bb7/go.mod h1:hLzf9Ts2fhebZrZAtq6BpbwXQLEZmUqa7ABJMkg/il8= +github.com/gophercloud/gophercloud/v2 v2.3.1-0.20241210154048-e3bceeff054e h1:rR9j9OPSJPt4ock5Y133MJKgi7H5H2H/c05s78V4oZs= +github.com/gophercloud/gophercloud/v2 v2.3.1-0.20241210154048-e3bceeff054e/go.mod h1:uJWNpTgJPSl2gyzJqcU/pIAhFUWvIkp8eE8M15n9rs4= +github.com/gophercloud/utils/v2 v2.0.0-20241209100706-e3a3b7c07d26 h1:N65GYmx5LrMeYdeXcxMESDU+2pDyAOXlFNlHl7siUwM= +github.com/gophercloud/utils/v2 v2.0.0-20241209100706-e3a3b7c07d26/go.mod h1:7SHUbtoiSYINNKgAVxse+PMhIio05IK7shHy8DVRaN0= github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= @@ -78,6 +88,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= +github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -91,6 +103,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= +github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= +github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= +github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -100,10 +116,10 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/onsi/ginkgo/v2 v2.20.0 h1:PE84V2mHqoT1sglvHc8ZdQtPcwmvvt29WLEEO3xmdZw= -github.com/onsi/ginkgo/v2 v2.20.0/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI= -github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= -github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +github.com/onsi/ginkgo/v2 v2.28.3 h1:4JvMdwtFU0imd8fHx25OJXoDMRexnf8v5NHKYSTTji4= +github.com/onsi/ginkgo/v2 v2.28.3/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44= +github.com/onsi/gomega v1.40.0 h1:Vtol0e1MghCD2ZVIilPDIg44XSL9l2QAn8ZNaljWcJc= +github.com/onsi/gomega v1.40.0/go.mod h1:M/Uqpu/8qTjtzCLUA2zJHX9Iilrau25x1PdoSRbWh5A= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -117,8 +133,8 @@ github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= @@ -133,38 +149,54 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= -github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= +github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= -go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= -go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ= -go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= -go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= -go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= -go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= -go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= -go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= -go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -172,54 +204,60 @@ golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= -golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= -golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= -golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= -golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= -golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +golang.org/x/tools/go/expect v0.1.0-deprecated h1:jY2C5HGYR5lqex3gEniOQL0r7Dq5+VGVgY1nudX5lXY= +golang.org/x/tools/go/expect v0.1.0-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= +golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= +golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1:7whR9kGa5LUwFtpLm2ArCEejtnxlGeLbAyjFY8sGNFw= -google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= -google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= -google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -227,26 +265,25 @@ gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSP gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.31.1 h1:Xe1hX/fPW3PXYYv8BlozYqw63ytA92snr96zMW9gWTU= -k8s.io/api v0.31.1/go.mod h1:sbN1g6eY6XVLeqNsZGLnI5FwVseTrZX7Fv3O26rhAaI= -k8s.io/apiextensions-apiserver v0.31.0 h1:fZgCVhGwsclj3qCw1buVXCV6khjRzKC5eCFt24kyLSk= -k8s.io/apiextensions-apiserver v0.31.0/go.mod h1:b9aMDEYaEe5sdK+1T0KU78ApR/5ZVp4i56VacZYEHxk= -k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U= -k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= -k8s.io/apiserver v0.31.0 h1:p+2dgJjy+bk+B1Csz+mc2wl5gHwvNkC9QJV+w55LVrY= -k8s.io/apiserver v0.31.0/go.mod h1:KI9ox5Yu902iBnnyMmy7ajonhKnkeZYJhTZ/YI+WEMk= -k8s.io/client-go v0.31.1 h1:f0ugtWSbWpxHR7sjVpQwuvw9a3ZKLXX0u0itkFXufb0= -k8s.io/client-go v0.31.1/go.mod h1:sKI8871MJN2OyeqRlmA4W4KM9KBdBUpDLu/43eGemCg= -k8s.io/code-generator v0.31.1 h1:GvkRZEP2g2UnB2QKT2Dgc/kYxIkDxCHENv2Q1itioVs= -k8s.io/code-generator v0.31.1/go.mod h1:oL2ky46L48osNqqZAeOcWWy0S5BXj50vVdwOtTefqIs= -k8s.io/component-base v0.31.0 h1:/KIzGM5EvPNQcYgwq5NwoQBaOlVFrghoVGr8lG6vNRs= -k8s.io/component-base v0.31.0/go.mod h1:TYVuzI1QmN4L5ItVdMSXKvH7/DtvIuas5/mm8YT3rTo= +k8s.io/api v0.31.14 h1:xYn/S/WFJsksI7dk/5uBRd3Umm/D8W5g7sRnd4csotA= +k8s.io/api v0.31.14/go.mod h1:K8fvRey4z73RAuxBZCma7WtY8WFvkViYhfFLCMT4xgA= +k8s.io/apiextensions-apiserver v0.31.2 h1:W8EwUb8+WXBLu56ser5IudT2cOho0gAKeTOnywBLxd0= +k8s.io/apiextensions-apiserver v0.31.2/go.mod h1:i+Geh+nGCJEGiCGR3MlBDkS7koHIIKWVfWeRFiOsUcM= +k8s.io/apimachinery v0.31.14 h1:/eMIwjv+GFm6A/sSGlB1NupBU6wTDPhEWsju0Fj69kY= +k8s.io/apimachinery v0.31.14/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/apiserver v0.31.2 h1:VUzOEUGRCDi6kX1OyQ801m4A7AUPglpsmGvdsekmcI4= +k8s.io/apiserver v0.31.2/go.mod h1:o3nKZR7lPlJqkU5I3Ove+Zx3JuoFjQobGX1Gctw6XuE= +k8s.io/client-go v0.31.14 h1:d4/G0xfksNIbMWH7ghjzOwC5bTAwQ20gABTjZw7fLlQ= +k8s.io/client-go v0.31.14/go.mod h1:0uRpRB7r5QwtsbxEngZPkbcIVoNdAQAPIcopgiXjhQc= +k8s.io/code-generator v0.31.14 h1:Qn+Lo0jvH8Z7YlpWle3SjjkQAkDDS0BvefC6ldoRbuk= +k8s.io/code-generator v0.31.14/go.mod h1:O1gjNfUL1q1FaoASAWQW6Iu2Taahark2McS+rBxv/Ic= +k8s.io/component-base v0.31.2 h1:Z1J1LIaC0AV+nzcPRFqfK09af6bZ4D1nAOpWsy9owlA= +k8s.io/component-base v0.31.2/go.mod h1:9PeyyFN/drHjtJZMCTkSpQJS3U9OXORnHQqMLDz0sUQ= k8s.io/gengo/v2 v2.0.0-20240228010128-51d4e06bde70 h1:NGrVE502P0s0/1hudf8zjgwki1X/TByhmAoILTarmzo= k8s.io/gengo/v2 v2.0.0-20240228010128-51d4e06bde70/go.mod h1:VH3AT8AaQOqiGjMF9p0/IM1Dj+82ZwjfxUP1IxaHE+8= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= @@ -257,11 +294,15 @@ k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1 k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 h1:2770sDpzrjjsAtVhSeUFseziht227YAWYHLGNM8QPwY= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= -sigs.k8s.io/controller-runtime v0.19.1 h1:Son+Q40+Be3QWb+niBXAg2vFiYWolDjjRfO8hn/cxOk= -sigs.k8s.io/controller-runtime v0.19.1/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= +sigs.k8s.io/controller-runtime v0.19.7 h1:DLABZfMr20A+AwCZOHhcbcu+TqBXnJZaVBri9K3EO48= +sigs.k8s.io/controller-runtime v0.19.7/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= -sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= -sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI= +sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/hack/collectlogs b/hack/collectlogs new file mode 100755 index 000000000..050009515 --- /dev/null +++ b/hack/collectlogs @@ -0,0 +1,42 @@ +#!/bin/bash +# +# Collect logs after an integration test failure. + +# We intentionally don't set '-e' (exit on first failure) since we don't want +# to fail on these diagnostic steps +set -uxo pipefail + +LOG_DIR=${LOG_DIR:-/tmp/artifacts} +mkdir -p "$LOG_DIR" + +cp -r /tmp/devstack-logs "${LOG_DIR}" +DEVSTACK_LOG_DIR="${LOG_DIR}/devstack-logs" + +# shellcheck disable=SC2024 +sudo journalctl -o short-precise --no-pager &> "$LOG_DIR/journal.log" +free -m > "$LOG_DIR/free.txt" +dpkg -l > "$LOG_DIR/dpkg-l.txt" +pip freeze > "$LOG_DIR/pip-freeze.txt" + +# shellcheck disable=SC2024 +sudo systemctl status "devstack@*" &> "$DEVSTACK_LOG_DIR/devstack-services.txt" +for service in $(systemctl list-units --output json devstack@* | jq -r '.[].unit | capture("devstack@(?[a-z\\-]+).service") | '.svc'') +do + journalctl -u "devstack@${service}.service" --no-tail > "${DEVSTACK_LOG_DIR}/${service}.log" +done +cp ./devstack/local.conf "$DEVSTACK_LOG_DIR" + +kubectl describe pods -n orc-system > "$LOG_DIR/orc-pod.txt" +kubectl logs -n orc-system -l control-plane=controller-manager --tail=-1 > "$LOG_DIR/orc-pod.log" + +kubectl get -n orc-system all -o yaml > "$LOG_DIR/orc-resources.yaml" + +mkdir "$LOG_DIR/orc-managed-resources" +for crd in config/crd/bases/openstack.k-orc.cloud_*; do + resource=${crd:39:-5} + kubectl get "${resource}" -o yaml > "$LOG_DIR/orc-managed-resources/${resource}.yaml" +done + +sudo find "$LOG_DIR" -type d -exec chmod 0755 {} \; +sudo find "$LOG_DIR" -type f -exec chmod 0644 {} \; + diff --git a/hack/e2e.sh b/hack/e2e.sh new file mode 100755 index 000000000..3a28e2dc4 --- /dev/null +++ b/hack/e2e.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +export OS_CLOUD=devstack + +sed "s/ devstack:/ openstack:/g" /etc/openstack/clouds.yaml > examples/credentials/clouds.yaml +kubectl apply -k examples/credentials-only --server-side +kubectl apply -k examples/centos-stream --server-side +kubectl wait --timeout=10m --for=condition=available image centos-stream-9 + +openstack image show "$(kubectl get image centos-stream-9 -o jsonpath='{.status.id}')" diff --git a/hack/ensure-trivy.sh b/hack/ensure-trivy.sh new file mode 100755 index 000000000..fd6c5ec0a --- /dev/null +++ b/hack/ensure-trivy.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +# Copyright 2023 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. + +set -o errexit +set -o nounset +set -o pipefail + +if [[ "${TRACE-0}" == "1" ]]; then + set -o xtrace +fi + +VERSION=${1} + +GO_OS="$(go env GOOS)" +if [[ "${GO_OS}" == "linux" ]]; then + TRIVY_OS="Linux" +elif [[ "${GO_OS}" == "darwin"* ]]; then + TRIVY_OS="macOS" +fi + +GO_ARCH="$(go env GOARCH)" +if [[ "${GO_ARCH}" == "amd" ]]; then + TRIVY_ARCH="32bit" +elif [[ "${GO_ARCH}" == "amd64"* ]]; then + TRIVY_ARCH="64bit" +elif [[ "${GO_ARCH}" == "arm" ]]; then + TRIVY_ARCH="ARM" +elif [[ "${GO_ARCH}" == "arm64" ]]; then + TRIVY_ARCH="ARM64" +fi + +TOOL_BIN=bin +mkdir -p ${TOOL_BIN} + +TRIVY="${TOOL_BIN}/trivy/${VERSION}/trivy" + +# Downloads trivy scanner +if [ ! -f "$TRIVY" ]; then + curl -L -o ${TOOL_BIN}/trivy.tar.gz "https://github.com/aquasecurity/trivy/releases/download/v${VERSION}/trivy_${VERSION}_${TRIVY_OS}-${TRIVY_ARCH}.tar.gz" + mkdir -p "${TOOL_BIN}/trivy/${VERSION}" + tar -xf "${TOOL_BIN}/trivy.tar.gz" -C "${TOOL_BIN}/trivy/${VERSION}" trivy + chmod +x "${TOOL_BIN}/trivy/${VERSION}/trivy" + rm "${TOOL_BIN}/trivy.tar.gz" +fi diff --git a/hack/verify-container-images.sh b/hack/verify-container-images.sh new file mode 100755 index 000000000..8bb8b3102 --- /dev/null +++ b/hack/verify-container-images.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +# Copyright 2022 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. + +set -o errexit +set -o nounset +set -o pipefail + +if [[ "${TRACE-0}" == "1" ]]; then + set -o xtrace +fi + +VERSION=${1} +GO_ARCH="$(go env GOARCH)" +DB_MIRROR="public.ecr.aws/aquasecurity/trivy-db" + +REPO_ROOT=$(git rev-parse --show-toplevel) +"${REPO_ROOT}/hack/ensure-trivy.sh" "${VERSION}" + +TRIVY="${REPO_ROOT}/bin/trivy/${VERSION}/trivy" + +# Build the container image to be scanned +make IMG=quay.io/orc/openstack-resource-controller-${GO_ARCH}:dev docker-build + +# Scan the images +"${TRIVY}" image --db-repository="${DB_MIRROR}" -q --exit-code 1 --ignore-unfixed --severity MEDIUM,HIGH,CRITICAL quay.io/orc/openstack-resource-controller-"${GO_ARCH}":dev && R1=$? || R1=$? + +echo "" +BRed='\033[1;31m' +BGreen='\033[1;32m' +NC='\033[0m' # No + +if [ "$R1" -ne "0" ] +then + echo -e "${BRed}Check container images failed! There are vulnerabilities to be fixed${NC}" + exit 1 +fi + +echo -e "${BGreen}Check container images passed! No vulnerability found${NC}" diff --git a/internal/controllers/common/conditions.go b/internal/controllers/common/conditions.go new file mode 100644 index 000000000..3fbcd5e4e --- /dev/null +++ b/internal/controllers/common/conditions.go @@ -0,0 +1,95 @@ +/* +Copyright 2024 The ORC 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 common + +import ( + "errors" + + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + applyconfigv1 "k8s.io/client-go/applyconfigurations/meta/v1" + "k8s.io/utils/ptr" + + orcv1alpha1 "github.com/k-orc/openstack-resource-controller/api/v1alpha1" + "github.com/k-orc/openstack-resource-controller/internal/util/applyconfigs" + orcerrors "github.com/k-orc/openstack-resource-controller/internal/util/errors" +) + +type WithConditionsApplyConfiguration[T any] interface { + WithConditions(...*applyconfigv1.ConditionApplyConfiguration) T +} + +func SetCommonConditions[T any](orcObject orcv1alpha1.ObjectWithConditions, applyConfig WithConditionsApplyConfiguration[T], isAvailable, isUpToDate bool, progressMessage *string, err error, now metav1.Time) { + availableCondition := applyconfigv1.Condition(). + WithType(orcv1alpha1.ConditionAvailable). + WithObservedGeneration(orcObject.GetGeneration()) + progressingCondition := applyconfigv1.Condition(). + WithType(orcv1alpha1.ConditionProgressing). + WithObservedGeneration(orcObject.GetGeneration()) + + if err == nil { + if isUpToDate { + progressingCondition. + WithStatus(metav1.ConditionFalse). + WithReason(orcv1alpha1.ConditionReasonSuccess). + WithMessage("OpenStack resource is up to date") + } else { + progressingCondition. + WithStatus(metav1.ConditionTrue). + WithReason(orcv1alpha1.ConditionReasonProgressing). + WithMessage(ptr.Deref(progressMessage, "Reconciliation is progressing")) + } + } else { + var terminalError *orcerrors.TerminalError + if errors.As(err, &terminalError) { + progressingCondition. + WithStatus(metav1.ConditionFalse). + WithReason(terminalError.Reason). + WithMessage(terminalError.Message) + } else { + progressingCondition. + WithStatus(metav1.ConditionTrue). + WithReason(orcv1alpha1.ConditionReasonTransientError). + WithMessage(err.Error()) + } + } + + if isAvailable { + availableCondition. + WithStatus(metav1.ConditionTrue). + WithReason(orcv1alpha1.ConditionReasonSuccess). + WithMessage("OpenStack resource is available") + } else { + // Copy reason and message from progressing + availableCondition. + WithStatus(metav1.ConditionFalse). + WithReason(*progressingCondition.Reason). + WithMessage(*progressingCondition.Message) + } + + // Maintain condition timestamps if they haven't changed + // This also ensures that we don't generate an update event if nothing has changed + for _, condition := range []*applyconfigv1.ConditionApplyConfiguration{availableCondition, progressingCondition} { + previous := meta.FindStatusCondition(orcObject.GetConditions(), *condition.Type) + if previous != nil && applyconfigs.ConditionsEqual(previous, condition) { + condition.WithLastTransitionTime(previous.LastTransitionTime) + } else { + condition.WithLastTransitionTime(now) + } + } + + applyConfig.WithConditions(availableCondition, progressingCondition) +} diff --git a/internal/controllers/common/deletion_guard.go b/internal/controllers/common/deletion_guard.go new file mode 100644 index 000000000..8c7370f45 --- /dev/null +++ b/internal/controllers/common/deletion_guard.go @@ -0,0 +1,156 @@ +/* +Copyright 2024 The ORC 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 common + +import ( + "context" + "fmt" + "slices" + "strings" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/workqueue" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +type pointerToObject[T any] interface { + *T + client.Object +} + +// A deletion guard is a controller which prevents the deletion of objects that objects of another type depend on. +// +// Example: Network and Subnet +// +// We add a deletion guard to network that prevents the network from being +// deleted if it is still in use by any subnet. It is added by the subnet +// controller, but it is a separate controller which reconciles network objects. + +func AddDeletionGuard[guardedP pointerToObject[guarded], dependencyP pointerToObject[dependency], guarded, dependency any]( + mgr ctrl.Manager, finalizer string, fieldOwner client.FieldOwner, + getGuardedRefsFromDependency func(client.Object) []string, + getDependenciesFromGuarded func(context.Context, client.Client, guardedP) ([]dependency, error), +) error { + // deletionGuard reconciles the guarded object + // It adds a finalizer to any guarded object which is not marked as deleted + // If the guarded object is marked deleted, we remove the finalizer only if there are no dependent objects + deletionGuard := reconcile.Func(func(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { + log := ctrl.LoggerFrom(ctx, "name", req.Name, "namespace", req.Namespace) + log.V(5).Info("Reconciling deletion guard") + + k8sClient := mgr.GetClient() + + var guarded guardedP = new(guarded) + err := k8sClient.Get(ctx, req.NamespacedName, guarded) + if err != nil { + if apierrors.IsNotFound(err) { + return ctrl.Result{}, nil + } + return ctrl.Result{}, err + } + + // If the object hasn't been deleted, we simply check that it has our finalizer + if guarded.GetDeletionTimestamp().IsZero() { + if !slices.Contains(guarded.GetFinalizers(), finalizer) { + log.V(4).Info("Adding finalizer") + patch := SetFinalizerPatch(guarded, finalizer) + return ctrl.Result{}, k8sClient.Patch(ctx, guarded, patch, client.ForceOwnership, fieldOwner) + } + + log.V(5).Info("Finalizer already present") + return ctrl.Result{}, nil + } + + log.V(4).Info("Handling delete") + + dependencies, err := getDependenciesFromGuarded(ctx, k8sClient, guarded) + if err != nil { + return reconcile.Result{}, nil + } + if len(dependencies) == 0 { + log.V(4).Info("Removing finalizer") + patch := RemoveFinalizerPatch(guarded) + return ctrl.Result{}, k8sClient.Patch(ctx, guarded, patch, client.ForceOwnership, fieldOwner) + } + log.V(5).Info("Waiting for dependencies", "dependencies", len(dependencies)) + return ctrl.Result{}, nil + }) + + var guardedSpecimen guardedP = new(guarded) + var dependencySpecimen dependencyP = new(dependency) + + scheme := mgr.GetScheme() + guardedName, err := prettyName(guardedSpecimen, scheme) + if err != nil { + return err + } + dependencyName, err := prettyName(dependencySpecimen, scheme) + if err != nil { + return err + } + + controllerName := guardedName + "_deletion_guard_for_" + dependencyName + + // Register deletionGuard with the manager as a reconciler of guarded. + // We also watch dependency, but we're only interested in deletion events. + // We need to ensure that if the guarded object is marked deleted we will + // continue to call deletionGuard every time a dependent object is deleted + // so that we will eventually be called when the last dependent object is + // deleted and we can remove the dependency. + err = builder.ControllerManagedBy(mgr). + For(guardedSpecimen). + Watches(dependencySpecimen, + handler.Funcs{ + DeleteFunc: func(ctx context.Context, evt event.TypedDeleteEvent[client.Object], q workqueue.TypedRateLimitingInterface[reconcile.Request]) { + for _, guarded := range getGuardedRefsFromDependency(evt.Object) { + q.Add(reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: evt.Object.GetNamespace(), + Name: guarded, + }, + }) + } + }, + }, + ). + Named(controllerName). + Complete(deletionGuard) + + if err != nil { + return fmt.Errorf("failed to construct %s deletion guard for %s controller: %w", guardedName, dependencyName, err) + } + + return nil +} + +func prettyName(obj runtime.Object, scheme *runtime.Scheme) (string, error) { + gvks, _, err := scheme.ObjectKinds(obj) + if err != nil { + return "", fmt.Errorf("looking up GVK for guarded object %T: %w", obj, err) + } + if len(gvks) == 0 { + return "", fmt.Errorf("no registered kind for guarded object %T", obj) + } + + return strings.ToLower(gvks[0].Kind), nil +} diff --git a/internal/controllers/common/finalizer.go b/internal/controllers/common/finalizer.go new file mode 100644 index 000000000..8bdc4a4cc --- /dev/null +++ b/internal/controllers/common/finalizer.go @@ -0,0 +1,61 @@ +/* +Copyright 2024 The ORC 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 common + +import ( + "k8s.io/apimachinery/pkg/types" + applyconfigv1 "k8s.io/client-go/applyconfigurations/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/k-orc/openstack-resource-controller/internal/util/applyconfigs" +) + +type metaApplyConfig struct { + applyconfigv1.TypeMetaApplyConfiguration `json:",inline"` + applyconfigv1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"` +} + +func metaApplyConfigFromObject(obj client.Object) metaApplyConfig { + gvk := obj.GetObjectKind().GroupVersionKind() + + applyConfig := metaApplyConfig{} + + // Type meta + applyConfig. + WithAPIVersion(gvk.GroupVersion().String()). + WithKind(gvk.Kind) + + // Object meta + applyConfig. + WithName(obj.GetName()). + WithNamespace(obj.GetNamespace()). + WithUID(obj.GetUID()) // For safety: ensure we don't accidentally create a new object if we race with delete + + return applyConfig +} + +// SetFinalizerPatch returns an apply configuration which adds a finalizer +func SetFinalizerPatch(obj client.Object, finalizer string) client.Patch { + applyConfig := metaApplyConfigFromObject(obj) + applyConfig.WithFinalizers(finalizer) + return applyconfigs.Patch(types.ApplyPatchType, applyConfig) +} + +// RemoveFinalizerPatch returns an apply configuration which removes a finalizer +func RemoveFinalizerPatch(obj client.Object) client.Patch { + applyConfig := metaApplyConfigFromObject(obj) + return applyconfigs.Patch(types.ApplyPatchType, applyConfig) +} diff --git a/internal/controllers/export/setup.go b/internal/controllers/export/setup.go index 306f790e7..d59fb414f 100644 --- a/internal/controllers/export/setup.go +++ b/internal/controllers/export/setup.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Kubernetes Authors. +Copyright 2024 The ORC Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package controllers +package export // This file provides a minimal exported interface to non-exported controllers @@ -25,6 +25,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller" ) -type SetupWithManager interface { +type Controller interface { SetupWithManager(context.Context, ctrl.Manager, controller.Options) error + GetName() string } diff --git a/internal/controllers/generic/actuator.go b/internal/controllers/generic/actuator.go new file mode 100644 index 000000000..f03905c4d --- /dev/null +++ b/internal/controllers/generic/actuator.go @@ -0,0 +1,280 @@ +/* +Copyright 2024 The ORC 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 generic + +import ( + "context" + "fmt" + "time" + + "github.com/go-logr/logr" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + orcv1alpha1 "github.com/k-orc/openstack-resource-controller/api/v1alpha1" + orcerrors "github.com/k-orc/openstack-resource-controller/internal/util/errors" +) + +const ( + // The time to wait between checking if a delete was successful + deletePollingPeriod = 1 * time.Second + + // The time to wait before reconciling again when we are waiting for some change in OpenStack + externalUpdatePollingPeriod = 15 * time.Second +) + +type BaseResourceActuator[osResourcePT any] interface { + client.Object + + GetManagementPolicy() orcv1alpha1.ManagementPolicy + GetManagedOptions() *orcv1alpha1.ManagedOptions + + GetResourceID(osResource osResourcePT) string + + GetOSResourceByStatusID(ctx context.Context) (bool, osResourcePT, error) + GetOSResourceBySpec(ctx context.Context) (osResourcePT, error) +} + +type CreateResourceActuator[osResourcePT any] interface { + BaseResourceActuator[osResourcePT] + + GetOSResourceByImportID(ctx context.Context) (bool, osResourcePT, error) + GetOSResourceByImportFilter(ctx context.Context) (bool, osResourcePT, error) + CreateResource(ctx context.Context) ([]WaitingOnEvent, osResourcePT, error) +} + +type DeleteResourceActuator[osResourcePT any] interface { + BaseResourceActuator[osResourcePT] + + DeleteResource(ctx context.Context, osResource osResourcePT) ([]WaitingOnEvent, error) +} + +func GetOrCreateOSResource[osResourcePT *osResourceT, osResourceT any](ctx context.Context, log logr.Logger, k8sClient client.Client, actuator CreateResourceActuator[osResourcePT]) ([]WaitingOnEvent, osResourcePT, error) { + // Get by status ID + if hasStatusID, osResource, err := actuator.GetOSResourceByStatusID(ctx); hasStatusID { + if orcerrors.IsNotFound(err) { + // An OpenStack resource we previously referenced has been deleted unexpectedly. We can't recover from this. + err = orcerrors.Terminal(orcv1alpha1.ConditionReasonUnrecoverableError, "resource has been deleted from OpenStack") + } + if osResource != nil { + log.V(4).Info("Got existing OpenStack resource", "ID", actuator.GetResourceID(osResource)) + } + return nil, osResource, err + } + + // Import by ID + if hasImportID, osResource, err := actuator.GetOSResourceByImportID(ctx); hasImportID { + if orcerrors.IsNotFound(err) { + // We assume that a resource imported by ID must already exist. It's a terminal error if it doesn't. + err = orcerrors.Terminal(orcv1alpha1.ConditionReasonUnrecoverableError, "referenced resource does not exist in OpenStack") + } + if osResource != nil { + log.V(4).Info("Imported existing OpenStack resource by ID", "ID", actuator.GetResourceID(osResource)) + } + return nil, osResource, err + } + + // Import by filter + if hasImportFilter, osResource, err := actuator.GetOSResourceByImportFilter(ctx); hasImportFilter { + var waitEvents []WaitingOnEvent + if osResource == nil { + waitEvents = []WaitingOnEvent{WaitingOnOpenStackExternal(externalUpdatePollingPeriod)} + } + return waitEvents, osResource, err + } + + // Create + if actuator.GetManagementPolicy() == orcv1alpha1.ManagementPolicyUnmanaged { + // We never create an unmanaged resource + // API validation should have ensured that one of the above functions returned + return nil, nil, orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, "Not creating unmanaged resource") + } + + osResource, err := actuator.GetOSResourceBySpec(ctx) + if err != nil { + return nil, nil, err + } + if osResource != nil { + log.V(4).Info("Adopted previously created resource") + return nil, osResource, nil + } + + log.V(3).Info("Creating resource") + return actuator.CreateResource(ctx) +} + +func DeleteResource[osResourcePT *osResourceT, osResourceT any](ctx context.Context, log logr.Logger, obj DeleteResourceActuator[osResourcePT], onComplete func() error) (osResourcePT, ctrl.Result, error) { + // We always fetch the resource by ID so we can continue to report status even when waiting for a finalizer + hasStatusID, osResource, err := obj.GetOSResourceByStatusID(ctx) + if err != nil { + if !orcerrors.IsNotFound(err) { + return osResource, ctrl.Result{}, err + } + // Gophercloud can return an empty non-nil object when returning errors, + // which will confuse us below. + osResource = nil + } + + if len(obj.GetFinalizers()) > 1 { + log.V(4).Info("Deferring resource cleanup due to remaining external finalizers") + return osResource, ctrl.Result{}, nil + } + + // We won't delete the resource for an unmanaged object, or if onDelete is detach + managementPolicy := obj.GetManagementPolicy() + managedOptions := obj.GetManagedOptions() + if managementPolicy == orcv1alpha1.ManagementPolicyUnmanaged || managedOptions.GetOnDelete() == orcv1alpha1.OnDeleteDetach { + logPolicy := []any{"managementPolicy", managementPolicy} + if managementPolicy == orcv1alpha1.ManagementPolicyManaged { + logPolicy = append(logPolicy, "onDelete", managedOptions.GetOnDelete()) + } + log.V(4).Info("Not deleting OpenStack resource due to policy", logPolicy...) + return osResource, ctrl.Result{}, onComplete() + } + + // If status.ID was not set, we still need to check if there's an orphaned object. + if osResource == nil && !hasStatusID { + osResource, err = obj.GetOSResourceBySpec(ctx) + if err != nil { + return osResource, ctrl.Result{}, err + } + } + + if osResource == nil { + log.V(4).Info("Resource is no longer observed") + return osResource, ctrl.Result{}, onComplete() + } + + log.V(4).Info("Deleting OpenStack resource") + waitEvents, err := obj.DeleteResource(ctx, osResource) + if err != nil { + return osResource, ctrl.Result{}, err + } + + var requeue time.Duration + if len(waitEvents) > 0 { + requeue = MaxRequeue(waitEvents) + } else { + requeue = deletePollingPeriod + } + return osResource, ctrl.Result{RequeueAfter: requeue}, nil +} + +type WaitingOnEvent interface { + Message() string + Requeue() time.Duration +} + +type waitingOnType int + +const ( + WaitingOnCreation waitingOnType = iota + WaitingOnReady + WaitingOnDeletion +) + +type waitingOnORC struct { + kind string + name string + waitingOn waitingOnType +} + +var _ WaitingOnEvent = waitingOnORC{} + +func (e waitingOnORC) Message() string { + var outcome string + switch e.waitingOn { + case WaitingOnCreation: + outcome = "created" + case WaitingOnReady: + outcome = "ready" + case WaitingOnDeletion: + outcome = "deleted" + } + return fmt.Sprintf("Waiting for %s/%s to be %s", e.kind, e.name, outcome) +} + +func newWaitingOnORC(kind, name string, event waitingOnType) WaitingOnEvent { + return waitingOnORC{ + kind: kind, + name: name, + waitingOn: event, + } +} + +func WaitingOnORCExist(kind, name string) WaitingOnEvent { + return newWaitingOnORC(kind, name, WaitingOnCreation) +} + +func WaitingOnORCReady(kind, name string) WaitingOnEvent { + return newWaitingOnORC(kind, name, WaitingOnReady) +} + +func WaitingOnORCDeleted(kind, name string) WaitingOnEvent { + return newWaitingOnORC(kind, name, WaitingOnDeletion) +} + +func (e waitingOnORC) Requeue() time.Duration { + return 0 +} + +type waitingOnOpenStack struct { + waitingOn waitingOnType + pollingPeriod time.Duration +} + +var _ WaitingOnEvent = waitingOnOpenStack{} + +func newWaitingOnOpenStack(event waitingOnType, pollingPeriod time.Duration) WaitingOnEvent { + return waitingOnOpenStack{ + waitingOn: event, + pollingPeriod: pollingPeriod, + } +} + +func WaitingOnOpenStackExternal(pollingPeriod time.Duration) WaitingOnEvent { + return newWaitingOnOpenStack(WaitingOnCreation, pollingPeriod) +} + +func WaitingOnOpenStackReady(kind, name string, pollingPeriod time.Duration) WaitingOnEvent { + return newWaitingOnOpenStack(WaitingOnReady, pollingPeriod) +} + +func (e waitingOnOpenStack) Message() string { + var outcome string + switch e.waitingOn { + case WaitingOnCreation: + outcome = "be created externally" + case WaitingOnReady: + outcome = "be ready" + } + return fmt.Sprintf("Waiting for OpenStack resource to %s", outcome) +} + +func (e waitingOnOpenStack) Requeue() time.Duration { + return e.pollingPeriod +} + +func MaxRequeue(evts []WaitingOnEvent) time.Duration { + var ret time.Duration + for _, evt := range evts { + if evt.Requeue() > ret { + ret = evt.Requeue() + } + } + return ret +} diff --git a/internal/controllers/generic/dependency.go b/internal/controllers/generic/dependency.go new file mode 100644 index 000000000..bce6020df --- /dev/null +++ b/internal/controllers/generic/dependency.go @@ -0,0 +1,195 @@ +/* +Copyright 2024 The ORC 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 generic + +import ( + "context" + "fmt" + "iter" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + orcv1alpha1 "github.com/k-orc/openstack-resource-controller/api/v1alpha1" + ctrlcommon "github.com/k-orc/openstack-resource-controller/internal/controllers/common" +) + +// NewDependency returns a new Dependency, which can perform tasks necessary to manage a dependency between 2 object types. The 2 object types are: +// - Object: this is the 'source' object. +// - Dependency: this is the object that a 'source' object may depend on. +// +// For example, a Port may depend on a Subnet, because it references one or more +// Subnets in its Addresses. In this case 'Object' is the Port, and 'Dependency' +// is the subnet. +// +// NewDependency has several type parameters, but only the first 2 are required as all the rest can be inferred. The 2 required parameters are: +// - pointer to the List type of Object +// - pointer to the Dependency type +// +// NewDependency takes the following arguments: +// - indexName: a name representing the path to the Dependency reference in Object. +// - getDependencyRefs: a function that takes a pointer to Object and returns a slice of strings containing the names of Dependencies +// +// Taking the Port -> Subnet example, the type parameters are: +// - *PortList: pointer to the list type of Port +// - *Subnet: pointer to the Dependency type +// +// and the arguments are: +// - indexName: "spec.resource.addresses[].subnetRef" - a symbolic path to the subnet reference in a Port +// - getDependencyRefs: func(object *Port) []string{ ... returns a slice containing all subnetRefs in this Port's addresses ... } +func NewDependency[ + objectListTP objectListType[objectListT, objectT], + depTP dependencyType[depT], + + objectTP objectType[objectT], + objectT any, objectListT any, depT any, +](indexName string, getDependencyRefs func(objectTP) []string) Dependency[objectTP, objectListTP, depTP, objectT, objectListT, depT] { + return Dependency[objectTP, objectListTP, depTP, objectT, objectListT, depT]{ + indexName: indexName, + getDependencyRefs: getDependencyRefs, + } +} + +type Dependency[ + objectTP objectType[objectT], + objectListTP objectListType[objectListT, objectT], + depTP dependencyType[depT], + + objectT any, objectListT any, depT any, +] struct { + indexName string + getDependencyRefs func(objectTP) []string +} + +type objectType[objectT any] interface { + *objectT + client.Object +} + +type objectListType[objectListT any, objectT any] interface { + client.ObjectList + *objectListT + + GetItems() []objectT +} + +type dependencyType[depT any] interface { + *depT + client.Object + + // We expect callers to check conditions on dependencies + orcv1alpha1.ObjectWithConditions +} + +// GetDependencies returns an iterator over Dependencies for a given Object. For each dependency it returns: +// - the dependency's name +// - a Result of fetching the dependency +func (d *Dependency[objectTP, _, depTP, _, _, depT]) GetDependencies(ctx context.Context, k8sClient client.Client, obj objectTP) iter.Seq2[string, Result[depT]] { + depRefs := d.getDependencyRefs(obj) + return func(yield func(string, Result[depT]) bool) { + for _, depRef := range depRefs { + var dep depTP = new(depT) + err := k8sClient.Get(ctx, types.NamespacedName{Name: depRef, Namespace: obj.GetNamespace()}, dep) + + var r Result[depT] + if err != nil { + r = Err[depT](err) + } else { + r = Ok(dep) + } + if !yield(depRef, r) { + return + } + } + } +} + +// GetObjects returns a slice of all Objects which depend on the given Dependency +func (d *Dependency[_, objectListTP, depTP, objectT, objectListT, _]) GetObjects(ctx context.Context, k8sClient client.Client, dep depTP) ([]objectT, error) { + var objectList objectListTP = new(objectListT) + if err := k8sClient.List(ctx, objectList, client.InNamespace(dep.GetNamespace()), client.MatchingFields{d.indexName: dep.GetName()}); err != nil { + return nil, err + } + return objectList.GetItems(), nil +} + +// AddIndexer adds the required field indexer for this dependency to a manager +func (d *Dependency[objectTP, _, _, objectT, _, _]) AddIndexer(ctx context.Context, mgr ctrl.Manager) error { + return mgr.GetFieldIndexer().IndexField(ctx, objectTP(new(objectT)), d.indexName, func(cObj client.Object) []string { + obj, ok := cObj.(objectTP) + if !ok { + return nil + } + + return d.getDependencyRefs(obj) + }) +} + +// WatchEventHandler returns an EventHandler which maps a Dependency to all Objects which depend on it +func (d *Dependency[objectTP, _, depTP, _, _, depT]) WatchEventHandler(log logr.Logger, k8sClient client.Client) (handler.EventHandler, error) { + dependencySpecimen := depTP(new(depT)) + gvks, _, err := k8sClient.Scheme().ObjectKinds(dependencySpecimen) + if err != nil { + return nil, err + } + if len(gvks) == 0 { + return nil, fmt.Errorf("no registered GVK for %T", dependencySpecimen) + } + log = log.WithValues("watch", gvks[0].Kind) + + return handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request { + log := log.WithValues("name", obj.GetName(), "namespace", obj.GetNamespace()) + + dependency, ok := obj.(depTP) + if !ok { + log.Info("Watch got unexpected object type", "type", fmt.Sprintf("%T", obj)) + return nil + } + + objects, err := d.GetObjects(ctx, k8sClient, dependency) + if err != nil { + log.Error(err, "listing Routers") + return nil + } + requests := make([]reconcile.Request, len(objects)) + for i := range objects { + var object objectTP = &objects[i] + request := &requests[i] + + request.Name = object.GetName() + request.Namespace = object.GetNamespace() + } + return requests + }), nil +} + +// AddDeletionGuard adds a deletion guard controller to the given manager appropriate for this dependency +func (d *Dependency[objectTP, _, depTP, _, _, _]) AddDeletionGuard(mgr ctrl.Manager, finalizer string, fieldOwner client.FieldOwner) error { + getDependencyRefsForClientObject := func(cObj client.Object) []string { + obj, ok := cObj.(objectTP) + if !ok { + return nil + } + return d.getDependencyRefs(obj) + } + + return ctrlcommon.AddDeletionGuard[depTP, objectTP](mgr, finalizer, fieldOwner, getDependencyRefsForClientObject, d.GetObjects) +} diff --git a/internal/controllers/generic/result.go b/internal/controllers/generic/result.go new file mode 100644 index 000000000..77425d118 --- /dev/null +++ b/internal/controllers/generic/result.go @@ -0,0 +1,42 @@ +/* +Copyright 2024 The ORC 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 generic + +// NOTE(mdbooth): This is a stupid type without any of the useful properties of +// a proper Result type. However, I don't believe they can currently be +// implemented in Go. + +type Result[T any] struct { + ok *T + err error +} + +func (r Result[T]) Ok() *T { + return r.ok +} + +func (r Result[T]) Err() error { + return r.err +} + +func Ok[T any](v *T) Result[T] { + return Result[T]{ok: v} +} + +func Err[T any](err error) Result[T] { + return Result[T]{err: err} +} diff --git a/internal/controllers/image/actuator.go b/internal/controllers/image/actuator.go new file mode 100644 index 000000000..4308bf669 --- /dev/null +++ b/internal/controllers/image/actuator.go @@ -0,0 +1,260 @@ +/* +Copyright 2024 The ORC 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 image + +import ( + "context" + "fmt" + "reflect" + "slices" + + "github.com/gophercloud/gophercloud/v2/openstack/image/v2/images" + "k8s.io/utils/ptr" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + orcv1alpha1 "github.com/k-orc/openstack-resource-controller/api/v1alpha1" + "github.com/k-orc/openstack-resource-controller/internal/controllers/generic" + "github.com/k-orc/openstack-resource-controller/internal/osclients" + "github.com/k-orc/openstack-resource-controller/internal/scope" + orcerrors "github.com/k-orc/openstack-resource-controller/internal/util/errors" +) + +type imageActuator struct { + *orcv1alpha1.Image + osClient osclients.ImageClient +} + +func newActuator(ctx context.Context, k8sClient client.Client, scopeFactory scope.Factory, orcObject *orcv1alpha1.Image) (imageActuator, error) { + log := ctrl.LoggerFrom(ctx) + + clientScope, err := scopeFactory.NewClientScopeFromObject(ctx, k8sClient, log, orcObject) + if err != nil { + return imageActuator{}, err + } + osClient, err := clientScope.NewImageClient() + if err != nil { + return imageActuator{}, err + } + + return imageActuator{ + Image: orcObject, + osClient: osClient, + }, nil +} + +var _ generic.CreateResourceActuator[*images.Image] = imageActuator{} +var _ generic.DeleteResourceActuator[*images.Image] = imageActuator{} + +func (obj imageActuator) GetManagementPolicy() orcv1alpha1.ManagementPolicy { + return obj.Spec.ManagementPolicy +} + +func (obj imageActuator) GetManagedOptions() *orcv1alpha1.ManagedOptions { + return obj.Spec.ManagedOptions +} + +func (obj imageActuator) GetResourceID(osResource *images.Image) string { + return osResource.ID +} + +func (obj imageActuator) GetOSResourceByStatusID(ctx context.Context) (bool, *images.Image, error) { + if obj.Status.ID == nil { + return false, nil, nil + } + + image, err := obj.osClient.GetImage(*obj.Status.ID) + return true, image, err +} + +func (obj imageActuator) GetOSResourceBySpec(ctx context.Context) (*images.Image, error) { + if obj.Spec.Resource == nil { + return nil, nil + } + listOpts := listOptsFromCreation(obj.Image) + image, err := getGlanceImageFromList(ctx, listOpts, obj.osClient) + return image, err +} + +func (obj imageActuator) GetOSResourceByImportID(ctx context.Context) (bool, *images.Image, error) { + if obj.Spec.Import == nil { + return false, nil, nil + } + if obj.Spec.Import.ID == nil { + return false, nil, nil + } + + image, err := obj.osClient.GetImage(*obj.Spec.Import.ID) + return true, image, err +} + +func (obj imageActuator) GetOSResourceByImportFilter(ctx context.Context) (bool, *images.Image, error) { + if obj.Spec.Import == nil { + return false, nil, nil + } + if obj.Spec.Import.Filter == nil { + return false, nil, nil + } + + listOpts := listOptsFromImportFilter(obj.Spec.Import.Filter) + image, err := getGlanceImageFromList(ctx, listOpts, obj.osClient) + return true, image, err +} + +func (obj imageActuator) CreateResource(ctx context.Context) ([]generic.WaitingOnEvent, *images.Image, error) { + resource := obj.Spec.Resource + if resource == nil { + // Should have been caught by API validation + return nil, nil, orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, "Creation requested, but spec.resource is not set") + } + + if resource.Content == nil { + // Should have been caught by API validation + return nil, nil, orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, "Creation requested, but spec.resource.content is not set") + } + + tags := make([]string, len(resource.Tags)) + for i := range resource.Tags { + tags[i] = string(resource.Tags[i]) + } + // Sort tags before creation to simplify comparisons + slices.Sort(tags) + + var minDisk, minMemory int + properties := resource.Properties + additionalProperties := map[string]string{} + if properties != nil { + if properties.MinDiskGB != nil { + minDisk = *properties.MinDiskGB + } + if properties.MinMemoryMB != nil { + minMemory = *properties.MinMemoryMB + } + + if err := glancePropertiesFromStruct(properties.Hardware, additionalProperties); err != nil { + return nil, nil, orcerrors.Terminal(orcv1alpha1.ConditionReasonUnrecoverableError, "programming error", err) + } + } + + var visibility *images.ImageVisibility + if resource.Visibility != nil { + visibility = ptr.To(images.ImageVisibility(*resource.Visibility)) + } + + image, err := obj.osClient.CreateImage(ctx, &images.CreateOpts{ + Name: string(getResourceName(obj.Image)), + Visibility: visibility, + Tags: tags, + ContainerFormat: string(resource.Content.ContainerFormat), + DiskFormat: (string)(resource.Content.DiskFormat), + MinDisk: minDisk, + MinRAM: minMemory, + Protected: resource.Protected, + Properties: additionalProperties, + }) + + // We should require the spec to be updated before retrying a create which returned a conflict + if orcerrors.IsConflict(err) { + err = orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, "invalid configuration creating image: "+err.Error(), err) + } + + return nil, image, err +} + +func (obj imageActuator) DeleteResource(ctx context.Context, osResource *images.Image) ([]generic.WaitingOnEvent, error) { + return nil, obj.osClient.DeleteImage(ctx, osResource.ID) +} + +// getResourceName returns the name of the glance image we should use. +func getResourceName(orcImage *orcv1alpha1.Image) orcv1alpha1.OpenStackName { + if orcImage.Spec.Resource.Name != nil { + return *orcImage.Spec.Resource.Name + } + return orcv1alpha1.OpenStackName(orcImage.Name) +} + +func listOptsFromImportFilter(filter *orcv1alpha1.ImageFilter) images.ListOptsBuilder { + return images.ListOpts{Name: ptr.Deref(filter.Name, "")} +} + +// listOptsFromCreation returns a listOpts which will return the image which +// would have been created from the current spec and hopefully no other image. +// Its purpose is to automatically adopt an image that we created but failed to +// write to status.id. +func listOptsFromCreation(orcImage *orcv1alpha1.Image) images.ListOptsBuilder { + return images.ListOpts{Name: string(getResourceName(orcImage))} +} + +func getGlanceImageFromList(_ context.Context, listOpts images.ListOptsBuilder, imageClient osclients.ImageClient) (*images.Image, error) { + glanceImages, err := imageClient.ListImages(listOpts) + if err != nil { + return nil, err + } + + if len(glanceImages) == 1 { + return &glanceImages[0], nil + } + + // No image found + if len(glanceImages) == 0 { + return nil, nil + } + + // Multiple images found + return nil, orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, fmt.Sprintf("Expected to find exactly one image to import. Found %d", len(glanceImages))) +} + +// glancePropertiesFromStruct populates a properties struct using field values and glance tags defined on the given struct +// glance tags are defined in the API. +func glancePropertiesFromStruct(propStruct interface{}, properties map[string]string) error { + sp := reflect.ValueOf(propStruct) + if sp.Kind() != reflect.Pointer { + return fmt.Errorf("glancePropertiesFromStruct expects pointer to struct, got %T", propStruct) + } + if sp.IsZero() { + return nil + } + + s := sp.Elem() + st := s.Type() + if st.Kind() != reflect.Struct { + return fmt.Errorf("glancePropertiesFromStruct expects pointer to struct, got %T", propStruct) + } + + for i := range st.NumField() { + field := st.Field(i) + glanceTag, ok := field.Tag.Lookup(orcv1alpha1.GlanceTag) + if !ok { + panic(fmt.Errorf("glance tag not defined for field %s on struct %T", field.Name, st.Name)) + } + + value := s.Field(i) + if value.Kind() == reflect.Pointer { + if value.IsZero() { + continue + } + value = value.Elem() + } + + // Gophercloud takes only strings, but values may not be + // strings. Value.String() prints semantic information for + // non-strings, but Sprintf does what we want. + properties[glanceTag] = fmt.Sprintf("%v", value) + } + + return nil +} diff --git a/internal/controllers/image/controller.go b/internal/controllers/image/controller.go index 300e3cf84..db36debf6 100644 --- a/internal/controllers/image/controller.go +++ b/internal/controllers/image/controller.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Kubernetes Authors. +Copyright 2024 The ORC Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,16 +18,12 @@ package image import ( "context" - "fmt" "time" - "github.com/go-logr/logr" "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" - "sigs.k8s.io/controller-runtime/pkg/event" - "sigs.k8s.io/controller-runtime/pkg/predicate" orcv1alpha1 "github.com/k-orc/openstack-resource-controller/api/v1alpha1" @@ -43,8 +39,6 @@ const ( SSAFinalizerTxn = "finalizer" // Field owner of transient status. SSAStatusTxn = "status" - // Field owner of persistent id field. - SSAIDTxn = "id" ) // ssaFieldOwner returns the field owner for a specific named SSA transaction. @@ -53,8 +47,8 @@ func ssaFieldOwner(txn string) client.FieldOwner { } const ( - // The time to wait before reconciling again when we are expecting glance to finish some task and update status. - waitForGlanceImageStatusUpdate = 15 * time.Second + // The time to wait before reconciling again when we are expecting OpenStack to finish some task and update status. + externalUpdatePollingPeriod = 15 * time.Second // Size of the upload and download buffers. transferBufferSizeBytes = 64 * 1024 @@ -63,70 +57,35 @@ const ( maxDownloadAttempts = 5 ) +type imageReconcilerConstructor struct { + scopeFactory scope.Factory +} + +func New(scopeFactory scope.Factory) ctrlexport.Controller { + return imageReconcilerConstructor{scopeFactory: scopeFactory} +} + +func (imageReconcilerConstructor) GetName() string { + return "image" +} + // orcImageReconciler reconciles an ORC Image. type orcImageReconciler struct { - client client.Client - recorder record.EventRecorder - watchFilterValue string - scopeFactory scope.Factory - caCertificates []byte // PEM encoded ca certificates. + client client.Client + recorder record.EventRecorder + scopeFactory scope.Factory } -func New(client client.Client, recorder record.EventRecorder, watchFilterValue string, scopeFactory scope.Factory, caCertificates []byte) ctrlexport.SetupWithManager { - return &orcImageReconciler{ - client: client, - recorder: recorder, - watchFilterValue: watchFilterValue, - scopeFactory: scopeFactory, - caCertificates: caCertificates, +// SetupWithManager sets up the controller with the Manager. +func (c imageReconcilerConstructor) SetupWithManager(_ context.Context, mgr ctrl.Manager, options controller.Options) error { + reconciler := orcImageReconciler{ + client: mgr.GetClient(), + recorder: mgr.GetEventRecorderFor("orc-image-controller"), + scopeFactory: c.scopeFactory, } -} -// SetupWithManager sets up the controller with the Manager. -func (r *orcImageReconciler) SetupWithManager(_ context.Context, mgr ctrl.Manager, options controller.Options) error { - log := mgr.GetLogger() return ctrl.NewControllerManagedBy(mgr). For(&orcv1alpha1.Image{}). WithOptions(options). - WithEventFilter(needsReconcilePredicate(log)). - Complete(r) -} - -func needsReconcilePredicate(log logr.Logger) predicate.Predicate { - filter := func(obj client.Object, event string) bool { - log := log.WithValues("predicate", "NeedsReconcile", "event", event) - - orcImage, ok := obj.(*orcv1alpha1.Image) - if !ok { - log.V(0).Info("Expected Image", "type", fmt.Sprintf("%T", obj)) - return false - } - - // Always reconcile deleted objects. Note that we don't always - // get a Delete event for a deleted object. If the object was - // deleted while the controller was not running we will get a - // Create event for it when the controller syncs. - if !orcImage.DeletionTimestamp.IsZero() { - return true - } - - if !orcv1alpha1.IsReconciliationComplete(orcImage) { - return true - } - - log.V(4).Info("not reconciling image due to terminal state", "name", orcImage.GetName(), "namespace", orcImage.GetNamespace(), "generation", orcImage.GetGeneration()) - return false - } - - // We always reconcile create. We get a create event for every object when - // the controller restarts as the controller has no previously observed - // state at that time. This means that upgrading the controller will always - // re-reconcile objects. This has the advantage of being a way to address - // invalid state from controller bugs, but the disadvantage of potentially - // causing a 'thundering herd' when the controller restarts. - return predicate.Funcs{ - UpdateFunc: func(e event.UpdateEvent) bool { - return filter(e.ObjectNew, "Update") - }, - } + Complete(&reconciler) } diff --git a/internal/controllers/image/reconcile.go b/internal/controllers/image/reconcile.go index c8747ab38..3c8530e80 100644 --- a/internal/controllers/image/reconcile.go +++ b/internal/controllers/image/reconcile.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Kubernetes Authors. +Copyright 2024 The ORC Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -20,21 +20,21 @@ import ( "context" "errors" "fmt" - "reflect" - "slices" - "time" "github.com/gophercloud/gophercloud/v2/openstack/image/v2/images" apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" orcv1alpha1 "github.com/k-orc/openstack-resource-controller/api/v1alpha1" + "github.com/k-orc/openstack-resource-controller/internal/controllers/common" + "github.com/k-orc/openstack-resource-controller/internal/controllers/generic" osclients "github.com/k-orc/openstack-resource-controller/internal/osclients" + "github.com/k-orc/openstack-resource-controller/internal/util/applyconfigs" orcerrors "github.com/k-orc/openstack-resource-controller/internal/util/errors" - "github.com/k-orc/openstack-resource-controller/internal/util/ssa" orcapplyconfigv1alpha1 "github.com/k-orc/openstack-resource-controller/pkg/clients/applyconfiguration/api/v1alpha1" ) @@ -58,24 +58,10 @@ func (r *orcImageReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c return r.reconcileNormal(ctx, orcImage) } -func (r *orcImageReconciler) getImageClient(ctx context.Context, orcImage *orcv1alpha1.Image) (osclients.ImageClient, error) { - log := ctrl.LoggerFrom(ctx) - - clientScope, err := r.scopeFactory.NewClientScopeFromObject(ctx, r.client, r.caCertificates, log, orcImage) - if err != nil { - return nil, err - } - return clientScope.NewImageClient() -} - -func (r *orcImageReconciler) reconcileNormal(ctx context.Context, orcImage *orcv1alpha1.Image) (_ ctrl.Result, err error) { +func (r *orcImageReconciler) reconcileNormal(ctx context.Context, orcObject *orcv1alpha1.Image) (_ ctrl.Result, err error) { log := ctrl.LoggerFrom(ctx) log.V(3).Info("Reconciling image") - if !controllerutil.ContainsFinalizer(orcImage, Finalizer) { - return ctrl.Result{}, r.setFinalizer(ctx, orcImage) - } - var statusOpts []updateStatusOpt addStatus := func(opt updateStatusOpt) { statusOpts = append(statusOpts, opt) @@ -87,7 +73,7 @@ func (r *orcImageReconciler) reconcileNormal(ctx context.Context, orcImage *orcv addStatus(withError(err)) } - err = errors.Join(err, r.updateStatus(ctx, orcImage, statusOpts...)) + err = errors.Join(err, r.updateStatus(ctx, orcObject, statusOpts...)) var terminalError *orcerrors.TerminalError if errors.As(err, &terminalError) { @@ -96,77 +82,44 @@ func (r *orcImageReconciler) reconcileNormal(ctx context.Context, orcImage *orcv } }() - imageClient, err := r.getImageClient(ctx, orcImage) + if !controllerutil.ContainsFinalizer(orcObject, Finalizer) { + patch := common.SetFinalizerPatch(orcObject, Finalizer) + return ctrl.Result{}, r.client.Patch(ctx, orcObject, patch, client.ForceOwnership, ssaFieldOwner(SSAFinalizerTxn)) + } + + actuator, err := newActuator(ctx, r.client, r.scopeFactory, orcObject) if err != nil { return ctrl.Result{}, err } - var glanceImage *images.Image - switch { - case orcImage.Status.ID != nil: - log.V(4).Info("Fetching existing glance image", "ID", *orcImage.Status.ID) - glanceImage, err = imageClient.GetImage(*orcImage.Status.ID) - if err != nil { - if orcerrors.IsNotFound(err) { - // An image we previously referenced has been deleted unexpectedly. We can't recover from this. - return ctrl.Result{}, orcerrors.Terminal(orcv1alpha1.OpenStackConditionReasonUnrecoverableError, "image has been deleted from Glance") - } - return ctrl.Result{}, err - } - - case orcImage.Spec.Import != nil && orcImage.Spec.Import.ID != nil: - log.V(4).Info("Importing existing Glance image by ID") - glanceImage, err = imageClient.GetImage(*orcImage.Spec.Import.ID) - if err != nil { - if orcerrors.IsNotFound(err) { - // We assume that an image imported by ID must already exist. It's a terminal error if it doesn't. - return ctrl.Result{}, orcerrors.Terminal(orcv1alpha1.OpenStackConditionReasonUnrecoverableError, "referenced image does not exist in Glance") - } - return ctrl.Result{}, err - } - - case orcImage.Spec.Import != nil && orcImage.Spec.Import.Filter != nil: - log.V(4).Info("Importing existing Glance image by filter") - listOpts := listOptsFromImportFilter(orcImage.Spec.Import.Filter) - glanceImage, err = getGlanceImageFromList(ctx, listOpts, imageClient) - if err != nil { - return ctrl.Result{}, err - } - if glanceImage == nil { - log.V(3).Info("Glance image does not yet exist") - addStatus(withProgressMessage("Waiting for Glance image to be created externally")) - return ctrl.Result{RequeueAfter: waitForGlanceImageStatusUpdate}, err - } - - default: - log.V(4).Info("Checking for previously created image") - listOpts := listOptsFromCreation(orcImage) - glanceImage, err = getGlanceImageFromList(ctx, listOpts, imageClient) - if err != nil { - return ctrl.Result{}, err - } + waitEvents, osResource, err := generic.GetOrCreateOSResource(ctx, log, r.client, actuator) + if err != nil { + return ctrl.Result{}, err + } - if glanceImage == nil { - glanceImage, err = createImage(ctx, orcImage, imageClient) - if err != nil { - return ctrl.Result{}, err - } - } + if len(waitEvents) > 0 { + log.V(3).Info("Waiting on events before creation") + addStatus(withProgressMessage(waitEvents[0].Message())) + return ctrl.Result{RequeueAfter: generic.MaxRequeue(waitEvents)}, nil } - addStatus(withGlanceImage(glanceImage)) + if osResource == nil { + // Programming error: if we don't have a resource we should either have an error or be waiting on something + return ctrl.Result{}, fmt.Errorf("oResource is not set, but no wait events or error") + } - if orcImage.Status.ID == nil { - if err := r.setStatusID(ctx, orcImage, glanceImage.ID); err != nil { + addStatus(withResource(osResource)) + if orcObject.Status.ID == nil { + if err := r.setStatusID(ctx, orcObject, osResource.ID); err != nil { return ctrl.Result{}, err } } - log.V(4).Info("Got glance image", "status", glanceImage.Status) - log = log.WithValues("ID", glanceImage.ID) + log = log.WithValues("ID", osResource.ID) + log.V(4).Info("Got resource") ctx = ctrl.LoggerInto(ctx, log) - return r.handleImageUpload(ctx, imageClient, orcImage, glanceImage, addStatus) + return r.handleImageUpload(ctx, actuator.osClient, orcObject, osResource, addStatus) } func (r *orcImageReconciler) handleImageUpload(ctx context.Context, imageClient osclients.ImageClient, orcImage *orcv1alpha1.Image, glanceImage *images.Image, addStatus func(updateStatusOpt)) (_ ctrl.Result, err error) { @@ -180,7 +133,7 @@ func (r *orcImageReconciler) handleImageUpload(ctx context.Context, imageClient // "saving" is seen while uploading, but might be seen because our upload failed and glance hasn't reset yet. case images.ImageStatusImporting, images.ImageStatusSaving: addStatus(withProgressMessage(downloadingMessage("Glance is downloading image content", orcImage))) - return ctrl.Result{RequeueAfter: waitForGlanceImageStatusUpdate}, nil + return ctrl.Result{RequeueAfter: externalUpdatePollingPeriod}, nil // Newly created image, waiting for upload, or... previous upload was interrupted and has now reset case images.ImageStatusQueued: @@ -189,12 +142,12 @@ func (r *orcImageReconciler) handleImageUpload(ctx context.Context, imageClient addStatus(withProgressMessage("Waiting for glance image content to be uploaded externally")) return ctrl.Result{ - RequeueAfter: waitForGlanceImageStatusUpdate, + RequeueAfter: externalUpdatePollingPeriod, }, err } if ptr.Deref(orcImage.Status.DownloadAttempts, 0) >= maxDownloadAttempts { - return ctrl.Result{}, orcerrors.Terminal(orcv1alpha1.OpenStackConditionReasonInvalidConfiguration, fmt.Sprintf("Unable to download content after %d attempts", maxDownloadAttempts)) + return ctrl.Result{}, orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, fmt.Sprintf("Unable to download content after %d attempts", maxDownloadAttempts)) } canWebDownload, err := r.canWebDownload(ctx, orcImage, imageClient) @@ -226,15 +179,15 @@ func (r *orcImageReconciler) handleImageUpload(ctx context.Context, imageClient // Error cases case images.ImageStatusKilled: - return ctrl.Result{}, orcerrors.Terminal(orcv1alpha1.OpenStackConditionReasonUnrecoverableError, "a glance error occurred while saving image content") + return ctrl.Result{}, orcerrors.Terminal(orcv1alpha1.ConditionReasonUnrecoverableError, "a glance error occurred while saving image content") case images.ImageStatusDeleted, images.ImageStatusPendingDelete: - return ctrl.Result{}, orcerrors.Terminal(orcv1alpha1.OpenStackConditionReasonUnrecoverableError, "image status is deleting") + return ctrl.Result{}, orcerrors.Terminal(orcv1alpha1.ConditionReasonUnrecoverableError, "image status is deleting") default: return ctrl.Result{}, errors.New("unknown image status: " + string(glanceImage.Status)) } } -func (r *orcImageReconciler) reconcileDelete(ctx context.Context, orcImage *orcv1alpha1.Image) (_ ctrl.Result, err error) { +func (r *orcImageReconciler) reconcileDelete(ctx context.Context, orcObject *orcv1alpha1.Image) (_ ctrl.Result, err error) { log := ctrl.LoggerFrom(ctx) log.V(3).Info("Reconciling image delete") @@ -250,224 +203,24 @@ func (r *orcImageReconciler) reconcileDelete(ctx context.Context, orcImage *orcv if err != nil { addStatus(withError(err)) } - err = errors.Join(err, r.updateStatus(ctx, orcImage, statusOpts...)) + err = errors.Join(err, r.updateStatus(ctx, orcObject, statusOpts...)) } }() - // We won't delete the resource for an unmanaged object, or if onDelete is detach - if orcImage.Spec.ManagementPolicy == orcv1alpha1.ManagementPolicyUnmanaged || orcImage.Spec.ManagedOptions.GetOnDelete() == orcv1alpha1.OnDeleteDetach { - logPolicy := []any{"managementPolicy", orcImage.Spec.ManagementPolicy} - if orcImage.Spec.ManagementPolicy == orcv1alpha1.ManagementPolicyManaged { - logPolicy = append(logPolicy, "onDelete", orcImage.Spec.ManagedOptions.GetOnDelete()) - } - log.V(4).Info("Not deleting Glance image due to policy", logPolicy...) - } else { - imageClient, err := r.getImageClient(ctx, orcImage) - if err != nil { - return ctrl.Result{}, err - } - - var glanceImage *images.Image - glanceImage, err = getGlanceImage(ctx, orcImage, imageClient) - if err != nil && !orcerrors.IsNotFound(err) { - return ctrl.Result{}, err - } - addStatus(withGlanceImage(glanceImage)) - - // Delete any returned glance image, but don't clear the finalizer until getGlanceImage() returns nothing - if glanceImage != nil { - log.V(4).Info("Deleting image", "id", glanceImage.ID) - err := imageClient.DeleteImage(ctx, glanceImage.ID) - if err != nil { - return ctrl.Result{}, err - } - return ctrl.Result{RequeueAfter: 5 * time.Second}, nil - } - - log.V(4).Info("Image is deleted") - } - - deleted = true - - // Clear the finalizer - applyConfig := orcapplyconfigv1alpha1.Image(orcImage.Name, orcImage.Namespace).WithUID(orcImage.UID) - return ctrl.Result{}, r.client.Patch(ctx, orcImage, ssa.ApplyConfigPatch(applyConfig), client.ForceOwnership, ssaFieldOwner(SSAFinalizerTxn)) -} - -// getGlanceImage returns the glance image associated with an ORC Image, or nil if none was found. -// If Status.ImageID is set, it returns this image, or an error if it does not exist. -// Otherwise it looks for an existing image with the expected name. It returns nil if none exists. -func getGlanceImage(ctx context.Context, orcImage *orcv1alpha1.Image, imageClient osclients.ImageClient) (*images.Image, error) { - log := ctrl.LoggerFrom(ctx) - - log.V(4).Info("Looking for existing glance image to adopt") - - // Check for existing image by name in case we're adopting or failed to write to status - imageName := getImageName(orcImage) - glanceImages, err := imageClient.ListImages(images.ListOpts{Name: imageName}) - if err != nil { - return nil, err - } - switch { - case len(glanceImages) == 1: - image := &glanceImages[0] - log.V(3).Info("Adopting existing glance image", "imageID", image.ID) - return image, nil - case len(glanceImages) > 1: - return nil, orcerrors.Terminal(orcv1alpha1.OpenStackConditionReasonInvalidConfiguration, "found multiple images with name "+imageName) - } - - return nil, nil -} - -// getImageName returns the name of the glance image we should use. -func getImageName(orcImage *orcv1alpha1.Image) string { - if orcImage.Spec.Resource.Name != "" { - return orcImage.Spec.Resource.Name - } - return orcImage.Name -} - -func listOptsFromImportFilter(filter *orcv1alpha1.ImageFilter) images.ListOptsBuilder { - return images.ListOpts{Name: ptr.Deref(filter.Name, "")} -} - -// listOptsFromCreation returns a listOpts which will return the image which -// would have been created from the current spec and hopefully no other image. -// Its purpose is to automatically adopt an image that we created but failed to -// write to status.id. -func listOptsFromCreation(orcImage *orcv1alpha1.Image) images.ListOptsBuilder { - return images.ListOpts{Name: getImageName(orcImage)} -} - -func getGlanceImageFromList(_ context.Context, listOpts images.ListOptsBuilder, imageClient osclients.ImageClient) (*images.Image, error) { - glanceImages, err := imageClient.ListImages(listOpts) + actuator, err := newActuator(ctx, r.client, r.scopeFactory, orcObject) if err != nil { - return nil, err - } - - if len(glanceImages) == 1 { - return &glanceImages[0], nil - } - - // No image found - if len(glanceImages) == 0 { - return nil, nil - } - - // Multiple images found - return nil, orcerrors.Terminal(orcv1alpha1.OpenStackConditionReasonInvalidConfiguration, fmt.Sprintf("Expected to find exactly one image to import. Found %d", len(glanceImages))) -} - -// glancePropertiesFromStruct populates a properties struct using field values and glance tags defined on the given struct -// glance tags are defined in the API. -func glancePropertiesFromStruct(propStruct interface{}, properties map[string]string) error { - sp := reflect.ValueOf(propStruct) - if sp.Kind() != reflect.Pointer { - return fmt.Errorf("glancePropertiesFromStruct expects pointer to struct, got %T", propStruct) - } - if sp.IsZero() { - return nil - } - - s := sp.Elem() - st := s.Type() - if st.Kind() != reflect.Struct { - return fmt.Errorf("glancePropertiesFromStruct expects pointer to struct, got %T", propStruct) - } - - for i := range st.NumField() { - field := st.Field(i) - glanceTag, ok := field.Tag.Lookup(orcv1alpha1.GlanceTag) - if !ok { - panic(fmt.Errorf("glance tag not defined for field %s on struct %T", field.Name, st.Name)) - } - - value := s.Field(i) - if value.Kind() == reflect.Pointer { - if value.IsZero() { - continue - } - value = value.Elem() - } - - // Gophercloud takes only strings, but values may not be - // strings. Value.String() prints semantic information for - // non-strings, but Sprintf does what we want. - properties[glanceTag] = fmt.Sprintf("%v", value) - } - - return nil -} - -// createImage creates a Glance image for an ORC Image. -func createImage(ctx context.Context, orcImage *orcv1alpha1.Image, imageClient osclients.ImageClient) (*images.Image, error) { - if orcImage.Spec.ManagementPolicy == orcv1alpha1.ManagementPolicyUnmanaged { - // Should have been caught by API validation - return nil, orcerrors.Terminal(orcv1alpha1.OpenStackConditionReasonInvalidConfiguration, "Not creating unmanaged resource") - } - - log := ctrl.LoggerFrom(ctx) - log.V(3).Info("Creating image") - - resource := orcImage.Spec.Resource - - if resource == nil { - // Should have been caught by API validation - return nil, orcerrors.Terminal(orcv1alpha1.OpenStackConditionReasonInvalidConfiguration, "Creation requested, but spec.resource is not set") - } - - if resource.Content == nil { - // Should have been caught by API validation - return nil, orcerrors.Terminal(orcv1alpha1.OpenStackConditionReasonInvalidConfiguration, "Creation requested, but spec.resource.content is not set") - } - - tags := make([]string, len(resource.Tags)) - for i := range resource.Tags { - tags[i] = string(resource.Tags[i]) - } - // Sort tags before creation to simplify comparisons - slices.Sort(tags) - - var minDisk, minMemory int - properties := resource.Properties - additionalProperties := map[string]string{} - if properties != nil { - if properties.MinDiskGB != nil { - minDisk = *properties.MinDiskGB - } - if properties.MinMemoryMB != nil { - minMemory = *properties.MinMemoryMB - } - - if err := glancePropertiesFromStruct(properties.Hardware, additionalProperties); err != nil { - return nil, orcerrors.Terminal(orcv1alpha1.OpenStackConditionReasonUnrecoverableError, "programming error", err) - } + return ctrl.Result{}, err } - var visibility *images.ImageVisibility - if resource.Visibility != nil { - visibility = ptr.To(images.ImageVisibility(*resource.Visibility)) - } + osResource, result, err := generic.DeleteResource(ctx, log, actuator, func() error { + deleted = true - image, err := imageClient.CreateImage(ctx, &images.CreateOpts{ - Name: getImageName(orcImage), - Visibility: visibility, - Tags: tags, - ContainerFormat: string(resource.Content.ContainerFormat), - DiskFormat: (string)(resource.Content.DiskFormat), - MinDisk: minDisk, - MinRAM: minMemory, - Protected: resource.Protected, - Properties: additionalProperties, + // Clear the finalizer + applyConfig := orcapplyconfigv1alpha1.Image(orcObject.Name, orcObject.Namespace).WithUID(orcObject.UID) + return r.client.Patch(ctx, orcObject, applyconfigs.Patch(types.ApplyPatchType, applyConfig), client.ForceOwnership, ssaFieldOwner(SSAFinalizerTxn)) }) - - // We should require the spec to be updated before retrying a create which returned a conflict - if orcerrors.IsConflict(err) { - err = orcerrors.Terminal(orcv1alpha1.OpenStackConditionReasonInvalidConfiguration, "invalid configuration creating image: "+err.Error(), err) - } - - return image, err + addStatus(withResource(osResource)) + return result, err } func downloadingMessage(msg string, orcImage *orcv1alpha1.Image) string { diff --git a/internal/controllers/image/status.go b/internal/controllers/image/status.go index 289cab4a2..f9bad41e5 100644 --- a/internal/controllers/image/status.go +++ b/internal/controllers/image/status.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Kubernetes Authors. +Copyright 2024 The ORC Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -25,14 +25,15 @@ import ( "github.com/gophercloud/gophercloud/v2/openstack/image/v2/images" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" applyconfigv1 "k8s.io/client-go/applyconfigurations/meta/v1" "k8s.io/utils/ptr" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" orcv1alpha1 "github.com/k-orc/openstack-resource-controller/api/v1alpha1" + "github.com/k-orc/openstack-resource-controller/internal/util/applyconfigs" orcerrors "github.com/k-orc/openstack-resource-controller/internal/util/errors" - "github.com/k-orc/openstack-resource-controller/internal/util/ssa" orcapplyconfigv1alpha1 "github.com/k-orc/openstack-resource-controller/pkg/clients/applyconfiguration/api/v1alpha1" ) @@ -41,30 +42,6 @@ const ( glanceOSHashValue = "os_hash_value" ) -// setFinalizer sets a finalizer on the object in its own SSA transaction. -func (r *orcImageReconciler) setFinalizer(ctx context.Context, obj client.Object) error { - gvk := obj.GetObjectKind().GroupVersionKind() - - applyConfig := struct { - applyconfigv1.TypeMetaApplyConfiguration `json:",inline"` - applyconfigv1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"` - }{} - - // Type meta - applyConfig. - WithAPIVersion(gvk.GroupVersion().String()). - WithKind(gvk.Kind) - - // Object meta - applyConfig. - WithName(obj.GetName()). - WithNamespace(obj.GetNamespace()). - WithUID(obj.GetUID()). // For safety: ensure we don't accidentally create a new object if we race with delete - WithFinalizers(Finalizer) - - return r.client.Patch(ctx, obj, ssa.ApplyConfigPatch(applyConfig), client.ForceOwnership, ssaFieldOwner(SSAFinalizerTxn)) -} - // setStatusID sets a finalizer on the object in its own SSA transaction. func (r *orcImageReconciler) setStatusID(ctx context.Context, orcImage *orcv1alpha1.Image, id string) error { applyConfig := orcapplyconfigv1alpha1.Image(orcImage.Name, orcImage.Namespace). @@ -72,7 +49,7 @@ func (r *orcImageReconciler) setStatusID(ctx context.Context, orcImage *orcv1alp WithStatus(orcapplyconfigv1alpha1.ImageStatus(). WithID(id)) - return r.client.Status().Patch(ctx, orcImage, ssa.ApplyConfigPatch(applyConfig), client.ForceOwnership, ssaFieldOwner(SSAIDTxn)) + return r.client.Status().Patch(ctx, orcImage, applyconfigs.Patch(types.MergePatchType, applyConfig)) } type updateStatusOpts struct { @@ -84,7 +61,7 @@ type updateStatusOpts struct { type updateStatusOpt func(*updateStatusOpts) -func withGlanceImage(glanceImage *images.Image) updateStatusOpt { +func withResource(glanceImage *images.Image) updateStatusOpt { return func(opts *updateStatusOpts) { opts.glanceImage = glanceImage } @@ -164,17 +141,17 @@ func createStatusUpdate(ctx context.Context, orcImage *orcv1alpha1.Image, now me } availableCondition := applyconfigv1.Condition(). - WithType(orcv1alpha1.OpenStackConditionAvailable). + WithType(orcv1alpha1.ConditionAvailable). WithObservedGeneration(orcImage.Generation) progressingCondition := applyconfigv1.Condition(). - WithType(orcv1alpha1.OpenStackConditionProgressing). + WithType(orcv1alpha1.ConditionProgressing). WithObservedGeneration(orcImage.Generation) available := false if glanceImage != nil && glanceImage.Status == images.ImageStatusActive { availableCondition. WithStatus(metav1.ConditionTrue). - WithReason(orcv1alpha1.OpenStackConditionReasonSuccess). + WithReason(orcv1alpha1.ConditionReasonSuccess). WithMessage("Glance image is available") available = true } else { @@ -187,12 +164,12 @@ func createStatusUpdate(ctx context.Context, orcImage *orcv1alpha1.Image, now me if available { progressingCondition. WithStatus(metav1.ConditionFalse). - WithReason(orcv1alpha1.OpenStackConditionReasonSuccess). + WithReason(orcv1alpha1.ConditionReasonSuccess). WithMessage(*availableCondition.Message) } else { progressingCondition. WithStatus(metav1.ConditionTrue). - WithReason(orcv1alpha1.OpenStackConditionReasonProgressing) + WithReason(orcv1alpha1.ConditionReasonProgressing) if statusOpts.progressMessage == nil { progressingCondition.WithMessage("Reconciliation is progressing") @@ -210,7 +187,7 @@ func createStatusUpdate(ctx context.Context, orcImage *orcv1alpha1.Image, now me WithMessage(terminalError.Message) } else { progressingCondition. - WithReason(orcv1alpha1.OpenStackConditionReasonTransientError). + WithReason(orcv1alpha1.ConditionReasonTransientError). WithMessage(err.Error()) } } @@ -226,7 +203,7 @@ func createStatusUpdate(ctx context.Context, orcImage *orcv1alpha1.Image, now me // This also ensures that we don't generate an update event if nothing has changed for _, condition := range []*applyconfigv1.ConditionApplyConfiguration{availableCondition, progressingCondition} { previous := meta.FindStatusCondition(orcImage.Status.Conditions, *condition.Type) - if previous != nil && ssa.ConditionsEqual(previous, condition) { + if previous != nil && applyconfigs.ConditionsEqual(previous, condition) { condition.WithLastTransitionTime(previous.LastTransitionTime) } else { condition.WithLastTransitionTime(now) @@ -269,5 +246,5 @@ func (r *orcImageReconciler) updateStatus(ctx context.Context, orcImage *orcv1al statusUpdate := createStatusUpdate(ctx, orcImage, now, opts...) - return r.client.Status().Patch(ctx, orcImage, ssa.ApplyConfigPatch(statusUpdate), client.ForceOwnership, ssaFieldOwner(SSAStatusTxn)) + return r.client.Status().Patch(ctx, orcImage, applyconfigs.Patch(types.ApplyPatchType, statusUpdate), client.ForceOwnership, ssaFieldOwner(SSAStatusTxn)) } diff --git a/internal/controllers/image/status_test.go b/internal/controllers/image/status_test.go index 37f1778be..af0f202ad 100644 --- a/internal/controllers/image/status_test.go +++ b/internal/controllers/image/status_test.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Kubernetes Authors. +Copyright 2024 The ORC Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -109,14 +109,14 @@ func Test_orcImageReconciler_updateStatus(t *testing.T) { wantAvailable: func(now metav1.Time) *applyconfigv1.ConditionApplyConfiguration { return applyconfigv1.Condition(). WithStatus(metav1.ConditionFalse). - WithReason(orcv1alpha1.OpenStackConditionReasonProgressing). + WithReason(orcv1alpha1.ConditionReasonProgressing). WithMessage(progressingMsg). WithLastTransitionTime(now) }, wantProgressing: func(now metav1.Time) *applyconfigv1.ConditionApplyConfiguration { return applyconfigv1.Condition(). WithStatus(metav1.ConditionTrue). - WithReason(orcv1alpha1.OpenStackConditionReasonProgressing). + WithReason(orcv1alpha1.ConditionReasonProgressing). WithMessage(progressingMsg). WithLastTransitionTime(now) }, @@ -131,14 +131,14 @@ func Test_orcImageReconciler_updateStatus(t *testing.T) { wantAvailable: func(now metav1.Time) *applyconfigv1.ConditionApplyConfiguration { return applyconfigv1.Condition(). WithStatus(metav1.ConditionFalse). - WithReason(orcv1alpha1.OpenStackConditionReasonTransientError). + WithReason(orcv1alpha1.ConditionReasonTransientError). WithMessage("test-error"). WithLastTransitionTime(now) }, wantProgressing: func(now metav1.Time) *applyconfigv1.ConditionApplyConfiguration { return applyconfigv1.Condition(). WithStatus(metav1.ConditionFalse). - WithReason(orcv1alpha1.OpenStackConditionReasonTransientError). + WithReason(orcv1alpha1.ConditionReasonTransientError). WithMessage("test-error"). WithLastTransitionTime(now) }, @@ -147,20 +147,20 @@ func Test_orcImageReconciler_updateStatus(t *testing.T) { { name: "No image, no status, fatal error", args: args{ - err: orcerrors.Terminal(orcv1alpha1.OpenStackConditionReasonInvalidConfiguration, "invalid configuration", fmt.Errorf("test-error")), + err: orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, "invalid configuration", fmt.Errorf("test-error")), }, wantStatus: orcapplyconfigv1alpha1.ImageStatus, wantAvailable: func(now metav1.Time) *applyconfigv1.ConditionApplyConfiguration { return applyconfigv1.Condition(). WithStatus(metav1.ConditionFalse). - WithReason(orcv1alpha1.OpenStackConditionReasonInvalidConfiguration). + WithReason(orcv1alpha1.ConditionReasonInvalidConfiguration). WithMessage("invalid configuration"). WithLastTransitionTime(now) }, wantProgressing: func(now metav1.Time) *applyconfigv1.ConditionApplyConfiguration { return applyconfigv1.Condition(). WithStatus(metav1.ConditionFalse). - WithReason(orcv1alpha1.OpenStackConditionReasonInvalidConfiguration). + WithReason(orcv1alpha1.ConditionReasonInvalidConfiguration). WithMessage("invalid configuration"). WithLastTransitionTime(now) }, @@ -179,14 +179,14 @@ func Test_orcImageReconciler_updateStatus(t *testing.T) { wantAvailable: func(now metav1.Time) *applyconfigv1.ConditionApplyConfiguration { return applyconfigv1.Condition(). WithStatus(metav1.ConditionFalse). - WithReason(orcv1alpha1.OpenStackConditionReasonProgressing). + WithReason(orcv1alpha1.ConditionReasonProgressing). WithMessage(progressingMsg). WithLastTransitionTime(now) }, wantProgressing: func(now metav1.Time) *applyconfigv1.ConditionApplyConfiguration { return applyconfigv1.Condition(). WithStatus(metav1.ConditionTrue). - WithReason(orcv1alpha1.OpenStackConditionReasonProgressing). + WithReason(orcv1alpha1.ConditionReasonProgressing). WithMessage(progressingMsg). WithLastTransitionTime(now) }, @@ -210,14 +210,14 @@ func Test_orcImageReconciler_updateStatus(t *testing.T) { wantAvailable: func(now metav1.Time) *applyconfigv1.ConditionApplyConfiguration { return applyconfigv1.Condition(). WithStatus(metav1.ConditionTrue). - WithReason(orcv1alpha1.OpenStackConditionReasonSuccess). + WithReason(orcv1alpha1.ConditionReasonSuccess). WithMessage(successMsg). WithLastTransitionTime(now) }, wantProgressing: func(now metav1.Time) *applyconfigv1.ConditionApplyConfiguration { return applyconfigv1.Condition(). WithStatus(metav1.ConditionFalse). - WithReason(orcv1alpha1.OpenStackConditionReasonSuccess). + WithReason(orcv1alpha1.ConditionReasonSuccess). WithMessage(successMsg). WithLastTransitionTime(now) }, @@ -241,14 +241,14 @@ func Test_orcImageReconciler_updateStatus(t *testing.T) { wantAvailable: func(now metav1.Time) *applyconfigv1.ConditionApplyConfiguration { return applyconfigv1.Condition(). WithStatus(metav1.ConditionTrue). - WithReason(orcv1alpha1.OpenStackConditionReasonSuccess). + WithReason(orcv1alpha1.ConditionReasonSuccess). WithMessage(successMsg). WithLastTransitionTime(now) }, wantProgressing: func(now metav1.Time) *applyconfigv1.ConditionApplyConfiguration { return applyconfigv1.Condition(). WithStatus(metav1.ConditionFalse). - WithReason(orcv1alpha1.OpenStackConditionReasonSuccess). + WithReason(orcv1alpha1.ConditionReasonSuccess). WithMessage(successMsg). WithLastTransitionTime(now) }, @@ -263,19 +263,19 @@ func Test_orcImageReconciler_updateStatus(t *testing.T) { orcImage.Status.Conditions = []metav1.Condition{ { - Type: orcv1alpha1.OpenStackConditionAvailable, + Type: orcv1alpha1.ConditionAvailable, Status: metav1.ConditionTrue, ObservedGeneration: 1, LastTransitionTime: hourAgo, - Reason: orcv1alpha1.OpenStackConditionReasonSuccess, + Reason: orcv1alpha1.ConditionReasonSuccess, Message: successMsg, }, { - Type: orcv1alpha1.OpenStackConditionProgressing, + Type: orcv1alpha1.ConditionProgressing, Status: metav1.ConditionFalse, ObservedGeneration: 1, LastTransitionTime: hourAgo, - Reason: orcv1alpha1.OpenStackConditionReasonSuccess, + Reason: orcv1alpha1.ConditionReasonSuccess, Message: successMsg, }, } @@ -296,7 +296,7 @@ func Test_orcImageReconciler_updateStatus(t *testing.T) { hourAgo := metav1.NewTime(now.Add(-time.Hour)) return applyconfigv1.Condition(). WithStatus(metav1.ConditionTrue). - WithReason(orcv1alpha1.OpenStackConditionReasonSuccess). + WithReason(orcv1alpha1.ConditionReasonSuccess). WithMessage(successMsg). WithLastTransitionTime(hourAgo) }, @@ -304,7 +304,7 @@ func Test_orcImageReconciler_updateStatus(t *testing.T) { hourAgo := metav1.NewTime(now.Add(-time.Hour)) return applyconfigv1.Condition(). WithStatus(metav1.ConditionFalse). - WithReason(orcv1alpha1.OpenStackConditionReasonSuccess). + WithReason(orcv1alpha1.ConditionReasonSuccess). WithMessage(successMsg). WithLastTransitionTime(hourAgo) }, @@ -326,10 +326,10 @@ func Test_orcImageReconciler_updateStatus(t *testing.T) { } wantAvailable := tt.wantAvailable(now). - WithType(orcv1alpha1.OpenStackConditionAvailable). + WithType(orcv1alpha1.ConditionAvailable). WithObservedGeneration(1) wantProgressing := tt.wantProgressing(now). - WithType(orcv1alpha1.OpenStackConditionProgressing). + WithType(orcv1alpha1.ConditionProgressing). WithObservedGeneration(1) wantStatusUpdate := orcapplyconfigv1alpha1.Image(orcImage.Name, orcImage.Namespace). WithStatus(tt.wantStatus().WithConditions(wantAvailable, wantProgressing)) @@ -339,7 +339,7 @@ func Test_orcImageReconciler_updateStatus(t *testing.T) { glanceImage = tt.args.glanceImage() } - opts := append(tt.args.opts, withGlanceImage(glanceImage), withError(tt.args.err)) + opts := append(tt.args.opts, withResource(glanceImage), withError(tt.args.err)) // TODO: Consider rewriting to this to test // updateStatus() using fake client when we have diff --git a/internal/controllers/image/suite_test.go b/internal/controllers/image/suite_test.go index ab2e1f500..935f164ad 100644 --- a/internal/controllers/image/suite_test.go +++ b/internal/controllers/image/suite_test.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Kubernetes Authors. +Copyright 2024 The ORC Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/internal/controllers/image/upload.go b/internal/controllers/image/upload.go index 808ca2a73..3739d2942 100644 --- a/internal/controllers/image/upload.go +++ b/internal/controllers/image/upload.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Kubernetes Authors. +Copyright 2024 The ORC Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -62,7 +62,7 @@ func (r *orcImageReconciler) downloadProgressReporter(ctx context.Context, orcIm return func(progress int64) { if time.Now().After(nextUpdate) { msg := fmt.Sprintf("Downloaded %dMB"+ofTotal, int(progress/1024/1024)) - err := r.updateStatus(ctx, orcImage, withGlanceImage(glanceImage), + err := r.updateStatus(ctx, orcImage, withResource(glanceImage), withProgressMessage(downloadingMessage(msg, orcImage))) if err != nil { // Failure to update status here is not fatal @@ -85,7 +85,7 @@ func (r *orcImageReconciler) uploadImageContent(ctx context.Context, orcImage *o download := content.Download if download == nil { // Should have been caught by validation - return orcerrors.Terminal(orcv1alpha1.OpenStackConditionReasonInvalidConfiguration, "image source type URL has no url entry") + return orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, "image source type URL has no url entry") } req, err := http.NewRequestWithContext(ctx, http.MethodGet, download.URL, http.NoBody) @@ -128,7 +128,7 @@ func (r *orcImageReconciler) uploadImageContent(ctx context.Context, orcImage *o } } - err = r.updateStatus(ctx, orcImage, withGlanceImage(glanceImage), + err = r.updateStatus(ctx, orcImage, withResource(glanceImage), withIncrementDownloadAttempts(), withProgressMessage(downloadingMessage("Starting image upload", orcImage))) if err != nil { @@ -138,7 +138,7 @@ func (r *orcImageReconciler) uploadImageContent(ctx context.Context, orcImage *o err = imageClient.UploadData(ctx, glanceImage.ID, reader) if err != nil { if orcerrors.IsInvalidError(err) { - err = orcerrors.Terminal(orcv1alpha1.OpenStackConditionReasonInvalidConfiguration, err.Error(), err) + err = orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, err.Error(), err) } return fmt.Errorf("error writing data to glance: %w", err) } diff --git a/internal/controllers/image/upload_helpers.go b/internal/controllers/image/upload_helpers.go index fb3ac125c..6ef0c34ca 100644 --- a/internal/controllers/image/upload_helpers.go +++ b/internal/controllers/image/upload_helpers.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Kubernetes Authors. +Copyright 2024 The ORC Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -134,6 +134,6 @@ func newReaderWithDecompression(reader io.Reader, compression orcv1alpha1.ImageC return bzip2.NewReader(reader), nil default: msg := fmt.Sprintf("unsupported compression algorithm: %s", compression) - return nil, orcerrors.Terminal(orcv1alpha1.OpenStackConditionReasonInvalidConfiguration, msg) + return nil, orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, msg) } } diff --git a/internal/controllers/image/upload_test.go b/internal/controllers/image/upload_test.go index e6a9a628d..3761b5446 100644 --- a/internal/controllers/image/upload_test.go +++ b/internal/controllers/image/upload_test.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Kubernetes Authors. +Copyright 2024 The ORC Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -168,7 +168,7 @@ var _ = Describe("Upload tests", Ordered, func() { }) mockCtrl = gomock.NewController(GinkgoT()) - scopeFactory = scope.NewMockScopeFactory(mockCtrl, "") + scopeFactory = scope.NewMockScopeFactory(mockCtrl) reconciler = &orcImageReconciler{ client: k8sClient, scopeFactory: scopeFactory, diff --git a/internal/controllers/image/webdownload.go b/internal/controllers/image/webdownload.go index 59fda795e..b30bf4231 100644 --- a/internal/controllers/image/webdownload.go +++ b/internal/controllers/image/webdownload.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Kubernetes Authors. +Copyright 2024 The ORC Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -33,7 +33,7 @@ import ( func requireResource(orcImage *orcv1alpha1.Image) (*orcv1alpha1.ImageResourceSpec, error) { resource := orcImage.Spec.Resource if resource == nil { - return nil, orcerrors.Terminal(orcv1alpha1.OpenStackConditionReasonInvalidConfiguration, "resource not provided") + return nil, orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, "resource not provided") } return resource, nil @@ -45,7 +45,7 @@ func requireResourceContent(orcImage *orcv1alpha1.Image) (*orcv1alpha1.ImageCont return nil, err } if resource.Content == nil { - return nil, orcerrors.Terminal(orcv1alpha1.OpenStackConditionReasonInvalidConfiguration, "resource content not provided") + return nil, orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, "resource content not provided") } return resource.Content, nil } @@ -114,7 +114,7 @@ func (r *orcImageReconciler) webDownload(ctx context.Context, orcImage *orcv1alp resource := orcImage.Spec.Resource if resource == nil { // Should have been caught by validation - return orcerrors.Terminal(orcv1alpha1.OpenStackConditionReasonInvalidConfiguration, "resource not provided") + return orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, "resource not provided") } content, err := requireResourceContent(orcImage) diff --git a/internal/manager/manager.go b/internal/manager/manager.go new file mode 100644 index 000000000..ac62686b3 --- /dev/null +++ b/internal/manager/manager.go @@ -0,0 +1,156 @@ +/* +Copyright 2024. + +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 manager + +import ( + "context" + "crypto/tls" + "fmt" + + // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) + // to ensure that exec-entrypoint and run can make use of them. + _ "k8s.io/client-go/plugin/pkg/client/auth" + "k8s.io/client-go/rest" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/metrics/filters" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + "sigs.k8s.io/controller-runtime/pkg/webhook" + + "github.com/go-logr/logr" + "github.com/k-orc/openstack-resource-controller/internal/controllers/export" + // +kubebuilder:scaffold:imports +) + +type Options struct { + MetricsAddr string + EnableLeaderElection bool + ProbeAddr string + SecureMetrics bool + EnableHTTP2 bool + TLSOpts []func(*tls.Config) + ScopeCacheMaxSize int + WatchNamespaces []string +} + +func Run(ctx context.Context, opts *Options, restConfig *rest.Config, scheme *runtime.Scheme, setupLog, log logr.Logger, controllers []export.Controller) error { + // if the enable-http2 flag is false (the default), http/2 should be disabled + // due to its vulnerabilities. More specifically, disabling http/2 will + // prevent from being vulnerable to the HTTP/2 Stream Cancellation and + // Rapid Reset CVEs. For more information see: + // - https://github.com/advisories/GHSA-qppj-fm5r-hxr3 + // - https://github.com/advisories/GHSA-4374-p667-p6c8 + disableHTTP2 := func(c *tls.Config) { + setupLog.Info("disabling http/2") + c.NextProtos = []string{"http/1.1"} + } + + if !opts.EnableHTTP2 { + opts.TLSOpts = append(opts.TLSOpts, disableHTTP2) + } + + webhookServer := webhook.NewServer(webhook.Options{ + TLSOpts: opts.TLSOpts, + }) + + // Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server. + // More info: + // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.18.4/pkg/metrics/server + // - https://book.kubebuilder.io/reference/metrics.html + metricsServerOptions := metricsserver.Options{ + BindAddress: opts.MetricsAddr, + SecureServing: opts.SecureMetrics, + // TODO(user): TLSOpts is used to allow configuring the TLS config used for the server. If certificates are + // not provided, self-signed certificates will be generated by default. This option is not recommended for + // production environments as self-signed certificates do not offer the same level of trust and security + // as certificates issued by a trusted Certificate Authority (CA). The primary risk is potentially allowing + // unauthorized access to sensitive metrics data. Consider replacing with CertDir, CertName, and KeyName + // to provide certificates, ensuring the server communicates using trusted and secure certificates. + TLSOpts: opts.TLSOpts, + } + + if opts.SecureMetrics { + // FilterProvider is used to protect the metrics endpoint with authn/authz. + // These configurations ensure that only authorized users and service accounts + // can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info: + // https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.18.4/pkg/metrics/filters#WithAuthenticationAndAuthorization + metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization + } + + var watchNamespaces map[string]cache.Config + if len(opts.WatchNamespaces) > 0 { + watchNamespaces = make(map[string]cache.Config, len(watchNamespaces)) + for i := range opts.WatchNamespaces { + watchNamespaces[opts.WatchNamespaces[i]] = cache.Config{} + } + } + + mgr, err := ctrl.NewManager(restConfig, ctrl.Options{ + Scheme: scheme, + Metrics: metricsServerOptions, + WebhookServer: webhookServer, + HealthProbeBindAddress: opts.ProbeAddr, + Logger: log, + + Cache: cache.Options{ + DefaultNamespaces: watchNamespaces, + }, + + LeaderElection: opts.EnableLeaderElection, + LeaderElectionID: "f35396c5.k-orc.cloud", + // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily + // when the Manager ends. This requires the binary to immediately end when the + // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly + // speeds up voluntary leader transitions as the new leader don't have to wait + // LeaseDuration time first. + // + // In the default scaffold provided, the program ends immediately after + // the manager stops, so would be fine to enable this option. However, + // if you are doing or is intended to do any operation such as perform cleanups + // after the manager stops then its usage might be unsafe. + LeaderElectionReleaseOnCancel: true, + }) + if err != nil { + return fmt.Errorf("unable to start manager: %w", err) + } + + // +kubebuilder:scaffold:builder + + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { + return fmt.Errorf("unable to set up health check: %w", err) + } + if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { + return fmt.Errorf("unable to set up ready check: %w", err) + } + + for _, c := range controllers { + if err := c.SetupWithManager(ctx, mgr, controller.Options{}); err != nil { + return fmt.Errorf("unable to create %s controller: %w", c.GetName(), err) + } + } + + setupLog.Info("starting manager") + if err := mgr.Start(ctx); err != nil { + return fmt.Errorf("problem running manager: %w", err) + } + + return nil +} diff --git a/internal/osclients/compute.go b/internal/osclients/compute.go index 903258f15..bb009724e 100644 --- a/internal/osclients/compute.go +++ b/internal/osclients/compute.go @@ -1,5 +1,5 @@ /* -Copyright 2021 The Kubernetes Authors. +Copyright 2021 The ORC Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -27,8 +27,8 @@ import ( "github.com/gophercloud/gophercloud/v2/openstack/compute/v2/flavors" "github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servergroups" "github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers" + "github.com/gophercloud/gophercloud/v2/pagination" "github.com/gophercloud/utils/v2/openstack/clientconfig" - uflavors "github.com/gophercloud/utils/v2/openstack/compute/v2/flavors" ) /* @@ -46,11 +46,15 @@ const NovaMinimumMicroversion = "2.60" type ComputeClient interface { ListAvailabilityZones() ([]availabilityzones.AvailabilityZone, error) - GetFlavorFromName(flavor string) (*flavors.Flavor, error) - CreateServer(createOpts servers.CreateOptsBuilder, schedulerHints servers.SchedulerHintOptsBuilder) (*servers.Server, error) - DeleteServer(serverID string) error - GetServer(serverID string) (*servers.Server, error) - ListServers(listOpts servers.ListOptsBuilder) ([]servers.Server, error) + CreateFlavor(ctx context.Context, opts flavors.CreateOptsBuilder) (*flavors.Flavor, error) + GetFlavor(ctx context.Context, id string) (*flavors.Flavor, error) + DeleteFlavor(ctx context.Context, id string) error + ListFlavors(ctx context.Context, listOpts flavors.ListOptsBuilder) <-chan (Result[*flavors.Flavor]) + + CreateServer(ctx context.Context, createOpts servers.CreateOptsBuilder, schedulerHints servers.SchedulerHintOptsBuilder) (*servers.Server, error) + DeleteServer(ctx context.Context, serverID string) error + GetServer(ctx context.Context, serverID string) (*servers.Server, error) + ListServers(ctx context.Context, listOpts servers.ListOptsBuilder) <-chan (Result[*servers.Server]) ListAttachedInterfaces(serverID string) ([]attachinterfaces.Interface, error) DeleteAttachedInterface(serverID, portID string) error @@ -82,40 +86,78 @@ func (c computeClient) ListAvailabilityZones() ([]availabilityzones.Availability return availabilityzones.ExtractAvailabilityZones(allPages) } -func (c computeClient) GetFlavorFromName(flavor string) (*flavors.Flavor, error) { - flavorID, err := uflavors.IDFromName(context.TODO(), c.client, flavor) - if err != nil { - return nil, err - } - - return flavors.Get(context.TODO(), c.client, flavorID).Extract() -} - -func (c computeClient) CreateServer(createOpts servers.CreateOptsBuilder, schedulerHints servers.SchedulerHintOptsBuilder) (*servers.Server, error) { - return servers.Create(context.TODO(), c.client, createOpts, schedulerHints).Extract() -} - -func (c computeClient) DeleteServer(serverID string) error { - return servers.Delete(context.TODO(), c.client, serverID).ExtractErr() -} - -func (c computeClient) GetServer(serverID string) (*servers.Server, error) { - var server servers.Server - err := servers.Get(context.TODO(), c.client, serverID).ExtractInto(&server) - if err != nil { - return nil, err - } - return &server, nil -} - -func (c computeClient) ListServers(listOpts servers.ListOptsBuilder) ([]servers.Server, error) { - var serverList []servers.Server - allPages, err := servers.List(c.client, listOpts).AllPages(context.TODO()) - if err != nil { - return nil, err - } - err = servers.ExtractServersInto(allPages, &serverList) - return serverList, err +func (c computeClient) GetFlavor(ctx context.Context, id string) (*flavors.Flavor, error) { + return flavors.Get(ctx, c.client, id).Extract() +} + +func (c computeClient) CreateFlavor(ctx context.Context, opts flavors.CreateOptsBuilder) (*flavors.Flavor, error) { + return flavors.Create(ctx, c.client, opts).Extract() +} + +func (c computeClient) DeleteFlavor(ctx context.Context, id string) error { + return flavors.Delete(ctx, c.client, id).ExtractErr() +} + +func (c computeClient) ListFlavors(ctx context.Context, opts flavors.ListOptsBuilder) <-chan (Result[*flavors.Flavor]) { + ch := make(chan (Result[*flavors.Flavor])) + go func() { + defer close(ch) + if err := flavors.ListDetail(c.client, opts).EachPage(ctx, func(ctx context.Context, page pagination.Page) (bool, error) { + pageFlavors, err := flavors.ExtractFlavors(page) + if err != nil { + return false, err + } + for i := range pageFlavors { + select { + case <-ctx.Done(): + return false, ctx.Err() + default: + ch <- NewResultOk(&pageFlavors[i]) + } + } + return true, nil + }); err != nil { + ch <- NewResultErr[*flavors.Flavor](err) + } + }() + return ch +} + +func (c computeClient) CreateServer(ctx context.Context, createOpts servers.CreateOptsBuilder, schedulerHints servers.SchedulerHintOptsBuilder) (*servers.Server, error) { + return servers.Create(ctx, c.client, createOpts, schedulerHints).Extract() +} + +func (c computeClient) DeleteServer(ctx context.Context, serverID string) error { + return servers.Delete(ctx, c.client, serverID).ExtractErr() +} + +func (c computeClient) GetServer(ctx context.Context, serverID string) (*servers.Server, error) { + return servers.Get(ctx, c.client, serverID).Extract() +} + +func (c computeClient) ListServers(ctx context.Context, opts servers.ListOptsBuilder) <-chan (Result[*servers.Server]) { + ch := make(chan (Result[*servers.Server])) + go func() { + defer close(ch) + if err := servers.List(c.client, opts).EachPage(ctx, func(ctx context.Context, page pagination.Page) (bool, error) { + allItems, err := servers.ExtractServers(page) + if err != nil { + return false, err + } + for i := range allItems { + select { + case <-ctx.Done(): + return false, ctx.Err() + default: + ch <- NewResultOk(&allItems[i]) + } + } + return true, nil + }); err != nil { + ch <- NewResultErr[*servers.Server](err) + } + }() + return ch } func (c computeClient) ListAttachedInterfaces(serverID string) ([]attachinterfaces.Interface, error) { @@ -145,29 +187,47 @@ type computeErrorClient struct{ error } func NewComputeErrorClient(e error) ComputeClient { return computeErrorClient{e} } - -func (e computeErrorClient) ListAvailabilityZones() ([]availabilityzones.AvailabilityZone, error) { +func (e computeErrorClient) CreateFlavor(ctx context.Context, opts flavors.CreateOptsBuilder) (*flavors.Flavor, error) { return nil, e.error } +func (e computeErrorClient) GetFlavor(ctx context.Context, id string) (*flavors.Flavor, error) { + return nil, e.error +} +func (e computeErrorClient) DeleteFlavor(ctx context.Context, id string) error { + return e.error +} +func (e computeErrorClient) ListFlavors(ctx context.Context, listOpts flavors.ListOptsBuilder) <-chan (Result[*flavors.Flavor]) { + ch := make(chan (Result[*flavors.Flavor])) + go func() { + defer close(ch) + ch <- NewResultErr[*flavors.Flavor](e.error) + }() + return ch +} -func (e computeErrorClient) GetFlavorFromName(_ string) (*flavors.Flavor, error) { +func (e computeErrorClient) ListAvailabilityZones() ([]availabilityzones.AvailabilityZone, error) { return nil, e.error } -func (e computeErrorClient) CreateServer(_ servers.CreateOptsBuilder, _ servers.SchedulerHintOptsBuilder) (*servers.Server, error) { +func (e computeErrorClient) CreateServer(_ context.Context, _ servers.CreateOptsBuilder, _ servers.SchedulerHintOptsBuilder) (*servers.Server, error) { return nil, e.error } -func (e computeErrorClient) DeleteServer(_ string) error { +func (e computeErrorClient) DeleteServer(_ context.Context, _ string) error { return e.error } -func (e computeErrorClient) GetServer(_ string) (*servers.Server, error) { +func (e computeErrorClient) GetServer(_ context.Context, _ string) (*servers.Server, error) { return nil, e.error } -func (e computeErrorClient) ListServers(_ servers.ListOptsBuilder) ([]servers.Server, error) { - return nil, e.error +func (e computeErrorClient) ListServers(ctx context.Context, listOpts servers.ListOptsBuilder) <-chan (Result[*servers.Server]) { + ch := make(chan (Result[*servers.Server])) + go func() { + defer close(ch) + ch <- NewResultErr[*servers.Server](e.error) + }() + return ch } func (e computeErrorClient) ListAttachedInterfaces(_ string) ([]attachinterfaces.Interface, error) { diff --git a/internal/osclients/filter.go b/internal/osclients/filter.go new file mode 100644 index 000000000..7cc46e16b --- /dev/null +++ b/internal/osclients/filter.go @@ -0,0 +1,59 @@ +package osclients + +type result[T any] struct { + ok T + err error +} + +func (r result[T]) Ok() T { return r.ok } +func (r result[T]) Err() error { return r.err } + +// Result carries either a result or a non-nil error. +type Result[T any] interface { + Ok() T + Err() error +} + +func NewResultOk[T any](ok T) result[T] { + return result[T]{ok: ok} +} + +func NewResultErr[T any](err error) result[T] { + return result[T]{err: err} +} + +func Filter[T any, R Result[T]](in <-chan R, filters ...func(T) bool) <-chan R { + out := make(chan (R)) + go func() { + defer close(out) + next: + for result := range in { + if err := result.Err(); err != nil { + out <- result + continue + } + for _, filter := range filters { + if !filter(result.Ok()) { + continue next + } + } + out <- result + } + }() + return out +} + +func JustOne[T any, R Result[*T]](in <-chan R, duplicateError error) (*T, error) { + var found *T + for result := range in { + if err := result.Err(); err != nil { + return nil, err + } + if found != nil { + return nil, duplicateError + } + ok := result.Ok() + found = ok + } + return found, nil +} diff --git a/internal/osclients/filter_test.go b/internal/osclients/filter_test.go new file mode 100644 index 000000000..7c3637551 --- /dev/null +++ b/internal/osclients/filter_test.go @@ -0,0 +1,165 @@ +package osclients_test + +import ( + "context" + "fmt" + "testing" + + "github.com/k-orc/openstack-resource-controller/internal/osclients" +) + +func pack[T any](v ...T) []T { return v } + +func TestFilter(t *testing.T) { + checks := pack[func([]osclients.Result[int]) error] + filters := pack[func(int) bool] + + noError := func(results []osclients.Result[int]) error { + for _, res := range results { + if err := res.Err(); err != nil { + return fmt.Errorf("unexpected error: %w", err) + } + } + return nil + } + + hasValues := func(want ...int) func([]osclients.Result[int]) error { + return func(have []osclients.Result[int]) error { + if len(have) != len(want) { + return fmt.Errorf("expected %d results, got %d: %v", len(want), len(have), have) + } + for i := range want { + if want[i] != have[i].Ok() { + return fmt.Errorf("expected element %d to be %d, got %d", i, want[i], have[i].Ok()) + } + } + return nil + } + } + + iterator := func(ctx context.Context) <-chan osclients.Result[int] { + ch := make(chan (osclients.Result[int])) + go func() { + defer close(ch) + for i := 0; true; i++ { + if i > 9 { + return + } + select { + case <-ctx.Done(): + ch <- osclients.NewResultErr[int](ctx.Err()) + return + case ch <- osclients.NewResultOk(i): + } + } + }() + return ch + } + + filterEq := func(want int) func(int) bool { + return func(have int) bool { + return want == have + } + } + filterLT := func(maxN int) func(int) bool { + return func(have int) bool { + return have < maxN + } + } + filterGT := func(minN int) func(int) bool { + return func(have int) bool { + return have > minN + } + } + + for _, tc := range [...]struct { + name string + filters []func(int) bool + checks []func([]osclients.Result[int]) error + }{ + { + "returns all", + filters(), + checks(noError, hasValues(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)), + }, + { + "returns one", + filters(filterEq(5)), + checks(noError, hasValues(5)), + }, + { + "returns multiple", + filters(filterLT(5)), + checks(noError, hasValues(0, 1, 2, 3, 4)), + }, + { + "applies multiple filters", + filters(filterLT(5), filterGT(2)), + checks(noError, hasValues(3, 4)), + }, + { + "returns none", + filters(filterLT(2), filterGT(5)), + checks(noError, hasValues()), + }, + } { + t.Run(tc.name, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var results []osclients.Result[int] + for res := range osclients.Filter(iterator(ctx), tc.filters...) { + results = append(results, res) + } + + for _, check := range tc.checks { + if e := check(results); e != nil { + t.Error(e) + } + } + }) + } + + erroredIterator := func(ctx context.Context) <-chan osclients.Result[int] { + ch := make(chan (osclients.Result[int])) + go func() { + defer close(ch) + for i := 0; true; i++ { + if i >= 150 { + return + } + + if i == 123 { + ch <- osclients.NewResultErr[int](fmt.Errorf("test error")) + continue + } + select { + case <-ctx.Done(): + ch <- osclients.NewResultErr[int](ctx.Err()) + return + case ch <- osclients.NewResultOk(i): + } + } + }() + return ch + } + + t.Run("passes errors", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var i int + for res := range osclients.Filter(erroredIterator(ctx)) { + if i == 123 { + if res.Err() == nil { + t.Errorf("expected error, got nil") + } + } + i++ + } + + if i != 150 { + t.Errorf("expected 150 values, got %d", i) + } + }) +} diff --git a/internal/osclients/image.go b/internal/osclients/image.go index a3054f647..cff326ec6 100644 --- a/internal/osclients/image.go +++ b/internal/osclients/image.go @@ -1,5 +1,5 @@ /* -Copyright 2021 The Kubernetes Authors. +Copyright 2021 The ORC Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/internal/osclients/loadbalancer.go b/internal/osclients/loadbalancer.go index add9b0ef8..882b2eda3 100644 --- a/internal/osclients/loadbalancer.go +++ b/internal/osclients/loadbalancer.go @@ -1,5 +1,5 @@ /* -Copyright 2022 The Kubernetes Authors. +Copyright 2022 The ORC Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/internal/osclients/mock/compute.go b/internal/osclients/mock/compute.go index a444a79aa..d8c8422a1 100644 --- a/internal/osclients/mock/compute.go +++ b/internal/osclients/mock/compute.go @@ -25,6 +25,7 @@ limitations under the License. package mock import ( + context "context" reflect "reflect" attachinterfaces "github.com/gophercloud/gophercloud/v2/openstack/compute/v2/attachinterfaces" @@ -32,6 +33,7 @@ import ( flavors "github.com/gophercloud/gophercloud/v2/openstack/compute/v2/flavors" servergroups "github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servergroups" servers "github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers" + osclients "github.com/k-orc/openstack-resource-controller/internal/osclients" gomock "go.uber.org/mock/gomock" ) @@ -58,19 +60,34 @@ func (m *MockComputeClient) EXPECT() *MockComputeClientMockRecorder { return m.recorder } +// CreateFlavor mocks base method. +func (m *MockComputeClient) CreateFlavor(ctx context.Context, opts flavors.CreateOptsBuilder) (*flavors.Flavor, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateFlavor", ctx, opts) + ret0, _ := ret[0].(*flavors.Flavor) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateFlavor indicates an expected call of CreateFlavor. +func (mr *MockComputeClientMockRecorder) CreateFlavor(ctx, opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateFlavor", reflect.TypeOf((*MockComputeClient)(nil).CreateFlavor), ctx, opts) +} + // CreateServer mocks base method. -func (m *MockComputeClient) CreateServer(createOpts servers.CreateOptsBuilder, schedulerHints servers.SchedulerHintOptsBuilder) (*servers.Server, error) { +func (m *MockComputeClient) CreateServer(ctx context.Context, createOpts servers.CreateOptsBuilder, schedulerHints servers.SchedulerHintOptsBuilder) (*servers.Server, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateServer", createOpts, schedulerHints) + ret := m.ctrl.Call(m, "CreateServer", ctx, createOpts, schedulerHints) ret0, _ := ret[0].(*servers.Server) ret1, _ := ret[1].(error) return ret0, ret1 } // CreateServer indicates an expected call of CreateServer. -func (mr *MockComputeClientMockRecorder) CreateServer(createOpts, schedulerHints any) *gomock.Call { +func (mr *MockComputeClientMockRecorder) CreateServer(ctx, createOpts, schedulerHints any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateServer", reflect.TypeOf((*MockComputeClient)(nil).CreateServer), createOpts, schedulerHints) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateServer", reflect.TypeOf((*MockComputeClient)(nil).CreateServer), ctx, createOpts, schedulerHints) } // DeleteAttachedInterface mocks base method. @@ -87,48 +104,62 @@ func (mr *MockComputeClientMockRecorder) DeleteAttachedInterface(serverID, portI return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAttachedInterface", reflect.TypeOf((*MockComputeClient)(nil).DeleteAttachedInterface), serverID, portID) } +// DeleteFlavor mocks base method. +func (m *MockComputeClient) DeleteFlavor(ctx context.Context, id string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteFlavor", ctx, id) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteFlavor indicates an expected call of DeleteFlavor. +func (mr *MockComputeClientMockRecorder) DeleteFlavor(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteFlavor", reflect.TypeOf((*MockComputeClient)(nil).DeleteFlavor), ctx, id) +} + // DeleteServer mocks base method. -func (m *MockComputeClient) DeleteServer(serverID string) error { +func (m *MockComputeClient) DeleteServer(ctx context.Context, serverID string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteServer", serverID) + ret := m.ctrl.Call(m, "DeleteServer", ctx, serverID) ret0, _ := ret[0].(error) return ret0 } // DeleteServer indicates an expected call of DeleteServer. -func (mr *MockComputeClientMockRecorder) DeleteServer(serverID any) *gomock.Call { +func (mr *MockComputeClientMockRecorder) DeleteServer(ctx, serverID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteServer", reflect.TypeOf((*MockComputeClient)(nil).DeleteServer), serverID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteServer", reflect.TypeOf((*MockComputeClient)(nil).DeleteServer), ctx, serverID) } -// GetFlavorFromName mocks base method. -func (m *MockComputeClient) GetFlavorFromName(flavor string) (*flavors.Flavor, error) { +// GetFlavor mocks base method. +func (m *MockComputeClient) GetFlavor(ctx context.Context, id string) (*flavors.Flavor, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetFlavorFromName", flavor) + ret := m.ctrl.Call(m, "GetFlavor", ctx, id) ret0, _ := ret[0].(*flavors.Flavor) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetFlavorFromName indicates an expected call of GetFlavorFromName. -func (mr *MockComputeClientMockRecorder) GetFlavorFromName(flavor any) *gomock.Call { +// GetFlavor indicates an expected call of GetFlavor. +func (mr *MockComputeClientMockRecorder) GetFlavor(ctx, id any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFlavorFromName", reflect.TypeOf((*MockComputeClient)(nil).GetFlavorFromName), flavor) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFlavor", reflect.TypeOf((*MockComputeClient)(nil).GetFlavor), ctx, id) } // GetServer mocks base method. -func (m *MockComputeClient) GetServer(serverID string) (*servers.Server, error) { +func (m *MockComputeClient) GetServer(ctx context.Context, serverID string) (*servers.Server, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetServer", serverID) + ret := m.ctrl.Call(m, "GetServer", ctx, serverID) ret0, _ := ret[0].(*servers.Server) ret1, _ := ret[1].(error) return ret0, ret1 } // GetServer indicates an expected call of GetServer. -func (mr *MockComputeClientMockRecorder) GetServer(serverID any) *gomock.Call { +func (mr *MockComputeClientMockRecorder) GetServer(ctx, serverID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServer", reflect.TypeOf((*MockComputeClient)(nil).GetServer), serverID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServer", reflect.TypeOf((*MockComputeClient)(nil).GetServer), ctx, serverID) } // ListAttachedInterfaces mocks base method. @@ -161,6 +192,20 @@ func (mr *MockComputeClientMockRecorder) ListAvailabilityZones() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAvailabilityZones", reflect.TypeOf((*MockComputeClient)(nil).ListAvailabilityZones)) } +// ListFlavors mocks base method. +func (m *MockComputeClient) ListFlavors(ctx context.Context, listOpts flavors.ListOptsBuilder) <-chan osclients.Result[*flavors.Flavor] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListFlavors", ctx, listOpts) + ret0, _ := ret[0].(<-chan osclients.Result[*flavors.Flavor]) + return ret0 +} + +// ListFlavors indicates an expected call of ListFlavors. +func (mr *MockComputeClientMockRecorder) ListFlavors(ctx, listOpts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListFlavors", reflect.TypeOf((*MockComputeClient)(nil).ListFlavors), ctx, listOpts) +} + // ListServerGroups mocks base method. func (m *MockComputeClient) ListServerGroups() ([]servergroups.ServerGroup, error) { m.ctrl.T.Helper() @@ -177,16 +222,15 @@ func (mr *MockComputeClientMockRecorder) ListServerGroups() *gomock.Call { } // ListServers mocks base method. -func (m *MockComputeClient) ListServers(listOpts servers.ListOptsBuilder) ([]servers.Server, error) { +func (m *MockComputeClient) ListServers(ctx context.Context, listOpts servers.ListOptsBuilder) <-chan osclients.Result[*servers.Server] { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListServers", listOpts) - ret0, _ := ret[0].([]servers.Server) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret := m.ctrl.Call(m, "ListServers", ctx, listOpts) + ret0, _ := ret[0].(<-chan osclients.Result[*servers.Server]) + return ret0 } // ListServers indicates an expected call of ListServers. -func (mr *MockComputeClientMockRecorder) ListServers(listOpts any) *gomock.Call { +func (mr *MockComputeClientMockRecorder) ListServers(ctx, listOpts any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListServers", reflect.TypeOf((*MockComputeClient)(nil).ListServers), listOpts) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListServers", reflect.TypeOf((*MockComputeClient)(nil).ListServers), ctx, listOpts) } diff --git a/internal/osclients/mock/doc.go b/internal/osclients/mock/doc.go index 3c1d2a0fe..076352333 100644 --- a/internal/osclients/mock/doc.go +++ b/internal/osclients/mock/doc.go @@ -1,5 +1,5 @@ /* -Copyright 2021 The Kubernetes Authors. +Copyright 2021 The ORC Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/internal/osclients/mock/networking.go b/internal/osclients/mock/networking.go index 0d4cff5b3..460380476 100644 --- a/internal/osclients/mock/networking.go +++ b/internal/osclients/mock/networking.go @@ -25,6 +25,7 @@ limitations under the License. package mock import ( + context "context" reflect "reflect" extensions "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions" @@ -37,6 +38,7 @@ import ( networks "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/networks" ports "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports" subnets "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/subnets" + pagination "github.com/gophercloud/gophercloud/v2/pagination" gomock "go.uber.org/mock/gomock" ) @@ -64,18 +66,18 @@ func (m *MockNetworkClient) EXPECT() *MockNetworkClientMockRecorder { } // AddRouterInterface mocks base method. -func (m *MockNetworkClient) AddRouterInterface(id string, opts routers.AddInterfaceOptsBuilder) (*routers.InterfaceInfo, error) { +func (m *MockNetworkClient) AddRouterInterface(ctx context.Context, id string, opts routers.AddInterfaceOptsBuilder) (*routers.InterfaceInfo, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AddRouterInterface", id, opts) + ret := m.ctrl.Call(m, "AddRouterInterface", ctx, id, opts) ret0, _ := ret[0].(*routers.InterfaceInfo) ret1, _ := ret[1].(error) return ret0, ret1 } // AddRouterInterface indicates an expected call of AddRouterInterface. -func (mr *MockNetworkClientMockRecorder) AddRouterInterface(id, opts any) *gomock.Call { +func (mr *MockNetworkClientMockRecorder) AddRouterInterface(ctx, id, opts any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddRouterInterface", reflect.TypeOf((*MockNetworkClient)(nil).AddRouterInterface), id, opts) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddRouterInterface", reflect.TypeOf((*MockNetworkClient)(nil).AddRouterInterface), ctx, id, opts) } // CreateFloatingIP mocks base method. @@ -94,93 +96,92 @@ func (mr *MockNetworkClientMockRecorder) CreateFloatingIP(opts any) *gomock.Call } // CreateNetwork mocks base method. -func (m *MockNetworkClient) CreateNetwork(opts networks.CreateOptsBuilder) (*networks.Network, error) { +func (m *MockNetworkClient) CreateNetwork(ctx context.Context, opts networks.CreateOptsBuilder) networks.CreateResult { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateNetwork", opts) - ret0, _ := ret[0].(*networks.Network) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret := m.ctrl.Call(m, "CreateNetwork", ctx, opts) + ret0, _ := ret[0].(networks.CreateResult) + return ret0 } // CreateNetwork indicates an expected call of CreateNetwork. -func (mr *MockNetworkClientMockRecorder) CreateNetwork(opts any) *gomock.Call { +func (mr *MockNetworkClientMockRecorder) CreateNetwork(ctx, opts any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateNetwork", reflect.TypeOf((*MockNetworkClient)(nil).CreateNetwork), opts) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateNetwork", reflect.TypeOf((*MockNetworkClient)(nil).CreateNetwork), ctx, opts) } // CreatePort mocks base method. -func (m *MockNetworkClient) CreatePort(opts ports.CreateOptsBuilder) (*ports.Port, error) { +func (m *MockNetworkClient) CreatePort(ctx context.Context, opts ports.CreateOptsBuilder) (*ports.Port, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreatePort", opts) + ret := m.ctrl.Call(m, "CreatePort", ctx, opts) ret0, _ := ret[0].(*ports.Port) ret1, _ := ret[1].(error) return ret0, ret1 } // CreatePort indicates an expected call of CreatePort. -func (mr *MockNetworkClientMockRecorder) CreatePort(opts any) *gomock.Call { +func (mr *MockNetworkClientMockRecorder) CreatePort(ctx, opts any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePort", reflect.TypeOf((*MockNetworkClient)(nil).CreatePort), opts) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePort", reflect.TypeOf((*MockNetworkClient)(nil).CreatePort), ctx, opts) } // CreateRouter mocks base method. -func (m *MockNetworkClient) CreateRouter(opts routers.CreateOptsBuilder) (*routers.Router, error) { +func (m *MockNetworkClient) CreateRouter(ctx context.Context, opts routers.CreateOptsBuilder) (*routers.Router, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateRouter", opts) + ret := m.ctrl.Call(m, "CreateRouter", ctx, opts) ret0, _ := ret[0].(*routers.Router) ret1, _ := ret[1].(error) return ret0, ret1 } // CreateRouter indicates an expected call of CreateRouter. -func (mr *MockNetworkClientMockRecorder) CreateRouter(opts any) *gomock.Call { +func (mr *MockNetworkClientMockRecorder) CreateRouter(ctx, opts any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateRouter", reflect.TypeOf((*MockNetworkClient)(nil).CreateRouter), opts) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateRouter", reflect.TypeOf((*MockNetworkClient)(nil).CreateRouter), ctx, opts) } // CreateSecGroup mocks base method. -func (m *MockNetworkClient) CreateSecGroup(opts groups.CreateOptsBuilder) (*groups.SecGroup, error) { +func (m *MockNetworkClient) CreateSecGroup(ctx context.Context, opts groups.CreateOptsBuilder) (*groups.SecGroup, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateSecGroup", opts) + ret := m.ctrl.Call(m, "CreateSecGroup", ctx, opts) ret0, _ := ret[0].(*groups.SecGroup) ret1, _ := ret[1].(error) return ret0, ret1 } // CreateSecGroup indicates an expected call of CreateSecGroup. -func (mr *MockNetworkClientMockRecorder) CreateSecGroup(opts any) *gomock.Call { +func (mr *MockNetworkClientMockRecorder) CreateSecGroup(ctx, opts any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSecGroup", reflect.TypeOf((*MockNetworkClient)(nil).CreateSecGroup), opts) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSecGroup", reflect.TypeOf((*MockNetworkClient)(nil).CreateSecGroup), ctx, opts) } -// CreateSecGroupRule mocks base method. -func (m *MockNetworkClient) CreateSecGroupRule(opts rules.CreateOptsBuilder) (*rules.SecGroupRule, error) { +// CreateSecGroupRules mocks base method. +func (m *MockNetworkClient) CreateSecGroupRules(ctx context.Context, opts []rules.CreateOpts) ([]rules.SecGroupRule, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateSecGroupRule", opts) - ret0, _ := ret[0].(*rules.SecGroupRule) + ret := m.ctrl.Call(m, "CreateSecGroupRules", ctx, opts) + ret0, _ := ret[0].([]rules.SecGroupRule) ret1, _ := ret[1].(error) return ret0, ret1 } -// CreateSecGroupRule indicates an expected call of CreateSecGroupRule. -func (mr *MockNetworkClientMockRecorder) CreateSecGroupRule(opts any) *gomock.Call { +// CreateSecGroupRules indicates an expected call of CreateSecGroupRules. +func (mr *MockNetworkClientMockRecorder) CreateSecGroupRules(ctx, opts any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSecGroupRule", reflect.TypeOf((*MockNetworkClient)(nil).CreateSecGroupRule), opts) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSecGroupRules", reflect.TypeOf((*MockNetworkClient)(nil).CreateSecGroupRules), ctx, opts) } // CreateSubnet mocks base method. -func (m *MockNetworkClient) CreateSubnet(opts subnets.CreateOptsBuilder) (*subnets.Subnet, error) { +func (m *MockNetworkClient) CreateSubnet(ctx context.Context, opts subnets.CreateOptsBuilder) (*subnets.Subnet, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateSubnet", opts) + ret := m.ctrl.Call(m, "CreateSubnet", ctx, opts) ret0, _ := ret[0].(*subnets.Subnet) ret1, _ := ret[1].(error) return ret0, ret1 } // CreateSubnet indicates an expected call of CreateSubnet. -func (mr *MockNetworkClientMockRecorder) CreateSubnet(opts any) *gomock.Call { +func (mr *MockNetworkClientMockRecorder) CreateSubnet(ctx, opts any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSubnet", reflect.TypeOf((*MockNetworkClient)(nil).CreateSubnet), opts) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSubnet", reflect.TypeOf((*MockNetworkClient)(nil).CreateSubnet), ctx, opts) } // CreateTrunk mocks base method. @@ -213,87 +214,87 @@ func (mr *MockNetworkClientMockRecorder) DeleteFloatingIP(id any) *gomock.Call { } // DeleteNetwork mocks base method. -func (m *MockNetworkClient) DeleteNetwork(id string) error { +func (m *MockNetworkClient) DeleteNetwork(ctx context.Context, id string) networks.DeleteResult { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteNetwork", id) - ret0, _ := ret[0].(error) + ret := m.ctrl.Call(m, "DeleteNetwork", ctx, id) + ret0, _ := ret[0].(networks.DeleteResult) return ret0 } // DeleteNetwork indicates an expected call of DeleteNetwork. -func (mr *MockNetworkClientMockRecorder) DeleteNetwork(id any) *gomock.Call { +func (mr *MockNetworkClientMockRecorder) DeleteNetwork(ctx, id any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteNetwork", reflect.TypeOf((*MockNetworkClient)(nil).DeleteNetwork), id) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteNetwork", reflect.TypeOf((*MockNetworkClient)(nil).DeleteNetwork), ctx, id) } // DeletePort mocks base method. -func (m *MockNetworkClient) DeletePort(id string) error { +func (m *MockNetworkClient) DeletePort(ctx context.Context, id string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeletePort", id) + ret := m.ctrl.Call(m, "DeletePort", ctx, id) ret0, _ := ret[0].(error) return ret0 } // DeletePort indicates an expected call of DeletePort. -func (mr *MockNetworkClientMockRecorder) DeletePort(id any) *gomock.Call { +func (mr *MockNetworkClientMockRecorder) DeletePort(ctx, id any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePort", reflect.TypeOf((*MockNetworkClient)(nil).DeletePort), id) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePort", reflect.TypeOf((*MockNetworkClient)(nil).DeletePort), ctx, id) } // DeleteRouter mocks base method. -func (m *MockNetworkClient) DeleteRouter(id string) error { +func (m *MockNetworkClient) DeleteRouter(ctx context.Context, id string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteRouter", id) + ret := m.ctrl.Call(m, "DeleteRouter", ctx, id) ret0, _ := ret[0].(error) return ret0 } // DeleteRouter indicates an expected call of DeleteRouter. -func (mr *MockNetworkClientMockRecorder) DeleteRouter(id any) *gomock.Call { +func (mr *MockNetworkClientMockRecorder) DeleteRouter(ctx, id any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteRouter", reflect.TypeOf((*MockNetworkClient)(nil).DeleteRouter), id) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteRouter", reflect.TypeOf((*MockNetworkClient)(nil).DeleteRouter), ctx, id) } // DeleteSecGroup mocks base method. -func (m *MockNetworkClient) DeleteSecGroup(id string) error { +func (m *MockNetworkClient) DeleteSecGroup(ctx context.Context, id string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteSecGroup", id) + ret := m.ctrl.Call(m, "DeleteSecGroup", ctx, id) ret0, _ := ret[0].(error) return ret0 } // DeleteSecGroup indicates an expected call of DeleteSecGroup. -func (mr *MockNetworkClientMockRecorder) DeleteSecGroup(id any) *gomock.Call { +func (mr *MockNetworkClientMockRecorder) DeleteSecGroup(ctx, id any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSecGroup", reflect.TypeOf((*MockNetworkClient)(nil).DeleteSecGroup), id) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSecGroup", reflect.TypeOf((*MockNetworkClient)(nil).DeleteSecGroup), ctx, id) } // DeleteSecGroupRule mocks base method. -func (m *MockNetworkClient) DeleteSecGroupRule(id string) error { +func (m *MockNetworkClient) DeleteSecGroupRule(ctx context.Context, id string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteSecGroupRule", id) + ret := m.ctrl.Call(m, "DeleteSecGroupRule", ctx, id) ret0, _ := ret[0].(error) return ret0 } // DeleteSecGroupRule indicates an expected call of DeleteSecGroupRule. -func (mr *MockNetworkClientMockRecorder) DeleteSecGroupRule(id any) *gomock.Call { +func (mr *MockNetworkClientMockRecorder) DeleteSecGroupRule(ctx, id any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSecGroupRule", reflect.TypeOf((*MockNetworkClient)(nil).DeleteSecGroupRule), id) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSecGroupRule", reflect.TypeOf((*MockNetworkClient)(nil).DeleteSecGroupRule), ctx, id) } // DeleteSubnet mocks base method. -func (m *MockNetworkClient) DeleteSubnet(id string) error { +func (m *MockNetworkClient) DeleteSubnet(ctx context.Context, id string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteSubnet", id) + ret := m.ctrl.Call(m, "DeleteSubnet", ctx, id) ret0, _ := ret[0].(error) return ret0 } // DeleteSubnet indicates an expected call of DeleteSubnet. -func (mr *MockNetworkClientMockRecorder) DeleteSubnet(id any) *gomock.Call { +func (mr *MockNetworkClientMockRecorder) DeleteSubnet(ctx, id any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSubnet", reflect.TypeOf((*MockNetworkClient)(nil).DeleteSubnet), id) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSubnet", reflect.TypeOf((*MockNetworkClient)(nil).DeleteSubnet), ctx, id) } // DeleteTrunk mocks base method. @@ -326,93 +327,92 @@ func (mr *MockNetworkClientMockRecorder) GetFloatingIP(id any) *gomock.Call { } // GetNetwork mocks base method. -func (m *MockNetworkClient) GetNetwork(id string) (*networks.Network, error) { +func (m *MockNetworkClient) GetNetwork(ctx context.Context, id string) networks.GetResult { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetNetwork", id) - ret0, _ := ret[0].(*networks.Network) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret := m.ctrl.Call(m, "GetNetwork", ctx, id) + ret0, _ := ret[0].(networks.GetResult) + return ret0 } // GetNetwork indicates an expected call of GetNetwork. -func (mr *MockNetworkClientMockRecorder) GetNetwork(id any) *gomock.Call { +func (mr *MockNetworkClientMockRecorder) GetNetwork(ctx, id any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNetwork", reflect.TypeOf((*MockNetworkClient)(nil).GetNetwork), id) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNetwork", reflect.TypeOf((*MockNetworkClient)(nil).GetNetwork), ctx, id) } // GetPort mocks base method. -func (m *MockNetworkClient) GetPort(id string) (*ports.Port, error) { +func (m *MockNetworkClient) GetPort(ctx context.Context, id string) (*ports.Port, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetPort", id) + ret := m.ctrl.Call(m, "GetPort", ctx, id) ret0, _ := ret[0].(*ports.Port) ret1, _ := ret[1].(error) return ret0, ret1 } // GetPort indicates an expected call of GetPort. -func (mr *MockNetworkClientMockRecorder) GetPort(id any) *gomock.Call { +func (mr *MockNetworkClientMockRecorder) GetPort(ctx, id any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPort", reflect.TypeOf((*MockNetworkClient)(nil).GetPort), id) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPort", reflect.TypeOf((*MockNetworkClient)(nil).GetPort), ctx, id) } // GetRouter mocks base method. -func (m *MockNetworkClient) GetRouter(id string) (*routers.Router, error) { +func (m *MockNetworkClient) GetRouter(ctx context.Context, id string) (*routers.Router, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetRouter", id) + ret := m.ctrl.Call(m, "GetRouter", ctx, id) ret0, _ := ret[0].(*routers.Router) ret1, _ := ret[1].(error) return ret0, ret1 } // GetRouter indicates an expected call of GetRouter. -func (mr *MockNetworkClientMockRecorder) GetRouter(id any) *gomock.Call { +func (mr *MockNetworkClientMockRecorder) GetRouter(ctx, id any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRouter", reflect.TypeOf((*MockNetworkClient)(nil).GetRouter), id) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRouter", reflect.TypeOf((*MockNetworkClient)(nil).GetRouter), ctx, id) } // GetSecGroup mocks base method. -func (m *MockNetworkClient) GetSecGroup(id string) (*groups.SecGroup, error) { +func (m *MockNetworkClient) GetSecGroup(ctx context.Context, id string) (*groups.SecGroup, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSecGroup", id) + ret := m.ctrl.Call(m, "GetSecGroup", ctx, id) ret0, _ := ret[0].(*groups.SecGroup) ret1, _ := ret[1].(error) return ret0, ret1 } // GetSecGroup indicates an expected call of GetSecGroup. -func (mr *MockNetworkClientMockRecorder) GetSecGroup(id any) *gomock.Call { +func (mr *MockNetworkClientMockRecorder) GetSecGroup(ctx, id any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSecGroup", reflect.TypeOf((*MockNetworkClient)(nil).GetSecGroup), id) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSecGroup", reflect.TypeOf((*MockNetworkClient)(nil).GetSecGroup), ctx, id) } // GetSecGroupRule mocks base method. -func (m *MockNetworkClient) GetSecGroupRule(id string) (*rules.SecGroupRule, error) { +func (m *MockNetworkClient) GetSecGroupRule(ctx context.Context, id string) (*rules.SecGroupRule, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSecGroupRule", id) + ret := m.ctrl.Call(m, "GetSecGroupRule", ctx, id) ret0, _ := ret[0].(*rules.SecGroupRule) ret1, _ := ret[1].(error) return ret0, ret1 } // GetSecGroupRule indicates an expected call of GetSecGroupRule. -func (mr *MockNetworkClientMockRecorder) GetSecGroupRule(id any) *gomock.Call { +func (mr *MockNetworkClientMockRecorder) GetSecGroupRule(ctx, id any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSecGroupRule", reflect.TypeOf((*MockNetworkClient)(nil).GetSecGroupRule), id) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSecGroupRule", reflect.TypeOf((*MockNetworkClient)(nil).GetSecGroupRule), ctx, id) } // GetSubnet mocks base method. -func (m *MockNetworkClient) GetSubnet(id string) (*subnets.Subnet, error) { +func (m *MockNetworkClient) GetSubnet(ctx context.Context, id string) (*subnets.Subnet, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSubnet", id) + ret := m.ctrl.Call(m, "GetSubnet", ctx, id) ret0, _ := ret[0].(*subnets.Subnet) ret1, _ := ret[1].(error) return ret0, ret1 } // GetSubnet indicates an expected call of GetSubnet. -func (mr *MockNetworkClientMockRecorder) GetSubnet(id any) *gomock.Call { +func (mr *MockNetworkClientMockRecorder) GetSubnet(ctx, id any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubnet", reflect.TypeOf((*MockNetworkClient)(nil).GetSubnet), id) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubnet", reflect.TypeOf((*MockNetworkClient)(nil).GetSubnet), ctx, id) } // ListExtensions mocks base method. @@ -446,12 +446,11 @@ func (mr *MockNetworkClientMockRecorder) ListFloatingIP(opts any) *gomock.Call { } // ListNetwork mocks base method. -func (m *MockNetworkClient) ListNetwork(opts networks.ListOptsBuilder) ([]networks.Network, error) { +func (m *MockNetworkClient) ListNetwork(opts networks.ListOptsBuilder) pagination.Pager { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListNetwork", opts) - ret0, _ := ret[0].([]networks.Network) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret0, _ := ret[0].(pagination.Pager) + return ret0 } // ListNetwork indicates an expected call of ListNetwork. @@ -461,78 +460,78 @@ func (mr *MockNetworkClientMockRecorder) ListNetwork(opts any) *gomock.Call { } // ListPort mocks base method. -func (m *MockNetworkClient) ListPort(opts ports.ListOptsBuilder) ([]ports.Port, error) { +func (m *MockNetworkClient) ListPort(ctx context.Context, opts ports.ListOptsBuilder) ([]ports.Port, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListPort", opts) + ret := m.ctrl.Call(m, "ListPort", ctx, opts) ret0, _ := ret[0].([]ports.Port) ret1, _ := ret[1].(error) return ret0, ret1 } // ListPort indicates an expected call of ListPort. -func (mr *MockNetworkClientMockRecorder) ListPort(opts any) *gomock.Call { +func (mr *MockNetworkClientMockRecorder) ListPort(ctx, opts any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPort", reflect.TypeOf((*MockNetworkClient)(nil).ListPort), opts) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPort", reflect.TypeOf((*MockNetworkClient)(nil).ListPort), ctx, opts) } // ListRouter mocks base method. -func (m *MockNetworkClient) ListRouter(opts routers.ListOpts) ([]routers.Router, error) { +func (m *MockNetworkClient) ListRouter(ctx context.Context, opts routers.ListOpts) ([]routers.Router, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListRouter", opts) + ret := m.ctrl.Call(m, "ListRouter", ctx, opts) ret0, _ := ret[0].([]routers.Router) ret1, _ := ret[1].(error) return ret0, ret1 } // ListRouter indicates an expected call of ListRouter. -func (mr *MockNetworkClientMockRecorder) ListRouter(opts any) *gomock.Call { +func (mr *MockNetworkClientMockRecorder) ListRouter(ctx, opts any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRouter", reflect.TypeOf((*MockNetworkClient)(nil).ListRouter), opts) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRouter", reflect.TypeOf((*MockNetworkClient)(nil).ListRouter), ctx, opts) } // ListSecGroup mocks base method. -func (m *MockNetworkClient) ListSecGroup(opts groups.ListOpts) ([]groups.SecGroup, error) { +func (m *MockNetworkClient) ListSecGroup(ctx context.Context, opts groups.ListOpts) ([]groups.SecGroup, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListSecGroup", opts) + ret := m.ctrl.Call(m, "ListSecGroup", ctx, opts) ret0, _ := ret[0].([]groups.SecGroup) ret1, _ := ret[1].(error) return ret0, ret1 } // ListSecGroup indicates an expected call of ListSecGroup. -func (mr *MockNetworkClientMockRecorder) ListSecGroup(opts any) *gomock.Call { +func (mr *MockNetworkClientMockRecorder) ListSecGroup(ctx, opts any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListSecGroup", reflect.TypeOf((*MockNetworkClient)(nil).ListSecGroup), opts) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListSecGroup", reflect.TypeOf((*MockNetworkClient)(nil).ListSecGroup), ctx, opts) } // ListSecGroupRule mocks base method. -func (m *MockNetworkClient) ListSecGroupRule(opts rules.ListOpts) ([]rules.SecGroupRule, error) { +func (m *MockNetworkClient) ListSecGroupRule(ctx context.Context, opts rules.ListOpts) ([]rules.SecGroupRule, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListSecGroupRule", opts) + ret := m.ctrl.Call(m, "ListSecGroupRule", ctx, opts) ret0, _ := ret[0].([]rules.SecGroupRule) ret1, _ := ret[1].(error) return ret0, ret1 } // ListSecGroupRule indicates an expected call of ListSecGroupRule. -func (mr *MockNetworkClientMockRecorder) ListSecGroupRule(opts any) *gomock.Call { +func (mr *MockNetworkClientMockRecorder) ListSecGroupRule(ctx, opts any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListSecGroupRule", reflect.TypeOf((*MockNetworkClient)(nil).ListSecGroupRule), opts) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListSecGroupRule", reflect.TypeOf((*MockNetworkClient)(nil).ListSecGroupRule), ctx, opts) } // ListSubnet mocks base method. -func (m *MockNetworkClient) ListSubnet(opts subnets.ListOptsBuilder) ([]subnets.Subnet, error) { +func (m *MockNetworkClient) ListSubnet(ctx context.Context, opts subnets.ListOptsBuilder) ([]subnets.Subnet, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListSubnet", opts) + ret := m.ctrl.Call(m, "ListSubnet", ctx, opts) ret0, _ := ret[0].([]subnets.Subnet) ret1, _ := ret[1].(error) return ret0, ret1 } // ListSubnet indicates an expected call of ListSubnet. -func (mr *MockNetworkClientMockRecorder) ListSubnet(opts any) *gomock.Call { +func (mr *MockNetworkClientMockRecorder) ListSubnet(ctx, opts any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListSubnet", reflect.TypeOf((*MockNetworkClient)(nil).ListSubnet), opts) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListSubnet", reflect.TypeOf((*MockNetworkClient)(nil).ListSubnet), ctx, opts) } // ListTrunk mocks base method. @@ -566,18 +565,18 @@ func (mr *MockNetworkClientMockRecorder) ListTrunkSubports(trunkID any) *gomock. } // RemoveRouterInterface mocks base method. -func (m *MockNetworkClient) RemoveRouterInterface(id string, opts routers.RemoveInterfaceOptsBuilder) (*routers.InterfaceInfo, error) { +func (m *MockNetworkClient) RemoveRouterInterface(ctx context.Context, id string, opts routers.RemoveInterfaceOptsBuilder) (*routers.InterfaceInfo, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RemoveRouterInterface", id, opts) + ret := m.ctrl.Call(m, "RemoveRouterInterface", ctx, id, opts) ret0, _ := ret[0].(*routers.InterfaceInfo) ret1, _ := ret[1].(error) return ret0, ret1 } // RemoveRouterInterface indicates an expected call of RemoveRouterInterface. -func (mr *MockNetworkClientMockRecorder) RemoveRouterInterface(id, opts any) *gomock.Call { +func (mr *MockNetworkClientMockRecorder) RemoveRouterInterface(ctx, id, opts any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveRouterInterface", reflect.TypeOf((*MockNetworkClient)(nil).RemoveRouterInterface), id, opts) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveRouterInterface", reflect.TypeOf((*MockNetworkClient)(nil).RemoveRouterInterface), ctx, id, opts) } // RemoveSubports mocks base method. @@ -595,18 +594,18 @@ func (mr *MockNetworkClientMockRecorder) RemoveSubports(id, opts any) *gomock.Ca } // ReplaceAllAttributesTags mocks base method. -func (m *MockNetworkClient) ReplaceAllAttributesTags(resourceType, resourceID string, opts attributestags.ReplaceAllOptsBuilder) ([]string, error) { +func (m *MockNetworkClient) ReplaceAllAttributesTags(ctx context.Context, resourceType, resourceID string, opts attributestags.ReplaceAllOptsBuilder) ([]string, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ReplaceAllAttributesTags", resourceType, resourceID, opts) + ret := m.ctrl.Call(m, "ReplaceAllAttributesTags", ctx, resourceType, resourceID, opts) ret0, _ := ret[0].([]string) ret1, _ := ret[1].(error) return ret0, ret1 } // ReplaceAllAttributesTags indicates an expected call of ReplaceAllAttributesTags. -func (mr *MockNetworkClientMockRecorder) ReplaceAllAttributesTags(resourceType, resourceID, opts any) *gomock.Call { +func (mr *MockNetworkClientMockRecorder) ReplaceAllAttributesTags(ctx, resourceType, resourceID, opts any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReplaceAllAttributesTags", reflect.TypeOf((*MockNetworkClient)(nil).ReplaceAllAttributesTags), resourceType, resourceID, opts) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReplaceAllAttributesTags", reflect.TypeOf((*MockNetworkClient)(nil).ReplaceAllAttributesTags), ctx, resourceType, resourceID, opts) } // UpdateFloatingIP mocks base method. @@ -625,18 +624,17 @@ func (mr *MockNetworkClientMockRecorder) UpdateFloatingIP(id, opts any) *gomock. } // UpdateNetwork mocks base method. -func (m *MockNetworkClient) UpdateNetwork(id string, opts networks.UpdateOptsBuilder) (*networks.Network, error) { +func (m *MockNetworkClient) UpdateNetwork(ctx context.Context, id string, opts networks.UpdateOptsBuilder) networks.UpdateResult { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateNetwork", id, opts) - ret0, _ := ret[0].(*networks.Network) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret := m.ctrl.Call(m, "UpdateNetwork", ctx, id, opts) + ret0, _ := ret[0].(networks.UpdateResult) + return ret0 } // UpdateNetwork indicates an expected call of UpdateNetwork. -func (mr *MockNetworkClientMockRecorder) UpdateNetwork(id, opts any) *gomock.Call { +func (mr *MockNetworkClientMockRecorder) UpdateNetwork(ctx, id, opts any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateNetwork", reflect.TypeOf((*MockNetworkClient)(nil).UpdateNetwork), id, opts) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateNetwork", reflect.TypeOf((*MockNetworkClient)(nil).UpdateNetwork), ctx, id, opts) } // UpdatePort mocks base method. @@ -655,46 +653,46 @@ func (mr *MockNetworkClientMockRecorder) UpdatePort(id, opts any) *gomock.Call { } // UpdateRouter mocks base method. -func (m *MockNetworkClient) UpdateRouter(id string, opts routers.UpdateOptsBuilder) (*routers.Router, error) { +func (m *MockNetworkClient) UpdateRouter(ctx context.Context, id string, opts routers.UpdateOptsBuilder) (*routers.Router, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateRouter", id, opts) + ret := m.ctrl.Call(m, "UpdateRouter", ctx, id, opts) ret0, _ := ret[0].(*routers.Router) ret1, _ := ret[1].(error) return ret0, ret1 } // UpdateRouter indicates an expected call of UpdateRouter. -func (mr *MockNetworkClientMockRecorder) UpdateRouter(id, opts any) *gomock.Call { +func (mr *MockNetworkClientMockRecorder) UpdateRouter(ctx, id, opts any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateRouter", reflect.TypeOf((*MockNetworkClient)(nil).UpdateRouter), id, opts) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateRouter", reflect.TypeOf((*MockNetworkClient)(nil).UpdateRouter), ctx, id, opts) } // UpdateSecGroup mocks base method. -func (m *MockNetworkClient) UpdateSecGroup(id string, opts groups.UpdateOptsBuilder) (*groups.SecGroup, error) { +func (m *MockNetworkClient) UpdateSecGroup(ctx context.Context, id string, opts groups.UpdateOptsBuilder) (*groups.SecGroup, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateSecGroup", id, opts) + ret := m.ctrl.Call(m, "UpdateSecGroup", ctx, id, opts) ret0, _ := ret[0].(*groups.SecGroup) ret1, _ := ret[1].(error) return ret0, ret1 } // UpdateSecGroup indicates an expected call of UpdateSecGroup. -func (mr *MockNetworkClientMockRecorder) UpdateSecGroup(id, opts any) *gomock.Call { +func (mr *MockNetworkClientMockRecorder) UpdateSecGroup(ctx, id, opts any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateSecGroup", reflect.TypeOf((*MockNetworkClient)(nil).UpdateSecGroup), id, opts) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateSecGroup", reflect.TypeOf((*MockNetworkClient)(nil).UpdateSecGroup), ctx, id, opts) } // UpdateSubnet mocks base method. -func (m *MockNetworkClient) UpdateSubnet(id string, opts subnets.UpdateOptsBuilder) (*subnets.Subnet, error) { +func (m *MockNetworkClient) UpdateSubnet(ctx context.Context, id string, opts subnets.UpdateOptsBuilder) (*subnets.Subnet, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateSubnet", id, opts) + ret := m.ctrl.Call(m, "UpdateSubnet", ctx, id, opts) ret0, _ := ret[0].(*subnets.Subnet) ret1, _ := ret[1].(error) return ret0, ret1 } // UpdateSubnet indicates an expected call of UpdateSubnet. -func (mr *MockNetworkClientMockRecorder) UpdateSubnet(id, opts any) *gomock.Call { +func (mr *MockNetworkClientMockRecorder) UpdateSubnet(ctx, id, opts any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateSubnet", reflect.TypeOf((*MockNetworkClient)(nil).UpdateSubnet), id, opts) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateSubnet", reflect.TypeOf((*MockNetworkClient)(nil).UpdateSubnet), ctx, id, opts) } diff --git a/internal/osclients/networking.go b/internal/osclients/networking.go index 18b5c5088..76964285b 100644 --- a/internal/osclients/networking.go +++ b/internal/osclients/networking.go @@ -1,5 +1,5 @@ /* -Copyright 2021 The Kubernetes Authors. +Copyright 2021 The ORC Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -32,6 +32,7 @@ import ( "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/networks" "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports" "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/subnets" + "github.com/gophercloud/gophercloud/v2/pagination" "github.com/gophercloud/utils/v2/openstack/clientconfig" ) @@ -42,10 +43,10 @@ type NetworkClient interface { GetFloatingIP(id string) (*floatingips.FloatingIP, error) UpdateFloatingIP(id string, opts floatingips.UpdateOptsBuilder) (*floatingips.FloatingIP, error) - ListPort(opts ports.ListOptsBuilder) ([]ports.Port, error) - CreatePort(opts ports.CreateOptsBuilder) (*ports.Port, error) - DeletePort(id string) error - GetPort(id string) (*ports.Port, error) + ListPort(ctx context.Context, opts ports.ListOptsBuilder) ([]ports.Port, error) + CreatePort(ctx context.Context, opts ports.CreateOptsBuilder) (*ports.Port, error) + DeletePort(ctx context.Context, id string) error + GetPort(ctx context.Context, id string) (*ports.Port, error) UpdatePort(id string, opts ports.UpdateOptsBuilder) (*ports.Port, error) ListTrunk(opts trunks.ListOptsBuilder) ([]trunks.Trunk, error) @@ -55,46 +56,48 @@ type NetworkClient interface { ListTrunkSubports(trunkID string) ([]trunks.Subport, error) RemoveSubports(id string, opts trunks.RemoveSubportsOpts) error - ListRouter(opts routers.ListOpts) ([]routers.Router, error) - CreateRouter(opts routers.CreateOptsBuilder) (*routers.Router, error) - DeleteRouter(id string) error - GetRouter(id string) (*routers.Router, error) - UpdateRouter(id string, opts routers.UpdateOptsBuilder) (*routers.Router, error) - AddRouterInterface(id string, opts routers.AddInterfaceOptsBuilder) (*routers.InterfaceInfo, error) - RemoveRouterInterface(id string, opts routers.RemoveInterfaceOptsBuilder) (*routers.InterfaceInfo, error) - - ListSecGroup(opts groups.ListOpts) ([]groups.SecGroup, error) - CreateSecGroup(opts groups.CreateOptsBuilder) (*groups.SecGroup, error) - DeleteSecGroup(id string) error - GetSecGroup(id string) (*groups.SecGroup, error) - UpdateSecGroup(id string, opts groups.UpdateOptsBuilder) (*groups.SecGroup, error) - - ListSecGroupRule(opts rules.ListOpts) ([]rules.SecGroupRule, error) - CreateSecGroupRule(opts rules.CreateOptsBuilder) (*rules.SecGroupRule, error) - DeleteSecGroupRule(id string) error - GetSecGroupRule(id string) (*rules.SecGroupRule, error) - - ListNetwork(opts networks.ListOptsBuilder) ([]networks.Network, error) - CreateNetwork(opts networks.CreateOptsBuilder) (*networks.Network, error) - DeleteNetwork(id string) error - GetNetwork(id string) (*networks.Network, error) - UpdateNetwork(id string, opts networks.UpdateOptsBuilder) (*networks.Network, error) - - ListSubnet(opts subnets.ListOptsBuilder) ([]subnets.Subnet, error) - CreateSubnet(opts subnets.CreateOptsBuilder) (*subnets.Subnet, error) - DeleteSubnet(id string) error - GetSubnet(id string) (*subnets.Subnet, error) - UpdateSubnet(id string, opts subnets.UpdateOptsBuilder) (*subnets.Subnet, error) + ListRouter(ctx context.Context, opts routers.ListOpts) ([]routers.Router, error) + CreateRouter(ctx context.Context, opts routers.CreateOptsBuilder) (*routers.Router, error) + DeleteRouter(ctx context.Context, id string) error + GetRouter(ctx context.Context, id string) (*routers.Router, error) + UpdateRouter(ctx context.Context, id string, opts routers.UpdateOptsBuilder) (*routers.Router, error) + AddRouterInterface(ctx context.Context, id string, opts routers.AddInterfaceOptsBuilder) (*routers.InterfaceInfo, error) + RemoveRouterInterface(ctx context.Context, id string, opts routers.RemoveInterfaceOptsBuilder) (*routers.InterfaceInfo, error) + + ListSecGroup(ctx context.Context, opts groups.ListOpts) ([]groups.SecGroup, error) + CreateSecGroup(ctx context.Context, opts groups.CreateOptsBuilder) (*groups.SecGroup, error) + DeleteSecGroup(ctx context.Context, id string) error + GetSecGroup(ctx context.Context, id string) (*groups.SecGroup, error) + UpdateSecGroup(ctx context.Context, id string, opts groups.UpdateOptsBuilder) (*groups.SecGroup, error) + + ListSecGroupRule(ctx context.Context, opts rules.ListOpts) ([]rules.SecGroupRule, error) + CreateSecGroupRules(ctx context.Context, opts []rules.CreateOpts) ([]rules.SecGroupRule, error) + DeleteSecGroupRule(ctx context.Context, id string) error + GetSecGroupRule(ctx context.Context, id string) (*rules.SecGroupRule, error) + + ListNetwork(opts networks.ListOptsBuilder) pagination.Pager + CreateNetwork(ctx context.Context, opts networks.CreateOptsBuilder) networks.CreateResult + DeleteNetwork(ctx context.Context, id string) networks.DeleteResult + GetNetwork(ctx context.Context, id string) networks.GetResult + UpdateNetwork(ctx context.Context, id string, opts networks.UpdateOptsBuilder) networks.UpdateResult + + ListSubnet(ctx context.Context, opts subnets.ListOptsBuilder) ([]subnets.Subnet, error) + CreateSubnet(ctx context.Context, opts subnets.CreateOptsBuilder) (*subnets.Subnet, error) + DeleteSubnet(ctx context.Context, id string) error + GetSubnet(ctx context.Context, id string) (*subnets.Subnet, error) + UpdateSubnet(ctx context.Context, id string, opts subnets.UpdateOptsBuilder) (*subnets.Subnet, error) ListExtensions() ([]extensions.Extension, error) - ReplaceAllAttributesTags(resourceType string, resourceID string, opts attributestags.ReplaceAllOptsBuilder) ([]string, error) + ReplaceAllAttributesTags(ctx context.Context, resourceType string, resourceID string, opts attributestags.ReplaceAllOptsBuilder) ([]string, error) } type networkClient struct { serviceClient *gophercloud.ServiceClient } +var _ NetworkClient = &networkClient{} + // NewNetworkClient returns an instance of the networking service. func NewNetworkClient(providerClient *gophercloud.ProviderClient, providerClientOpts *clientconfig.ClientOpts) (NetworkClient, error) { serviceClient, err := openstack.NewNetworkV2(providerClient, gophercloud.EndpointOpts{ @@ -108,20 +111,20 @@ func NewNetworkClient(providerClient *gophercloud.ProviderClient, providerClient return networkClient{serviceClient}, nil } -func (c networkClient) AddRouterInterface(id string, opts routers.AddInterfaceOptsBuilder) (*routers.InterfaceInfo, error) { - return routers.AddInterface(context.TODO(), c.serviceClient, id, opts).Extract() +func (c networkClient) AddRouterInterface(ctx context.Context, id string, opts routers.AddInterfaceOptsBuilder) (*routers.InterfaceInfo, error) { + return routers.AddInterface(ctx, c.serviceClient, id, opts).Extract() } -func (c networkClient) RemoveRouterInterface(id string, opts routers.RemoveInterfaceOptsBuilder) (*routers.InterfaceInfo, error) { - return routers.RemoveInterface(context.TODO(), c.serviceClient, id, opts).Extract() +func (c networkClient) RemoveRouterInterface(ctx context.Context, id string, opts routers.RemoveInterfaceOptsBuilder) (*routers.InterfaceInfo, error) { + return routers.RemoveInterface(ctx, c.serviceClient, id, opts).Extract() } -func (c networkClient) ReplaceAllAttributesTags(resourceType string, resourceID string, opts attributestags.ReplaceAllOptsBuilder) ([]string, error) { - return attributestags.ReplaceAll(context.TODO(), c.serviceClient, resourceType, resourceID, opts).Extract() +func (c networkClient) ReplaceAllAttributesTags(ctx context.Context, resourceType string, resourceID string, opts attributestags.ReplaceAllOptsBuilder) ([]string, error) { + return attributestags.ReplaceAll(ctx, c.serviceClient, resourceType, resourceID, opts).Extract() } -func (c networkClient) ListRouter(opts routers.ListOpts) ([]routers.Router, error) { - allPages, err := routers.List(c.serviceClient, opts).AllPages(context.TODO()) +func (c networkClient) ListRouter(ctx context.Context, opts routers.ListOpts) ([]routers.Router, error) { + allPages, err := routers.List(c.serviceClient, opts).AllPages(ctx) if err != nil { return nil, err } @@ -156,24 +159,24 @@ func (c networkClient) UpdateFloatingIP(id string, opts floatingips.UpdateOptsBu return floatingips.Update(context.TODO(), c.serviceClient, id, opts).Extract() } -func (c networkClient) ListPort(opts ports.ListOptsBuilder) ([]ports.Port, error) { - allPages, err := ports.List(c.serviceClient, opts).AllPages(context.TODO()) +func (c networkClient) ListPort(ctx context.Context, opts ports.ListOptsBuilder) ([]ports.Port, error) { + allPages, err := ports.List(c.serviceClient, opts).AllPages(ctx) if err != nil { return nil, err } return ports.ExtractPorts(allPages) } -func (c networkClient) CreatePort(opts ports.CreateOptsBuilder) (*ports.Port, error) { - return ports.Create(context.TODO(), c.serviceClient, opts).Extract() +func (c networkClient) CreatePort(ctx context.Context, opts ports.CreateOptsBuilder) (*ports.Port, error) { + return ports.Create(ctx, c.serviceClient, opts).Extract() } -func (c networkClient) DeletePort(id string) error { - return ports.Delete(context.TODO(), c.serviceClient, id).ExtractErr() +func (c networkClient) DeletePort(ctx context.Context, id string) error { + return ports.Delete(ctx, c.serviceClient, id).ExtractErr() } -func (c networkClient) GetPort(id string) (*ports.Port, error) { - return ports.Get(context.TODO(), c.serviceClient, id).Extract() +func (c networkClient) GetPort(ctx context.Context, id string) (*ports.Port, error) { + return ports.Get(ctx, c.serviceClient, id).Extract() } func (c networkClient) UpdatePort(id string, opts ports.UpdateOptsBuilder) (*ports.Port, error) { @@ -205,91 +208,87 @@ func (c networkClient) ListTrunk(opts trunks.ListOptsBuilder) ([]trunks.Trunk, e return trunks.ExtractTrunks(allPages) } -func (c networkClient) CreateRouter(opts routers.CreateOptsBuilder) (*routers.Router, error) { - return routers.Create(context.TODO(), c.serviceClient, opts).Extract() +func (c networkClient) CreateRouter(ctx context.Context, opts routers.CreateOptsBuilder) (*routers.Router, error) { + return routers.Create(ctx, c.serviceClient, opts).Extract() } -func (c networkClient) DeleteRouter(id string) error { - return routers.Delete(context.TODO(), c.serviceClient, id).ExtractErr() +func (c networkClient) DeleteRouter(ctx context.Context, id string) error { + return routers.Delete(ctx, c.serviceClient, id).ExtractErr() } -func (c networkClient) GetRouter(id string) (*routers.Router, error) { - return routers.Get(context.TODO(), c.serviceClient, id).Extract() +func (c networkClient) GetRouter(ctx context.Context, id string) (*routers.Router, error) { + return routers.Get(ctx, c.serviceClient, id).Extract() } -func (c networkClient) UpdateRouter(id string, opts routers.UpdateOptsBuilder) (*routers.Router, error) { +func (c networkClient) UpdateRouter(ctx context.Context, id string, opts routers.UpdateOptsBuilder) (*routers.Router, error) { return routers.Update(context.TODO(), c.serviceClient, id, opts).Extract() } -func (c networkClient) ListSecGroup(opts groups.ListOpts) ([]groups.SecGroup, error) { - allPages, err := groups.List(c.serviceClient, opts).AllPages(context.TODO()) +func (c networkClient) ListSecGroup(ctx context.Context, opts groups.ListOpts) ([]groups.SecGroup, error) { + allPages, err := groups.List(c.serviceClient, opts).AllPages(ctx) if err != nil { return nil, err } return groups.ExtractGroups(allPages) } -func (c networkClient) CreateSecGroup(opts groups.CreateOptsBuilder) (*groups.SecGroup, error) { - return groups.Create(context.TODO(), c.serviceClient, opts).Extract() +func (c networkClient) CreateSecGroup(ctx context.Context, opts groups.CreateOptsBuilder) (*groups.SecGroup, error) { + return groups.Create(ctx, c.serviceClient, opts).Extract() } -func (c networkClient) DeleteSecGroup(id string) error { - return groups.Delete(context.TODO(), c.serviceClient, id).ExtractErr() +func (c networkClient) DeleteSecGroup(ctx context.Context, id string) error { + return groups.Delete(ctx, c.serviceClient, id).ExtractErr() } -func (c networkClient) GetSecGroup(id string) (*groups.SecGroup, error) { - return groups.Get(context.TODO(), c.serviceClient, id).Extract() +func (c networkClient) GetSecGroup(ctx context.Context, id string) (*groups.SecGroup, error) { + return groups.Get(ctx, c.serviceClient, id).Extract() } -func (c networkClient) UpdateSecGroup(id string, opts groups.UpdateOptsBuilder) (*groups.SecGroup, error) { - return groups.Update(context.TODO(), c.serviceClient, id, opts).Extract() +func (c networkClient) UpdateSecGroup(ctx context.Context, id string, opts groups.UpdateOptsBuilder) (*groups.SecGroup, error) { + return groups.Update(ctx, c.serviceClient, id, opts).Extract() } -func (c networkClient) ListSecGroupRule(opts rules.ListOpts) ([]rules.SecGroupRule, error) { - allPages, err := rules.List(c.serviceClient, opts).AllPages(context.TODO()) +func (c networkClient) ListSecGroupRule(ctx context.Context, opts rules.ListOpts) ([]rules.SecGroupRule, error) { + allPages, err := rules.List(c.serviceClient, opts).AllPages(ctx) if err != nil { return nil, err } return rules.ExtractRules(allPages) } -func (c networkClient) CreateSecGroupRule(opts rules.CreateOptsBuilder) (*rules.SecGroupRule, error) { - return rules.Create(context.TODO(), c.serviceClient, opts).Extract() +func (c networkClient) CreateSecGroupRules(ctx context.Context, opts []rules.CreateOpts) ([]rules.SecGroupRule, error) { + return rules.CreateBulk(ctx, c.serviceClient, opts).Extract() } -func (c networkClient) DeleteSecGroupRule(id string) error { - return rules.Delete(context.TODO(), c.serviceClient, id).ExtractErr() +func (c networkClient) DeleteSecGroupRule(ctx context.Context, id string) error { + return rules.Delete(ctx, c.serviceClient, id).ExtractErr() } -func (c networkClient) GetSecGroupRule(id string) (*rules.SecGroupRule, error) { - return rules.Get(context.TODO(), c.serviceClient, id).Extract() +func (c networkClient) GetSecGroupRule(ctx context.Context, id string) (*rules.SecGroupRule, error) { + return rules.Get(ctx, c.serviceClient, id).Extract() } -func (c networkClient) ListNetwork(opts networks.ListOptsBuilder) ([]networks.Network, error) { - allPages, err := networks.List(c.serviceClient, opts).AllPages(context.TODO()) - if err != nil { - return nil, err - } - return networks.ExtractNetworks(allPages) +func (c networkClient) ListNetwork(opts networks.ListOptsBuilder) pagination.Pager { + return networks.List(c.serviceClient, opts) } -func (c networkClient) CreateNetwork(opts networks.CreateOptsBuilder) (*networks.Network, error) { - return networks.Create(context.TODO(), c.serviceClient, opts).Extract() +func (c networkClient) CreateNetwork(ctx context.Context, opts networks.CreateOptsBuilder) networks.CreateResult { + return networks.Create(ctx, c.serviceClient, opts) } -func (c networkClient) DeleteNetwork(id string) error { - return networks.Delete(context.TODO(), c.serviceClient, id).ExtractErr() +func (c networkClient) DeleteNetwork(ctx context.Context, id string) networks.DeleteResult { + return networks.Delete(ctx, c.serviceClient, id) } -func (c networkClient) GetNetwork(id string) (*networks.Network, error) { - return networks.Get(context.TODO(), c.serviceClient, id).Extract() +func (c networkClient) GetNetwork(ctx context.Context, id string) networks.GetResult { + return networks.Get(ctx, c.serviceClient, id) } -func (c networkClient) UpdateNetwork(id string, opts networks.UpdateOptsBuilder) (*networks.Network, error) { - return networks.Update(context.TODO(), c.serviceClient, id, opts).Extract() +func (c networkClient) UpdateNetwork(ctx context.Context, id string, opts networks.UpdateOptsBuilder) networks.UpdateResult { + return networks.Update(ctx, c.serviceClient, id, opts) } -func (c networkClient) ListSubnet(opts subnets.ListOptsBuilder) ([]subnets.Subnet, error) { +func (c networkClient) ListSubnet(ctx context.Context, opts subnets.ListOptsBuilder) ([]subnets.Subnet, error) { allPages, err := subnets.List(c.serviceClient, opts).AllPages(context.TODO()) if err != nil { return nil, err @@ -297,20 +296,20 @@ func (c networkClient) ListSubnet(opts subnets.ListOptsBuilder) ([]subnets.Subne return subnets.ExtractSubnets(allPages) } -func (c networkClient) CreateSubnet(opts subnets.CreateOptsBuilder) (*subnets.Subnet, error) { - return subnets.Create(context.TODO(), c.serviceClient, opts).Extract() +func (c networkClient) CreateSubnet(ctx context.Context, opts subnets.CreateOptsBuilder) (*subnets.Subnet, error) { + return subnets.Create(ctx, c.serviceClient, opts).Extract() } -func (c networkClient) DeleteSubnet(id string) error { - return subnets.Delete(context.TODO(), c.serviceClient, id).ExtractErr() +func (c networkClient) DeleteSubnet(ctx context.Context, id string) error { + return subnets.Delete(ctx, c.serviceClient, id).ExtractErr() } -func (c networkClient) GetSubnet(id string) (*subnets.Subnet, error) { - return subnets.Get(context.TODO(), c.serviceClient, id).Extract() +func (c networkClient) GetSubnet(ctx context.Context, id string) (*subnets.Subnet, error) { + return subnets.Get(ctx, c.serviceClient, id).Extract() } -func (c networkClient) UpdateSubnet(id string, opts subnets.UpdateOptsBuilder) (*subnets.Subnet, error) { - return subnets.Update(context.TODO(), c.serviceClient, id, opts).Extract() +func (c networkClient) UpdateSubnet(ctx context.Context, id string, opts subnets.UpdateOptsBuilder) (*subnets.Subnet, error) { + return subnets.Update(ctx, c.serviceClient, id, opts).Extract() } func (c networkClient) ListExtensions() ([]extensions.Extension, error) { diff --git a/internal/osclients/volume.go b/internal/osclients/volume.go index 55221b295..b9fa8ea25 100644 --- a/internal/osclients/volume.go +++ b/internal/osclients/volume.go @@ -1,5 +1,5 @@ /* -Copyright 2021 The Kubernetes Authors. +Copyright 2021 The ORC Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/internal/scheme/scheme.go b/internal/scheme/scheme.go new file mode 100644 index 000000000..18abc98bb --- /dev/null +++ b/internal/scheme/scheme.go @@ -0,0 +1,39 @@ +/* +Copyright 2024. + +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 scheme + +import ( + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + + orcv1alpha1 "github.com/k-orc/openstack-resource-controller/api/v1alpha1" + // +kubebuilder:scaffold:imports +) + +func AddORCTypes(scheme *runtime.Scheme) { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + + utilruntime.Must(orcv1alpha1.AddToScheme(scheme)) + // +kubebuilder:scaffold:scheme +} + +func New() *runtime.Scheme { + scheme := runtime.NewScheme() + AddORCTypes(scheme) + return scheme +} diff --git a/internal/scope/hash.go b/internal/scope/hash.go index 6f74902ab..9dc9aa528 100644 --- a/internal/scope/hash.go +++ b/internal/scope/hash.go @@ -1,5 +1,5 @@ /* -Copyright 2022 The Kubernetes Authors. +Copyright 2022 The ORC Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/internal/scope/mock.go b/internal/scope/mock.go index 604d39db2..6303cc10a 100644 --- a/internal/scope/mock.go +++ b/internal/scope/mock.go @@ -1,5 +1,5 @@ /* -Copyright 2022 The Kubernetes Authors. +Copyright 2022 The ORC Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -40,11 +40,10 @@ type MockScopeFactory struct { ImageClient *mock.MockImageClient LbClient *mock.MockLbClient - projectID string clientScopeCreateError error } -func NewMockScopeFactory(mockCtrl *gomock.Controller, projectID string) *MockScopeFactory { +func NewMockScopeFactory(mockCtrl *gomock.Controller) *MockScopeFactory { computeClient := mock.NewMockComputeClient(mockCtrl) volumeClient := mock.NewMockVolumeClient(mockCtrl) imageClient := mock.NewMockImageClient(mockCtrl) @@ -57,7 +56,6 @@ func NewMockScopeFactory(mockCtrl *gomock.Controller, projectID string) *MockSco ImageClient: imageClient, NetworkClient: networkClient, LbClient: lbClient, - projectID: projectID, } } @@ -65,7 +63,7 @@ func (f *MockScopeFactory) SetClientScopeCreateError(err error) { f.clientScopeCreateError = err } -func (f *MockScopeFactory) NewClientScopeFromObject(_ context.Context, _ client.Client, _ []byte, _ logr.Logger, _ ...orcv1alpha1.CloudCredentialsRefProvider) (Scope, error) { +func (f *MockScopeFactory) NewClientScopeFromObject(_ context.Context, _ client.Client, _ logr.Logger, _ ...orcv1alpha1.CloudCredentialsRefProvider) (Scope, error) { if f.clientScopeCreateError != nil { return nil, f.clientScopeCreateError } @@ -92,10 +90,6 @@ func (f *MockScopeFactory) NewLbClient() (osclients.LbClient, error) { return f.LbClient, nil } -func (f *MockScopeFactory) ProjectID() string { - return f.projectID -} - func (f *MockScopeFactory) ExtractToken() (*tokens.Token, error) { return &tokens.Token{ExpiresAt: time.Now().Add(24 * time.Hour)}, nil } diff --git a/internal/scope/provider.go b/internal/scope/provider.go index 172491a73..5030195e2 100644 --- a/internal/scope/provider.go +++ b/internal/scope/provider.go @@ -1,5 +1,5 @@ /* -Copyright 2020 The Kubernetes Authors. +Copyright 2020 The ORC Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -48,10 +48,11 @@ const ( ) type providerScopeFactory struct { - clientCache *cache.LRUExpireCache + clientCache *cache.LRUExpireCache + defaultCACert []byte } -func (f *providerScopeFactory) NewClientScopeFromObject(ctx context.Context, ctrlClient client.Client, defaultCACert []byte, logger logr.Logger, objects ...orcv1alpha1.CloudCredentialsRefProvider) (Scope, error) { +func (f *providerScopeFactory) NewClientScopeFromObject(ctx context.Context, ctrlClient client.Client, logger logr.Logger, objects ...orcv1alpha1.CloudCredentialsRefProvider) (Scope, error) { namespace, credentialsRef := func() (*string, *orcv1alpha1.CloudCredentialsReference) { for _, o := range objects { namespace, credentialsRef := o.GetCloudCredentialsRef() @@ -77,7 +78,7 @@ func (f *providerScopeFactory) NewClientScopeFromObject(ctx context.Context, ctr } if caCert == nil { - caCert = defaultCACert + caCert = f.defaultCACert } if f.clientCache == nil { @@ -99,11 +100,10 @@ func getScopeCacheKey(cloud clientconfig.Cloud) (string, error) { type providerScope struct { providerClient *gophercloud.ProviderClient providerClientOpts *clientconfig.ClientOpts - projectID string } func NewProviderScope(cloud clientconfig.Cloud, caCert []byte, logger logr.Logger) (Scope, error) { - providerClient, clientOpts, projectID, err := NewProviderClient(cloud, caCert, logger) + providerClient, clientOpts, err := NewProviderClient(cloud, caCert, logger) if err != nil { return nil, err } @@ -111,7 +111,6 @@ func NewProviderScope(cloud clientconfig.Cloud, caCert []byte, logger logr.Logge return &providerScope{ providerClient: providerClient, providerClientOpts: clientOpts, - projectID: projectID, }, nil } @@ -143,10 +142,6 @@ func NewCachedProviderScope(cache *cache.LRUExpireCache, cloud clientconfig.Clou return scope, nil } -func (s *providerScope) ProjectID() string { - return s.projectID -} - func (s *providerScope) NewComputeClient() (clients.ComputeClient, error) { return clients.NewComputeClient(s.providerClient, s.providerClientOpts) } @@ -175,7 +170,7 @@ func (s *providerScope) ExtractToken() (*tokens.Token, error) { return tokens.Get(context.TODO(), client, s.providerClient.Token()).ExtractToken() } -func NewProviderClient(cloud clientconfig.Cloud, caCert []byte, logger logr.Logger) (*gophercloud.ProviderClient, *clientconfig.ClientOpts, string, error) { +func NewProviderClient(cloud clientconfig.Cloud, caCert []byte, logger logr.Logger) (*gophercloud.ProviderClient, *clientconfig.ClientOpts, error) { clientOpts := new(clientconfig.ClientOpts) // We explicitly disable reading auth data from env variables by setting an invalid EnvPrefix. @@ -192,13 +187,13 @@ func NewProviderClient(cloud clientconfig.Cloud, caCert []byte, logger logr.Logg opts, err := clientconfig.AuthOptions(clientOpts) if err != nil { - return nil, nil, "", fmt.Errorf("auth option failed for cloud %v: %v", cloud.Cloud, err) + return nil, nil, fmt.Errorf("auth option failed for cloud %v: %v", cloud.Cloud, err) } opts.AllowReauth = true provider, err := openstack.NewClient(opts.IdentityEndpoint) if err != nil { - return nil, nil, "", fmt.Errorf("create providerClient err: %v", err) + return nil, nil, fmt.Errorf("create providerClient err: %v", err) } ua := gophercloud.UserAgent{} @@ -229,15 +224,10 @@ func NewProviderClient(cloud clientconfig.Cloud, caCert []byte, logger logr.Logg } err = openstack.Authenticate(context.TODO(), provider, *opts) if err != nil { - return nil, nil, "", fmt.Errorf("providerClient authentication err: %v", err) - } - - projectID, err := getProjectIDFromAuthResult(provider.GetAuthResult()) - if err != nil { - return nil, nil, "", err + return nil, nil, fmt.Errorf("providerClient authentication err: %v", err) } - return provider, clientOpts, projectID, nil + return provider, clientOpts, nil } type gophercloudLogger struct { @@ -249,6 +239,8 @@ func (g gophercloudLogger) Printf(format string, args ...interface{}) { g.logger.Info(fmt.Sprintf(format, args...)) } +// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch + // getCloudFromSecret extract a Cloud from the given namespace:secretName. func getCloudFromSecret(ctx context.Context, ctrlClient client.Client, secretNamespace string, secretName string, cloudName string) (clientconfig.Cloud, []byte, error) { emptyCloud := clientconfig.Cloud{} @@ -288,21 +280,3 @@ func getCloudFromSecret(ctx context.Context, ctrlClient client.Client, secretNam return clouds.Clouds[cloudName], caCert, nil } - -// getProjectIDFromAuthResult handles different auth mechanisms to retrieve the -// current project id. Usually we use the Identity v3 Token mechanism that -// returns the project id in the response to the initial auth request. -func getProjectIDFromAuthResult(authResult gophercloud.AuthResult) (string, error) { - switch authResult := authResult.(type) { - case tokens.CreateResult: - project, err := authResult.ExtractProject() - if err != nil { - return "", fmt.Errorf("unable to extract project from CreateResult: %v", err) - } - - return project.ID, nil - - default: - return "", fmt.Errorf("unable to get the project id from auth response with type %T", authResult) - } -} diff --git a/internal/scope/scope.go b/internal/scope/scope.go index 6a620a299..69fede56e 100644 --- a/internal/scope/scope.go +++ b/internal/scope/scope.go @@ -1,5 +1,5 @@ /* -Copyright 2022 The Kubernetes Authors. +Copyright 2022 The ORC Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -29,20 +29,21 @@ import ( ) // NewFactory creates the default scope factory. It generates service clients which make OpenStack API calls against a running cloud. -func NewFactory(maxCacheSize int) Factory { +func NewFactory(maxCacheSize int, defaultCACert []byte) Factory { var c *cache.LRUExpireCache if maxCacheSize > 0 { c = cache.NewLRUExpireCache(maxCacheSize) } return &providerScopeFactory{ - clientCache: c, + clientCache: c, + defaultCACert: defaultCACert, } } // Factory instantiates a new Scope using credentials from an IdentityRefProvider. type Factory interface { // NewClientScopeFromObject creates a new scope from the first object which returns an OpenStackIdentityRef - NewClientScopeFromObject(ctx context.Context, ctrlClient client.Client, defaultCACert []byte, logger logr.Logger, objects ...orcv1alpha1.CloudCredentialsRefProvider) (Scope, error) + NewClientScopeFromObject(ctx context.Context, ctrlClient client.Client, logger logr.Logger, objects ...orcv1alpha1.CloudCredentialsRefProvider) (Scope, error) } // Scope contains arguments common to most operations. @@ -52,7 +53,6 @@ type Scope interface { NewImageClient() (osclients.ImageClient, error) NewNetworkClient() (osclients.NetworkClient, error) NewLbClient() (osclients.LbClient, error) - ProjectID() string ExtractToken() (*tokens.Token, error) } diff --git a/internal/util/ssa/conditions.go b/internal/util/applyconfigs/conditions.go similarity index 95% rename from internal/util/ssa/conditions.go rename to internal/util/applyconfigs/conditions.go index 4079f95b2..9e8e87cc2 100644 --- a/internal/util/ssa/conditions.go +++ b/internal/util/applyconfigs/conditions.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Kubernetes Authors. +Copyright 2024 The ORC Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package ssa +package applyconfigs import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/internal/util/ssa/applyconfigpatch.go b/internal/util/applyconfigs/patch.go similarity index 75% rename from internal/util/ssa/applyconfigpatch.go rename to internal/util/applyconfigs/patch.go index 65b73701d..fca059a3f 100644 --- a/internal/util/ssa/applyconfigpatch.go +++ b/internal/util/applyconfigs/patch.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Kubernetes Authors. +Copyright 2024 The ORC Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package ssa +package applyconfigs import ( "encoding/json" @@ -23,14 +23,15 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) -// applyConfigPatch uses server-side apply to patch the object. +// applyConfigPatch is a slightly more ergonomic version of client.RawPatch which json marshals its argument type applyConfigPatch struct { + patchType types.PatchType applyConfig interface{} } // Type implements Patch. func (p applyConfigPatch) Type() types.PatchType { - return types.ApplyPatchType + return p.patchType } // Data implements Patch. @@ -38,8 +39,9 @@ func (p applyConfigPatch) Data(_ client.Object) ([]byte, error) { return json.Marshal(p.applyConfig) } -func ApplyConfigPatch(applyConfig interface{}) client.Patch { +func Patch(patchType types.PatchType, applyConfig interface{}) client.Patch { return &applyConfigPatch{ + patchType: patchType, applyConfig: applyConfig, } } diff --git a/internal/util/errors/errors.go b/internal/util/errors/errors.go index 2ee3eaf9c..eafb5ecae 100644 --- a/internal/util/errors/errors.go +++ b/internal/util/errors/errors.go @@ -1,5 +1,5 @@ /* -Copyright 2020 The Kubernetes Authors. +Copyright 2020 The ORC Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/internal/util/errors/terminal.go b/internal/util/errors/terminal.go index 4e88356d9..6b51c0f9a 100644 --- a/internal/util/errors/terminal.go +++ b/internal/util/errors/terminal.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Kubernetes Authors. +Copyright 2024 The ORC Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/internal/version/version.go b/internal/version/version.go index ab2fd9749..c8c10ccee 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -1,5 +1,5 @@ /* -Copyright 2020 The Kubernetes Authors. +Copyright 2020 The ORC Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/imageresourcespec.go b/pkg/clients/applyconfiguration/api/v1alpha1/imageresourcespec.go index 9fce6d208..7e521bcc6 100644 --- a/pkg/clients/applyconfiguration/api/v1alpha1/imageresourcespec.go +++ b/pkg/clients/applyconfiguration/api/v1alpha1/imageresourcespec.go @@ -25,7 +25,7 @@ import ( // ImageResourceSpecApplyConfiguration represents a declarative configuration of the ImageResourceSpec type for use // with apply. type ImageResourceSpecApplyConfiguration struct { - Name *string `json:"name,omitempty"` + Name *v1alpha1.OpenStackName `json:"name,omitempty"` Protected *bool `json:"protected,omitempty"` Tags []v1alpha1.ImageTag `json:"tags,omitempty"` Visibility *v1alpha1.ImageVisibility `json:"visibility,omitempty"` @@ -42,7 +42,7 @@ func ImageResourceSpec() *ImageResourceSpecApplyConfiguration { // WithName sets the Name field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Name field is set to the value of the last call. -func (b *ImageResourceSpecApplyConfiguration) WithName(value string) *ImageResourceSpecApplyConfiguration { +func (b *ImageResourceSpecApplyConfiguration) WithName(value v1alpha1.OpenStackName) *ImageResourceSpecApplyConfiguration { b.Name = &value return b } diff --git a/pkg/predicates/readiness.go b/pkg/predicates/readiness.go index bc2d78c51..6b3b788f9 100644 --- a/pkg/predicates/readiness.go +++ b/pkg/predicates/readiness.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Kubernetes Authors. +Copyright 2024 The ORC Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -50,18 +50,28 @@ func NewBecameAvailable(log logr.Logger, specimen orcv1alpha1.ObjectWithConditio return objWithConditions } + log = log.WithValues("watchKind", fmt.Sprintf("%T", specimen)) + return availabilityChanged{ predicate.Funcs{ CreateFunc: func(e event.CreateEvent) bool { + log := log.WithValues("name", e.Object.GetName(), "namespace", e.Object.GetNamespace()) + log.V(5).Info("Observed create") + obj := getObjWithConditions(e.Object, "create") if obj == nil { return false } // Only reconcile if the new object is available - return orcv1alpha1.IsAvailable(obj) + available := orcv1alpha1.IsAvailable(obj) + + return available }, UpdateFunc: func(e event.UpdateEvent) bool { + log := log.WithValues("name", e.ObjectOld.GetName(), "namespace", e.ObjectOld.GetNamespace()) + log.V(5).Info("Observed update") + oldObj := getObjWithConditions(e.ObjectOld, "update") newObj := getObjWithConditions(e.ObjectNew, "update") diff --git a/pkg/controllers/alias.go b/test/apivalidations/common_test.go similarity index 58% rename from pkg/controllers/alias.go rename to test/apivalidations/common_test.go index 1cae3919d..18d8d61e8 100644 --- a/pkg/controllers/alias.go +++ b/test/apivalidations/common_test.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Kubernetes Authors. +Copyright 2024 The ORC Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,16 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -package controllers - -// This file provides a minimal exported interface to non-exported controllers. +package apivalidations import ( - "github.com/k-orc/openstack-resource-controller/internal/scope" - - imagecontroller "github.com/k-orc/openstack-resource-controller/internal/controllers/image" + applyconfigv1alpha1 "github.com/k-orc/openstack-resource-controller/pkg/clients/applyconfiguration/api/v1alpha1" ) -var ImageController = imagecontroller.New - -var NewScopeFactory = scope.NewFactory +func testCredentials() *applyconfigv1alpha1.CloudCredentialsReferenceApplyConfiguration { + return applyconfigv1alpha1.CloudCredentialsReference(). + WithSecretName("openstack-credentials"). + WithCloudName("openstack") +} diff --git a/test/apivalidations/image_test.go b/test/apivalidations/image_test.go index ab1027b85..baf366652 100644 --- a/test/apivalidations/image_test.go +++ b/test/apivalidations/image_test.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Kubernetes Authors. +Copyright 2024 The ORC Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -41,12 +41,6 @@ func imageStub(name string, namespace *corev1.Namespace) *orcv1alpha1.Image { return obj } -func testCredentials() *applyconfigv1alpha1.CloudCredentialsReferenceApplyConfiguration { - return applyconfigv1alpha1.CloudCredentialsReference(). - WithSecretName("openstack-credentials"). - WithCloudName("openstack") -} - func testResource() *applyconfigv1alpha1.ImageResourceSpecApplyConfiguration { return applyconfigv1alpha1.ImageResourceSpec(). WithContent(applyconfigv1alpha1.ImageContent(). @@ -285,7 +279,7 @@ var _ = Describe("ORC Image API validations", func() { It("should not permit modifying resource.name", func(ctx context.Context) { testMutability(ctx, namespace, - func(applyConfig *applyconfigv1alpha1.ImageApplyConfiguration) func(string) *applyconfigv1alpha1.ImageResourceSpecApplyConfiguration { + func(applyConfig *applyconfigv1alpha1.ImageApplyConfiguration) func(orcv1alpha1.OpenStackName) *applyconfigv1alpha1.ImageResourceSpecApplyConfiguration { return applyConfig.Spec.Resource.WithName }, "foo", "bar", true, diff --git a/test/apivalidations/suite_test.go b/test/apivalidations/suite_test.go index 70668f7dd..30f120ad3 100644 --- a/test/apivalidations/suite_test.go +++ b/test/apivalidations/suite_test.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Kubernetes Authors. +Copyright 2024 The ORC Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -29,6 +29,7 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/discovery" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" @@ -40,7 +41,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" orcv1alpha1 "github.com/k-orc/openstack-resource-controller/api/v1alpha1" - "github.com/k-orc/openstack-resource-controller/internal/util/ssa" + "github.com/k-orc/openstack-resource-controller/internal/util/applyconfigs" ) var ( @@ -157,5 +158,5 @@ func createNamespace() *corev1.Namespace { } func applyObj(ctx context.Context, obj client.Object, patch any) error { - return k8sClient.Patch(ctx, obj, ssa.ApplyConfigPatch(patch), client.ForceOwnership, client.FieldOwner("capo-apivalidations")) + return k8sClient.Patch(ctx, obj, applyconfigs.Patch(types.ApplyPatchType, patch), client.ForceOwnership, client.FieldOwner("capo-apivalidations")) }