diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index 94540566..00000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright 2020 The Kubernetes Authors. -# SPDX-License-Identifier: Apache-2.0 - -name: goreleaser - -on: - push: - tags: - - 'v[0-9]+.[0-9]+.[0-9]+' - -jobs: - goreleaser: - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set up Go - uses: actions/setup-go@v4 - with: - go-version-file: "go.mod" - cache-dependency-path: | - **/go.sum - **/go.mod - - - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v5 - with: - version: latest - args: release --rm-dist -f release/goreleaser.yml - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 47972ba7..c177a2fd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,3 @@ -# Copyright 2023 The Flux Authors. -# SPDX-License-Identifier: Apache-2.0 - name: test on: @@ -16,27 +13,21 @@ jobs: permissions: contents: read steps: - - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v4 + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Setup Go + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: - go-version: 1.25.x + go-version: 1.26.x cache-dependency-path: | **/go.sum **/go.mod - - - name: Test - run: | - make tidy - make test - - - name: Check if working tree is dirty + - name: Test + run: make + - name: Check if working tree is dirty run: | if [[ $(git diff --stat) != '' ]]; then git diff - echo 'run make test and commit changes' + echo 'run make and commit changes' exit 1 fi diff --git a/.gitignore b/.gitignore index 9c9be4f7..29bd7416 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,6 @@ vendor/ # go artifacts cover.out + +# local binaries +bin/ \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..b2103843 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,3 @@ +## Code of Conduct + +The Flux project follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1690bd6a..b1ba7c82 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,68 +1,68 @@ -# Contributing Guidelines +# Contributing -Welcome to Kubernetes. We are excited about the prospect of you joining our [community](https://git.k8s.io/community)! The Kubernetes community abides by the CNCF [code of conduct](code-of-conduct.md). Here is an excerpt: +This project is [Apache 2.0 licensed](LICENSE) and accepts contributions +via GitHub pull requests. This document outlines some of the conventions on +to make it easier to get your contribution accepted. -_As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities._ +We gratefully welcome improvements to issues and documentation as well as to +code. -## Getting Started +## Certificate of Origin -We have full documentation on how to get started contributing here: +By contributing to this project you agree to the Developer Certificate of +Origin (DCO). This document was created by the Linux Kernel community and is a +simple statement that you, as a contributor, have the legal right to make the +contribution. No action from you is required, but it's a good idea to see the +[DCO](DCO) file for details before you start contributing code to FluxCD +pkg. - +## Communications -- [Contributor License Agreement](https://git.k8s.io/community/CLA.md) Kubernetes projects require that you sign a Contributor License Agreement (CLA) before we can accept your pull requests -- [Kubernetes Contributor Guide](https://git.k8s.io/community/contributors/guide) - Main contributor documentation, or you can just jump directly to the [contributing section](https://git.k8s.io/community/contributors/guide#contributing) -- [Contributor Cheat Sheet](https://git.k8s.io/community/contributors/guide/contributor-cheatsheet/README.md) - Common resources for existing developers +The project uses Slack: To join the conversation, simply join the +[CNCF](https://slack.cncf.io/) Slack workspace and use the +[#flux](https://cloud-native.slack.com/messages/flux/) channel. -## Mentorship +The developers use a mailing list to discuss development as well. +Simply subscribe to [flux-dev on cncf.io](https://lists.cncf.io/g/cncf-flux-dev) +to join the conversation (this will also add an invitation to your +Google calendar for our [Flux +meeting](https://docs.google.com/document/d/1l_M0om0qUEN_NNiGgpqJ2tvsF2iioHkaARDeh6b70B0/edit#)). -- [Mentoring Initiatives](https://git.k8s.io/community/mentoring) - We have a diverse set of mentorship programs available that are always looking for volunteers! +### How to run the test suite -## Contact Information +You can run the unit tests by simply doing -- [Slack channel](https://kubernetes.slack.com/messages/sig-cli) -- [Mailing list](https://groups.google.com/forum/#!forum/kubernetes-sig-cli) +```bash +make test +``` -## Setup a Dev Environment +## Acceptance policy -- install [go](https://golang.org/doc/install) -- `export GO111MODULE=on` -- install [wire](https://github.com/google/wire/) +These things will make a PR more likely to be accepted: -## Build and Test +- a well-described requirement +- tests for new code +- tests for old code! +- new code and tests follow the conventions in old code and tests +- a good commit message (see below) +- all code must abide [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments) +- names should abide [What's in a name](https://talks.golang.org/2014/names.slide#1) +- code must build on both Linux and Darwin, via plain `go build` +- code should have appropriate test coverage and tests should be written + to work with `go test` -1. `go generate` - - Generates the `wire_gen.go` files -1. `go test ./...` - - Test the -1. `golint -min_confidence 0.9 ./...` - - Look for errors -1. `go build` - - Build the binary +In general, we will merge a PR once one maintainer has endorsed it. +For substantial changes, more people may become involved, and you might +get asked to resubmit the PR or divide the changes into more than one PR. -## Dependency Injection +### Format of the Commit Message -This repo uses Dependency Injection for wiring together the Commands. See the -[wire tutorial](https://github.com/google/wire/tree/master/_tutorial) for more on DI. +For Source Controller we prefer the following rules for good commit messages: -## Adding a Command +- Limit the subject to 50 characters and write as the continuation + of the sentence "If applied, this commit will ..." +- Explain what and why in the body, if more than a trivial change; + wrap it at 72 characters. -1. Add a new package for your cobra command under `cmd/` - - e.g. `kubectl apply status` would be added under `cmd/apply/status` - - Add it to the parent command - - Copy an existing command as an example -1. Add a new package that contains the library for your command under `internal/pkg` - - e.g. `kubectl apply status` library would be added under `internal/pkg/status` - - Invoke it from the command you added - - Copy an existing package as an example -1. Add the DI wiring for your library - - Edit `internal/pkg/wiring/wiring.go` - Add your struct to the `ProviderSet` list - - Edit `internal/pkg/wiring/wire.go` - Add an `Initialize` function for you struct - -## Adding a Library (non-internal) - -1. Add a new package for your library under `pkg` -1. Add a new package that contains the implementation under `internal/pkg` - - Invoke it from your public package +The [following article](https://chris.beams.io/posts/git-commit/#seven-rules) +has some more helpful advice on documenting your work. diff --git a/DCO b/DCO new file mode 100644 index 00000000..716561d5 --- /dev/null +++ b/DCO @@ -0,0 +1,36 @@ +Developer Certificate of Origin +Version 1.1 + +Copyright (C) 2004, 2006 The Linux Foundation and its contributors. +660 York Street, Suite 102, +San Francisco, CA 94110 USA + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + + +Developer's Certificate of Origin 1.1 + +By making a contribution to this project, I certify that: + +(a) The contribution was created in whole or in part by me and I + have the right to submit it under the open source license + indicated in the file; or + +(b) The contribution is based upon previous work that, to the best + of my knowledge, is covered under an appropriate open source + license and I have the right under that license to submit that + work with modifications, whether created in whole or in part + by me, under the same open source license (unless I am + permitted to submit under a different license), as indicated + in the file; or + +(c) The contribution was provided directly to me by some other + person who certified (a), (b) or (c) and I have not modified + it. + +(d) I understand and agree that this project and the contribution + are public and that a record of the contribution (including all + personal information I submit with it, including my sign-off) is + maintained indefinitely and may be redistributed consistent with + this project or the open source license(s) involved. diff --git a/LICENSE_TEMPLATE b/LICENSE_TEMPLATE deleted file mode 100644 index 0c2b3b65..00000000 --- a/LICENSE_TEMPLATE +++ /dev/null @@ -1,2 +0,0 @@ -Copyright {{.Year}} {{.Holder}} -SPDX-License-Identifier: Apache-2.0 diff --git a/LICENSE_TEMPLATE_GO b/LICENSE_TEMPLATE_GO deleted file mode 100644 index ef790b66..00000000 --- a/LICENSE_TEMPLATE_GO +++ /dev/null @@ -1,2 +0,0 @@ -// Copyright YEAR The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 diff --git a/MAINTAINERS b/MAINTAINERS new file mode 100644 index 00000000..966a4287 --- /dev/null +++ b/MAINTAINERS @@ -0,0 +1,7 @@ +The maintainers are generally available in Slack at +https://cloud-native.slack.com in #flux (https://cloud-native.slack.com/messages/CLAJ40HV3) +(obtain an invitation at https://slack.cncf.io/). + +The maintainers of this project as listed in + + https://github.com/fluxcd/community/blob/main/CORE-MAINTAINERS diff --git a/Makefile b/Makefile index adc4f8ae..f29bdf91 100644 --- a/Makefile +++ b/Makefile @@ -1,139 +1,81 @@ -# Copyright 2019 The Kubernetes Authors. +# Copyright 2026 The Flux Authors. # SPDX-License-Identifier: Apache-2.0 -GOPATH := $(shell go env GOPATH) -MYGOBIN := $(shell go env GOPATH)/bin -SHELL := /bin/bash -export PATH := $(MYGOBIN):$(PATH) +# Get the currently used golang install path +# (in GOPATH/bin, unless GOBIN is set). +ifeq (,$(shell go env GOBIN)) +GOBIN=$(shell go env GOPATH)/bin +else +GOBIN=$(shell go env GOBIN) +endif -.PHONY: all -all: generate license fix vet fmt test lint tidy - -"$(MYGOBIN)/stringer": - go install golang.org/x/tools/cmd/stringer@v0.12.0 - -"$(MYGOBIN)/addlicense": - go install github.com/google/addlicense@v1.0.0 - -"$(MYGOBIN)/golangci-lint": - go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.7.2 - -"$(MYGOBIN)/deepcopy-gen": - go install k8s.io/code-generator/cmd/deepcopy-gen@v0.25.2 - -"$(MYGOBIN)/ginkgo": - go install github.com/onsi/ginkgo/v2/ginkgo@v2.2.0 - -"$(MYGOBIN)/mdrip": - go install github.com/monopole/mdrip@v1.0.2 - -"$(MYGOBIN)/kind": - go install sigs.k8s.io/kind@v0.16.0 - -# The following target intended for reference by a file in -# https://github.com/kubernetes/test-infra/tree/master/config/jobs/kubernetes-sigs/cli-utils -.PHONY: prow-presubmit-check -prow-presubmit-check: \ - test lint verify-license - -.PHONY: prow-presubmit-check-e2e -prow-presubmit-check-e2e: \ - install-column-apt test-e2e verify-kapply-e2e - -.PHONY: prow-presubmit-check-stress -prow-presubmit-check-stress: \ - test-stress +# Setting SHELL to bash allows bash commands to be executed by recipes. +# Options are set to exit when a recipe line exits non-zero or a piped command fails. +SHELL = /usr/bin/env bash -o pipefail +.SHELLFLAGS = -ec -.PHONY: fix -fix: - go fix ./... +.PHONY: all +all: tidy vet fmt lint test .PHONY: fmt fmt: go fmt ./... -# Install column (required by verify-kapply-e2e) -# Update is included because the kubekins-e2e container build strips out the package cache. -# In newer versions of debian, column is in the bsdextrautils package, -# but in buster (used by kubekins-e2e) it's in bsdmainutils. -.PHONY: install-column-apt -install-column-apt: - apt-get update - apt-get install -y bsdmainutils - -.PHONY: generate-deepcopy -generate-deepcopy: "$(MYGOBIN)/deepcopy-gen" - hack/run-in-gopath.sh deepcopy-gen --input-dirs ./pkg/apis/... -O zz_generated.deepcopy --go-header-file ./LICENSE_TEMPLATE_GO - -.PHONY: generate -generate: "$(MYGOBIN)/stringer" generate-deepcopy - go generate ./... - -.PHONY: license -license: "$(MYGOBIN)/addlicense" - "$(MYGOBIN)/addlicense" -v -y 2021 -c "The Kubernetes Authors." -f LICENSE_TEMPLATE . - -.PHONY: verify-license -verify-license: "$(MYGOBIN)/addlicense" - "$(MYGOBIN)/addlicense" -check . +.PHONY: lint +lint: golangci-lint ## Run golangci linters and ESLint. + $(GOLANGCI_LINT) run .PHONY: tidy tidy: go mod tidy -.PHONY: lint -lint: "$(MYGOBIN)/golangci-lint" - "$(MYGOBIN)/golangci-lint" run ./... - .PHONY: test test: - go test -race -cover ./cmd/... ./pkg/... - -.PHONY: test-e2e -test-e2e: "$(MYGOBIN)/ginkgo" "$(MYGOBIN)/kind" - kind delete cluster --name=cli-utils-e2e && kind create cluster --name=cli-utils-e2e --wait 5m - "$(MYGOBIN)/ginkgo" -v ./test/e2e/... -- -v 3 - -.PHONY: test-e2e-focus -test-e2e-focus: "$(MYGOBIN)/ginkgo" "$(MYGOBIN)/kind" - kind delete cluster --name=cli-utils-e2e && kind create cluster --name=cli-utils-e2e --wait 5m - "$(MYGOBIN)"/ginkgo -v -focus ".*$(FOCUS).*" ./test/e2e/... -- -v 5 - -.PHONY: test-stress -test-stress: "$(MYGOBIN)/ginkgo" "$(MYGOBIN)/kind" - kind delete cluster --name=cli-utils-e2e && kind create cluster --name=cli-utils-e2e --wait 5m \ - --config=./test/stress/kind-cluster.yaml - kubectl wait nodes --for=condition=ready --all --timeout=5m - "$(MYGOBIN)/ginkgo" -v ./test/stress/... -- -v 3 + go test -race -cover ./pkg/... .PHONY: vet vet: go vet ./... -.PHONY: build -build: - go build -o bin/kapply github.com/fluxcd/cli-utils/cmd; - mv bin/kapply "$(MYGOBIN)" - -.PHONY: build-with-race-detector -build-with-race-detector: - go build -race -o bin/kapply github.com/fluxcd/cli-utils/cmd; - mv bin/kapply "$(MYGOBIN)" - -.PHONY: verify-kapply-e2e -verify-kapply-e2e: test-examples-e2e-kapply - -.PHONY: test-examples-e2e-kapply -test-examples-e2e-kapply: "$(MYGOBIN)/mdrip" "$(MYGOBIN)/kind" - ( \ - set -e; \ - /bin/rm -f bin/kapply; \ - /bin/rm -f "$(MYGOBIN)/kapply"; \ - echo "Installing kapply from ."; \ - make build-with-race-detector; \ - ./hack/testExamplesE2EAgainstKapply.sh .; \ - ) - -.PHONY: nuke -nuke: - sudo rm -rf "$(GOPATH)/pkg/mod/sigs.k8s.io" +##@ Dependencies + +## Location to install dependencies to +LOCALBIN ?= $(shell pwd)/bin +$(LOCALBIN): + mkdir -p $(LOCALBIN) + +## Tool Binaries +GOLANGCI_LINT = $(LOCALBIN)/golangci-lint-$(GOLANGCI_LINT_VERSION) +GOVULNCHECK ?= $(LOCALBIN)/govulncheck + +GOLANGCI_LINT_VERSION ?= v2.11.4 +.PHONY: golangci-lint +golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary. +$(GOLANGCI_LINT): $(LOCALBIN) + $(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/v2/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION)) + +.PHONY: govulncheck +govulncheck: $(GOVULNCHECK) ## Run govulncheck. +$(GOVULNCHECK): $(LOCALBIN) + $(call go-install-tool,$(GOVULNCHECK),golang.org/x/vuln/cmd/govulncheck,latest) + @$(GOVULNCHECK) ./... + +# 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 (ideally with version) +# $2 - package url which can be installed +# $3 - specific version of package +define go-install-tool +@[ -f $(1) ] || { \ +set -e; \ +package=$(2)@$(3) ;\ +echo "Downloading $${package}" ;\ +GOBIN=$(LOCALBIN) go install $${package} ;\ +mv "$$(echo "$(1)" | sed "s/-$(3)$$//")" $(1) ;\ +} +endef + +##@ General + +.PHONY: help +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) diff --git a/README.md b/README.md index 91edcdf2..54e393b6 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,8 @@ [![license](https://img.shields.io/github/license/fluxcd/cli-utils.svg)](https://github.com/fluxcd/cli-utils/blob/main/LICENSE) [![test](https://github.com/fluxcd/cli-utils/workflows/test/badge.svg)](https://github.com/fluxcd/cli-utils/actions) -This repository is a hard fork of [kubernetes-sigs/cli-utils](https://github.com/kubernetes-sigs/cli-utils). +This repository is a hard fork of [kubernetes-sigs/cli-utils](https://github.com/kubernetes-sigs/cli-utils) reducing it to the `kstatus` package +and adding extensions for Flux's use cases. We've forked `cli-utils` in 2023 as the upstream repo lagged months behind Kubernetes & Kustomize, which [blocked](https://github.com/fluxcd/flux2/issues/3564) our ability to use the latest Kustomize features in Flux. diff --git a/cmd/apply/cmdapply.go b/cmd/apply/cmdapply.go deleted file mode 100644 index bd81c678..00000000 --- a/cmd/apply/cmdapply.go +++ /dev/null @@ -1,171 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package apply - -import ( - "context" - "fmt" - "strings" - "time" - - "github.com/fluxcd/cli-utils/cmd/flagutils" - "github.com/fluxcd/cli-utils/pkg/apply" - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/pkg/manifestreader" - "github.com/fluxcd/cli-utils/pkg/printers" - "github.com/spf13/cobra" - "k8s.io/cli-runtime/pkg/genericclioptions" - cmdutil "k8s.io/kubectl/pkg/cmd/util" - "k8s.io/kubectl/pkg/util/i18n" -) - -func GetRunner(factory cmdutil.Factory, invFactory inventory.ClientFactory, - loader manifestreader.ManifestLoader, ioStreams genericclioptions.IOStreams) *Runner { - r := &Runner{ - ioStreams: ioStreams, - factory: factory, - invFactory: invFactory, - loader: loader, - } - cmd := &cobra.Command{ - Use: "apply (DIRECTORY | STDIN)", - DisableFlagsInUseLine: true, - Short: i18n.T("Apply a configuration to a resource by package directory or stdin"), - RunE: r.RunE, - } - - cmd.Flags().BoolVar(&r.serverSideOptions.ServerSideApply, "server-side", false, - "If true, apply merge patch is calculated on API server instead of client.") - cmd.Flags().BoolVar(&r.serverSideOptions.ForceConflicts, "force-conflicts", false, - "If true, overwrite applied fields on server if field manager conflict.") - cmd.Flags().StringVar(&r.serverSideOptions.FieldManager, "field-manager", common.DefaultFieldManager, - "The client owner of the fields being applied on the server-side.") - - cmd.Flags().StringVar(&r.output, "output", printers.DefaultPrinter(), - fmt.Sprintf("Output format, must be one of %s", strings.Join(printers.SupportedPrinters(), ","))) - cmd.Flags().DurationVar(&r.reconcileTimeout, "reconcile-timeout", time.Duration(0), - "Timeout threshold for waiting for all resources to reach the Current status.") - cmd.Flags().BoolVar(&r.noPrune, "no-prune", r.noPrune, - "If true, do not prune previously applied objects.") - cmd.Flags().StringVar(&r.prunePropagationPolicy, "prune-propagation-policy", - "Background", "Propagation policy for pruning") - cmd.Flags().DurationVar(&r.pruneTimeout, "prune-timeout", time.Duration(0), - "Timeout threshold for waiting for all pruned resources to be deleted") - cmd.Flags().StringVar(&r.inventoryPolicy, flagutils.InventoryPolicyFlag, flagutils.InventoryPolicyStrict, - "It determines the behavior when the resources don't belong to current inventory. Available options "+ - fmt.Sprintf("%q, %q and %q.", flagutils.InventoryPolicyStrict, flagutils.InventoryPolicyAdopt, flagutils.InventoryPolicyForceAdopt)) - cmd.Flags().DurationVar(&r.timeout, "timeout", 0, - "How long to wait before exiting") - cmd.Flags().BoolVar(&r.printStatusEvents, "status-events", false, - "Print status events (always enabled for table output)") - - r.Command = cmd - return r -} - -func Command(f cmdutil.Factory, invFactory inventory.ClientFactory, loader manifestreader.ManifestLoader, - ioStreams genericclioptions.IOStreams) *cobra.Command { - return GetRunner(f, invFactory, loader, ioStreams).Command -} - -type Runner struct { - Command *cobra.Command - ioStreams genericclioptions.IOStreams - factory cmdutil.Factory - invFactory inventory.ClientFactory - loader manifestreader.ManifestLoader - - serverSideOptions common.ServerSideOptions - output string - reconcileTimeout time.Duration - noPrune bool - prunePropagationPolicy string - pruneTimeout time.Duration - inventoryPolicy string - timeout time.Duration - printStatusEvents bool -} - -func (r *Runner) RunE(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - // If specified, cancel with timeout. - if r.timeout != 0 { - var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, r.timeout) - defer cancel() - } - - prunePropPolicy, err := flagutils.ConvertPropagationPolicy(r.prunePropagationPolicy) - if err != nil { - return err - } - inventoryPolicy, err := flagutils.ConvertInventoryPolicy(r.inventoryPolicy) - if err != nil { - return err - } - - if found := printers.ValidatePrinterType(r.output); !found { - return fmt.Errorf("unknown output type %q", r.output) - } - - // TODO: Fix DemandOneDirectory to no longer return FileNameFlags - // since we are no longer using them. - _, err = common.DemandOneDirectory(args) - if err != nil { - return err - } - reader, err := r.loader.ManifestReader(cmd.InOrStdin(), flagutils.PathFromArgs(args)) - if err != nil { - return err - } - objs, err := reader.Read() - if err != nil { - return err - } - - invObj, objs, err := inventory.SplitUnstructureds(objs) - if err != nil { - return err - } - inv := inventory.WrapInventoryInfoObj(invObj) - - invClient, err := r.invFactory.NewClient(r.factory) - if err != nil { - return err - } - - // Run the applier. It will return a channel where we can receive updates - // to keep track of progress and any issues. - a, err := apply.NewApplierBuilder(). - WithFactory(r.factory). - WithInventoryClient(invClient). - Build() - if err != nil { - return err - } - - // Always enable status events for the table printer - if r.output == printers.TablePrinter { - r.printStatusEvents = true - } - - ch := a.Run(ctx, inv, objs, apply.ApplierOptions{ - ServerSideOptions: r.serverSideOptions, - ReconcileTimeout: r.reconcileTimeout, - // If we are not waiting for status, tell the applier to not - // emit the events. - EmitStatusEvents: r.printStatusEvents, - NoPrune: r.noPrune, - DryRunStrategy: common.DryRunNone, - PrunePropagationPolicy: prunePropPolicy, - PruneTimeout: r.pruneTimeout, - InventoryPolicy: inventoryPolicy, - }) - - // The printer will print updates from the channel. It will block - // until the channel is closed. - printer := printers.GetPrinter(r.output, r.ioStreams) - return printer.Print(ch, common.DryRunNone, r.printStatusEvents) -} diff --git a/cmd/destroy/cmddestroy.go b/cmd/destroy/cmddestroy.go deleted file mode 100644 index 59b5266a..00000000 --- a/cmd/destroy/cmddestroy.go +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package destroy - -import ( - "context" - "fmt" - "strings" - "time" - - "github.com/fluxcd/cli-utils/cmd/flagutils" - "github.com/fluxcd/cli-utils/pkg/apply" - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/pkg/manifestreader" - "github.com/fluxcd/cli-utils/pkg/printers" - "github.com/spf13/cobra" - "k8s.io/cli-runtime/pkg/genericclioptions" - cmdutil "k8s.io/kubectl/pkg/cmd/util" - "k8s.io/kubectl/pkg/util/i18n" -) - -// GetRunner creates and returns the Runner which stores the cobra command. -func GetRunner(factory cmdutil.Factory, invFactory inventory.ClientFactory, - loader manifestreader.ManifestLoader, ioStreams genericclioptions.IOStreams) *Runner { - r := &Runner{ - ioStreams: ioStreams, - factory: factory, - invFactory: invFactory, - loader: loader, - } - cmd := &cobra.Command{ - Use: "destroy (DIRECTORY | STDIN)", - DisableFlagsInUseLine: true, - Short: i18n.T("Destroy all the resources related to configuration"), - RunE: r.RunE, - } - - cmd.Flags().StringVar(&r.output, "output", printers.DefaultPrinter(), - fmt.Sprintf("Output format, must be one of %s", strings.Join(printers.SupportedPrinters(), ","))) - cmd.Flags().StringVar(&r.inventoryPolicy, flagutils.InventoryPolicyFlag, flagutils.InventoryPolicyStrict, - "It determines the behavior when the resources don't belong to current inventory. Available options "+ - fmt.Sprintf("%q, %q and %q.", flagutils.InventoryPolicyStrict, flagutils.InventoryPolicyAdopt, flagutils.InventoryPolicyForceAdopt)) - cmd.Flags().DurationVar(&r.deleteTimeout, "delete-timeout", time.Duration(0), - "Timeout threshold for waiting for all deleted resources to complete deletion") - cmd.Flags().StringVar(&r.deletePropagationPolicy, "delete-propagation-policy", - "Background", "Propagation policy for deletion") - cmd.Flags().DurationVar(&r.timeout, "timeout", 0, - "How long to wait before exiting") - cmd.Flags().BoolVar(&r.printStatusEvents, "status-events", false, - "Print status events (always enabled for table output)") - - r.Command = cmd - return r -} - -// Command creates the Runner, returning the cobra command associated with it. -func Command(f cmdutil.Factory, invFactory inventory.ClientFactory, loader manifestreader.ManifestLoader, - ioStreams genericclioptions.IOStreams) *cobra.Command { - return GetRunner(f, invFactory, loader, ioStreams).Command -} - -// Runner encapsulates data necessary to run the destroy command. -type Runner struct { - Command *cobra.Command - ioStreams genericclioptions.IOStreams - factory cmdutil.Factory - invFactory inventory.ClientFactory - loader manifestreader.ManifestLoader - - output string - deleteTimeout time.Duration - deletePropagationPolicy string - inventoryPolicy string - timeout time.Duration - printStatusEvents bool -} - -func (r *Runner) RunE(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - // If specified, cancel with timeout. - if r.timeout != 0 { - var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, r.timeout) - defer cancel() - } - - deletePropPolicy, err := flagutils.ConvertPropagationPolicy(r.deletePropagationPolicy) - if err != nil { - return err - } - inventoryPolicy, err := flagutils.ConvertInventoryPolicy(r.inventoryPolicy) - if err != nil { - return err - } - - if found := printers.ValidatePrinterType(r.output); !found { - return fmt.Errorf("unknown output type %q", r.output) - } - - // Retrieve the inventory object. - reader, err := r.loader.ManifestReader(cmd.InOrStdin(), flagutils.PathFromArgs(args)) - if err != nil { - return err - } - objs, err := reader.Read() - if err != nil { - return err - } - invObj, _, err := inventory.SplitUnstructureds(objs) - if err != nil { - return err - } - inv := inventory.WrapInventoryInfoObj(invObj) - - invClient, err := r.invFactory.NewClient(r.factory) - if err != nil { - return err - } - d, err := apply.NewDestroyerBuilder(). - WithFactory(r.factory). - WithInventoryClient(invClient). - Build() - if err != nil { - return err - } - - // Always enable status events for the table printer - if r.output == printers.TablePrinter { - r.printStatusEvents = true - } - - // Run the destroyer. It will return a channel where we can receive updates - // to keep track of progress and any issues. - ch := d.Run(ctx, inv, apply.DestroyerOptions{ - DeleteTimeout: r.deleteTimeout, - DeletePropagationPolicy: deletePropPolicy, - InventoryPolicy: inventoryPolicy, - EmitStatusEvents: r.printStatusEvents, - }) - - // The printer will print updates from the channel. It will block - // until the channel is closed. - printer := printers.GetPrinter(r.output, r.ioStreams) - return printer.Print(ch, common.DryRunNone, r.printStatusEvents) -} diff --git a/cmd/flagutils/utils.go b/cmd/flagutils/utils.go deleted file mode 100644 index 3e2d102a..00000000 --- a/cmd/flagutils/utils.go +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package flagutils - -import ( - "fmt" - - "github.com/fluxcd/cli-utils/pkg/inventory" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -const ( - InventoryPolicyFlag = "inventory-policy" - InventoryPolicyStrict = "strict" - InventoryPolicyAdopt = "adopt" - InventoryPolicyForceAdopt = "force-adopt" -) - -// ConvertPropagationPolicy converts a propagationPolicy described as a -// string to a DeletionPropagation type that is passed into the Applier. -func ConvertPropagationPolicy(propagationPolicy string) (metav1.DeletionPropagation, error) { - switch propagationPolicy { - case string(metav1.DeletePropagationForeground): - return metav1.DeletePropagationForeground, nil - case string(metav1.DeletePropagationBackground): - return metav1.DeletePropagationBackground, nil - case string(metav1.DeletePropagationOrphan): - return metav1.DeletePropagationOrphan, nil - default: - return metav1.DeletePropagationBackground, fmt.Errorf( - "prune propagation policy must be one of Background, Foreground, Orphan") - } -} - -func ConvertInventoryPolicy(policy string) (inventory.Policy, error) { - switch policy { - case InventoryPolicyStrict: - return inventory.PolicyMustMatch, nil - case InventoryPolicyAdopt: - return inventory.PolicyAdoptIfNoInventory, nil - case InventoryPolicyForceAdopt: - return inventory.PolicyAdoptAll, nil - default: - return inventory.PolicyMustMatch, fmt.Errorf( - "inventory policy must be one of strict, adopt") - } -} - -// PathFromArgs returns the path which is a positional arg from args list -// returns "-" if there is length of args is 0, which implies no path is provided -func PathFromArgs(args []string) string { - if len(args) == 0 { - return "-" - } - return args[0] -} diff --git a/cmd/flagutils/utils_test.go b/cmd/flagutils/utils_test.go deleted file mode 100644 index 5b672136..00000000 --- a/cmd/flagutils/utils_test.go +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package flagutils - -import ( - "fmt" - "testing" - - "github.com/fluxcd/cli-utils/pkg/inventory" -) - -func TestConvertInventoryPolicy(t *testing.T) { - testcases := []struct { - value string - policy inventory.Policy - err error - }{ - { - value: "strict", - policy: inventory.PolicyMustMatch, - }, - { - value: "adopt", - policy: inventory.PolicyAdoptIfNoInventory, - }, - { - value: "force-adopt", - policy: inventory.PolicyAdoptAll, - }, - { - value: "random", - err: fmt.Errorf("inventory policy must be one of strict, adopt"), - }, - } - for _, tc := range testcases { - t.Run(tc.value, func(t *testing.T) { - policy, err := ConvertInventoryPolicy(tc.value) - if tc.err == nil { - if err != nil { - t.Errorf("unexpected error %v", err) - } - if policy != tc.policy { - t.Errorf("expected %v but got %v", policy, tc.policy) - } - } - if err == nil && tc.err != nil { - t.Errorf("expected an error, but not happened") - } - }) - } -} diff --git a/cmd/initcmd/cmdinit.go b/cmd/initcmd/cmdinit.go deleted file mode 100644 index 1f789546..00000000 --- a/cmd/initcmd/cmdinit.go +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package initcmd - -import ( - "github.com/fluxcd/cli-utils/pkg/config" - "github.com/spf13/cobra" - "k8s.io/cli-runtime/pkg/genericclioptions" - cmdutil "k8s.io/kubectl/pkg/cmd/util" - "k8s.io/kubectl/pkg/util/i18n" -) - -// InitRunner encapsulates the structures for the init command. -type InitRunner struct { - Command *cobra.Command - InitOptions *config.InitOptions -} - -// GetInitRunner builds and returns the InitRunner. Connects the InitOptions.Run -// to the cobra command. -func GetInitRunner(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *InitRunner { - io := config.NewInitOptions(f, ioStreams) - cmd := &cobra.Command{ - Use: "init DIRECTORY", - DisableFlagsInUseLine: true, - Short: i18n.T("Create a prune manifest ConfigMap as a inventory object"), - RunE: func(cmd *cobra.Command, args []string) error { - err := io.Complete(args) - if err != nil { - return err - } - return io.Run() - }, - } - cmd.Flags().StringVarP(&io.InventoryID, "inventory-id", "i", "", "Identifier for group of applied resources. Must be composed of valid label characters.") - i := &InitRunner{ - Command: cmd, - InitOptions: io, - } - return i -} - -// NewCmdInit returns the cobra command for the init command. -func NewCmdInit(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command { - return GetInitRunner(f, ioStreams).Command -} diff --git a/cmd/main.go b/cmd/main.go deleted file mode 100644 index 118a365a..00000000 --- a/cmd/main.go +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package main - -import ( - "context" - "flag" - "fmt" - "os" - "strings" - "time" - - "github.com/spf13/cobra" - "k8s.io/cli-runtime/pkg/genericclioptions" - "k8s.io/client-go/rest" - "k8s.io/component-base/cli" - "k8s.io/klog/v2" - "k8s.io/kubectl/pkg/cmd/util" - - "github.com/fluxcd/cli-utils/cmd/apply" - "github.com/fluxcd/cli-utils/cmd/destroy" - "github.com/fluxcd/cli-utils/cmd/initcmd" - "github.com/fluxcd/cli-utils/cmd/preview" - "github.com/fluxcd/cli-utils/cmd/status" - "github.com/fluxcd/cli-utils/pkg/flowcontrol" - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/pkg/manifestreader" - - // This is here rather than in the libraries because of - // https://github.com/kubernetes-sigs/kustomize/issues/2060 - _ "k8s.io/client-go/plugin/pkg/client/auth" -) - -func main() { - cmd := &cobra.Command{ - Use: "kapply", - Short: "Perform cluster operations using declarative configuration", - Long: "Perform cluster operations using declarative configuration", - // We silence error reporting from Cobra here since we want to improve - // the error messages coming from the commands. - SilenceErrors: true, - SilenceUsage: true, - } - - // configure kubectl dependencies and flags - flags := cmd.PersistentFlags() - kubeConfigFlags := genericclioptions.NewConfigFlags(true).WithDeprecatedPasswordFlag() - kubeConfigFlags.AddFlags(flags) - matchVersionKubeConfigFlags := util.NewMatchVersionFlags(kubeConfigFlags) - matchVersionKubeConfigFlags.AddFlags(flags) - flags.AddGoFlagSet(flag.CommandLine) - f := util.NewFactory(matchVersionKubeConfigFlags) - - // Update ConfigFlags before subcommands run that talk to the server. - preRunE := newConfigFilerPreRunE(f, kubeConfigFlags) - - ioStreams := genericclioptions.IOStreams{ - In: os.Stdin, - Out: os.Stdout, - ErrOut: os.Stderr, - } - - loader := manifestreader.NewManifestLoader(f) - invFactory := inventory.ClusterClientFactory{StatusPolicy: inventory.StatusPolicyNone} - - names := []string{"init", "apply", "destroy", "diff", "preview", "status"} - subCmds := []*cobra.Command{ - initcmd.NewCmdInit(f, ioStreams), - apply.Command(f, invFactory, loader, ioStreams), - destroy.Command(f, invFactory, loader, ioStreams), - preview.Command(f, invFactory, loader, ioStreams), - status.Command(context.TODO(), f, invFactory, status.NewInventoryLoader(loader)), - } - for _, subCmd := range subCmds { - subCmd.PreRunE = preRunE - updateHelp(names, subCmd) - cmd.AddCommand(subCmd) - } - - code := cli.Run(cmd) - os.Exit(code) -} - -// updateHelp replaces `kubectl` help messaging with `kapply` help messaging -func updateHelp(names []string, c *cobra.Command) { - for i := range names { - name := names[i] - c.Short = strings.ReplaceAll(c.Short, "kubectl "+name, "kapply "+name) - c.Long = strings.ReplaceAll(c.Long, "kubectl "+name, "kapply "+name) - c.Example = strings.ReplaceAll(c.Example, "kubectl "+name, "kapply "+name) - } -} - -// newConfigFilerPreRunE returns a cobra command PreRunE function that -// performs a lookup to determine if server-side throttling is enabled. If so, -// client-side throttling is disabled in the ConfigFlags. -func newConfigFilerPreRunE(f util.Factory, configFlags *genericclioptions.ConfigFlags) func(*cobra.Command, []string) error { - return func(_ *cobra.Command, args []string) error { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - restConfig, err := f.ToRESTConfig() - if err != nil { - return err - } - enabled, err := flowcontrol.IsEnabled(ctx, restConfig) - if err != nil { - return fmt.Errorf("checking server-side throttling enablement: %w", err) - } - if enabled { - // Disable client-side throttling. - klog.V(3).Infof("Client-side throttling disabled") - // WrapConfigFn will affect future Factory.ToRESTConfig() calls. - configFlags.WrapConfigFn = func(cfg *rest.Config) *rest.Config { - cfg.QPS = -1 - cfg.Burst = -1 - return cfg - } - } - return nil - } -} diff --git a/cmd/preview/cmdpreview.go b/cmd/preview/cmdpreview.go deleted file mode 100644 index 0067d878..00000000 --- a/cmd/preview/cmdpreview.go +++ /dev/null @@ -1,185 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package preview - -import ( - "context" - "fmt" - "strings" - "time" - - "github.com/fluxcd/cli-utils/cmd/flagutils" - "github.com/fluxcd/cli-utils/pkg/apply" - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/pkg/manifestreader" - "github.com/fluxcd/cli-utils/pkg/printers" - "github.com/spf13/cobra" - "k8s.io/cli-runtime/pkg/genericclioptions" - cmdutil "k8s.io/kubectl/pkg/cmd/util" - "k8s.io/kubectl/pkg/util/i18n" -) - -var ( - noPrune = false - previewDestroy = false -) - -// GetRunner creates and returns the Runner which stores the cobra command. -func GetRunner(factory cmdutil.Factory, invFactory inventory.ClientFactory, - loader manifestreader.ManifestLoader, ioStreams genericclioptions.IOStreams) *Runner { - r := &Runner{ - factory: factory, - invFactory: invFactory, - loader: loader, - ioStreams: ioStreams, - } - cmd := &cobra.Command{ - Use: "preview (DIRECTORY | STDIN)", - DisableFlagsInUseLine: true, - Short: i18n.T("Preview the apply of a configuration"), - Args: cobra.MaximumNArgs(1), - RunE: r.RunE, - } - - cmd.Flags().BoolVar(&noPrune, "no-prune", noPrune, "If true, do not prune previously applied objects.") - cmd.Flags().BoolVar(&r.serverSideOptions.ServerSideApply, "server-side", false, - "If true, preview runs in the server instead of the client.") - cmd.Flags().BoolVar(&r.serverSideOptions.ForceConflicts, "force-conflicts", false, - "If true during server-side preview, do not report field conflicts.") - cmd.Flags().StringVar(&r.serverSideOptions.FieldManager, "field-manager", common.DefaultFieldManager, - "If true during server-side preview, sets field owner.") - cmd.Flags().BoolVar(&previewDestroy, "destroy", previewDestroy, "If true, preview of destroy operations will be displayed.") - cmd.Flags().StringVar(&r.output, "output", printers.DefaultPrinter(), - fmt.Sprintf("Output format, must be one of %s", strings.Join(printers.SupportedPrinters(), ","))) - cmd.Flags().StringVar(&r.inventoryPolicy, flagutils.InventoryPolicyFlag, flagutils.InventoryPolicyStrict, - "It determines the behavior when the resources don't belong to current inventory. Available options "+ - fmt.Sprintf("%q, %q and %q.", flagutils.InventoryPolicyStrict, flagutils.InventoryPolicyAdopt, flagutils.InventoryPolicyForceAdopt)) - cmd.Flags().DurationVar(&r.timeout, "timeout", 0, - "How long to wait before exiting") - - r.Command = cmd - return r -} - -// Command creates the Runner, returning the cobra command associated with it. -func Command(f cmdutil.Factory, invFactory inventory.ClientFactory, loader manifestreader.ManifestLoader, - ioStreams genericclioptions.IOStreams) *cobra.Command { - return GetRunner(f, invFactory, loader, ioStreams).Command -} - -// Runner encapsulates data necessary to run the preview command. -type Runner struct { - Command *cobra.Command - factory cmdutil.Factory - invFactory inventory.ClientFactory - loader manifestreader.ManifestLoader - ioStreams genericclioptions.IOStreams - - serverSideOptions common.ServerSideOptions - output string - inventoryPolicy string - timeout time.Duration -} - -// RunE is the function run from the cobra command. -func (r *Runner) RunE(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - // If specified, cancel with timeout. - if r.timeout != 0 { - var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, r.timeout) - defer cancel() - } - - var ch <-chan event.Event - - drs := common.DryRunClient - if r.serverSideOptions.ServerSideApply { - drs = common.DryRunServer - } - - inventoryPolicy, err := flagutils.ConvertInventoryPolicy(r.inventoryPolicy) - if err != nil { - return err - } - - reader, err := r.loader.ManifestReader(cmd.InOrStdin(), flagutils.PathFromArgs(args)) - if err != nil { - return err - } - - if found := printers.ValidatePrinterType(r.output); !found { - return fmt.Errorf("unknown output type %q", r.output) - } - - objs, err := reader.Read() - if err != nil { - return err - } - - invObj, objs, err := inventory.SplitUnstructureds(objs) - if err != nil { - return err - } - inv := inventory.WrapInventoryInfoObj(invObj) - - invClient, err := r.invFactory.NewClient(r.factory) - if err != nil { - return err - } - - // if destroy flag is set in preview, transmit it to destroyer DryRunStrategy flag - // and pivot execution to destroy with dry-run - if !previewDestroy { - _, err = common.DemandOneDirectory(args) - if err != nil { - return err - } - a, err := apply.NewApplierBuilder(). - WithFactory(r.factory). - WithInventoryClient(invClient). - Build() - if err != nil { - return err - } - - // Run the applier. It will return a channel where we can receive updates - // to keep track of progress and any issues. - ch = a.Run(ctx, inv, objs, apply.ApplierOptions{ - EmitStatusEvents: false, - NoPrune: noPrune, - DryRunStrategy: drs, - ServerSideOptions: r.serverSideOptions, - InventoryPolicy: inventoryPolicy, - }) - } else { - d, err := apply.NewDestroyerBuilder(). - WithFactory(r.factory). - WithInventoryClient(invClient). - Build() - if err != nil { - return err - } - ch = d.Run(ctx, inv, apply.DestroyerOptions{ - InventoryPolicy: inventoryPolicy, - DryRunStrategy: drs, - }) - } - - // Print the preview strategy unless the output format is json. - if r.output != printers.JSONPrinter { - if drs.ServerDryRun() { - fmt.Println("Preview strategy: server") - } else { - fmt.Println("Preview strategy: client") - } - } - - // The printer will print updates from the channel. It will block - // until the channel is closed. - printer := printers.GetPrinter(r.output, r.ioStreams) - return printer.Print(ch, drs, false) // Do not print status -} diff --git a/cmd/status/cmdstatus.go b/cmd/status/cmdstatus.go deleted file mode 100644 index 1f869561..00000000 --- a/cmd/status/cmdstatus.go +++ /dev/null @@ -1,376 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package status - -import ( - "context" - "fmt" - "strings" - "time" - - "github.com/fluxcd/cli-utils/cmd/flagutils" - "github.com/fluxcd/cli-utils/cmd/status/printers" - "github.com/fluxcd/cli-utils/cmd/status/printers/printer" - "github.com/fluxcd/cli-utils/pkg/apply/poller" - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/pkg/kstatus/polling" - "github.com/fluxcd/cli-utils/pkg/kstatus/polling/aggregator" - "github.com/fluxcd/cli-utils/pkg/kstatus/polling/collector" - "github.com/fluxcd/cli-utils/pkg/kstatus/polling/event" - "github.com/fluxcd/cli-utils/pkg/kstatus/status" - "github.com/fluxcd/cli-utils/pkg/manifestreader" - "github.com/fluxcd/cli-utils/pkg/object" - printcommon "github.com/fluxcd/cli-utils/pkg/print/common" - pkgprinters "github.com/fluxcd/cli-utils/pkg/printers" - "github.com/spf13/cobra" - "k8s.io/cli-runtime/pkg/genericclioptions" - cmdutil "k8s.io/kubectl/pkg/cmd/util" - "k8s.io/kubectl/pkg/util/slice" -) - -const ( - Known = "known" - Current = "current" - Deleted = "deleted" - Forever = "forever" -) - -const ( - Local = "local" - Remote = "remote" -) - -var ( - PollUntilOptions = []string{Known, Current, Deleted, Forever} -) - -func GetRunner(ctx context.Context, factory cmdutil.Factory, - invFactory inventory.ClientFactory, loader Loader) *Runner { - r := &Runner{ - ctx: ctx, - factory: factory, - invFactory: invFactory, - loader: loader, - PollerFactoryFunc: pollerFactoryFunc, - } - c := &cobra.Command{ - Use: "status (DIRECTORY | STDIN)", - PreRunE: r.preRunE, - RunE: r.runE, - } - c.Flags().DurationVar(&r.period, "poll-period", 2*time.Second, - "Polling period for resource statuses.") - c.Flags().StringVar(&r.pollUntil, "poll-until", "known", - "When to stop polling. Must be one of 'known', 'current', 'deleted', or 'forever'.") - c.Flags().StringVar(&r.output, "output", "events", "Output format.") - c.Flags().DurationVar(&r.timeout, "timeout", 0, - "How long to wait before exiting") - c.Flags().StringVar(&r.invType, "inv-type", Local, "Type of the inventory info, must be local or remote") - c.Flags().StringVar(&r.inventoryNames, "inv-names", "", "Names of targeted inventory: inv1,inv2,...") - c.Flags().StringVar(&r.namespaces, "namespaces", "", "Names of targeted namespaces: ns1,ns2,...") - c.Flags().StringVar(&r.statuses, "statuses", "", "Targeted status: st1,st2...") - - r.Command = c - return r -} - -func Command(ctx context.Context, f cmdutil.Factory, - invFactory inventory.ClientFactory, loader Loader) *cobra.Command { - return GetRunner(ctx, f, invFactory, loader).Command -} - -// Runner captures the parameters for the command and contains -// the run function. -type Runner struct { - ctx context.Context - Command *cobra.Command - factory cmdutil.Factory - invFactory inventory.ClientFactory - loader Loader - - period time.Duration - pollUntil string - timeout time.Duration - output string - - invType string - inventoryNames string - inventoryNameSet map[string]bool - namespaces string - namespaceSet map[string]bool - statuses string - statusSet map[string]bool - - PollerFactoryFunc func(cmdutil.Factory) (poller.Poller, error) -} - -func (r *Runner) preRunE(*cobra.Command, []string) error { - if !slice.ContainsString(PollUntilOptions, r.pollUntil, nil) { - return fmt.Errorf("pollUntil must be one of %s", strings.Join(PollUntilOptions, ",")) - } - - if found := pkgprinters.ValidatePrinterType(r.output); !found { - return fmt.Errorf("unknown output type %q", r.output) - } - - if r.invType != Local && r.invType != Remote { - return fmt.Errorf("inv-type flag should be either local or remote") - } - - if r.invType == Local && r.inventoryNames != "" { - return fmt.Errorf("inv-names flag should only be used when inv-type is set to remote") - } - - if r.inventoryNames != "" { - r.inventoryNameSet = make(map[string]bool) - for _, name := range strings.Split(r.inventoryNames, ",") { - r.inventoryNameSet[name] = true - } - } - - if r.namespaces != "" { - r.namespaceSet = make(map[string]bool) - for _, ns := range strings.Split(r.namespaces, ",") { - r.namespaceSet[ns] = true - } - } - - if r.statuses != "" { - r.statusSet = make(map[string]bool) - for _, st := range strings.Split(r.statuses, ",") { - parsedST := strings.ToLower(st) - r.statusSet[parsedST] = true - } - } - - return nil -} - -// Load inventory info from local storage -// and get info from the cluster based on the local info -// wrap it to be a map mapping from string to objectMetadataSet -func (r *Runner) loadInvFromDisk(cmd *cobra.Command, args []string) (*printer.PrintData, error) { - inv, err := r.loader.GetInvInfo(cmd, args) - if err != nil { - return nil, err - } - - invClient, err := r.invFactory.NewClient(r.factory) - if err != nil { - return nil, err - } - - // Based on the inventory template manifest we look up the inventory - // from the live state using the inventory client. - identifiers, err := invClient.GetClusterObjs(inv) - if err != nil { - return nil, err - } - - printData := printer.PrintData{ - Identifiers: object.ObjMetadataSet{}, - InvNameMap: make(map[object.ObjMetadata]string), - StatusSet: r.statusSet, - } - - for _, obj := range identifiers { - // check if the object is under one of the targeted namespaces - if _, ok := r.namespaceSet[obj.Namespace]; ok || len(r.namespaceSet) == 0 { - // add to the map for future reference - printData.InvNameMap[obj] = inv.Name() - // append to identifiers - printData.Identifiers = append(printData.Identifiers, obj) - } - } - return &printData, nil -} - -// Retrieve a list of inventory object from the cluster -func (r *Runner) listInvFromCluster() (*printer.PrintData, error) { - invClient, err := r.invFactory.NewClient(r.factory) - if err != nil { - return nil, err - } - - // initialize maps in printData - printData := printer.PrintData{ - Identifiers: object.ObjMetadataSet{}, - InvNameMap: make(map[object.ObjMetadata]string), - StatusSet: r.statusSet, - } - - identifiersMap, err := invClient.ListClusterInventoryObjs(r.ctx) - if err != nil { - return nil, err - } - - for invName, identifiers := range identifiersMap { - // Check if there are targeted inventory names and include the current inventory name - if _, ok := r.inventoryNameSet[invName]; !ok && len(r.inventoryNameSet) != 0 { - continue - } - // Filter objects - for _, obj := range identifiers { - // check if the object is under one of the targeted namespaces - if _, ok := r.namespaceSet[obj.Namespace]; ok || len(r.namespaceSet) == 0 { - // add to the map for future reference - printData.InvNameMap[obj] = invName - // append to identifiers - printData.Identifiers = append(printData.Identifiers, obj) - } - } - } - return &printData, nil -} - -// runE implements the logic of the command and will delegate to the -// poller to compute status for each of the resources. One of the printer -// implementations takes care of printing the output. -func (r *Runner) runE(cmd *cobra.Command, args []string) error { - var printData *printer.PrintData - var err error - switch r.invType { - case Local: - if len(args) != 0 { - printcommon.SprintfWithColor(printcommon.YELLOW, - "Warning: Path is assigned while list flag is enabled, ignore the path") - } - printData, err = r.loadInvFromDisk(cmd, args) - case Remote: - printData, err = r.listInvFromCluster() - default: - return fmt.Errorf("invType must be either local or remote") - } - if err != nil { - return err - } - - // Exit here if the inventory is empty. - if len(printData.Identifiers) == 0 { - _, _ = fmt.Fprint(cmd.OutOrStdout(), "no resources found in the inventory\n") - return nil - } - - statusPoller, err := r.PollerFactoryFunc(r.factory) - if err != nil { - return err - } - - // Fetch a printer implementation based on the desired output format as - // specified in the output flag. - printer, err := printers.CreatePrinter(r.output, genericclioptions.IOStreams{ - In: cmd.InOrStdin(), - Out: cmd.OutOrStdout(), - ErrOut: cmd.ErrOrStderr(), - }, printData) - if err != nil { - return fmt.Errorf("error creating printer: %w", err) - } - - // If the user has specified a timeout, we create a context with timeout, - // otherwise we create a context with cancel. - ctx := cmd.Context() - var cancel func() - if r.timeout != 0 { - ctx, cancel = context.WithTimeout(ctx, r.timeout) - } else { - ctx, cancel = context.WithCancel(ctx) - } - defer cancel() - - // Choose the appropriate ObserverFunc based on the criteria for when - // the command should exit. - var cancelFunc collector.ObserverFunc - switch r.pollUntil { - case "known": - cancelFunc = allKnownNotifierFunc(cancel) - case "current": - cancelFunc = desiredStatusNotifierFunc(cancel, status.CurrentStatus) - case "deleted": - cancelFunc = desiredStatusNotifierFunc(cancel, status.NotFoundStatus) - case "forever": - cancelFunc = func(*collector.ResourceStatusCollector, event.Event) {} - default: - return fmt.Errorf("unknown value for pollUntil: %q", r.pollUntil) - } - - eventChannel := statusPoller.Poll(ctx, printData.Identifiers, polling.PollOptions{ - PollInterval: r.period, - }) - - return printer.Print(eventChannel, printData.Identifiers, cancelFunc) -} - -// desiredStatusNotifierFunc returns an Observer function for the -// ResourceStatusCollector that will cancel the context (using the cancelFunc) -// when all resources have reached the desired status. -func desiredStatusNotifierFunc(cancelFunc context.CancelFunc, - desired status.Status) collector.ObserverFunc { - return func(rsc *collector.ResourceStatusCollector, _ event.Event) { - var rss []*event.ResourceStatus - for _, rs := range rsc.ResourceStatuses { - rss = append(rss, rs) - } - aggStatus := aggregator.AggregateStatus(rss, desired) - if aggStatus == desired { - cancelFunc() - } - } -} - -// allKnownNotifierFunc returns an Observer function for the -// ResourceStatusCollector that will cancel the context (using the cancelFunc) -// when all resources have a known status. -func allKnownNotifierFunc(cancelFunc context.CancelFunc) collector.ObserverFunc { - return func(rsc *collector.ResourceStatusCollector, _ event.Event) { - for _, rs := range rsc.ResourceStatuses { - if rs.Status == status.UnknownStatus { - return - } - } - cancelFunc() - } -} - -func pollerFactoryFunc(f cmdutil.Factory) (poller.Poller, error) { - return polling.NewStatusPollerFromFactory(f, polling.Options{}) -} - -type Loader interface { - GetInvInfo(cmd *cobra.Command, args []string) (inventory.Info, error) -} - -type InventoryLoader struct { - Loader manifestreader.ManifestLoader -} - -func NewInventoryLoader(loader manifestreader.ManifestLoader) *InventoryLoader { - return &InventoryLoader{ - Loader: loader, - } -} - -func (ir *InventoryLoader) GetInvInfo(cmd *cobra.Command, args []string) (inventory.Info, error) { - _, err := common.DemandOneDirectory(args) - if err != nil { - return nil, err - } - - reader, err := ir.Loader.ManifestReader(cmd.InOrStdin(), flagutils.PathFromArgs(args)) - if err != nil { - return nil, err - } - objs, err := reader.Read() - if err != nil { - return nil, err - } - - invObj, _, err := inventory.SplitUnstructureds(objs) - if err != nil { - return nil, err - } - inv := inventory.WrapInventoryInfoObj(invObj) - return inv, nil -} diff --git a/cmd/status/cmdstatus_test.go b/cmd/status/cmdstatus_test.go deleted file mode 100644 index a61dfba2..00000000 --- a/cmd/status/cmdstatus_test.go +++ /dev/null @@ -1,612 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package status - -import ( - "bytes" - "context" - "encoding/json" - "strings" - "testing" - "time" - - "github.com/fluxcd/cli-utils/pkg/apply/poller" - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/pkg/kstatus/polling" - pollevent "github.com/fluxcd/cli-utils/pkg/kstatus/polling/event" - "github.com/fluxcd/cli-utils/pkg/kstatus/status" - "github.com/fluxcd/cli-utils/pkg/manifestreader" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/spf13/cobra" - "github.com/stretchr/testify/assert" - "k8s.io/apimachinery/pkg/runtime/schema" - cmdtesting "k8s.io/kubectl/pkg/cmd/testing" - cmdutil "k8s.io/kubectl/pkg/cmd/util" -) - -var ( - inventoryTemplate = ` -kind: ConfigMap -apiVersion: v1 -metadata: - labels: - cli-utils.sigs.k8s.io/inventory-id: test - name: foo - namespace: default -` - depObject = object.ObjMetadata{ - Name: "foo", - Namespace: "default", - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - }, - } - - stsObject = object.ObjMetadata{ - Name: "bar", - Namespace: "default", - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "StatefulSet", - }, - } -) - -type fakePoller struct { - events []pollevent.Event -} - -func (f *fakePoller) Poll(ctx context.Context, _ object.ObjMetadataSet, - _ polling.PollOptions) <-chan pollevent.Event { - eventChannel := make(chan pollevent.Event) - go func() { - defer close(eventChannel) - for _, e := range f.events { - eventChannel <- e - } - <-ctx.Done() - }() - return eventChannel -} - -func TestCommand(t *testing.T) { - testCases := map[string]struct { - pollUntil string - printer string - timeout time.Duration - input string - inventory object.ObjMetadataSet - events []pollevent.Event - expectedErrMsg string - expectedOutput string - }{ - "no inventory template": { - pollUntil: "known", - printer: "events", - input: "", - expectedErrMsg: "Package uninitialized. Please run \"init\" command.", - }, - "no inventory in live state": { - pollUntil: "known", - printer: "events", - input: inventoryTemplate, - expectedOutput: "no resources found in the inventory\n", - }, - "wait for all known": { - pollUntil: "known", - printer: "events", - input: inventoryTemplate, - inventory: object.ObjMetadataSet{ - depObject, - stsObject, - }, - events: []pollevent.Event{ - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: depObject, - Status: status.InProgressStatus, - Message: "inProgress", - }, - }, - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: stsObject, - Status: status.CurrentStatus, - Message: "current", - }, - }, - }, - expectedOutput: ` -foo/deployment.apps/default/foo is InProgress: inProgress -foo/statefulset.apps/default/bar is Current: current -`, - }, - "wait for all current": { - pollUntil: "current", - printer: "events", - input: inventoryTemplate, - inventory: object.ObjMetadataSet{ - depObject, - stsObject, - }, - events: []pollevent.Event{ - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: depObject, - Status: status.InProgressStatus, - Message: "inProgress", - }, - }, - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: stsObject, - Status: status.InProgressStatus, - Message: "inProgress", - }, - }, - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: stsObject, - Status: status.CurrentStatus, - Message: "current", - }, - }, - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: depObject, - Status: status.CurrentStatus, - Message: "current", - }, - }, - }, - expectedOutput: ` -foo/deployment.apps/default/foo is InProgress: inProgress -foo/statefulset.apps/default/bar is InProgress: inProgress -foo/statefulset.apps/default/bar is Current: current -foo/deployment.apps/default/foo is Current: current -`, - }, - "wait for all deleted": { - pollUntil: "deleted", - printer: "events", - input: inventoryTemplate, - inventory: object.ObjMetadataSet{ - depObject, - stsObject, - }, - events: []pollevent.Event{ - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: stsObject, - Status: status.NotFoundStatus, - Message: "notFound", - }, - }, - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: depObject, - Status: status.NotFoundStatus, - Message: "notFound", - }, - }, - }, - expectedOutput: ` -foo/statefulset.apps/default/bar is NotFound: notFound -foo/deployment.apps/default/foo is NotFound: notFound -`, - }, - "forever with timeout": { - pollUntil: "forever", - printer: "events", - timeout: 2 * time.Second, - input: inventoryTemplate, - inventory: object.ObjMetadataSet{ - depObject, - stsObject, - }, - events: []pollevent.Event{ - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: stsObject, - Status: status.InProgressStatus, - Message: "inProgress", - }, - }, - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: depObject, - Status: status.InProgressStatus, - Message: "inProgress", - }, - }, - }, - expectedOutput: ` -foo/statefulset.apps/default/bar is InProgress: inProgress -foo/deployment.apps/default/foo is InProgress: inProgress -`, - }, - } - - jsonTestCases := map[string]struct { - pollUntil string - printer string - timeout time.Duration - input string - inventory object.ObjMetadataSet - events []pollevent.Event - expectedErrMsg string - expectedOutput []map[string]interface{} - }{ - "wait for all known json": { - pollUntil: "known", - printer: "json", - input: inventoryTemplate, - inventory: object.ObjMetadataSet{ - depObject, - stsObject, - }, - events: []pollevent.Event{ - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: depObject, - Status: status.InProgressStatus, - Message: "inProgress", - }, - }, - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: stsObject, - Status: status.CurrentStatus, - Message: "current", - }, - }, - }, - expectedOutput: []map[string]interface{}{ - { - "group": "apps", - "kind": "Deployment", - "namespace": "default", - "name": "foo", - "timestamp": "", - "type": "status", - "inventory-name": "foo", - "status": "InProgress", - "message": "inProgress", - }, - { - "group": "apps", - "kind": "StatefulSet", - "namespace": "default", - "name": "bar", - "timestamp": "", - "type": "status", - "inventory-name": "foo", - "status": "Current", - "message": "current", - }, - }, - }, - "wait for all current json": { - pollUntil: "current", - printer: "json", - input: inventoryTemplate, - inventory: object.ObjMetadataSet{ - depObject, - stsObject, - }, - events: []pollevent.Event{ - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: depObject, - Status: status.InProgressStatus, - Message: "inProgress", - }, - }, - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: stsObject, - Status: status.InProgressStatus, - Message: "inProgress", - }, - }, - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: stsObject, - Status: status.CurrentStatus, - Message: "current", - }, - }, - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: depObject, - Status: status.CurrentStatus, - Message: "current", - }, - }, - }, - expectedOutput: []map[string]interface{}{ - { - "group": "apps", - "kind": "Deployment", - "namespace": "default", - "name": "foo", - "timestamp": "", - "type": "status", - "inventory-name": "foo", - "status": "InProgress", - "message": "inProgress", - }, - { - "group": "apps", - "kind": "StatefulSet", - "namespace": "default", - "name": "bar", - "timestamp": "", - "type": "status", - "inventory-name": "foo", - "status": "InProgress", - "message": "inProgress", - }, - { - "group": "apps", - "kind": "StatefulSet", - "namespace": "default", - "name": "bar", - "timestamp": "", - "type": "status", - "inventory-name": "foo", - "status": "Current", - "message": "current", - }, - { - "group": "apps", - "kind": "Deployment", - "namespace": "default", - "name": "foo", - "timestamp": "", - "type": "status", - "inventory-name": "foo", - "status": "Current", - "message": "current", - }, - }, - }, - "wait for all deleted json": { - pollUntil: "deleted", - printer: "json", - input: inventoryTemplate, - inventory: object.ObjMetadataSet{ - depObject, - stsObject, - }, - events: []pollevent.Event{ - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: stsObject, - Status: status.NotFoundStatus, - Message: "notFound", - }, - }, - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: depObject, - Status: status.NotFoundStatus, - Message: "notFound", - }, - }, - }, - expectedOutput: []map[string]interface{}{ - { - "group": "apps", - "kind": "StatefulSet", - "namespace": "default", - "name": "bar", - "timestamp": "", - "type": "status", - "inventory-name": "foo", - "status": "NotFound", - "message": "notFound", - }, - { - "group": "apps", - "kind": "Deployment", - "namespace": "default", - "name": "foo", - "timestamp": "", - "type": "status", - "inventory-name": "foo", - "status": "NotFound", - "message": "notFound", - }, - }, - }, - "forever with timeout json": { - pollUntil: "forever", - printer: "json", - timeout: 2 * time.Second, - input: inventoryTemplate, - inventory: object.ObjMetadataSet{ - depObject, - stsObject, - }, - events: []pollevent.Event{ - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: stsObject, - Status: status.InProgressStatus, - Message: "inProgress", - }, - }, - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: depObject, - Status: status.InProgressStatus, - Message: "inProgress", - }, - }, - }, - expectedOutput: []map[string]interface{}{ - { - "group": "apps", - "kind": "StatefulSet", - "namespace": "default", - "name": "bar", - "timestamp": "", - "type": "status", - "inventory-name": "foo", - "status": "InProgress", - "message": "inProgress", - }, - { - "group": "apps", - "kind": "Deployment", - "namespace": "default", - "name": "foo", - "timestamp": "", - "type": "status", - "inventory-name": "foo", - "status": "InProgress", - "message": "inProgress", - }, - }, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - tf := cmdtesting.NewTestFactory().WithNamespace("namespace") - defer tf.Cleanup() - - loader := manifestreader.NewFakeLoader(tf, tc.inventory) - runner := &Runner{ - factory: tf, - invFactory: inventory.FakeClientFactory(tc.inventory), - loader: NewInventoryLoader(loader), - PollerFactoryFunc: func(c cmdutil.Factory) (poller.Poller, error) { - return &fakePoller{tc.events}, nil - }, - - pollUntil: tc.pollUntil, - output: tc.printer, - timeout: tc.timeout, - invType: Local, - } - - cmd := &cobra.Command{ - PreRunE: runner.preRunE, - RunE: runner.runE, - } - cmd.SetIn(strings.NewReader(tc.input)) - var buf bytes.Buffer - cmd.SetOut(&buf) - cmd.SetArgs([]string{}) - - err := cmd.Execute() - - if tc.expectedErrMsg != "" { - if !assert.Error(t, err) { - t.FailNow() - } - assert.Contains(t, err.Error(), tc.expectedErrMsg) - return - } - - assert.NoError(t, err) - assert.Equal(t, strings.TrimSpace(buf.String()), strings.TrimSpace(tc.expectedOutput)) - }) - } - - for tn, tc := range jsonTestCases { - t.Run(tn, func(t *testing.T) { - tf := cmdtesting.NewTestFactory().WithNamespace("namespace") - defer tf.Cleanup() - - loader := manifestreader.NewFakeLoader(tf, tc.inventory) - runner := &Runner{ - factory: tf, - invFactory: inventory.FakeClientFactory(tc.inventory), - loader: NewInventoryLoader(loader), - PollerFactoryFunc: func(c cmdutil.Factory) (poller.Poller, error) { - return &fakePoller{tc.events}, nil - }, - - pollUntil: tc.pollUntil, - output: tc.printer, - timeout: tc.timeout, - invType: Local, - } - - cmd := &cobra.Command{ - RunE: runner.runE, - } - cmd.SetIn(strings.NewReader(tc.input)) - var buf bytes.Buffer - cmd.SetOut(&buf) - cmd.SetArgs([]string{}) - - err := cmd.Execute() - if tc.expectedErrMsg != "" { - if !assert.Error(t, err) { - t.FailNow() - } - assert.Contains(t, err.Error(), tc.expectedErrMsg) - return - } - - assert.NoError(t, err) - actual := strings.Split(buf.String(), "\n") - assertOutput(t, tc.expectedOutput, actual) - }) - } -} - -// nolint:unparam -func assertOutput(t *testing.T, expectedOutput []map[string]interface{}, actual []string) bool { - for i, expectedMap := range expectedOutput { - if len(expectedMap) == 0 { - return assert.Empty(t, actual[i]) - } - - var m map[string]interface{} - err := json.Unmarshal([]byte(actual[i]), &m) - if !assert.NoError(t, err) { - return false - } - - if _, found := expectedMap["timestamp"]; found { - if _, ok := m["timestamp"]; ok { - delete(expectedMap, "timestamp") - delete(m, "timestamp") - } else { - t.Error("expected to find key 'timestamp', but didn't") - return false - } - } - if !assert.Equal(t, expectedMap, m) { - return false - } - } - return true -} diff --git a/cmd/status/printers/event/printer.go b/cmd/status/printers/event/printer.go deleted file mode 100644 index eed58273..00000000 --- a/cmd/status/printers/event/printer.go +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package event - -import ( - "fmt" - "strings" - - "github.com/fluxcd/cli-utils/cmd/status/printers/printer" - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/kstatus/polling/collector" - pollevent "github.com/fluxcd/cli-utils/pkg/kstatus/polling/event" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/print/list" - "github.com/fluxcd/cli-utils/pkg/printers/events" - "k8s.io/cli-runtime/pkg/genericclioptions" -) - -// Printer implements the Printer interface and outputs the resource -// status information as a list of events as they happen. -type Printer struct { - Formatter list.Formatter - IOStreams genericclioptions.IOStreams - Data *printer.PrintData -} - -// NewPrinter returns a new instance of the eventPrinter. -func NewPrinter(ioStreams genericclioptions.IOStreams, printData *printer.PrintData) *Printer { - return &Printer{ - Formatter: events.NewFormatter(ioStreams, common.DryRunNone), - IOStreams: ioStreams, - Data: printData, - } -} - -// Print takes an event channel and outputs the status events on the channel -// until the channel is closed. The provided cancelFunc is consulted on -// every event and is responsible for stopping the poller when appropriate. -// This function will block. -func (ep *Printer) Print(ch <-chan pollevent.Event, identifiers object.ObjMetadataSet, - cancelFunc collector.ObserverFunc) error { - coll := collector.NewResourceStatusCollector(identifiers) - // The actual work is done by the collector, which will invoke the - // callback on every event. In the callback we print the status - // information and call the cancelFunc which is responsible for - // stopping the poller at the correct time. - done := coll.ListenWithObserver(ch, collector.ObserverFunc( - func(statusCollector *collector.ResourceStatusCollector, e pollevent.Event) { - err := ep.printStatusEvent(e) - if err != nil { - panic(err) - } - cancelFunc(statusCollector, e) - }), - ) - // Listen to the channel until it is closed. - var err error - for msg := range done { - err = msg.Err - } - return err -} - -func (ep *Printer) printStatusEvent(se pollevent.Event) error { - switch se.Type { - case pollevent.ResourceUpdateEvent: - id := se.Resource.Identifier - var invName string - var ok bool - if invName, ok = ep.Data.InvNameMap[id]; !ok { - return fmt.Errorf("%s: resource not found", id) - } - // filter out status that are not assigned - statusString := se.Resource.Status.String() - if _, ok := ep.Data.StatusSet[strings.ToLower(statusString)]; len(ep.Data.StatusSet) != 0 && !ok { - return nil - } - _, err := fmt.Fprintf(ep.IOStreams.Out, "%s/%s/%s/%s is %s: %s\n", invName, - strings.ToLower(id.GroupKind.String()), id.Namespace, id.Name, statusString, se.Resource.Message) - return err - case pollevent.ErrorEvent: - return ep.Formatter.FormatErrorEvent(event.ErrorEvent{ - Err: se.Error, - }) - } - return nil -} diff --git a/cmd/status/printers/json/printer.go b/cmd/status/printers/json/printer.go deleted file mode 100644 index 570a4bd3..00000000 --- a/cmd/status/printers/json/printer.go +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package json - -import ( - "encoding/json" - "fmt" - "strings" - "time" - - "github.com/fluxcd/cli-utils/cmd/status/printers/printer" - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/kstatus/polling/collector" - pollevent "github.com/fluxcd/cli-utils/pkg/kstatus/polling/event" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/print/list" - jsonprinter "github.com/fluxcd/cli-utils/pkg/printers/json" - "k8s.io/cli-runtime/pkg/genericclioptions" -) - -// Printer implements the Printer interface and outputs the resource -// status information as a list of events as they happen. -type Printer struct { - Formatter list.Formatter - IOStreams genericclioptions.IOStreams - Data *printer.PrintData -} - -// NewPrinter returns a new instance of the eventPrinter. -func NewPrinter(ioStreams genericclioptions.IOStreams, printData *printer.PrintData) *Printer { - return &Printer{ - Formatter: jsonprinter.NewFormatter(ioStreams, common.DryRunNone), - IOStreams: ioStreams, - Data: printData, - } -} - -// Print takes an event channel and outputs the status events on the channel -// until the channel is closed. The provided cancelFunc is consulted on -// every event and is responsible for stopping the poller when appropriate. -// This function will block. -func (ep *Printer) Print(ch <-chan pollevent.Event, identifiers object.ObjMetadataSet, - cancelFunc collector.ObserverFunc) error { - coll := collector.NewResourceStatusCollector(identifiers) - // The actual work is done by the collector, which will invoke the - // callback on every event. In the callback we print the status - // information and call the cancelFunc which is responsible for - // stopping the poller at the correct time. - done := coll.ListenWithObserver(ch, collector.ObserverFunc( - func(statusCollector *collector.ResourceStatusCollector, e pollevent.Event) { - err := ep.printStatusEvent(e) - if err != nil { - panic(err) - } - cancelFunc(statusCollector, e) - }), - ) - // Listen to the channel until it is closed. - var err error - for msg := range done { - err = msg.Err - } - return err -} - -func (ep *Printer) printStatusEvent(se pollevent.Event) error { - switch se.Type { - case pollevent.ResourceUpdateEvent: - id := se.Resource.Identifier - var invName string - var ok bool - if invName, ok = ep.Data.InvNameMap[id]; !ok { - return fmt.Errorf("%s: resource not found", id) - } - // filter out status that are not assigned - statusString := se.Resource.Status.String() - if _, ok := ep.Data.StatusSet[strings.ToLower(statusString)]; len(ep.Data.StatusSet) != 0 && !ok { - return nil - } - eventInfo := ep.createJSONObj(id) - eventInfo["inventory-name"] = invName - eventInfo["status"] = statusString - eventInfo["message"] = se.Resource.Message - b, err := json.Marshal(eventInfo) - if err != nil { - return err - } - _, err = fmt.Fprintf(ep.IOStreams.Out, "%s\n", string(b)) - return err - case pollevent.ErrorEvent: - return ep.Formatter.FormatErrorEvent(event.ErrorEvent{ - Err: se.Error, - }) - } - return nil -} - -func (ep *Printer) createJSONObj(id object.ObjMetadata) map[string]interface{} { - return map[string]interface{}{ - "group": id.GroupKind.Group, - "kind": id.GroupKind.Kind, - "namespace": id.Namespace, - "name": id.Name, - "timestamp": time.Now().UTC().Format(time.RFC3339), - "type": "status", - } -} diff --git a/cmd/status/printers/printer/printer.go b/cmd/status/printers/printer/printer.go deleted file mode 100644 index 7459b9fa..00000000 --- a/cmd/status/printers/printer/printer.go +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package printer - -import ( - "github.com/fluxcd/cli-utils/pkg/kstatus/polling/collector" - "github.com/fluxcd/cli-utils/pkg/kstatus/polling/event" - "github.com/fluxcd/cli-utils/pkg/object" -) - -// PrintData records data required for printing -type PrintData struct { - Identifiers object.ObjMetadataSet - InvNameMap map[object.ObjMetadata]string - StatusSet map[string]bool -} - -// Printer defines an interface for outputting information about status of -// resources. Different implementations allow output formats tailored to -// different use cases. -type Printer interface { - - // Print tells the printer to start outputting data. The stop parameter - // is a channel that the caller will use to signal to the printer that it - // needs to stop and shut down. The channel returned can be used by the - // printer implementation to signal that it has outputted all the data it - // needs to, and that it has completed shutting down. The latter is important - // to make sure the printer has a chance to output all data before the - // program terminates. - Print(ch <-chan event.Event, identifiers object.ObjMetadataSet, cancelFunc collector.ObserverFunc) error -} diff --git a/cmd/status/printers/printers.go b/cmd/status/printers/printers.go deleted file mode 100644 index 5b379d2d..00000000 --- a/cmd/status/printers/printers.go +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package printers - -import ( - "github.com/fluxcd/cli-utils/cmd/status/printers/event" - "github.com/fluxcd/cli-utils/cmd/status/printers/json" - "github.com/fluxcd/cli-utils/cmd/status/printers/printer" - "github.com/fluxcd/cli-utils/cmd/status/printers/table" - "k8s.io/cli-runtime/pkg/genericclioptions" -) - -// CreatePrinter return an implementation of the Printer interface. The -// actual implementation is based on the printerType requested. -func CreatePrinter(printerType string, ioStreams genericclioptions.IOStreams, printData *printer.PrintData) (printer.Printer, error) { - switch printerType { - case "table": - return table.NewPrinter(ioStreams, printData), nil - case "json": - return json.NewPrinter(ioStreams, printData), nil - default: - return event.NewPrinter(ioStreams, printData), nil - } -} diff --git a/cmd/status/printers/table/adapter.go b/cmd/status/printers/table/adapter.go deleted file mode 100644 index 5d3919e7..00000000 --- a/cmd/status/printers/table/adapter.go +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package table - -import ( - "strings" - - "github.com/fluxcd/cli-utils/pkg/kstatus/polling/collector" - pe "github.com/fluxcd/cli-utils/pkg/kstatus/polling/event" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/print/table" -) - -// CollectorAdapter wraps the ResourceStatusCollector and -// provides a set of functions that matches the interfaces -// needed by the BaseTablePrinter. -type CollectorAdapter struct { - collector *collector.ResourceStatusCollector - invNameMap map[object.ObjMetadata]string - statusSet map[string]bool -} - -type ResourceInfo struct { - resourceStatus *pe.ResourceStatus - invName string -} - -func (r *ResourceInfo) Identifier() object.ObjMetadata { - return r.resourceStatus.Identifier -} - -func (r *ResourceInfo) ResourceStatus() *pe.ResourceStatus { - return r.resourceStatus -} - -func (r *ResourceInfo) SubResources() []table.Resource { - var subResources []table.Resource - for _, rs := range r.resourceStatus.GeneratedResources { - subResources = append(subResources, &ResourceInfo{ - resourceStatus: rs, - invName: r.invName, - }) - } - return subResources -} - -type ResourceState struct { - resources []table.Resource - err error -} - -func (rss *ResourceState) Resources() []table.Resource { - return rss.resources -} - -func (rss *ResourceState) Error() error { - return rss.err -} - -func (ca *CollectorAdapter) LatestStatus() *ResourceState { - observation := ca.collector.LatestObservation() - var resources []table.Resource - for _, resourceStatus := range observation.ResourceStatuses { - if _, ok := ca.statusSet[strings.ToLower(resourceStatus.Status.String())]; len(ca.statusSet) == 0 || ok { - resources = append(resources, &ResourceInfo{ - resourceStatus: resourceStatus, - invName: ca.invNameMap[resourceStatus.Identifier], - }) - } - } - return &ResourceState{ - resources: resources, - err: observation.Error, - } -} diff --git a/cmd/status/printers/table/printer.go b/cmd/status/printers/table/printer.go deleted file mode 100644 index c0614657..00000000 --- a/cmd/status/printers/table/printer.go +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package table - -import ( - "fmt" - "io" - "time" - - "github.com/fluxcd/cli-utils/cmd/status/printers/printer" - "github.com/fluxcd/cli-utils/pkg/kstatus/polling/collector" - "github.com/fluxcd/cli-utils/pkg/kstatus/polling/event" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/print/table" - "k8s.io/cli-runtime/pkg/genericclioptions" -) - -const ( - // updateInterval defines how often the printer will update the UI. - updateInterval = 1 * time.Second -) - -// Printer is an implementation of the Printer interface that outputs -// status information about resources in a table format with in-place updates. -type Printer struct { - IOStreams genericclioptions.IOStreams - PrintData *printer.PrintData -} - -// NewPrinter returns a new instance of the tablePrinter. -func NewPrinter(ioStreams genericclioptions.IOStreams, printData *printer.PrintData) *Printer { - return &Printer{ - IOStreams: ioStreams, - PrintData: printData, - } -} - -// Print take an event channel and outputs the status events on the channel -// until the channel is closed . -func (t *Printer) Print(ch <-chan event.Event, identifiers object.ObjMetadataSet, - cancelFunc collector.ObserverFunc) error { - coll := collector.NewResourceStatusCollector(identifiers) - stop := make(chan struct{}) - - // Start the goroutine that is responsible for - // printing the latest state on a regular cadence. - printCompleted := t.runPrintLoop(&CollectorAdapter{ - collector: coll, - invNameMap: t.PrintData.InvNameMap, - statusSet: t.PrintData.StatusSet, - }, stop) - - // Make the collector start listening on the eventChannel. - done := coll.ListenWithObserver(ch, cancelFunc) - - // Block until all the collector has shut down. This means the - // eventChannel has been closed and all events have been processed. - var err error - for msg := range done { - err = msg.Err - } - - // Close the stop channel to notify the print goroutine that it should - // shut down. - close(stop) - - // Wait until the printCompleted channel is closed. This means - // the printer has updated the UI with the latest state and - // exited from the goroutine. - <-printCompleted - return err -} - -var invNameColumn = table.ColumnDef{ - ColumnName: "inventory_name", - ColumnHeader: "INVENTORY_NAME", - ColumnWidth: 30, - PrintResourceFunc: func(w io.Writer, width int, r table.Resource) (int, error) { - group := r.(*ResourceInfo).invName - if len(group) > width { - group = group[:width] - } - _, err := fmt.Fprint(w, group) - return len(group), err - }, -} - -var columns = []table.ColumnDefinition{ - table.MustColumn("namespace"), - table.MustColumn("resource"), - table.MustColumn("status"), - table.MustColumn("conditions"), - table.MustColumn("age"), - table.MustColumn("message"), - invNameColumn, -} - -// Print prints the table of resources with their statuses until the -// provided stop channel is closed. -func (t *Printer) runPrintLoop(coll *CollectorAdapter, stop <-chan struct{}) <-chan struct{} { - finished := make(chan struct{}) - - baseTablePrinter := table.BaseTablePrinter{ - IOStreams: t.IOStreams, - Columns: columns, - } - - linesPrinted := baseTablePrinter.PrintTable(coll.LatestStatus(), 0) - - go func() { - defer close(finished) - ticker := time.NewTicker(updateInterval) - for { - select { - case <-stop: - ticker.Stop() - linesPrinted = baseTablePrinter.PrintTable( - coll.LatestStatus(), linesPrinted) - return - case <-ticker.C: - linesPrinted = baseTablePrinter.PrintTable( - coll.LatestStatus(), linesPrinted) - } - } - }() - - return finished -} diff --git a/code-of-conduct.md b/code-of-conduct.md deleted file mode 100644 index 0d15c00c..00000000 --- a/code-of-conduct.md +++ /dev/null @@ -1,3 +0,0 @@ -# Kubernetes Community Code of Conduct - -Please refer to our [Kubernetes Community Code of Conduct](https://git.k8s.io/community/code-of-conduct.md) diff --git a/examples/alphaTestExamples/MultipleServices.md b/examples/alphaTestExamples/MultipleServices.md deleted file mode 100644 index 2dc8f9b6..00000000 --- a/examples/alphaTestExamples/MultipleServices.md +++ /dev/null @@ -1,133 +0,0 @@ -[kind]: https://github.com/kubernetes-sigs/kind - -# Demo: Multiple Services - -The following demonstrates applying and destroying multiple services to a `kind` cluster. - -Steps: -1. Download the resources files for wordpress, mysql services. -2. Spin-up kubernetes cluster on local using [kind]. -3. Deploy the wordpress, mysql services using kapply and verify the status. -4. Destroy wordpress service and verify that only wordpress service is destroyed. - -First define a place to work: - - -``` -DEMO_HOME=$(mktemp -d) -``` - -Alternatively, use - -> ``` -> DEMO_HOME=~/hello -> ``` - -## Establish the base - -Download the example configs for services `mysql` and `wordpress` - -``` -BASE=$DEMO_HOME/base -mkdir -p $BASE -OUTPUT=$DEMO_HOME/output -mkdir -p $OUTPUT -GREEN='\033[0;32m' -RED='\033[0;31m' -NC='\033[0m' # No Color - -mkdir $BASE/wordpress -mkdir $BASE/mysql - -curl -s -o "$BASE/wordpress/#1.yaml" "https://raw.githubusercontent.com\ -/kubernetes-sigs/kustomize\ -/master/examples/wordpress/wordpress\ -/{deployment,service}.yaml" - -curl -s -o "$BASE/mysql/#1.yaml" "https://raw.githubusercontent.com\ -/kubernetes-sigs/kustomize\ -/master/examples/wordpress/mysql\ -/{secret,deployment,service}.yaml" - -function expectedOutputLine() { - if ! grep -q "$@" "$OUTPUT/status"; then - echo -e "${RED}Error: output line not found${NC}" - echo -e "${RED}Expected: $@${NC}" - exit 1 - else - echo -e "${GREEN}Success: output line found${NC}" - fi -} -``` - -Use the kapply init command to generate the inventory template. This contains -the namespace and inventory id used by apply to create inventory objects. - -``` -kapply init $BASE/mysql | tee $OUTPUT/status -expectedOutputLine "namespace: default is used for inventory object" - -kapply init $BASE/wordpress | tee $OUTPUT/status -expectedOutputLine "namespace: default is used for inventory object" -``` - -Delete any existing kind cluster and create a new one. By default the name of the cluster is "kind" - -``` -kind delete cluster -kind create cluster -``` - -Let's apply the mysql service - -``` -kapply apply $BASE/mysql --reconcile-timeout=120s --status-events | tee $OUTPUT/status - -expectedOutputLine "deployment.apps/mysql is Current: Deployment is available. Replicas: 1" - -expectedOutputLine "secret/mysql-pass is Current: Resource is always ready" - -expectedOutputLine "service/mysql is Current: Service is ready" - -# Verify that we have the mysql resources in the cluster. -kubectl get all --no-headers --selector=app=mysql | wc -l | xargs | tee $OUTPUT/status -expectedOutputLine "4" - -# Verify that we don't have any of the wordpress resources in the cluster. -kubectl get all --no-headers --selector=app=wordpress | wc -l | xargs | tee $OUTPUT/status -expectedOutputLine "0" -``` - -And the apply the wordpress service - -``` -kapply apply $BASE/wordpress --reconcile-timeout=120s --status-events | tee $OUTPUT/status - -expectedOutputLine "service/wordpress is Current: Service is ready" - -expectedOutputLine "deployment.apps/wordpress is Current: Deployment is available. Replicas: 1" - -# Verify that we now have the wordpress resources in the cluster. -kubectl get all --no-headers --selector=app=wordpress | wc -l | xargs | tee $OUTPUT/status -expectedOutputLine "4" -``` - -Destroy one service and make sure that only that service is destroyed and clean-up the cluster. - -``` -kapply destroy $BASE/wordpress | tee $OUTPUT/status; - -expectedOutputLine "service/wordpress delete successful" -expectedOutputLine "deployment.apps/wordpress delete successful" -expectedOutputLine "delete result: 2 attempted, 2 successful, 0 skipped, 0 failed" -expectedOutputLine "reconcile result: 2 attempted, 2 successful, 0 skipped, 0 failed, 0 timed out" - -# Verify that we still have the mysql resources in the cluster. -kubectl get all --no-headers --selector=app=mysql | wc -l | xargs | tee $OUTPUT/status -expectedOutputLine "4" - -# TODO: When we implement wait for prune/destroy, add a check here to make -# sure the wordpress resources are actually deleted. - -kind delete cluster; -``` diff --git a/examples/alphaTestExamples/crds.md b/examples/alphaTestExamples/crds.md deleted file mode 100644 index cc059753..00000000 --- a/examples/alphaTestExamples/crds.md +++ /dev/null @@ -1,143 +0,0 @@ -[kind]: https://github.com/kubernetes-sigs/kind - -# Demo: CRDs - -This demo shows how it is possible to apply both a CRD and a CR -using the CRD, in the same apply operation. This is not something -that is possible with kubectl. - -First define a place to work: - - -``` -DEMO_HOME=$(mktemp -d) -``` - -Alternatively, use - -> ``` -> DEMO_HOME=~/hello -> ``` - -## Establish the base - -Create the CRD and a CR. - - -``` -BASE=$DEMO_HOME/base -mkdir -p $BASE -OUTPUT=$DEMO_HOME/output -mkdir -p $OUTPUT -GREEN='\033[0;32m' -RED='\033[0;31m' -NC='\033[0m' # No Color - -function expectedOutputLine() { - if ! grep -q "$@" "$OUTPUT/status"; then - echo -e "${RED}Error: output line not found${NC}" - echo -e "${RED}Expected: $@${NC}" - exit 1 - else - echo -e "${GREEN}Success: output line found${NC}" - fi -} -``` - -CRD - - -``` -cat <$BASE/crd.yaml -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: foos.custom.io -spec: - group: custom.io - names: - kind: Foo - plural: foos - scope: Namespaced - versions: - - name: v1alpha1 - schema: - openAPIV3Schema: - description: A sample CRD - properties: - apiVersion: - description: 'APIVersion' - type: string - kind: - description: 'Kind' - type: string - metadata: - type: object - spec: - description: The spec for the CRD - properties: - name: - description: Name - type: string - required: - - name - type: object - type: object - served: true - storage: true - subresources: {} -EOF -``` - -CR - - -``` -cat <$BASE/cr.yaml -apiVersion: custom.io/v1alpha1 -kind: Foo -metadata: - name: example-foo -spec: - name: abc -EOF -``` - -## Run end-to-end tests - -The following requires installation of [kind]. - -Delete any existing kind cluster and create a new one. By default the name of the cluster is "kind" - -``` -kind delete cluster -kind create cluster -``` - -We will install this in the default namespace. - -Use the kapply init command to generate the inventory template. This contains -the namespace and inventory id used by apply to create inventory objects. - -``` -kapply init $BASE - -ls -1 $BASE | tee $OUTPUT/status -expectedOutputLine "inventory-template.yaml" -``` - -Use the `kapply` binary in `MYGOBIN` to apply both the CRD and the CR. - -``` -kapply apply $BASE --reconcile-timeout=1m --status-events | tee $OUTPUT/status - -expectedOutputLine "foo.custom.io/example-foo is Current: Resource is current" - -kubectl get crd --no-headers | awk '{print $1}' | tee $OUTPUT/status -expectedOutputLine "foos.custom.io" - -kubectl get foos.custom.io --no-headers | awk '{print $1}' | tee $OUTPUT/status -expectedOutputLine "example-foo" - -kind delete cluster -``` diff --git a/examples/alphaTestExamples/helloapp.md b/examples/alphaTestExamples/helloapp.md deleted file mode 100644 index 0a43d2ef..00000000 --- a/examples/alphaTestExamples/helloapp.md +++ /dev/null @@ -1,264 +0,0 @@ -[hello]: https://github.com/monopole/hello -[kind]: https://github.com/kubernetes-sigs/kind - -# Demo: hello app - -This demo helps you to deploy an example hello app end-to-end using `kapply`. - -Steps: -1. Create the resources files. -2. Spin-up kubernetes cluster on local using [kind]. -3. Deploy, modify and delete the app using `kapply` and verify the status. - -First define a place to work: - - -``` -DEMO_HOME=$(mktemp -d) -``` - -Alternatively, use - -> ``` -> DEMO_HOME=~/hello -> ``` - -## Establish the base - -Let's run the [hello] service. - - -``` -BASE=$DEMO_HOME/base -mkdir -p $BASE -OUTPUT=$DEMO_HOME/output -mkdir -p $OUTPUT -GREEN='\033[0;32m' -RED='\033[0;31m' -NC='\033[0m' # No Color - -function expectedOutputLine() { - if ! grep -q "$@" "$OUTPUT/status"; then - echo -e "${RED}Error: output line not found${NC}" - echo -e "${RED}Expected: $@${NC}" - exit 1 - else - echo -e "${GREEN}Success: output line found${NC}" - fi -} - -function expectedNotFound() { - if grep -q "$@" $OUTPUT/status; then - echo -e "${RED}Error: output line found:${NC}" - echo -e "${RED}Found: $@${NC}" - exit 1 - else - echo -e "${GREEN}Success: output line not found found${NC}" - fi -} -``` - -Let's add a simple config map resource in `base` - - -``` -cat <$BASE/configMap.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: the-map1 - namespace: hellospace -data: - altGreeting: "Good Morning!" - enableRisky: "false" -EOF -``` - -Create a deployment file with the following example configuration - - -``` -cat <$BASE/deployment.yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - labels: - app: hello - name: the-deployment - namespace: hellospace -spec: - replicas: 3 - selector: - matchLabels: - app: hello - template: - metadata: - labels: - app: hello - deployment: hello - spec: - containers: - - command: - - /hello - - --port=8080 - - --enableRiskyFeature=\$(ENABLE_RISKY) - env: - - name: ALT_GREETING - valueFrom: - configMapKeyRef: - key: altGreeting - name: the-map1 - - name: ENABLE_RISKY - valueFrom: - configMapKeyRef: - key: enableRisky - name: the-map1 - image: monopole/hello:1 - name: the-container - ports: - - containerPort: 8080 - protocol: TCP -EOF -``` - -Create `service.yaml` pointing to the deployment created above - - -``` -cat <$BASE/service.yaml -kind: Service -apiVersion: v1 -metadata: - name: the-service - namespace: hellospace -spec: - selector: - deployment: hello - type: LoadBalancer - ports: - - protocol: TCP - port: 8666 - targetPort: 8080 -EOF -``` - -## Run end-to-end tests - -The following requires installation of [kind]. - -Delete any existing kind cluster and create a new one. By default the name of the cluster is "kind" - -``` -kind delete cluster -kind create cluster -``` - -Create the `hellospace` namespace where we will install the resources. - -``` -kubectl create namespace hellospace -``` - -Use the kapply init command to generate the inventory template. This contains -the namespace and inventory id used by apply to create inventory objects. - -``` -kapply init $BASE - -ls -1 $BASE | tee $OUTPUT/status -expectedOutputLine "inventory-template.yaml" -``` - -Run preview to check which commands will be executed - -``` -kapply preview $BASE | tee $OUTPUT/status - -expectedOutputLine "apply result: 3 attempted, 3 successful, 0 skipped, 0 failed" - -kapply preview $BASE --server-side | tee $OUTPUT/status - -expectedOutputLine "apply result: 3 attempted, 3 successful, 0 skipped, 0 failed" - -# Verify that preview didn't create any resources. -kubectl get all -n hellospace 2>&1 | tee $OUTPUT/status -expectedOutputLine "No resources found in hellospace namespace." -``` - -Use the `kapply` binary in `MYGOBIN` to apply a deployment and verify it is successful. - -``` -kapply apply $BASE --reconcile-timeout=1m --status-events | tee $OUTPUT/status - -expectedOutputLine "deployment.apps/the-deployment is Current: Deployment is available. Replicas: 3" - -expectedOutputLine "service/the-service is Current: Service is ready" - -expectedOutputLine "configmap/the-map1 is Current: Resource is always ready" - -# Verify that we have the pods running in the cluster -kubectl get --no-headers pod -n hellospace | wc -l | xargs | tee $OUTPUT/status -expectedOutputLine "3" -``` - -Now let's replace the configMap with configMap2 apply the config, fetch and verify the status. -This should delete the-map1 from deployment and add the-map2. - -``` -cat <$BASE/configMap2.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: the-map2 - namespace: hellospace -data: - altGreeting: "Good Evening!" - enableRisky: "false" -EOF - -rm $BASE/configMap.yaml - -kapply apply $BASE --reconcile-timeout=120s --status-events | tee $OUTPUT/status - -expectedOutputLine "configmap/the-map2 is Current: Resource is always ready" - -expectedOutputLine "configmap/the-map1 prune successful" - -# Verify that the new configmap has been created and the old one pruned. -kubectl get cm -n hellospace --no-headers | awk '{print $1}' | tee $OUTPUT/status -expectedOutputLine "the-map2" -expectedNotFound "the-map1" -``` - -Clean-up the cluster - -``` -kapply preview $BASE --destroy | tee $OUTPUT/status - -expectedOutputLine "deployment.apps/the-deployment delete successful" -expectedOutputLine "configmap/the-map2 delete successful" -expectedOutputLine "service/the-service delete successful" -expectedOutputLine "delete result: 3 attempted, 3 successful, 0 skipped, 0 failed" - -kapply preview $BASE --destroy --server-side | tee $OUTPUT/status - -expectedOutputLine "deployment.apps/the-deployment delete successful" -expectedOutputLine "configmap/the-map2 delete successful" -expectedOutputLine "service/the-service delete successful" -expectedOutputLine "delete result: 3 attempted, 3 successful, 0 skipped, 0 failed" - -# Verify that preview all resources are still there after running preview. -kubectl get --no-headers all -n hellospace | wc -l | xargs | tee $OUTPUT/status -expectedOutputLine "6" - -kapply destroy $BASE | tee $OUTPUT/status; - -expectedOutputLine "deployment.apps/the-deployment delete successful" -expectedOutputLine "configmap/the-map2 delete successful" -expectedOutputLine "service/the-service delete successful" -expectedOutputLine "delete result: 3 attempted, 3 successful, 0 skipped, 0 failed" -expectedOutputLine "reconcile result: 3 attempted, 3 successful, 0 skipped, 0 failed, 0 timed out" -expectedNotFound "prune result" - -kind delete cluster; -``` diff --git a/examples/alphaTestExamples/init.md b/examples/alphaTestExamples/init.md deleted file mode 100644 index b8a88c2a..00000000 --- a/examples/alphaTestExamples/init.md +++ /dev/null @@ -1,163 +0,0 @@ -[kind]: https://github.com/kubernetes-sigs/kind - -# Demo: Init Command - -This demo shows how the kapply init command works. - -First define a place to work: - - -``` -DEMO_HOME=$(mktemp -d) -``` - -Alternatively, use - -> ``` -> DEMO_HOME=~/demo -> ``` - -## Establish the base - - -``` -BASE=$DEMO_HOME/base -mkdir -p $BASE -OUTPUT=$DEMO_HOME/output -mkdir -p $OUTPUT -GREEN='\033[0;32m' -RED='\033[0;31m' -NC='\033[0m' # No Color - -function expectedOutputLine() { - if ! grep -q "$@" "$OUTPUT/status"; then - echo -e "${RED}Error: output line not found${NC}" - echo -e "${RED}Expected: $@${NC}" - exit 1 - else - echo -e "${GREEN}Success: output line found${NC}" - fi -} -``` - -## Create the first "app" - -Create the config yaml for three config maps: (cm-a, cm-b, cm-c). - - -``` -cat <$BASE/namespace.yaml -apiVersion: v1 -kind: Namespace -metadata: - name: test-namespace -EOF - -cat <$BASE/config-map-a.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: cm-a - namespace: test-namespace - labels: - name: test-config-map-label -EOF - -cat <$BASE/config-map-b.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: cm-b - namespace: test-namespace - labels: - name: test-config-map-label -EOF - -cat <$BASE/config-map-c.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: cm-c - namespace: test-namespace - labels: - name: test-config-map-label -EOF -``` - -## Run end-to-end tests - -The following requires installation of [kind]. - -Delete any existing kind cluster and create a new one. By default the name of the cluster is "kind". - - -``` -kind delete cluster -kind create cluster -``` - -Use the kapply init command to generate the inventory template. This contains -the namespace and inventory id used by apply to create inventory objects. - -``` -kapply init $BASE | tee $OUTPUT/status -expectedOutputLine "namespace: test-namespace is used for inventory object" -``` - -Add another ConfigMap (cm-d) which is in the default namespace. The init -command should calculate the namespace to be default, since not all -objects are in the test-namespace. - - -``` - -cat <$BASE/config-map-d.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: cm-d - labels: - name: test-config-map-label -EOF - -# Remove the initial inventory template. -rm -f $BASE/inventory-template.yaml - -kapply init $BASE | tee $OUTPUT/status -expectedOutputLine "namespace: default is used for inventory object" -``` - -Remove the ConfigMap (cm-d) which is in the default namespace, and -add a cluster-scoped object. This cluster-scoped object should not -be used in the init namespace calculations, so we should calculate the -namespace as test-namespace. - - -``` - -# Remove the initial inventory template. -rm -f $BASE/inventory-template.yaml - -# Remove the ConfigMap in the default namespace. -rm -f $BASE/config-map-d.yaml - -# Add cluster-scoped resource--cluster-role -cat <$BASE/cluster-role.yaml -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - # "namespace" omitted since ClusterRoles are not namespaced - name: secret-reader -rules: -- apiGroups: [""] - # - # at the HTTP level, the name of the resource for accessing Secret - # objects is "secrets" - resources: ["secrets"] - verbs: ["get", "watch", "list"] -EOF - -kapply init $BASE | tee $OUTPUT/status -expectedOutputLine "namespace: test-namespace is used for inventory object" -``` - diff --git a/examples/alphaTestExamples/inventoryNamespace.md b/examples/alphaTestExamples/inventoryNamespace.md deleted file mode 100644 index 20b46834..00000000 --- a/examples/alphaTestExamples/inventoryNamespace.md +++ /dev/null @@ -1,134 +0,0 @@ -[kind]: https://github.com/kubernetes-sigs/kind - -# Demo: Inventory with Namespace - -This demo shows that the namespace the inventory object -is applied into will get applied first, so the inventory -object will always have a namespace to be applied into. - -First define a place to work: - - -``` -DEMO_HOME=$(mktemp -d) -``` - -Alternatively, use - -> ``` -> DEMO_HOME=~/hello -> ``` - -## Establish the base - - -``` -BASE=$DEMO_HOME/base -mkdir -p $BASE -OUTPUT=$DEMO_HOME/output -mkdir -p $OUTPUT -GREEN='\033[0;32m' -RED='\033[0;31m' -NC='\033[0m' # No Color - -function expectedOutputLine() { - if ! grep -q "$@" "$OUTPUT/status"; then - echo -e "${RED}Error: output line not found${NC}" - echo -e "${RED}Expected: $@${NC}" - exit 1 - else - echo -e "${GREEN}Success: output line found${NC}" - fi -} -``` - -## Create the "app" - -Create the config yaml for a config map and a namespace: (cm-a, test-namespace). - - -``` -cat <$BASE/config-map-a.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: cm-a - namespace: test-namespace - labels: - name: test-config-map-label -EOF - -cat <$BASE/test-namespace.yaml -apiVersion: v1 -kind: Namespace -metadata: - name: test-namespace -EOF -``` - -## Run end-to-end tests - -The following requires installation of [kind]. - -Delete any existing kind cluster and create a new one. By default the name of the cluster is "kind". - - -``` -kind delete cluster -kind create cluster -``` - -Use the kapply init command to generate the inventory template. This contains -the namespace and inventory id used by apply to create inventory objects. - -``` -kapply init --namespace=test-namespace $BASE | tee $OUTPUT/status -expectedOutputLine "namespace: test-namespace is used for inventory object" -``` - -Apply the "app" to the cluster. The test-namespace should be configured, and -the config map should be created, and no resources should be pruned. The -test-namespace is created first, so the following resources within the namespace -(including the inventory object) will not fail. - -``` -kapply apply $BASE --reconcile-timeout=1m | tee $OUTPUT/status -expectedOutputLine "namespace/test-namespace apply successful" -expectedOutputLine "configmap/cm-a apply successful" -expectedOutputLine "apply result: 2 attempted, 2 successful, 0 skipped, 0 failed" -expectedOutputLine "reconcile result: 2 attempted, 2 successful, 0 skipped, 0 failed, 0 timed out" - -# There should be only one inventory object -kubectl get cm -n test-namespace --selector='cli-utils.sigs.k8s.io/inventory-id' --no-headers | wc -l | tee $OUTPUT/status -expectedOutputLine "1" - -# Capture the inventory object name for later testing -invName=$(kubectl get cm -n test-namespace --selector='cli-utils.sigs.k8s.io/inventory-id' --no-headers | awk '{print $1}') - -# There should be one config map that is not the inventory object -kubectl get cm -n test-namespace --selector='name=test-config-map-label' --no-headers | wc -l | tee $OUTPUT/status -expectedOutputLine "1" - -# ConfigMap cm-a had been created in the cluster -kubectl get configmap/cm-a -n test-namespace --no-headers | wc -l | tee $OUTPUT/status -expectedOutputLine "1" -``` - -Now delete the inventory namespace from the local config. Ensure -that the subsequent apply does not prune this omitted namespace. - -``` -rm -f $BASE/test-namespace.yaml -kapply apply $BASE --reconcile-timeout=1m | tee $OUTPUT/status -expectedOutputLine "prune result: 1 attempted, 0 successful, 1 skipped, 0 failed" -expectedOutputLine "reconcile result: 1 attempted, 0 successful, 1 skipped, 0 failed, 0 timed out" - -# Inventory namespace should still exist -kubectl get ns test-namespace --no-headers | wc -l | tee $OUTPUT/status - -# Inventory object should still exist -kubectl get cm/${invName} -n test-namespace --no-headers | wc -l | tee $OUTPUT/status - -# ConfigMap cm-a should still exist -kubectl get configmap/cm-a -n test-namespace --no-headers | wc -l | tee $OUTPUT/status -expectedOutputLine "1" diff --git a/examples/alphaTestExamples/pruneAndDelete.md b/examples/alphaTestExamples/pruneAndDelete.md deleted file mode 100644 index 13e601b8..00000000 --- a/examples/alphaTestExamples/pruneAndDelete.md +++ /dev/null @@ -1,218 +0,0 @@ -[kind]: https://github.com/kubernetes-sigs/kind - -# Demo: Lifecycle directives - -This demo shows how it is possible to use a lifecycle directive to -change the behavior of prune and delete for specific resources. - -First define a place to work: - - -``` -DEMO_HOME=$(mktemp -d) -``` - -Alternatively, use - -> ``` -> DEMO_HOME=~/hello -> ``` - -## Establish the base - - -``` -BASE=$DEMO_HOME/base -mkdir -p $BASE -OUTPUT=$DEMO_HOME/output -mkdir -p $OUTPUT -GREEN='\033[0;32m' -RED='\033[0;31m' -NC='\033[0m' # No Color - -function expectedOutputLine() { - if ! grep -q "$@" "$OUTPUT/status"; then - echo -e "${RED}Error: output line not found${NC}" - echo -e "${RED}Expected: $@${NC}" - exit 1 - else - echo -e "${GREEN}Success: output line found${NC}" - fi -} - -function expectedNotFound() { - if grep -q "$@" $OUTPUT/status; then - echo -e "${RED}Error: output line found:${NC}" - echo -e "${RED}Found: $@${NC}" - exit 1 - else - echo -e "${GREEN}Success: output line not found found${NC}" - fi -} -``` - -In this example we will just use three ConfigMap resources for simplicity, but -of course any type of resource can be used. - -- the first ConfigMap resource does not have any annotations; -- the second ConfigMap resource has the **cli-utils.sigs.k8s.io/on-remove** annotation with the value of **keep**; -- the third ConfigMap resource has the **client.lifecycle.config.k8s.io/deletion** annotation with the value of **detach**. - -These two annotations tell the kapply tool that a resource should not be deleted, even -if it would otherwise be pruned or deleted with the destroy command. - - -``` -cat <$BASE/configMap1.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: firstmap -data: - artist: Ornette Coleman - album: The shape of jazz to come -EOF -``` - -This ConfigMap includes the **cli-utils.sigs.k8s.io/on-remove** annotation - - -``` -cat <$BASE/configMap2.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: secondmap - annotations: - cli-utils.sigs.k8s.io/on-remove: keep -data: - artist: Husker Du - album: New Day Rising -EOF -``` - - -This ConfigMap includes the **client.lifecycle.config.k8s.io/deletion** annotation - - -``` -cat <$BASE/configMap3.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: thirdmap - annotations: - client.lifecycle.config.k8s.io/deletion: detach -data: - artist: Husker Du - album: New Day Rising -EOF -``` - -## Run end-to-end tests - -The following requires installation of [kind]. - -Delete any existing kind cluster and create a new one. By default the name of the cluster is "kind" - -``` -kind delete cluster -kind create cluster -``` - -Use the kapply init command to generate the inventory template. This contains -the namespace and inventory id used by apply to create inventory objects. - -``` -kapply init $BASE | tee $OUTPUT/status -expectedOutputLine "namespace: default is used for inventory object" - -``` - -Apply the three resources to the cluster. - -``` -kapply apply $BASE --reconcile-timeout=1m | tee $OUTPUT/status - -expectedOutputLine "apply result: 3 attempted, 3 successful, 0 skipped, 0 failed" -expectedOutputLine "reconcile result: 3 attempted, 3 successful, 0 skipped, 0 failed, 0 timed out" -``` - -Use the preview command to show what will happen if we run destroy. This should -show that secondmap and thirdmap will not be deleted even when using the destroy -command. - -``` -kapply preview --destroy $BASE | tee $OUTPUT/status - -expectedOutputLine "configmap/firstmap delete successful" -expectedOutputLine 'configmap/secondmap delete skipped: annotation prevents deletion ("cli-utils.sigs.k8s.io/on-remove": "keep")' -expectedOutputLine 'configmap/thirdmap delete skipped: annotation prevents deletion ("client.lifecycle.config.k8s.io/deletion": "detach")' -expectedOutputLine "delete result: 3 attempted, 1 successful, 2 skipped, 0 failed" -``` - -We run the destroy command and see that the resource without the annotations (firstmap) -has been deleted, while the resources with the annotations (secondmap and thirdmap) are still in the -cluster. - -``` -kapply destroy $BASE | tee $OUTPUT/status - -expectedOutputLine "configmap/firstmap delete successful" -expectedOutputLine 'configmap/secondmap delete skipped: annotation prevents deletion ("cli-utils.sigs.k8s.io/on-remove": "keep")' -expectedOutputLine 'configmap/thirdmap delete skipped: annotation prevents deletion ("client.lifecycle.config.k8s.io/deletion": "detach")' -expectedOutputLine "configmap/firstmap reconcile successful" -expectedOutputLine "configmap/secondmap reconcile skipped" -expectedOutputLine "configmap/thirdmap reconcile skipped" -expectedOutputLine "delete result: 3 attempted, 1 successful, 2 skipped, 0 failed" -expectedOutputLine "reconcile result: 3 attempted, 1 successful, 2 skipped, 0 failed, 0 timed out" -expectedNotFound "prune result" - -kubectl get cm --no-headers | awk '{print $1}' | tee $OUTPUT/status -expectedOutputLine "secondmap" - -kubectl get cm --no-headers | awk '{print $1}' | tee $OUTPUT/status -expectedOutputLine "thirdmap" -``` - -Apply the resources back to the cluster so we can demonstrate the lifecycle -directive with pruning. - -``` -kapply apply $BASE --inventory-policy=adopt --reconcile-timeout=1m | tee $OUTPUT/status -``` - -Delete the manifest for secondmap and thirdmap - -``` -rm $BASE/configMap2.yaml - -rm $BASE/configMap3.yaml -``` - -Run preview to see that while secondmap and thirdmap would normally be pruned, they -will instead be skipped due to the lifecycle directive. - -``` -kapply preview $BASE | tee $OUTPUT/status - -expectedOutputLine "configmap/secondmap prune skipped" -expectedOutputLine "configmap/thirdmap prune skipped" -``` - -Run apply and verify that secondmap and thirdmap are still in the cluster. - -``` -kapply apply $BASE | tee $OUTPUT/status - -expectedOutputLine "configmap/secondmap prune skipped" -expectedOutputLine "configmap/thirdmap prune skipped" - -kubectl get cm --no-headers | awk '{print $1}' | tee $OUTPUT/status -expectedOutputLine "secondmap" - -kubectl get cm --no-headers | awk '{print $1}' | tee $OUTPUT/status -expectedOutputLine "thirdmap" - -kind delete cluster; -``` diff --git a/examples/alphaTestExamples/pruneBasic.md b/examples/alphaTestExamples/pruneBasic.md deleted file mode 100644 index 2c7bb15c..00000000 --- a/examples/alphaTestExamples/pruneBasic.md +++ /dev/null @@ -1,185 +0,0 @@ -[kind]: https://github.com/kubernetes-sigs/kind - -# Demo: Basic Prune - -This demo shows basic pruning behavior by creating an -"app" with three config maps. After the initial apply of the -"app", pruning is demonstrated by locally deleting one -of the config maps, and applying again. - -First define a place to work: - - -``` -DEMO_HOME=$(mktemp -d) -``` - -Alternatively, use - -> ``` -> DEMO_HOME=~/hello -> ``` - -## Establish the base - - -``` -BASE=$DEMO_HOME/base -mkdir -p $BASE -OUTPUT=$DEMO_HOME/output -mkdir -p $OUTPUT -GREEN='\033[0;32m' -RED='\033[0;31m' -NC='\033[0m' # No Color - -function expectedOutputLine() { - if ! grep -q "$@" "$OUTPUT/status"; then - echo -e "${RED}Error: output line not found${NC}" - echo -e "${RED}Expected: $@${NC}" - exit 1 - else - echo -e "${GREEN}Success: output line found${NC}" - fi -} -``` - -## Create the first "app" - -Create the config yaml for three config maps: (cm-a, cm-b, cm-c). - - -``` -cat <$BASE/config-map-a.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: cm-a - labels: - name: test-config-map-label -EOF - -cat <$BASE/config-map-b.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: cm-b - labels: - name: test-config-map-label -EOF - -cat <$BASE/config-map-c.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: cm-c - labels: - name: test-config-map-label -EOF -``` - -## Run end-to-end tests - -The following requires installation of [kind]. - -Delete any existing kind cluster and create a new one. By default the name of the cluster is "kind". - - -``` -kind delete cluster -kind create cluster -``` - -Use the kapply init command to generate the inventory template. This contains -the namespace and inventory id used by apply to create inventory objects. - -``` -kapply init $BASE | tee $OUTPUT/status -expectedOutputLine "namespace: default is used for inventory object" -``` - -Apply the "app" to the cluster. All the config maps should be created, and -no resources should be pruned. - -``` -kapply apply $BASE --reconcile-timeout=1m | tee $OUTPUT/status -expectedOutputLine "configmap/cm-a apply successful" -expectedOutputLine "configmap/cm-b apply successful" -expectedOutputLine "configmap/cm-c apply successful" -expectedOutputLine "apply result: 3 attempted, 3 successful, 0 skipped, 0 failed" -expectedOutputLine "reconcile result: 3 attempted, 3 successful, 0 skipped, 0 failed, 0 timed out" - -# There should be only one inventory object -kubectl get cm --selector='cli-utils.sigs.k8s.io/inventory-id' --no-headers | wc -l | tee $OUTPUT/status -expectedOutputLine "1" -# Capture the inventory object name for later testing -invName=$(kubectl get cm --selector='cli-utils.sigs.k8s.io/inventory-id' --no-headers | awk '{print $1}') -# There should be three config maps -kubectl get cm --selector='name=test-config-map-label' --no-headers | wc -l | tee $OUTPUT/status -expectedOutputLine "3" -# ConfigMap cm-a had been created in the cluster -kubectl get configmap/cm-a --no-headers | wc -l | tee $OUTPUT/status -expectedOutputLine "1" -# ConfigMap cm-b had been created in the cluster -kubectl get configmap/cm-b --no-headers | wc -l | tee $OUTPUT/status -expectedOutputLine "1" -# ConfigMap cm-c had been created in the cluster -kubectl get configmap/cm-c --no-headers | wc -l | tee $OUTPUT/status -expectedOutputLine "1" -``` - -## Update the "app" to remove a config map, and add another config map. - -Remove cm-a. - -Create a fourth config map--cm-d. - -``` -rm -f $BASE/config-map-a.yaml - -cat <$BASE/config-map-d.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: cm-d - labels: - name: test-config-map-label -EOF -``` - -## Apply the updated "app" - -cm-a should be pruned (since it has been deleted locally). - -cm-b, cm-c should be unchanged. - -cm-d should be created. - -``` -kapply apply $BASE --reconcile-timeout=1m | tee $OUTPUT/status -expectedOutputLine "configmap/cm-a prune successful" -expectedOutputLine "configmap/cm-b apply successful" -expectedOutputLine "configmap/cm-c apply successful" -expectedOutputLine "configmap/cm-d apply successful" -expectedOutputLine "apply result: 3 attempted, 3 successful, 0 skipped, 0 failed" -expectedOutputLine "prune result: 1 attempted, 1 successful, 0 skipped, 0 failed" -expectedOutputLine "reconcile result: 4 attempted, 4 successful, 0 skipped, 0 failed, 0 timed out" - -# There should be only one inventory object -kubectl get cm --selector='cli-utils.sigs.k8s.io/inventory-id' --no-headers | wc -l | tee $OUTPUT/status -expectedOutputLine "1" -# The inventory object should have the same name -kubectl get configmap/${invName} --no-headers | tee $OUTPUT/status -expectedOutputLine "${invName}" -# There should be three config maps -kubectl get cm --selector='name=test-config-map-label' --no-headers | wc -l | tee $OUTPUT/status -expectedOutputLine "3" -# ConfigMap cm-b had been created in the cluster -kubectl get configmap/cm-b --no-headers | wc -l | tee $OUTPUT/status -expectedOutputLine "1" -# ConfigMap cm-c had been created in the cluster -kubectl get configmap/cm-c --no-headers | wc -l | tee $OUTPUT/status -expectedOutputLine "1" -# ConfigMap cm-d had been created in the cluster -kubectl get configmap/cm-d --no-headers | wc -l | tee $OUTPUT/status -expectedOutputLine "1" -``` diff --git a/examples/alphaTestExamples/pruneNamespace.md b/examples/alphaTestExamples/pruneNamespace.md deleted file mode 100644 index 19a5a449..00000000 --- a/examples/alphaTestExamples/pruneNamespace.md +++ /dev/null @@ -1,203 +0,0 @@ -[kind]: https://github.com/kubernetes-sigs/kind - -# Demo: Namespaces and Prune - -This demo shows that namespaces will **not** be pruned if -there are objects remaining in them. The namespace for the -inventory object will be the default namespace, since not -all of the app objects are in the same namespace (pod-b -is in the default namespace). When pod-a, pod-b, and -the test-namespace are omitted from the subsequent apply -the test-namespace will be considered for pruning, but it -will **not** happend because there is still one object -in the namespace--pod-c. - -First define a place to work: - - -``` -DEMO_HOME=$(mktemp -d) -``` - -Alternatively, use - -> ``` -> DEMO_HOME=~/demo -> ``` - -## Establish the base - - -``` -BASE=$DEMO_HOME/base -mkdir -p $BASE -OUTPUT=$DEMO_HOME/output -mkdir -p $OUTPUT -GREEN='\033[0;32m' -RED='\033[0;31m' -NC='\033[0m' # No Color - -function expectedOutputLine() { - if ! grep -q "$@" "$OUTPUT/status"; then - echo -e "${RED}Error: output line not found${NC}" - echo -e "${RED}Expected: $@${NC}" - exit 1 - else - echo -e "${GREEN}Success: output line found${NC}" - fi -} -``` - -## Create the first "app" - -Create the config yaml for three config maps: (cm-a, cm-b, cm-c). - - -``` -cat <$BASE/namespace.yaml -apiVersion: v1 -kind: Namespace -metadata: - name: test-namespace -EOF - -cat <$BASE/config-map-a.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: cm-a - namespace: test-namespace - labels: - name: test-config-map-label -EOF - -cat <$BASE/config-map-b.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: cm-b - labels: - name: test-config-map-label -EOF - -cat <$BASE/config-map-c.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: cm-c - namespace: test-namespace - labels: - name: test-config-map-label -EOF -``` - -## Run end-to-end tests - -The following requires installation of [kind]. - -Delete any existing kind cluster and create a new one. By default the name of the cluster is "kind". - - -``` -kind delete cluster -kind create cluster -``` - -Use the kapply init command to generate the inventory template. This contains -the namespace and inventory id used by apply to create inventory objects. - -``` -kapply init $BASE | tee $OUTPUT/status -expectedOutputLine "namespace: default is used for inventory object" -``` - -Apply the "app" to the cluster. All the config maps should be created, and -no resources should be pruned. - -``` -kapply apply $BASE --reconcile-timeout=1m | tee $OUTPUT/status -expectedOutputLine "namespace/test-namespace apply successful" -expectedOutputLine "configmap/cm-a apply successful" -expectedOutputLine "configmap/cm-b apply successful" -expectedOutputLine "configmap/cm-c apply successful" -expectedOutputLine "apply result: 4 attempted, 4 successful, 0 skipped, 0 failed" -expectedOutputLine "reconcile result: 4 attempted, 4 successful, 0 skipped, 0 failed, 0 timed out" - -# There should be only one inventory object -kubectl get cm --selector='cli-utils.sigs.k8s.io/inventory-id' --no-headers | wc -l | tee $OUTPUT/status -expectedOutputLine "1" -# Capture the inventory object name for later testing -invName=$(kubectl get cm --selector='cli-utils.sigs.k8s.io/inventory-id' --no-headers | awk '{print $1}') -# There should be four config maps: one inventory in default, two in test-namespace, one in default namespace -kubectl get cm --selector='cli-utils.sigs.k8s.io/inventory-id' --no-headers | wc -l | tee $OUTPUT/status -expectedOutputLine "1" -kubectl get cm -n test-namespace --selector='name=test-config-map-label' --no-headers | wc -l | tee $OUTPUT/status -expectedOutputLine "2" -kubectl get cm --selector='name=test-config-map-label' --no-headers | wc -l | tee $OUTPUT/status -expectedOutputLine "1" -# ConfigMap cm-a had been created in the cluster -kubectl get configmap/cm-a -n test-namespace --no-headers | wc -l | tee $OUTPUT/status -expectedOutputLine "1" -# ConfigMap cm-b had been created in the cluster -kubectl get configmap/cm-b --no-headers | wc -l | tee $OUTPUT/status -expectedOutputLine "1" -# ConfigMap cm-c had been created in the cluster -kubectl get configmap/cm-c -n test-namespace --no-headers | wc -l | tee $OUTPUT/status -expectedOutputLine "1" -``` - -## Update the "app" to remove a two of the config maps, and the -namespace. - -Remove test-namespace -Remove cm-a -Remove cm-b - - -``` - -rm -f $BASE/namespace.yaml -rm -f $BASE/config-map-a.yaml -rm -f $BASE/config-map-b.yaml - -``` - -## Apply the updated "app" - -cm-a should be pruned (since it has been deleted locally). -cm-b should be pruned (since it has been deleted locally). -test-namespace should **not** be pruned. - - -``` -kapply apply $BASE --reconcile-timeout=1m | tee $OUTPUT/status - -expectedOutputLine "configmap/cm-c apply skipped: dependency scheduled for delete: _test-namespace__Namespace" -expectedOutputLine "configmap/cm-c reconcile skipped" - -expectedOutputLine "configmap/cm-a prune successful" -expectedOutputLine "configmap/cm-b prune successful" -expectedOutputLine "namespace/test-namespace prune skipped: namespace still in use: test-namespace" - -expectedOutputLine "namespace/test-namespace reconcile skipped" -expectedOutputLine "configmap/cm-a reconcile successful" -expectedOutputLine "configmap/cm-b reconcile successful" -expectedOutputLine "configmap/cm-c reconcile skipped" - -expectedOutputLine "apply result: 1 attempted, 0 successful, 1 skipped, 0 failed" -expectedOutputLine "prune result: 3 attempted, 2 successful, 1 skipped, 0 failed" -expectedOutputLine "reconcile result: 4 attempted, 2 successful, 2 skipped, 0 failed, 0 timed out" - -# The test-namespace should not be pruned. -kubectl get ns test-namespace --no-headers | wc -l | tee $OUTPUT/status -expectedOutputLine "1" -# Inventory object should have two items: namespace and cm-c. -kubectl get cm --selector='cli-utils.sigs.k8s.io/inventory-id' --no-headers | awk '{print $2}' | tee $OUTPUT/status -expectedOutputLine "2" -# The inventory object should have the same name -kubectl get configmap/${invName} --no-headers | tee $OUTPUT/status -expectedOutputLine "${invName}" -# ConfigMap cm-c remains in the cluster. -kubectl get configmap/cm-c -n test-namespace --no-headers | wc -l | tee $OUTPUT/status -expectedOutputLine "1" -``` diff --git a/examples/alphaTestExamples/serverSideApply.md b/examples/alphaTestExamples/serverSideApply.md deleted file mode 100644 index 0d27ff2f..00000000 --- a/examples/alphaTestExamples/serverSideApply.md +++ /dev/null @@ -1,137 +0,0 @@ -[kind]: https://github.com/kubernetes-sigs/kind - -# Demo: Server Side Apply - -This demo shows how to invoke server-side apply, -instead of the default client-side apply. - -First define a place to work: - - -``` -DEMO_HOME=$(mktemp -d) -``` - -Alternatively, use - -> ``` -> DEMO_HOME=~/hello -> ``` - -## Establish the base - - -``` -BASE=$DEMO_HOME/base -mkdir -p $BASE -OUTPUT=$DEMO_HOME/output -mkdir -p $OUTPUT -GREEN='\033[0;32m' -RED='\033[0;31m' -NC='\033[0m' # No Color - -function expectedOutputLine() { - if ! grep -q "$@" "$OUTPUT/status"; then - echo -e "${RED}Error: output line not found${NC}" - echo -e "${RED}Expected: $@${NC}" - exit 1 - else - echo -e "${GREEN}Success: output line found${NC}" - fi -} -``` - -## Create the first "app" - -Create the config yaml for two config maps: (cm-a, cm-b). - - -``` -cat <$BASE/config-map-a.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: cm-a - labels: - name: test-config-map-label -EOF - -cat <$BASE/config-map-b.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: cm-b - labels: - name: test-config-map-label -data: - foo: sean -EOF -``` - -## Run end-to-end tests - -The following requires installation of [kind]. - -Delete any existing kind cluster and create a new one. By default the name of the cluster is "kind". - - -``` -kind delete cluster -kind create cluster -``` - -Use the kapply init command to generate the inventory template. This contains -the namespace and inventory id used by apply to create inventory objects. - -``` -kapply init $BASE | tee $OUTPUT/status -expectedOutputLine "namespace: default is used for inventory object" -``` - -Apply the "app" to the cluster. All the config maps should be created, and -no resources should be pruned. - -``` -kapply apply $BASE --server-side --reconcile-timeout=1m | tee $OUTPUT/status -expectedOutputLine "configmap/cm-a apply successful" -expectedOutputLine "configmap/cm-b apply successful" -expectedOutputLine "apply result: 2 attempted, 2 successful, 0 skipped, 0 failed" -expectedOutputLine "reconcile result: 2 attempted, 2 successful, 0 skipped, 0 failed, 0 timed out" - -# There should be only one inventory object -kubectl get cm --selector='cli-utils.sigs.k8s.io/inventory-id' --no-headers | wc -l | tee $OUTPUT/status -expectedOutputLine "1" -# There should be two config maps that are not the inventory object -kubectl get cm --selector='name=test-config-map-label' --no-headers | wc -l | tee $OUTPUT/status -expectedOutputLine "2" -# ConfigMap cm-a had been created in the cluster -kubectl get configmap/cm-a --no-headers | wc -l | tee $OUTPUT/status -expectedOutputLine "1" -# ConfigMap cm-b had been created in the cluster -kubectl get configmap/cm-b --no-headers | wc -l | tee $OUTPUT/status -expectedOutputLine "1" -``` - -Update a config map to update a field owned by the default field manager. -Update both config maps, using a different field-manager to create a -conflict, but the the --force-conflicts flag to overwrite successfully. -The conflicting field is "data.foo". - -``` -cat <$BASE/config-map-b.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: cm-b - labels: - name: test-config-map-label -data: - foo: baz -EOF - -kapply apply $BASE --server-side --field-manager=sean --force-conflicts --reconcile-timeout=1m | tee $OUTPUT/status -expectedOutputLine "configmap/cm-a apply successful" -expectedOutputLine "configmap/cm-b apply successful" -expectedOutputLine "apply result: 2 attempted, 2 successful, 0 skipped, 0 failed" -expectedOutputLine "reconcile result: 2 attempted, 2 successful, 0 skipped, 0 failed, 0 timed out" -``` diff --git a/go.mod b/go.mod index 5bf9bfac..bc2ec50e 100644 --- a/go.mod +++ b/go.mod @@ -4,19 +4,12 @@ go 1.25.0 require ( github.com/google/go-cmp v0.7.0 - github.com/google/uuid v1.6.0 - github.com/onsi/ginkgo/v2 v2.28.1 github.com/onsi/gomega v1.39.0 - github.com/spf13/cobra v1.10.2 - github.com/spyzhov/ajson v0.9.6 github.com/stretchr/testify v1.11.1 - gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.35.3 - k8s.io/apiextensions-apiserver v0.35.3 k8s.io/apimachinery v0.35.3 k8s.io/cli-runtime v0.35.3 k8s.io/client-go v0.35.3 - k8s.io/component-base v0.35.3 k8s.io/klog/v2 v2.130.1 k8s.io/kubectl v0.35.3 k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 @@ -28,31 +21,23 @@ require ( require ( github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect - github.com/Masterminds/semver/v3 v3.4.0 // indirect - github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chai2010/gettext-go v1.0.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect - github.com/evanphx/json-patch v5.7.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect - github.com/fatih/camelcase v1.0.0 // indirect - github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-errors/errors v1.5.1 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-openapi/jsonpointer v0.21.1 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.1 // indirect - github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/gnostic-models v0.7.0 // indirect - github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/jonboulle/clockwork v0.5.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect @@ -63,21 +48,17 @@ require ( github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/onsi/ginkgo/v2 v2.28.1 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_golang v1.23.2 // indirect - github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.66.1 // indirect github.com/prometheus/procfs v0.17.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/spf13/cobra v1.10.2 // indirect github.com/spf13/pflag v1.0.9 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xlab/treeprint v1.2.0 // indirect - go.opentelemetry.io/otel v1.37.0 // indirect - go.opentelemetry.io/otel/trace v1.37.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/mod v0.32.0 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sync v0.19.0 // indirect @@ -85,12 +66,12 @@ require ( golang.org/x/term v0.39.0 // indirect golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.12.0 // indirect - golang.org/x/tools v0.41.0 // indirect - gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect google.golang.org/protobuf v1.36.8 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - k8s.io/component-helpers v0.35.3 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/apiextensions-apiserver v0.35.3 // indirect + k8s.io/component-base v0.35.3 // indirect k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/kustomize/api v0.21.1 // indirect diff --git a/go.sum b/go.sum index 84d21ea0..0ebd1be3 100644 --- a/go.sum +++ b/go.sum @@ -21,24 +21,12 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI= -github.com/evanphx/json-patch v5.7.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= -github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= -github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= -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-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= @@ -53,8 +41,6 @@ github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZ github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= 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/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= @@ -62,8 +48,6 @@ github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7O 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-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc= github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -72,32 +56,18 @@ github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJr github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= -github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= 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/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= -github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY= -github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= -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-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= @@ -118,8 +88,6 @@ github.com/onsi/gomega v1.39.0 h1:y2ROC3hKFmQZJNFeGAMeHZKkjBL65mIZcvrLQBF9k6Q= github.com/onsi/gomega v1.39.0/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= -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= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -141,8 +109,6 @@ github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spyzhov/ajson v0.9.6 h1:iJRDaLa+GjhCDAt1yFtU/LKMtLtsNVKkxqlpvrHHlpQ= -github.com/spyzhov/ajson v0.9.6/go.mod h1:a6oSw0MMb7Z5aD2tPoPO+jq11ETKgXUr2XktHdT8Wt8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= @@ -151,22 +117,10 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= -go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= -go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= -go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= -go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= 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/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -196,8 +150,6 @@ golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= -gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= -gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -222,8 +174,6 @@ k8s.io/client-go v0.35.3 h1:s1lZbpN4uI6IxeTM2cpdtrwHcSOBML1ODNTCCfsP1pg= k8s.io/client-go v0.35.3/go.mod h1:RzoXkc0mzpWIDvBrRnD+VlfXP+lRzqQjCmKtiwZ8Q9c= k8s.io/component-base v0.35.3 h1:mbKbzoIMy7JDWS/wqZobYW1JDVRn/RKRaoMQHP9c4P0= k8s.io/component-base v0.35.3/go.mod h1:IZ8LEG30kPN4Et5NeC7vjNv5aU73ku5MS15iZyvyMYk= -k8s.io/component-helpers v0.35.3 h1:Rl2p3wNMC0YU21rziLkWXavr7MwkB5Td3lNZ/+gYGm8= -k8s.io/component-helpers v0.35.3/go.mod h1:8BkyfcBA6XsCtFYxDB+mCfZqM6P39Aco12AKigNn0C8= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= diff --git a/hack/run-in-gopath.sh b/hack/run-in-gopath.sh deleted file mode 100755 index 73b977bc..00000000 --- a/hack/run-in-gopath.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env bash -# Copyright 2021 The Kubernetes Authors. -# SPDX-License-Identifier: Apache-2.0 - -set -o errexit -o nounset -o pipefail -o posix - -PKG_PATH="github.com/fluxcd/cli-utils" - -REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" - -# Make a new temporary GOPATH directory -export GOPATH=$(mktemp -d -t cli-utils-gopath.XXXXXXXXXX) -# Clean up on exit (modcache has read-only files, so clean that first) -trap "go clean -modcache && rm '${GOPATH}/src/${PKG_PATH}' && rm -rf '${GOPATH}'" EXIT - -# Make sure we can read, write, and delete -chmod a+rw "${GOPATH}" - -# Use a temporary cache -export GOCACHE="${GOPATH}/cache" - -# Create a symlink for the local repo in the GOPATH -mkdir -p "${GOPATH}/src/${PKG_PATH}" -rm -r "${GOPATH}/src/${PKG_PATH}" -ln -s "${REPO_ROOT}" "${GOPATH}/src/${PKG_PATH}" - -# Make sure our own Go binaries are in PATH. -export PATH="${GOPATH}/bin:${PATH}" - -# Set GOROOT so binaries that parse code can work properly. -export GOROOT=$(go env GOROOT) - -# Unset GOBIN in case it already exists in the current session. -unset GOBIN - -# enter the GOPATH before executing the command -cd "${GOPATH}/src/${PKG_PATH}" - -# Run the user-provided command. -"${@}" - -# exit the GOPATH before deleting it -cd "${REPO_ROOT}" diff --git a/hack/testExamplesE2EAgainstKapply.sh b/hack/testExamplesE2EAgainstKapply.sh deleted file mode 100755 index 73b89aff..00000000 --- a/hack/testExamplesE2EAgainstKapply.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env bash -# -# Copyright 2019 The Kubernetes Authors. -# SPDX-License-Identifier: Apache-2.0 - -set -o nounset -set -o errexit -set -o pipefail - -GREEN='\033[0;32m' -RED='\033[0;31m' -NC='\033[0m' # No Color - -results=() -failed=0 - -function run_test() { - mdrip -alsologtostderr -v 10 --blockTimeOut 6m0s --mode test \ - --label testE2EAgainstLatestRelease "${1}" -} - -for path in examples/alphaTestExamples/*.md; do - test_name="$(basename "${path}")" - echo "-----------------------------------" - echo "Example Test: ${test_name}" - echo "-----------------------------------" - if run_test "${path}"; then - echo - echo -e "${GREEN}Example Test Succeeded: ${test_name}${NC}" - results+=("${test_name}\t${GREEN}Succeeded${NC}") - else - echo - echo -e "${RED}Example Test Failed: ${test_name}${NC}" - let "failed+=1" - results+=("${test_name}\t${RED}Failed${NC}") - fi - echo -done - -( - echo -e "TEST\tRESULT" - for result in "${results[@]}"; do - echo -e "${result}" - done -) | column -t - -echo - -if [[ ${failed} -gt 0 ]]; then - echo -e "${RED}Example Tests Failed${NC}" - exit 1 -else - echo -e "${GREEN}Example Tests Succeeded${NC}" - exit 0 -fi diff --git a/pkg/apis/actuation/actuationstatus_string.go b/pkg/apis/actuation/actuationstatus_string.go deleted file mode 100644 index 7725bef8..00000000 --- a/pkg/apis/actuation/actuationstatus_string.go +++ /dev/null @@ -1,26 +0,0 @@ -// Code generated by "stringer -type=ActuationStatus -linecomment"; DO NOT EDIT. - -package actuation - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[ActuationPending-0] - _ = x[ActuationSucceeded-1] - _ = x[ActuationSkipped-2] - _ = x[ActuationFailed-3] -} - -const _ActuationStatus_name = "PendingSucceededSkippedFailed" - -var _ActuationStatus_index = [...]uint8{0, 7, 16, 23, 29} - -func (i ActuationStatus) String() string { - if i < 0 || i >= ActuationStatus(len(_ActuationStatus_index)-1) { - return "ActuationStatus(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _ActuationStatus_name[_ActuationStatus_index[i]:_ActuationStatus_index[i+1]] -} diff --git a/pkg/apis/actuation/actuationstrategy_string.go b/pkg/apis/actuation/actuationstrategy_string.go deleted file mode 100644 index 73e8aef3..00000000 --- a/pkg/apis/actuation/actuationstrategy_string.go +++ /dev/null @@ -1,24 +0,0 @@ -// Code generated by "stringer -type=ActuationStrategy -linecomment"; DO NOT EDIT. - -package actuation - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[ActuationStrategyApply-0] - _ = x[ActuationStrategyDelete-1] -} - -const _ActuationStrategy_name = "ApplyDelete" - -var _ActuationStrategy_index = [...]uint8{0, 5, 11} - -func (i ActuationStrategy) String() string { - if i < 0 || i >= ActuationStrategy(len(_ActuationStrategy_index)-1) { - return "ActuationStrategy(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _ActuationStrategy_name[_ActuationStrategy_index[i]:_ActuationStrategy_index[i+1]] -} diff --git a/pkg/apis/actuation/doc.go b/pkg/apis/actuation/doc.go deleted file mode 100644 index ec85998d..00000000 --- a/pkg/apis/actuation/doc.go +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -// Package actuation contains API Schema definitions for the -// cli-utils.kubernetes.io API group. -// +k8s:deepcopy-gen=package -// +groupName=cli-utils.kubernetes.io -package actuation // import "github.com/fluxcd/cli-utils/pkg/apis/actuation" diff --git a/pkg/apis/actuation/reconcilestatus_string.go b/pkg/apis/actuation/reconcilestatus_string.go deleted file mode 100644 index 0237cc9b..00000000 --- a/pkg/apis/actuation/reconcilestatus_string.go +++ /dev/null @@ -1,27 +0,0 @@ -// Code generated by "stringer -type=ReconcileStatus -linecomment"; DO NOT EDIT. - -package actuation - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[ReconcilePending-0] - _ = x[ReconcileSucceeded-1] - _ = x[ReconcileSkipped-2] - _ = x[ReconcileFailed-3] - _ = x[ReconcileTimeout-4] -} - -const _ReconcileStatus_name = "PendingSucceededSkippedFailedTimeout" - -var _ReconcileStatus_index = [...]uint8{0, 7, 16, 23, 29, 36} - -func (i ReconcileStatus) String() string { - if i < 0 || i >= ReconcileStatus(len(_ReconcileStatus_index)-1) { - return "ReconcileStatus(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _ReconcileStatus_name[_ReconcileStatus_index[i]:_ReconcileStatus_index[i+1]] -} diff --git a/pkg/apis/actuation/types.go b/pkg/apis/actuation/types.go deleted file mode 100644 index 0f1dd642..00000000 --- a/pkg/apis/actuation/types.go +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package actuation - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" -) - -// Inventory represents the inventory object in memory. -// Inventory is currently only used for in-memory storage and not serialized to -// disk or to the API server. -type Inventory struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec InventorySpec `json:"spec,omitempty"` - Status InventoryStatus `json:"status,omitempty"` -} - -// InventorySpec is the specification of the desired/expected inventory state. -type InventorySpec struct { - Objects []ObjectReference `json:"objects,omitempty"` -} - -// InventoryStatus is the status of the current/last-known inventory state. -type InventoryStatus struct { - Objects []ObjectStatus `json:"objects,omitempty"` -} - -// ObjectReference is a reference to a KRM resource by name and kind. -// -// Kubernetes only stores one API Version for each Kind at any given time, -// so version is not used when referencing objects. -type ObjectReference struct { - // Kind identifies a REST resource within a Group. - // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - Kind string `json:"kind,omitempty"` - - // Group identifies an API namespace for REST resources. - // If group is omitted, it is treated as the "core" group. - // More info: https://kubernetes.io/docs/reference/using-api/#api-groups - // +optional - Group string `json:"group,omitempty"` - - // Name identifies an object instance of a REST resource. - // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - Name string `json:"name,omitempty"` - - // Namespace identifies a group of objects across REST resources. - // If namespace is specified, the resource must be namespace-scoped. - // If namespace is omitted, the resource must be cluster-scoped. - // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ - // +optional - Namespace string `json:"namespace,omitempty"` -} - -// ObjectStatus is a snapshot of the actuation and reconciliation status of a -// referenced object. -type ObjectStatus struct { - ObjectReference `json:",inline"` - - // Strategy indicates the method of actuation (apply or delete) used or planned to be used. - Strategy ActuationStrategy `json:"strategy,omitempty"` - // Actuation indicates whether actuation has been performed yet and how it went. - Actuation ActuationStatus `json:"actuation,omitempty"` - // Reconcile indicates whether reconciliation has been performed yet and how it went. - Reconcile ReconcileStatus `json:"reconcile,omitempty"` - - // UID is the last known UID (after apply or before delete). - // This can help identify if the object has been replaced. - // +optional - UID types.UID `json:"uid,omitempty"` - // Generation is the last known Generation (after apply or before delete). - // This can help identify if the object has been modified. - // Generation is not available for deleted objects. - // +optional - Generation int64 `json:"generation,omitempty"` -} - -//nolint:revive // consistent prefix improves tab-completion for enums -//go:generate stringer -type=ActuationStrategy -linecomment -type ActuationStrategy int - -const ( - ActuationStrategyApply ActuationStrategy = iota // Apply - ActuationStrategyDelete // Delete -) - -//nolint:revive // consistent prefix improves tab-completion for enums -//go:generate stringer -type=ActuationStatus -linecomment -type ActuationStatus int - -const ( - ActuationPending ActuationStatus = iota // Pending - ActuationSucceeded // Succeeded - ActuationSkipped // Skipped - ActuationFailed // Failed -) - -//go:generate stringer -type=ReconcileStatus -linecomment -type ReconcileStatus int - -const ( - ReconcilePending ReconcileStatus = iota // Pending - ReconcileSucceeded // Succeeded - ReconcileSkipped // Skipped - ReconcileFailed // Failed - ReconcileTimeout // Timeout -) diff --git a/pkg/apis/actuation/zz_generated.deepcopy.go b/pkg/apis/actuation/zz_generated.deepcopy.go deleted file mode 100644 index 85312d83..00000000 --- a/pkg/apis/actuation/zz_generated.deepcopy.go +++ /dev/null @@ -1,104 +0,0 @@ -//go:build !ignore_autogenerated -// +build !ignore_autogenerated - -// Copyright 2024 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -// Code generated by deepcopy-gen. DO NOT EDIT. - -package actuation - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Inventory) DeepCopyInto(out *Inventory) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - in.Status.DeepCopyInto(&out.Status) - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Inventory. -func (in *Inventory) DeepCopy() *Inventory { - if in == nil { - return nil - } - out := new(Inventory) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *InventorySpec) DeepCopyInto(out *InventorySpec) { - *out = *in - if in.Objects != nil { - in, out := &in.Objects, &out.Objects - *out = make([]ObjectReference, len(*in)) - copy(*out, *in) - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InventorySpec. -func (in *InventorySpec) DeepCopy() *InventorySpec { - if in == nil { - return nil - } - out := new(InventorySpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *InventoryStatus) DeepCopyInto(out *InventoryStatus) { - *out = *in - if in.Objects != nil { - in, out := &in.Objects, &out.Objects - *out = make([]ObjectStatus, len(*in)) - copy(*out, *in) - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InventoryStatus. -func (in *InventoryStatus) DeepCopy() *InventoryStatus { - if in == nil { - return nil - } - out := new(InventoryStatus) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ObjectReference) DeepCopyInto(out *ObjectReference) { - *out = *in - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObjectReference. -func (in *ObjectReference) DeepCopy() *ObjectReference { - if in == nil { - return nil - } - out := new(ObjectReference) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ObjectStatus) DeepCopyInto(out *ObjectStatus) { - *out = *in - out.ObjectReference = in.ObjectReference - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObjectStatus. -func (in *ObjectStatus) DeepCopy() *ObjectStatus { - if in == nil { - return nil - } - out := new(ObjectStatus) - in.DeepCopyInto(out) - return out -} diff --git a/pkg/apply/applier.go b/pkg/apply/applier.go deleted file mode 100644 index e7f01245..00000000 --- a/pkg/apply/applier.go +++ /dev/null @@ -1,353 +0,0 @@ -// Copyright 2019 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package apply - -import ( - "context" - "fmt" - "time" - - "github.com/fluxcd/cli-utils/pkg/apis/actuation" - "github.com/fluxcd/cli-utils/pkg/apply/cache" - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/apply/filter" - "github.com/fluxcd/cli-utils/pkg/apply/info" - "github.com/fluxcd/cli-utils/pkg/apply/mutator" - "github.com/fluxcd/cli-utils/pkg/apply/prune" - "github.com/fluxcd/cli-utils/pkg/apply/solver" - "github.com/fluxcd/cli-utils/pkg/apply/taskrunner" - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/pkg/kstatus/watcher" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/object/validation" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/sets" - "k8s.io/client-go/discovery" - "k8s.io/client-go/dynamic" - "k8s.io/klog/v2" -) - -// Applier performs the step of applying a set of resources into a cluster, -// conditionally waits for all of them to be fully reconciled and finally -// performs prune to clean up any resources that has been deleted. -// The applier performs its function by executing a list queue of tasks, -// each of which is one of the steps in the process of applying a set -// of resources to the cluster. The actual execution of these tasks are -// handled by a StatusRunner. So the taskqueue is effectively a -// specification that is executed by the StatusRunner. Based on input -// parameters and/or the set of resources that needs to be applied to the -// cluster, different sets of tasks might be needed. -type Applier struct { - pruner *prune.Pruner - statusWatcher watcher.StatusWatcher - invClient inventory.Client - client dynamic.Interface - openAPIGetter discovery.OpenAPISchemaInterface - mapper meta.RESTMapper - infoHelper info.Helper -} - -// prepareObjects returns the set of objects to apply and to prune or -// an error if one occurred. -func (a *Applier) prepareObjects(localInv inventory.Info, localObjs object.UnstructuredSet, - o ApplierOptions) (object.UnstructuredSet, object.UnstructuredSet, error) { - if localInv == nil { - return nil, nil, fmt.Errorf("the local inventory can't be nil") - } - if err := inventory.ValidateNoInventory(localObjs); err != nil { - return nil, nil, err - } - // Add the inventory annotation to the resources being applied. - for _, localObj := range localObjs { - inventory.AddInventoryIDAnnotation(localObj, localInv) - } - // If the inventory uses the Name strategy and an inventory ID is provided, - // verify that the existing inventory object (if there is one) has an ID - // label that matches. - // TODO(seans): This inventory id validation should happen in destroy and status. - if localInv.Strategy() == inventory.NameStrategy && localInv.ID() != "" { - prevInvObjs, err := a.invClient.GetClusterInventoryObjs(localInv) - if err != nil { - return nil, nil, err - } - if len(prevInvObjs) > 1 { - panic(fmt.Errorf("found %d inv objects with Name strategy", len(prevInvObjs))) - } - if len(prevInvObjs) == 1 { - invObj := prevInvObjs[0] - val := invObj.GetLabels()[common.InventoryLabel] - if val != localInv.ID() { - return nil, nil, fmt.Errorf("inventory-id of inventory object in cluster doesn't match provided id %q", localInv.ID()) - } - } - } - pruneObjs, err := a.pruner.GetPruneObjs(localInv, localObjs, prune.Options{ - DryRunStrategy: o.DryRunStrategy, - }) - if err != nil { - return nil, nil, err - } - return localObjs, pruneObjs, nil -} - -// Run performs the Apply step. This happens asynchronously with updates -// on progress and any errors reported back on the event channel. -// Cancelling the operation or setting timeout on how long to Wait -// for it complete can be done with the passed in context. -// Note: There isn't currently any way to interrupt the operation -// before all the given resources have been applied to the cluster. Any -// cancellation or timeout will only affect how long we Wait for the -// resources to become current. -func (a *Applier) Run(ctx context.Context, invInfo inventory.Info, objects object.UnstructuredSet, options ApplierOptions) <-chan event.Event { - klog.V(4).Infof("apply run for %d objects", len(objects)) - eventChannel := make(chan event.Event) - setDefaults(&options) - go func() { - defer close(eventChannel) - // Validate the resources to make sure we catch those problems early - // before anything has been updated in the cluster. - vCollector := &validation.Collector{} - validator := &validation.Validator{ - Collector: vCollector, - Mapper: a.mapper, - } - validator.Validate(objects) - - // Decide which objects to apply and which to prune - applyObjs, pruneObjs, err := a.prepareObjects(invInfo, objects, options) - if err != nil { - handleError(eventChannel, err) - return - } - klog.V(4).Infof("calculated %d apply objs; %d prune objs", len(applyObjs), len(pruneObjs)) - - // Build a TaskContext for passing info between tasks - resourceCache := cache.NewResourceCacheMap() - taskContext := taskrunner.NewTaskContext(eventChannel, resourceCache) - - // Fetch the queue (channel) of tasks that should be executed. - klog.V(4).Infoln("applier building task queue...") - // Build list of apply validation filters. - applyFilters := []filter.ValidationFilter{ - filter.InventoryPolicyApplyFilter{ - Client: a.client, - Mapper: a.mapper, - Inv: invInfo, - InvPolicy: options.InventoryPolicy, - }, - filter.DependencyFilter{ - TaskContext: taskContext, - ActuationStrategy: actuation.ActuationStrategyApply, - DryRunStrategy: options.DryRunStrategy, - }, - } - // Build list of prune validation filters. - pruneFilters := []filter.ValidationFilter{ - filter.PreventRemoveFilter{}, - filter.InventoryPolicyPruneFilter{ - Inv: invInfo, - InvPolicy: options.InventoryPolicy, - }, - filter.LocalNamespacesFilter{ - LocalNamespaces: localNamespaces(invInfo, object.UnstructuredSetToObjMetadataSet(objects)), - }, - filter.DependencyFilter{ - TaskContext: taskContext, - ActuationStrategy: actuation.ActuationStrategyDelete, - DryRunStrategy: options.DryRunStrategy, - }, - } - // Build list of apply mutators. - applyMutators := []mutator.Interface{ - &mutator.ApplyTimeMutator{ - Client: a.client, - Mapper: a.mapper, - ResourceCache: resourceCache, - }, - } - taskBuilder := &solver.TaskQueueBuilder{ - Pruner: a.pruner, - DynamicClient: a.client, - OpenAPIGetter: a.openAPIGetter, - InfoHelper: a.infoHelper, - Mapper: a.mapper, - InvClient: a.invClient, - Collector: vCollector, - ApplyFilters: applyFilters, - ApplyMutators: applyMutators, - PruneFilters: pruneFilters, - } - opts := solver.Options{ - ServerSideOptions: options.ServerSideOptions, - ReconcileTimeout: options.ReconcileTimeout, - Destroy: false, - Prune: !options.NoPrune, - DryRunStrategy: options.DryRunStrategy, - PrunePropagationPolicy: options.PrunePropagationPolicy, - PruneTimeout: options.PruneTimeout, - InventoryPolicy: options.InventoryPolicy, - } - - // Build the ordered set of tasks to execute. - taskQueue := taskBuilder. - WithApplyObjects(applyObjs). - WithPruneObjects(pruneObjs). - WithInventory(invInfo). - Build(taskContext, opts) - - klog.V(4).Infof("validation errors: %d", len(vCollector.Errors)) - klog.V(4).Infof("invalid objects: %d", len(vCollector.InvalidIds)) - - // Handle validation errors - switch options.ValidationPolicy { - case validation.ExitEarly: - err = vCollector.ToError() - if err != nil { - handleError(eventChannel, err) - return - } - case validation.SkipInvalid: - for _, err := range vCollector.Errors { - handleValidationError(eventChannel, err) - } - default: - handleError(eventChannel, fmt.Errorf("invalid ValidationPolicy: %q", options.ValidationPolicy)) - return - } - - // Register invalid objects to be retained in the inventory, if present. - for _, id := range vCollector.InvalidIds { - taskContext.AddInvalidObject(id) - } - - // Send event to inform the caller about the resources that - // will be applied/pruned. - eventChannel <- event.Event{ - Type: event.InitType, - InitEvent: event.InitEvent{ - ActionGroups: taskQueue.ToActionGroups(), - }, - } - // Create a new TaskStatusRunner to execute the taskQueue. - klog.V(4).Infoln("applier building TaskStatusRunner...") - allIDs := object.UnstructuredSetToObjMetadataSet(append(applyObjs, pruneObjs...)) - statusWatcher := a.statusWatcher - // Disable watcher for dry runs - if opts.DryRunStrategy.ClientOrServerDryRun() { - statusWatcher = watcher.BlindStatusWatcher{} - } - runner := taskrunner.NewTaskStatusRunner(allIDs, statusWatcher) - klog.V(4).Infoln("applier running TaskStatusRunner...") - err = runner.Run(ctx, taskContext, taskQueue.ToChannel(), taskrunner.Options{ - EmitStatusEvents: options.EmitStatusEvents, - WatcherRESTScopeStrategy: options.WatcherRESTScopeStrategy, - }) - if err != nil { - handleError(eventChannel, err) - return - } - }() - return eventChannel -} - -type ApplierOptions struct { - // Encapsulates the fields for server-side apply. - ServerSideOptions common.ServerSideOptions - - // ReconcileTimeout defines whether the applier should wait - // until all applied resources have been reconciled, and if so, - // how long to wait. - ReconcileTimeout time.Duration - - // EmitStatusEvents defines whether status events should be - // emitted on the eventChannel to the caller. - EmitStatusEvents bool - - // NoPrune defines whether pruning of previously applied - // objects should happen after apply. - NoPrune bool - - // DryRunStrategy defines whether changes should actually be performed, - // or if it is just talk and no action. - DryRunStrategy common.DryRunStrategy - - // PrunePropagationPolicy defines the deletion propagation policy - // that should be used for pruning. If this is not provided, the - // default is to use the Background policy. - PrunePropagationPolicy metav1.DeletionPropagation - - // PruneTimeout defines whether we should wait for all resources - // to be fully deleted after pruning, and if so, how long we should - // wait. - PruneTimeout time.Duration - - // InventoryPolicy defines the inventory policy of apply. - InventoryPolicy inventory.Policy - - // ValidationPolicy defines how to handle invalid objects. - ValidationPolicy validation.Policy - - // RESTScopeStrategy specifies which strategy to use when listing and - // watching resources. By default, the strategy is selected automatically. - WatcherRESTScopeStrategy watcher.RESTScopeStrategy -} - -// setDefaults set the options to the default values if they -// have not been provided. -func setDefaults(o *ApplierOptions) { - if o.PrunePropagationPolicy == "" { - o.PrunePropagationPolicy = metav1.DeletePropagationBackground - } -} - -func handleError(eventChannel chan event.Event, err error) { - eventChannel <- event.Event{ - Type: event.ErrorType, - ErrorEvent: event.ErrorEvent{ - Err: err, - }, - } -} - -// localNamespaces stores a set of strings of all the namespaces -// for the passed non cluster-scoped localObjs, plus the namespace -// of the passed inventory object. This is used to skip deleting -// namespaces which have currently applied objects in them. -func localNamespaces(localInv inventory.Info, localObjs []object.ObjMetadata) sets.String { // nolint:staticcheck - namespaces := sets.NewString() - for _, obj := range localObjs { - if obj.Namespace != "" { - namespaces.Insert(obj.Namespace) - } - } - invNamespace := localInv.Namespace() - if invNamespace != "" { - namespaces.Insert(invNamespace) - } - return namespaces -} - -func handleValidationError(eventChannel chan<- event.Event, err error) { - switch tErr := err.(type) { - case *validation.Error: - // handle validation error about one or more specific objects - eventChannel <- event.Event{ - Type: event.ValidationType, - ValidationEvent: event.ValidationEvent{ - Identifiers: tErr.Identifiers(), - Error: tErr, - }, - } - default: - // handle general validation error (no specific object) - eventChannel <- event.Event{ - Type: event.ValidationType, - ValidationEvent: event.ValidationEvent{ - Error: tErr, - }, - } - } -} diff --git a/pkg/apply/applier_builder.go b/pkg/apply/applier_builder.go deleted file mode 100644 index 6a3c9b9d..00000000 --- a/pkg/apply/applier_builder.go +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package apply - -import ( - "github.com/fluxcd/cli-utils/pkg/apply/info" - "github.com/fluxcd/cli-utils/pkg/apply/prune" - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/pkg/kstatus/watcher" - "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/cli-runtime/pkg/resource" - "k8s.io/client-go/discovery" - "k8s.io/client-go/dynamic" - "k8s.io/client-go/rest" - "k8s.io/kubectl/pkg/cmd/util" -) - -type ApplierBuilder struct { - commonBuilder -} - -// NewApplierBuilder returns a new ApplierBuilder. -func NewApplierBuilder() *ApplierBuilder { - return &ApplierBuilder{ - // Defaults, if any, go here. - } -} - -func (b *ApplierBuilder) Build() (*Applier, error) { - bx, err := b.finalize() - if err != nil { - return nil, err - } - return &Applier{ - pruner: &prune.Pruner{ - InvClient: bx.invClient, - Client: bx.client, - Mapper: bx.mapper, - }, - statusWatcher: bx.statusWatcher, - invClient: bx.invClient, - client: bx.client, - openAPIGetter: bx.discoClient, - mapper: bx.mapper, - infoHelper: info.NewHelper(bx.mapper, bx.unstructuredClientForMapping), - }, nil -} - -func (b *ApplierBuilder) WithFactory(factory util.Factory) *ApplierBuilder { - b.factory = factory - return b -} - -func (b *ApplierBuilder) WithInventoryClient(invClient inventory.Client) *ApplierBuilder { - b.invClient = invClient - return b -} - -func (b *ApplierBuilder) WithDynamicClient(client dynamic.Interface) *ApplierBuilder { - b.client = client - return b -} - -func (b *ApplierBuilder) WithDiscoveryClient(discoClient discovery.CachedDiscoveryInterface) *ApplierBuilder { - b.discoClient = discoClient - return b -} - -func (b *ApplierBuilder) WithRestMapper(mapper meta.RESTMapper) *ApplierBuilder { - b.mapper = mapper - return b -} - -func (b *ApplierBuilder) WithRestConfig(restConfig *rest.Config) *ApplierBuilder { - b.restConfig = restConfig - return b -} - -func (b *ApplierBuilder) WithUnstructuredClientForMapping(unstructuredClientForMapping func(*meta.RESTMapping) (resource.RESTClient, error)) *ApplierBuilder { - b.unstructuredClientForMapping = unstructuredClientForMapping - return b -} - -func (b *ApplierBuilder) WithStatusWatcher(statusWatcher watcher.StatusWatcher) *ApplierBuilder { - b.statusWatcher = statusWatcher - return b -} diff --git a/pkg/apply/applier_test.go b/pkg/apply/applier_test.go deleted file mode 100644 index 36a3a065..00000000 --- a/pkg/apply/applier_test.go +++ /dev/null @@ -1,2069 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package apply - -import ( - "context" - "sync" - "testing" - "time" - - "github.com/fluxcd/cli-utils/pkg/apis/actuation" - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/inventory" - pollevent "github.com/fluxcd/cli-utils/pkg/kstatus/polling/event" - "github.com/fluxcd/cli-utils/pkg/kstatus/status" - "github.com/fluxcd/cli-utils/pkg/kstatus/watcher" - "github.com/fluxcd/cli-utils/pkg/multierror" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/object/validation" - "github.com/fluxcd/cli-utils/pkg/testutil" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "k8s.io/apimachinery/pkg/util/validation/field" - "k8s.io/kubectl/pkg/scheme" -) - -var ( - codec = scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) - resources = map[string]string{ - "deployment": ` -apiVersion: apps/v1 -kind: Deployment -metadata: - name: foo - namespace: default - uid: dep-uid - generation: 1 -spec: - replicas: 1 -`, - "secret": ` -apiVersion: v1 -kind: Secret -metadata: - name: secret - namespace: default - uid: secret-uid - generation: 1 -type: Opaque -spec: - foo: bar -`, - "inventory": ` -apiVersion: v1 -kind: ConfigMap -metadata: - name: test-inventory-obj - namespace: test-namespace - labels: - cli-utils.sigs.k8s.io/inventory-id: test-app-label -data: {} -`, - "obj1": ` -apiVersion: v1 -kind: Pod -metadata: - name: obj1 - namespace: test-namespace -spec: {} -`, - "obj2": ` -apiVersion: v1 -kind: Pod -metadata: - name: obj2 - namespace: test-namespace -spec: {} -`, - "clusterScopedObj": ` -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: cluster-scoped-1 -`, - } -) - -//nolint:dupl // event lists are very similar -func TestApplier(t *testing.T) { - testCases := map[string]struct { - namespace string - // resources input to applier - resources object.UnstructuredSet - // inventory input to applier - invInfo inventoryInfo - // objects in the cluster - clusterObjs object.UnstructuredSet - // options input to applier.Run - options ApplierOptions - // fake input events from the statusWatcher - statusEvents []pollevent.Event - // expected output status events (async) - expectedStatusEvents []testutil.ExpEvent - // expected output events - expectedEvents []testutil.ExpEvent - // true if runTimeout is expected to have caused cancellation - expectRunTimeout bool - // true if testTimeout is expected to have caused cancellation - expectTestTimeout bool - }{ - "initial apply without status or prune": { - namespace: "default", - resources: object.UnstructuredSet{ - testutil.Unstructured(t, resources["deployment"]), - }, - invInfo: inventoryInfo{ - name: "abc-123", - namespace: "default", - id: "test", - }, - clusterObjs: object.UnstructuredSet{}, - options: ApplierOptions{ - NoPrune: true, - InventoryPolicy: inventory.PolicyMustMatch, - }, - expectedEvents: []testutil.ExpEvent{ - { - EventType: event.InitType, - InitEvent: &testutil.ExpInitEvent{}, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "inventory-add-0", - Action: event.InventoryAction, - Type: event.Started, - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "inventory-add-0", - Action: event.InventoryAction, - Type: event.Finished, - }, - }, - - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "apply-0", - Action: event.ApplyAction, - Type: event.Started, - }, - }, - { - EventType: event.ApplyType, - ApplyEvent: &testutil.ExpApplyEvent{ - GroupName: "apply-0", - Status: event.ApplySuccessful, // Create new - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "apply-0", - Action: event.ApplyAction, - Type: event.Finished, - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "wait-0", - Action: event.WaitAction, - Type: event.Started, - }, - }, - { - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcilePending, - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - }, - }, - // Timeout waiting for status event saying deployment is current - // TODO: update inventory after timeout - // { - // EventType: event.ActionGroupType, - // ActionGroupEvent: &testutil.ExpActionGroupEvent{ - // GroupName: "wait-0", - // Action: event.WaitAction, - // Type: event.Finished, - // }, - // }, - // { - // EventType: event.ActionGroupType, - // ActionGroupEvent: &testutil.ExpActionGroupEvent{ - // GroupName: "inventory-set-0", - // Action: event.InventoryAction, - // Type: event.Started, - // }, - // }, - // { - // EventType: event.ActionGroupType, - // ActionGroupEvent: &testutil.ExpActionGroupEvent{ - // GroupName: "inventory-set-0", - // Action: event.InventoryAction, - // Type: event.Finished, - // }, - // }, - }, - expectTestTimeout: true, - }, - "first apply multiple resources with status and prune": { - namespace: "default", - resources: object.UnstructuredSet{ - testutil.Unstructured(t, resources["deployment"]), - testutil.Unstructured(t, resources["secret"]), - }, - invInfo: inventoryInfo{ - name: "inv-123", - namespace: "default", - id: "test", - }, - clusterObjs: object.UnstructuredSet{}, - options: ApplierOptions{ - ReconcileTimeout: time.Minute, - InventoryPolicy: inventory.PolicyMustMatch, - EmitStatusEvents: true, - }, - statusEvents: []pollevent.Event{ - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - Status: status.InProgressStatus, - Resource: testutil.Unstructured(t, resources["deployment"]), - }, - }, - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - Status: status.CurrentStatus, - Resource: testutil.Unstructured(t, resources["deployment"]), - }, - }, - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: testutil.ToIdentifier(t, resources["secret"]), - Status: status.CurrentStatus, - Resource: testutil.Unstructured(t, resources["secret"]), - }, - }, - }, - expectedStatusEvents: []testutil.ExpEvent{ - { - EventType: event.StatusType, - StatusEvent: &testutil.ExpStatusEvent{ - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - Status: status.InProgressStatus, - }, - }, - { - EventType: event.StatusType, - StatusEvent: &testutil.ExpStatusEvent{ - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - Status: status.CurrentStatus, - }, - }, - { - EventType: event.StatusType, - StatusEvent: &testutil.ExpStatusEvent{ - Identifier: testutil.ToIdentifier(t, resources["secret"]), - Status: status.CurrentStatus, - }, - }, - }, - expectedEvents: []testutil.ExpEvent{ - { - EventType: event.InitType, - InitEvent: &testutil.ExpInitEvent{}, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "inventory-add-0", - Action: event.InventoryAction, - Type: event.Started, - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "inventory-add-0", - Action: event.InventoryAction, - Type: event.Finished, - }, - }, - - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "apply-0", - Action: event.ApplyAction, - Type: event.Started, - }, - }, - // Secrets applied before Deployments (see pkg/ordering) - { - EventType: event.ApplyType, - ApplyEvent: &testutil.ExpApplyEvent{ - GroupName: "apply-0", - Status: event.ApplySuccessful, // Create new - Identifier: testutil.ToIdentifier(t, resources["secret"]), - }, - }, - { - EventType: event.ApplyType, - ApplyEvent: &testutil.ExpApplyEvent{ - GroupName: "apply-0", - Status: event.ApplySuccessful, // Create new - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "apply-0", - Action: event.ApplyAction, - Type: event.Finished, - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "wait-0", - Action: event.WaitAction, - Type: event.Started, - }, - }, - // Wait events with same status sorted by Identifier (see pkg/testutil) - { - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcilePending, - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - }, - }, - { - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcilePending, - Identifier: testutil.ToIdentifier(t, resources["secret"]), - }, - }, - // Wait events with same status sorted by Identifier (see pkg/testutil) - { - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcileSuccessful, - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - }, - }, - { - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcileSuccessful, - Identifier: testutil.ToIdentifier(t, resources["secret"]), - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "wait-0", - Action: event.WaitAction, - Type: event.Finished, - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "inventory-set-0", - Action: event.InventoryAction, - Type: event.Started, - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "inventory-set-0", - Action: event.InventoryAction, - Type: event.Finished, - }, - }, - }, - }, - "apply multiple existing resources with status and prune": { - namespace: "default", - resources: object.UnstructuredSet{ - testutil.Unstructured(t, resources["deployment"]), - testutil.Unstructured(t, resources["secret"]), - }, - invInfo: inventoryInfo{ - name: "inv-123", - namespace: "default", - id: "test", - set: object.ObjMetadataSet{ - object.UnstructuredToObjMetadata( - testutil.Unstructured(t, resources["deployment"]), - ), - }, - }, - clusterObjs: object.UnstructuredSet{ - testutil.Unstructured(t, resources["deployment"]), - }, - options: ApplierOptions{ - ReconcileTimeout: time.Minute, - InventoryPolicy: inventory.PolicyAdoptIfNoInventory, - EmitStatusEvents: true, - }, - statusEvents: []pollevent.Event{ - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - Status: status.CurrentStatus, - Resource: testutil.Unstructured(t, resources["deployment"]), - }, - }, - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: testutil.ToIdentifier(t, resources["secret"]), - Status: status.CurrentStatus, - Resource: testutil.Unstructured(t, resources["secret"]), - }, - }, - }, - expectedStatusEvents: []testutil.ExpEvent{ - { - EventType: event.StatusType, - StatusEvent: &testutil.ExpStatusEvent{ - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - Status: status.CurrentStatus, - }, - }, - { - EventType: event.StatusType, - StatusEvent: &testutil.ExpStatusEvent{ - Identifier: testutil.ToIdentifier(t, resources["secret"]), - Status: status.CurrentStatus, - }, - }, - }, - expectedEvents: []testutil.ExpEvent{ - { - EventType: event.InitType, - InitEvent: &testutil.ExpInitEvent{}, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "inventory-add-0", - Action: event.InventoryAction, - Type: event.Started, - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "inventory-add-0", - Action: event.InventoryAction, - Type: event.Finished, - }, - }, - - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "apply-0", - Action: event.ApplyAction, - Type: event.Started, - }, - }, - // Apply Secrets before Deployments (see ordering.SortableMetas) - { - EventType: event.ApplyType, - ApplyEvent: &testutil.ExpApplyEvent{ - GroupName: "apply-0", - Status: event.ApplySuccessful, // Create new - Identifier: testutil.ToIdentifier(t, resources["secret"]), - }, - }, - { - EventType: event.ApplyType, - ApplyEvent: &testutil.ExpApplyEvent{ - GroupName: "apply-0", - Status: event.ApplySuccessful, // Update existing - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "apply-0", - Action: event.ApplyAction, - Type: event.Finished, - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "wait-0", - Action: event.WaitAction, - Type: event.Started, - }, - }, - // Wait events with same status sorted by Identifier (see pkg/testutil) - { - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcilePending, - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - }, - }, - { - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcilePending, - Identifier: testutil.ToIdentifier(t, resources["secret"]), - }, - }, - // Wait events with same status sorted by Identifier (see pkg/testutil) - { - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcileSuccessful, - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - }, - }, - { - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcileSuccessful, - Identifier: testutil.ToIdentifier(t, resources["secret"]), - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "wait-0", - Action: event.WaitAction, - Type: event.Finished, - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "inventory-set-0", - Action: event.InventoryAction, - Type: event.Started, - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "inventory-set-0", - Action: event.InventoryAction, - Type: event.Finished, - }, - }, - }, - }, - "apply no resources and prune all existing": { - namespace: "default", - resources: object.UnstructuredSet{}, - invInfo: inventoryInfo{ - name: "inv-123", - namespace: "default", - id: "test", - set: object.ObjMetadataSet{ - object.UnstructuredToObjMetadata( - testutil.Unstructured(t, resources["deployment"]), - ), - object.UnstructuredToObjMetadata( - testutil.Unstructured(t, resources["secret"]), - ), - }, - }, - clusterObjs: object.UnstructuredSet{ - testutil.Unstructured(t, resources["deployment"], testutil.AddOwningInv(t, "test")), - testutil.Unstructured(t, resources["secret"], testutil.AddOwningInv(t, "test")), - }, - options: ApplierOptions{ - InventoryPolicy: inventory.PolicyMustMatch, - EmitStatusEvents: true, - }, - statusEvents: []pollevent.Event{ - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - Status: status.InProgressStatus, - }, - }, - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: testutil.ToIdentifier(t, resources["secret"]), - Status: status.InProgressStatus, - }, - }, - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - Status: status.NotFoundStatus, - }, - }, - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: testutil.ToIdentifier(t, resources["secret"]), - Status: status.NotFoundStatus, - }, - }, - }, - expectedStatusEvents: []testutil.ExpEvent{ - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "inventory-add-0", - Action: event.InventoryAction, - Type: event.Started, - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "inventory-add-0", - Action: event.InventoryAction, - Type: event.Finished, - }, - }, - { - EventType: event.StatusType, - StatusEvent: &testutil.ExpStatusEvent{ - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - Status: status.InProgressStatus, - }, - }, - { - EventType: event.StatusType, - StatusEvent: &testutil.ExpStatusEvent{ - Identifier: testutil.ToIdentifier(t, resources["secret"]), - Status: status.InProgressStatus, - }, - }, - { - EventType: event.StatusType, - StatusEvent: &testutil.ExpStatusEvent{ - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - Status: status.NotFoundStatus, - }, - }, - { - EventType: event.StatusType, - StatusEvent: &testutil.ExpStatusEvent{ - Identifier: testutil.ToIdentifier(t, resources["secret"]), - Status: status.NotFoundStatus, - }, - }, - }, - expectedEvents: []testutil.ExpEvent{ - { - EventType: event.InitType, - InitEvent: &testutil.ExpInitEvent{}, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "prune-0", - Action: event.PruneAction, - Type: event.Started, - }, - }, - // Prune Deployments before Secrets (see ordering.SortableMetas) - { - EventType: event.PruneType, - PruneEvent: &testutil.ExpPruneEvent{ - GroupName: "prune-0", - Status: event.PruneSuccessful, - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - }, - }, - { - EventType: event.PruneType, - PruneEvent: &testutil.ExpPruneEvent{ - GroupName: "prune-0", - Status: event.PruneSuccessful, - Identifier: testutil.ToIdentifier(t, resources["secret"]), - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "prune-0", - Action: event.PruneAction, - Type: event.Finished, - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "wait-0", - Action: event.WaitAction, - Type: event.Started, - }, - }, - // Wait events with same status sorted by Identifier (see pkg/testutil) - { - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcilePending, - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - }, - }, - { - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcilePending, - Identifier: testutil.ToIdentifier(t, resources["secret"]), - }, - }, - // Wait events with same status sorted by Identifier (see pkg/testutil) - { - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcileSuccessful, - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - }, - }, - { - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcileSuccessful, - Identifier: testutil.ToIdentifier(t, resources["secret"]), - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "wait-0", - Action: event.WaitAction, - Type: event.Finished, - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "inventory-set-0", - Action: event.InventoryAction, - Type: event.Started, - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "inventory-set-0", - Action: event.InventoryAction, - Type: event.Finished, - }, - }, - }, - }, - "apply resource with existing object belonging to different inventory": { - namespace: "default", - resources: object.UnstructuredSet{ - testutil.Unstructured(t, resources["deployment"]), - }, - invInfo: inventoryInfo{ - name: "abc-123", - namespace: "default", - id: "test", - }, - clusterObjs: object.UnstructuredSet{ - testutil.Unstructured(t, resources["deployment"], testutil.AddOwningInv(t, "unmatched")), - }, - options: ApplierOptions{ - ReconcileTimeout: time.Minute, - InventoryPolicy: inventory.PolicyMustMatch, - EmitStatusEvents: true, - }, - // There could be some status events for the existing Deployment, - // but we can't always expect to receive them before the applier - // exits, because the WaitTask is skipped when the ApplyTask errors. - // So don't bother sending or expecting them. - statusEvents: []pollevent.Event{}, - expectedStatusEvents: []testutil.ExpEvent{}, - expectedEvents: []testutil.ExpEvent{ - { - EventType: event.InitType, - InitEvent: &testutil.ExpInitEvent{}, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "inventory-add-0", - Action: event.InventoryAction, - Type: event.Started, - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "inventory-add-0", - Action: event.InventoryAction, - Type: event.Finished, - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "apply-0", - Action: event.ApplyAction, - Type: event.Started, - }, - }, - { - EventType: event.ApplyType, - ApplyEvent: &testutil.ExpApplyEvent{ - GroupName: "apply-0", - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - Status: event.ApplySkipped, - Error: &inventory.PolicyPreventedActuationError{ - Strategy: actuation.ActuationStrategyApply, - Policy: inventory.PolicyMustMatch, - Status: inventory.NoMatch, - }, - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "apply-0", - Action: event.ApplyAction, - Type: event.Finished, - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "wait-0", - Action: event.WaitAction, - Type: event.Started, - }, - }, - { - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcileSkipped, - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "wait-0", - Action: event.WaitAction, - Type: event.Finished, - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "inventory-set-0", - Action: event.InventoryAction, - Type: event.Started, - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "inventory-set-0", - Action: event.InventoryAction, - Type: event.Finished, - }, - }, - }, - }, - "resources belonging to a different inventory should not be pruned": { - namespace: "default", - resources: object.UnstructuredSet{}, - invInfo: inventoryInfo{ - name: "abc-123", - namespace: "default", - id: "test", - set: object.ObjMetadataSet{ - object.UnstructuredToObjMetadata( - testutil.Unstructured(t, resources["deployment"]), - ), - }, - }, - clusterObjs: object.UnstructuredSet{ - testutil.Unstructured(t, resources["deployment"], testutil.AddOwningInv(t, "unmatched")), - }, - options: ApplierOptions{ - InventoryPolicy: inventory.PolicyMustMatch, - EmitStatusEvents: true, - }, - expectedEvents: []testutil.ExpEvent{ - { - EventType: event.InitType, - InitEvent: &testutil.ExpInitEvent{}, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "inventory-add-0", - Action: event.InventoryAction, - Type: event.Started, - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "inventory-add-0", - Action: event.InventoryAction, - Type: event.Finished, - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "prune-0", - Action: event.PruneAction, - Type: event.Started, - }, - }, - { - EventType: event.PruneType, - PruneEvent: &testutil.ExpPruneEvent{ - GroupName: "prune-0", - Status: event.PruneSkipped, - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - Error: &inventory.PolicyPreventedActuationError{ - Strategy: actuation.ActuationStrategyDelete, - Policy: inventory.PolicyMustMatch, - Status: inventory.NoMatch, - }, - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "prune-0", - Action: event.PruneAction, - Type: event.Finished, - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "wait-0", - Action: event.WaitAction, - Type: event.Started, - }, - }, - { - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcileSkipped, - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "wait-0", - Action: event.WaitAction, - Type: event.Finished, - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "inventory-set-0", - Action: event.InventoryAction, - Type: event.Started, - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "inventory-set-0", - Action: event.InventoryAction, - Type: event.Finished, - }, - }, - }, - }, - "prune with inventory object annotation matched": { - namespace: "default", - resources: object.UnstructuredSet{}, - invInfo: inventoryInfo{ - name: "abc-123", - namespace: "default", - id: "test", - set: object.ObjMetadataSet{ - object.UnstructuredToObjMetadata( - testutil.Unstructured(t, resources["deployment"]), - ), - }, - }, - clusterObjs: object.UnstructuredSet{ - testutil.Unstructured(t, resources["deployment"], testutil.AddOwningInv(t, "test")), - }, - options: ApplierOptions{ - InventoryPolicy: inventory.PolicyMustMatch, - EmitStatusEvents: true, - }, - statusEvents: []pollevent.Event{ - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - Status: status.InProgressStatus, - }, - }, - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - Status: status.NotFoundStatus, - }, - }, - }, - expectedStatusEvents: []testutil.ExpEvent{ - { - EventType: event.StatusType, - StatusEvent: &testutil.ExpStatusEvent{ - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - Status: status.InProgressStatus, - }, - }, - { - EventType: event.StatusType, - StatusEvent: &testutil.ExpStatusEvent{ - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - Status: status.NotFoundStatus, - }, - }, - }, - expectedEvents: []testutil.ExpEvent{ - { - EventType: event.InitType, - InitEvent: &testutil.ExpInitEvent{}, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "inventory-add-0", - Action: event.InventoryAction, - Type: event.Started, - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "inventory-add-0", - Action: event.InventoryAction, - Type: event.Finished, - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "prune-0", - Action: event.PruneAction, - Type: event.Started, - }, - }, - { - EventType: event.PruneType, - PruneEvent: &testutil.ExpPruneEvent{ - GroupName: "prune-0", - Status: event.PruneSuccessful, - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "prune-0", - Action: event.PruneAction, - Type: event.Finished, - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "wait-0", - Action: event.WaitAction, - Type: event.Started, - }, - }, - // Wait events sorted Pending > Successful (see pkg/testutil) - { - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcilePending, - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - }, - }, - { - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcileSuccessful, - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "wait-0", - Action: event.WaitAction, - Type: event.Finished, - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "inventory-set-0", - Action: event.InventoryAction, - Type: event.Started, - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "inventory-set-0", - Action: event.InventoryAction, - Type: event.Finished, - }, - }, - }, - }, - "SkipInvalid - skip invalid objects and apply valid objects": { - namespace: "default", - resources: object.UnstructuredSet{ - testutil.Unstructured(t, resources["deployment"], JSONPathSetter{ - "$.metadata.name", "", - }), - testutil.Unstructured(t, resources["deployment"], JSONPathSetter{ - "$.kind", "", - }), - testutil.Unstructured(t, resources["secret"]), - }, - invInfo: inventoryInfo{ - name: "inv-123", - namespace: "default", - id: "test", - }, - clusterObjs: object.UnstructuredSet{}, - options: ApplierOptions{ - ReconcileTimeout: time.Minute, - InventoryPolicy: inventory.PolicyAdoptIfNoInventory, - EmitStatusEvents: true, - ValidationPolicy: validation.SkipInvalid, - }, - statusEvents: []pollevent.Event{ - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: testutil.ToIdentifier(t, resources["secret"]), - Status: status.CurrentStatus, - Resource: testutil.Unstructured(t, resources["secret"]), - }, - }, - }, - expectedStatusEvents: []testutil.ExpEvent{ - { - EventType: event.StatusType, - StatusEvent: &testutil.ExpStatusEvent{ - Identifier: testutil.ToIdentifier(t, resources["secret"]), - Status: status.CurrentStatus, - }, - }, - }, - expectedEvents: []testutil.ExpEvent{ - { - EventType: event.ValidationType, - ValidationEvent: &testutil.ExpValidationEvent{ - Identifiers: object.ObjMetadataSet{ - object.UnstructuredToObjMetadata( - testutil.Unstructured(t, resources["deployment"], JSONPathSetter{ - "$.metadata.name", "", - }), - ), - }, - Error: testutil.EqualErrorString(validation.NewError( - field.Required(field.NewPath("metadata", "name"), "name is required"), - object.UnstructuredToObjMetadata( - testutil.Unstructured(t, resources["deployment"], JSONPathSetter{ - "$.metadata.name", "", - }), - ), - ).Error()), - }, - }, - { - EventType: event.ValidationType, - ValidationEvent: &testutil.ExpValidationEvent{ - Identifiers: object.ObjMetadataSet{ - object.UnstructuredToObjMetadata( - testutil.Unstructured(t, resources["deployment"], JSONPathSetter{ - "$.kind", "", - }), - ), - }, - Error: testutil.EqualErrorString(validation.NewError( - field.Required(field.NewPath("kind"), "kind is required"), - object.UnstructuredToObjMetadata( - testutil.Unstructured(t, resources["deployment"], JSONPathSetter{ - "$.kind", "", - }), - ), - ).Error()), - }, - }, - { - EventType: event.InitType, - InitEvent: &testutil.ExpInitEvent{}, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "inventory-add-0", - Action: event.InventoryAction, - Type: event.Started, - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "inventory-add-0", - Action: event.InventoryAction, - Type: event.Finished, - }, - }, - - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "apply-0", - Action: event.ApplyAction, - Type: event.Started, - }, - }, - // Secret applied - { - EventType: event.ApplyType, - ApplyEvent: &testutil.ExpApplyEvent{ - GroupName: "apply-0", - Status: event.ApplySuccessful, // Create new - Identifier: testutil.ToIdentifier(t, resources["secret"]), - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "apply-0", - Action: event.ApplyAction, - Type: event.Finished, - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "wait-0", - Action: event.WaitAction, - Type: event.Started, - }, - }, - // Wait events sorted Pending > Successful (see pkg/testutil) - { - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcilePending, - Identifier: testutil.ToIdentifier(t, resources["secret"]), - }, - }, - { - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcileSuccessful, - Identifier: testutil.ToIdentifier(t, resources["secret"]), - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "wait-0", - Action: event.WaitAction, - Type: event.Finished, - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "inventory-set-0", - Action: event.InventoryAction, - Type: event.Started, - }, - }, - { - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - GroupName: "inventory-set-0", - Action: event.InventoryAction, - Type: event.Finished, - }, - }, - }, - }, - "ExitEarly - exit early on invalid objects and skip valid objects": { - namespace: "default", - resources: object.UnstructuredSet{ - testutil.Unstructured(t, resources["deployment"], JSONPathSetter{ - "$.metadata.name", "", - }), - testutil.Unstructured(t, resources["deployment"], JSONPathSetter{ - "$.kind", "", - }), - testutil.Unstructured(t, resources["secret"]), - }, - invInfo: inventoryInfo{ - name: "inv-123", - namespace: "default", - id: "test", - }, - clusterObjs: object.UnstructuredSet{}, - options: ApplierOptions{ - ReconcileTimeout: time.Minute, - InventoryPolicy: inventory.PolicyAdoptIfNoInventory, - EmitStatusEvents: true, - ValidationPolicy: validation.ExitEarly, - }, - statusEvents: []pollevent.Event{}, - expectedStatusEvents: []testutil.ExpEvent{}, - expectedEvents: []testutil.ExpEvent{ - { - EventType: event.ErrorType, - ErrorEvent: &testutil.ExpErrorEvent{ - Err: testutil.EqualErrorString(multierror.New( - validation.NewError( - field.Required(field.NewPath("metadata", "name"), "name is required"), - object.UnstructuredToObjMetadata( - testutil.Unstructured(t, resources["deployment"], JSONPathSetter{ - "$.metadata.name", "", - }), - ), - ), - validation.NewError( - field.Required(field.NewPath("kind"), "kind is required"), - object.UnstructuredToObjMetadata( - testutil.Unstructured(t, resources["deployment"], JSONPathSetter{ - "$.kind", "", - }), - ), - ), - ).Error()), - }, - }, - }, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - statusWatcher := newFakeWatcher(tc.statusEvents) - - // Only feed valid objects into the TestApplier. - // Invalid objects should not generate API requests. - validObjs := object.UnstructuredSet{} - for _, obj := range tc.resources { - id := object.UnstructuredToObjMetadata(obj) - if id.GroupKind.Kind == "" || id.Name == "" { - continue - } - validObjs = append(validObjs, obj) - } - - applier := newTestApplier(t, - tc.invInfo, - validObjs, - tc.clusterObjs, - statusWatcher, - ) - - // Context for Applier.Run - runCtx, runCancel := context.WithCancel(context.Background()) - defer runCancel() // cleanup - - // Context for this test (in case Applier.Run never closes the event channel) - testTimeout := 10 * time.Second - testCtx, testCancel := context.WithTimeout(context.Background(), testTimeout) - defer testCancel() // cleanup - - eventChannel := applier.Run(runCtx, tc.invInfo.toWrapped(), tc.resources, tc.options) - - // only start sending events once - var once sync.Once - - var events []event.Event - - loop: - for { - select { - case <-testCtx.Done(): - // Test timed out - runCancel() - if tc.expectTestTimeout { - assert.Equal(t, context.DeadlineExceeded, testCtx.Err(), "Applier.Run failed to exit, but not because of expected timeout") - } else { - t.Errorf("Applier.Run failed to exit (timeout: %s)", testTimeout) - } - break loop - - case e, ok := <-eventChannel: - if !ok { - // Event channel closed - testCancel() - break loop - } - if e.Type == event.ActionGroupType && - e.ActionGroupEvent.Status == event.Finished { - // Send events after the first apply/prune task ends - if e.ActionGroupEvent.Action == event.ApplyAction || - e.ActionGroupEvent.Action == event.PruneAction { - once.Do(func() { - // start events - statusWatcher.Start() - }) - } - } - events = append(events, e) - } - } - - // Convert events to test events for comparison - receivedEvents := testutil.EventsToExpEvents(events) - - // Validate & remove expected status events - for _, e := range tc.expectedStatusEvents { - var removed int - receivedEvents, removed = testutil.RemoveEqualEvents(receivedEvents, e) - if removed < 1 { - t.Errorf("Expected status event not received: %#v", e.StatusEvent) - } - } - - // sort to allow comparison of multiple apply/prune tasks in the same task group - testutil.SortExpEvents(receivedEvents) - - // Validate the rest of the events - testutil.AssertEqual(t, tc.expectedEvents, receivedEvents, - "Actual events (%d) do not match expected events (%d)", - len(receivedEvents), len(tc.expectedEvents)) - - // Validate that the expected timeout was the cause of the run completion. - // just in case something else cancelled the run - switch { - case tc.expectRunTimeout: - assert.Equal(t, context.DeadlineExceeded, runCtx.Err(), "Applier.Run exited, but not by expected context timeout") - case tc.expectTestTimeout: - assert.Equal(t, context.Canceled, runCtx.Err(), "Applier.Run exited, but not because of expected context cancellation") - default: - assert.Nil(t, runCtx.Err(), "Applier.Run exited, but context error is not nil") - } - }) - } -} - -func TestApplierCancel(t *testing.T) { - testCases := map[string]struct { - // resources input to applier - resources object.UnstructuredSet - // inventory input to applier - invInfo inventoryInfo - // objects in the cluster - clusterObjs object.UnstructuredSet - // options input to applier.Run - options ApplierOptions - // timeout for applier.Run - runTimeout time.Duration - // timeout for the test - testTimeout time.Duration - // fake input events from the statusWatcher - statusEvents []pollevent.Event - // expected output status events (async) - expectedStatusEvents []testutil.ExpEvent - // expected output events - expectedEvents []testutil.ExpEvent - // true if runTimeout is expected to have caused cancellation - expectRunTimeout bool - }{ - "cancelled by caller while waiting for reconcile": { - expectRunTimeout: true, - runTimeout: 2 * time.Second, - testTimeout: 30 * time.Second, - resources: object.UnstructuredSet{ - testutil.Unstructured(t, resources["deployment"]), - }, - invInfo: inventoryInfo{ - name: "abc-123", - namespace: "test", - id: "test", - }, - clusterObjs: object.UnstructuredSet{}, - options: ApplierOptions{ - // EmitStatusEvents required to test event output - EmitStatusEvents: true, - NoPrune: true, - InventoryPolicy: inventory.PolicyMustMatch, - // ReconcileTimeout required to enable WaitTasks - ReconcileTimeout: 1 * time.Minute, - }, - statusEvents: []pollevent.Event{ - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - Status: status.InProgressStatus, - Resource: testutil.Unstructured(t, resources["deployment"]), - }, - }, - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - Status: status.InProgressStatus, - Resource: testutil.Unstructured(t, resources["deployment"]), - }, - }, - // Resource never becomes Current, blocking applier.Run from exiting - }, - expectedStatusEvents: []testutil.ExpEvent{ - { - EventType: event.StatusType, - StatusEvent: &testutil.ExpStatusEvent{ - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - Status: status.InProgressStatus, - }, - }, - }, - expectedEvents: []testutil.ExpEvent{ - { - // InitTask - EventType: event.InitType, - InitEvent: &testutil.ExpInitEvent{}, - }, - { - // InvAddTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-add-0", - Type: event.Started, - }, - }, - { - // InvAddTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-add-0", - Type: event.Finished, - }, - }, - { - // ApplyTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-0", - Type: event.Started, - }, - }, - { - // Apply Deployment - EventType: event.ApplyType, - ApplyEvent: &testutil.ExpApplyEvent{ - GroupName: "apply-0", - Status: event.ApplySuccessful, - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - }, - }, - { - // ApplyTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-0", - Type: event.Finished, - }, - }, - { - // WaitTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Started, - }, - }, - { - // Deployment reconcile pending. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - Status: event.ReconcilePending, - }, - }, - // Deployment never becomes Current. - // WaitTask is expected to be cancelled before ReconcileTimeout. - // Cancelled WaitTask do not sent individual timeout WaitEvents - { - // WaitTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Finished, // TODO: add Cancelled event type - }, - }, - // TODO: Update the inventory after cancellation - // { - // // InvSetTask start - // EventType: event.ActionGroupType, - // ActionGroupEvent: &testutil.ExpActionGroupEvent{ - // Action: event.InventoryAction, - // GroupName: "inventory-set-0", - // Type: event.Started, - // }, - // }, - // { - // // InvSetTask finished - // EventType: event.ActionGroupType, - // ActionGroupEvent: &testutil.ExpActionGroupEvent{ - // Action: event.InventoryAction, - // GroupName: "inventory-set-0", - // Type: event.Finished, - // }, - // }, - { - // Error - EventType: event.ErrorType, - ErrorEvent: &testutil.ExpErrorEvent{ - Err: context.DeadlineExceeded, - }, - }, - }, - }, - "completed with timeout": { - expectRunTimeout: false, - runTimeout: 10 * time.Second, - testTimeout: 30 * time.Second, - resources: object.UnstructuredSet{ - testutil.Unstructured(t, resources["deployment"]), - }, - invInfo: inventoryInfo{ - name: "abc-123", - namespace: "test", - id: "test", - }, - clusterObjs: object.UnstructuredSet{}, - options: ApplierOptions{ - // EmitStatusEvents required to test event output - EmitStatusEvents: true, - NoPrune: true, - InventoryPolicy: inventory.PolicyMustMatch, - // ReconcileTimeout required to enable WaitTasks - ReconcileTimeout: 1 * time.Minute, - }, - statusEvents: []pollevent.Event{ - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - Status: status.InProgressStatus, - Resource: testutil.Unstructured(t, resources["deployment"]), - }, - }, - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - Status: status.CurrentStatus, - Resource: testutil.Unstructured(t, resources["deployment"]), - }, - }, - // Resource becoming Current should unblock applier.Run WaitTask - }, - expectedStatusEvents: []testutil.ExpEvent{ - { - EventType: event.StatusType, - StatusEvent: &testutil.ExpStatusEvent{ - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - Status: status.InProgressStatus, - }, - }, - { - EventType: event.StatusType, - StatusEvent: &testutil.ExpStatusEvent{ - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - Status: status.CurrentStatus, - }, - }, - }, - expectedEvents: []testutil.ExpEvent{ - { - // InitTask - EventType: event.InitType, - InitEvent: &testutil.ExpInitEvent{}, - }, - { - // InvAddTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-add-0", - Type: event.Started, - }, - }, - { - // InvAddTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-add-0", - Type: event.Finished, - }, - }, - { - // ApplyTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-0", - Type: event.Started, - }, - }, - { - // Apply Deployment - EventType: event.ApplyType, - ApplyEvent: &testutil.ExpApplyEvent{ - GroupName: "apply-0", - Status: event.ApplySuccessful, - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - }, - }, - { - // ApplyTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-0", - Type: event.Finished, - }, - }, - { - // WaitTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Started, - }, - }, - // Wait events sorted Pending > Successful (see pkg/testutil) - { - // Deployment reconcile pending. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - Status: event.ReconcilePending, - }, - }, - { - // Deployment becomes Current. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - Status: event.ReconcileSuccessful, - }, - }, - { - // WaitTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Finished, - }, - }, - { - // InvSetTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-set-0", - Type: event.Started, - }, - }, - { - // InvSetTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-set-0", - Type: event.Finished, - }, - }, - }, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - statusWatcher := newFakeWatcher(tc.statusEvents) - - applier := newTestApplier(t, - tc.invInfo, - tc.resources, - tc.clusterObjs, - statusWatcher, - ) - - // Context for Applier.Run - runCtx, runCancel := context.WithTimeout(context.Background(), tc.runTimeout) - defer runCancel() // cleanup - - // Context for this test (in case Applier.Run never closes the event channel) - testCtx, testCancel := context.WithTimeout(context.Background(), tc.testTimeout) - defer testCancel() // cleanup - - eventChannel := applier.Run(runCtx, tc.invInfo.toWrapped(), tc.resources, tc.options) - - // only start sending events once - var once sync.Once - - var events []event.Event - - loop: - for { - select { - case <-testCtx.Done(): - // Test timed out - runCancel() - t.Errorf("Applier.Run failed to respond to cancellation (expected: %s, timeout: %s)", tc.runTimeout, tc.testTimeout) - break loop - - case e, ok := <-eventChannel: - if !ok { - // Event channel closed - testCancel() - break loop - } - events = append(events, e) - - if e.Type == event.ActionGroupType && - e.ActionGroupEvent.Status == event.Finished { - // Send events after the first apply/prune task ends - if e.ActionGroupEvent.Action == event.ApplyAction || - e.ActionGroupEvent.Action == event.PruneAction { - once.Do(func() { - // start events - statusWatcher.Start() - }) - } - } - } - } - - // Convert events to test events for comparison - receivedEvents := testutil.EventsToExpEvents(events) - - // Validate & remove expected status events - for _, e := range tc.expectedStatusEvents { - var removed int - receivedEvents, removed = testutil.RemoveEqualEvents(receivedEvents, e) - if removed < 1 { - t.Errorf("Expected status event not received: %#v", e.StatusEvent) - } - } - - // sort to allow comparison of multiple wait events - testutil.SortExpEvents(receivedEvents) - - // Validate the rest of the events - testutil.AssertEqual(t, tc.expectedEvents, receivedEvents, - "Actual events (%d) do not match expected events (%d)", - len(receivedEvents), len(tc.expectedEvents)) - - // Validate that the expected timeout was the cause of the run completion. - // just in case something else cancelled the run - if tc.expectRunTimeout { - assert.Equal(t, context.DeadlineExceeded, runCtx.Err(), "Applier.Run exited, but not by expected timeout") - } else { - assert.NoError(t, runCtx.Err(), "Applier.Run exited, but not by expected timeout") - } - }) - } -} - -func TestReadAndPrepareObjectsNilInv(t *testing.T) { - applier := Applier{} - _, _, err := applier.prepareObjects(nil, object.UnstructuredSet{}, ApplierOptions{}) - assert.Error(t, err) -} - -func TestReadAndPrepareObjects(t *testing.T) { - inventoryObj := testutil.Unstructured(t, resources["inventory"]) - inventory := inventory.WrapInventoryInfoObj(inventoryObj) - - obj1 := testutil.Unstructured(t, resources["obj1"]) - obj2 := testutil.Unstructured(t, resources["obj2"]) - clusterScopedObj := testutil.Unstructured(t, resources["clusterScopedObj"]) - - testCases := map[string]struct { - // objects in the cluster - clusterObjs object.UnstructuredSet - // inventory input to applier - invInfo inventoryInfo - // resources input to applier - resources object.UnstructuredSet - // expected objects to apply - applyObjs object.UnstructuredSet - // expected objects to prune - pruneObjs object.UnstructuredSet - // expected error - isError bool - }{ - "objects include inventory": { - invInfo: inventoryInfo{ - name: inventory.Name(), - namespace: inventory.Namespace(), - id: inventory.ID(), - }, - resources: object.UnstructuredSet{inventoryObj}, - isError: true, - }, - "empty inventory, empty objects, apply none, prune none": { - invInfo: inventoryInfo{ - name: inventory.Name(), - namespace: inventory.Namespace(), - id: inventory.ID(), - }, - }, - "one in inventory, empty objects, prune one": { - clusterObjs: object.UnstructuredSet{obj1}, - invInfo: inventoryInfo{ - name: inventory.Name(), - namespace: inventory.Namespace(), - id: inventory.ID(), - set: object.ObjMetadataSet{ - object.UnstructuredToObjMetadata(obj1), - }, - }, - pruneObjs: object.UnstructuredSet{obj1}, - }, - "all in inventory, apply all": { - invInfo: inventoryInfo{ - name: inventory.Name(), - namespace: inventory.Namespace(), - id: inventory.ID(), - set: object.ObjMetadataSet{ - object.UnstructuredToObjMetadata(obj1), - object.UnstructuredToObjMetadata(clusterScopedObj), - }, - }, - resources: object.UnstructuredSet{obj1, clusterScopedObj}, - applyObjs: object.UnstructuredSet{obj1, clusterScopedObj}, - }, - "disjoint set, apply new, prune old": { - clusterObjs: object.UnstructuredSet{obj2}, - invInfo: inventoryInfo{ - name: inventory.Name(), - namespace: inventory.Namespace(), - id: inventory.ID(), - set: object.ObjMetadataSet{ - object.UnstructuredToObjMetadata(obj2), - }, - }, - resources: object.UnstructuredSet{obj1, clusterScopedObj}, - applyObjs: object.UnstructuredSet{obj1, clusterScopedObj}, - pruneObjs: object.UnstructuredSet{obj2}, - }, - "most in inventory, apply all": { - clusterObjs: object.UnstructuredSet{obj2}, - invInfo: inventoryInfo{ - name: inventory.Name(), - namespace: inventory.Namespace(), - id: inventory.ID(), - set: object.ObjMetadataSet{ - object.UnstructuredToObjMetadata(obj2), - }, - }, - resources: object.UnstructuredSet{obj1, obj2, clusterScopedObj}, - applyObjs: object.UnstructuredSet{obj1, obj2, clusterScopedObj}, - pruneObjs: object.UnstructuredSet{}, - }, - } - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - applier := newTestApplier(t, - tc.invInfo, - tc.resources, - tc.clusterObjs, - // no events needed for prepareObjects - watcher.BlindStatusWatcher{}, - ) - - applyObjs, pruneObjs, err := applier.prepareObjects(tc.invInfo.toWrapped(), tc.resources, ApplierOptions{}) - if tc.isError { - assert.Error(t, err) - return - } - require.NoError(t, err) - - testutil.AssertEqual(t, applyObjs, tc.applyObjs, - "Actual applied objects (%d) do not match expected applied objects (%d)", - len(applyObjs), len(tc.applyObjs)) - - testutil.AssertEqual(t, pruneObjs, tc.pruneObjs, - "Actual pruned objects (%d) do not match expected pruned objects (%d)", - len(pruneObjs), len(tc.pruneObjs)) - }) - } -} diff --git a/pkg/apply/builder.go b/pkg/apply/builder.go deleted file mode 100644 index 4fccbd56..00000000 --- a/pkg/apply/builder.go +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright 2022 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package apply - -import ( - "errors" - "fmt" - - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/pkg/kstatus/watcher" - "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/cli-runtime/pkg/resource" - "k8s.io/client-go/discovery" - "k8s.io/client-go/dynamic" - "k8s.io/client-go/rest" - "k8s.io/kubectl/pkg/cmd/util" -) - -type commonBuilder struct { - // factory is only used to retrieve things that have not been provided explicitly. - factory util.Factory - invClient inventory.Client - client dynamic.Interface - discoClient discovery.CachedDiscoveryInterface - mapper meta.RESTMapper - restConfig *rest.Config - unstructuredClientForMapping func(*meta.RESTMapping) (resource.RESTClient, error) - statusWatcher watcher.StatusWatcher -} - -func (cb *commonBuilder) finalize() (*commonBuilder, error) { - cx := *cb // make a copy before mutating any fields. Shallow copy is good enough. - var err error - if cx.invClient == nil { - return nil, errors.New("inventory client must be provided") - } - if cx.client == nil { - if cx.factory == nil { - return nil, fmt.Errorf("a factory must be provided or all other options: %v", err) - } - cx.client, err = cx.factory.DynamicClient() - if err != nil { - return nil, fmt.Errorf("error getting dynamic client: %v", err) - } - } - if cx.discoClient == nil { - if cx.factory == nil { - return nil, fmt.Errorf("a factory must be provided or all other options: %v", err) - } - cx.discoClient, err = cx.factory.ToDiscoveryClient() - if err != nil { - return nil, fmt.Errorf("error getting discovery client: %v", err) - } - } - if cx.mapper == nil { - if cx.factory == nil { - return nil, fmt.Errorf("a factory must be provided or all other options: %v", err) - } - cx.mapper, err = cx.factory.ToRESTMapper() - if err != nil { - return nil, fmt.Errorf("error getting rest mapper: %v", err) - } - } - if cx.restConfig == nil { - if cx.factory == nil { - return nil, fmt.Errorf("a factory must be provided or all other options: %v", err) - } - cx.restConfig, err = cx.factory.ToRESTConfig() - if err != nil { - return nil, fmt.Errorf("error getting rest config: %v", err) - } - } - if cx.unstructuredClientForMapping == nil { - if cx.factory == nil { - return nil, fmt.Errorf("a factory must be provided or all other options: %v", err) - } - cx.unstructuredClientForMapping = cx.factory.UnstructuredClientForMapping - } - if cx.statusWatcher == nil { - cx.statusWatcher = watcher.NewDefaultStatusWatcher(cx.client, cx.mapper) - } - return &cx, nil -} diff --git a/pkg/apply/cache/resource_cache.go b/pkg/apply/cache/resource_cache.go deleted file mode 100644 index 2b89c57f..00000000 --- a/pkg/apply/cache/resource_cache.go +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package cache - -import ( - "github.com/fluxcd/cli-utils/pkg/kstatus/status" - "github.com/fluxcd/cli-utils/pkg/object" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -// ResourceStatus wraps an unstructured resource object, combined with the -// computed status (whether the status matches the spec). -type ResourceStatus struct { - // Resource is the last known value retrieved from the cluster - Resource *unstructured.Unstructured - // Status of the resource - Status status.Status - // StatusMessage is the human readable reason for the status - StatusMessage string -} - -// ResourceCache stores CachedResource objects -type ResourceCache interface { - ResourceCacheReader - // Load one or more resources into the cache, generating the ObjMetadata - // from the objects. - Load(...ResourceStatus) - // Put the resource into the cache using the specified ID. - Put(object.ObjMetadata, ResourceStatus) - // Remove the resource associated with the ID from the cache. - Remove(object.ObjMetadata) - // Clear the cache. - Clear() -} - -// ResourceCacheReader retrieves CachedResource objects -type ResourceCacheReader interface { - // Get the resource associated with the ID from the cache. - // If not cached, status will be Unknown and resource will be nil. - Get(object.ObjMetadata) ResourceStatus -} diff --git a/pkg/apply/cache/resource_cache_map.go b/pkg/apply/cache/resource_cache_map.go deleted file mode 100644 index 33e75b12..00000000 --- a/pkg/apply/cache/resource_cache_map.go +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package cache - -import ( - "sync" - - "github.com/fluxcd/cli-utils/pkg/kstatus/status" - "github.com/fluxcd/cli-utils/pkg/object" - "k8s.io/klog/v2" -) - -// ResourceCacheMap stores ResourceStatus objects in a map indexed by resource ID. -// ResourceCacheMap is thread-safe. -type ResourceCacheMap struct { - mu sync.RWMutex - cache map[object.ObjMetadata]ResourceStatus -} - -// NewResourceCacheMap returns a new empty ResourceCacheMap -func NewResourceCacheMap() *ResourceCacheMap { - return &ResourceCacheMap{ - cache: make(map[object.ObjMetadata]ResourceStatus), - } -} - -// Load resources into the cache, generating the ID from the resource itself. -// Existing resources with the same ID will be replaced. -func (rc *ResourceCacheMap) Load(values ...ResourceStatus) { - rc.mu.Lock() - defer rc.mu.Unlock() - - for _, value := range values { - id := object.UnstructuredToObjMetadata(value.Resource) - rc.cache[id] = value - } -} - -// Put the resource into the cache using the supplied ID, replacing any -// existing resource with the same ID. -func (rc *ResourceCacheMap) Put(id object.ObjMetadata, value ResourceStatus) { - rc.mu.Lock() - defer rc.mu.Unlock() - - rc.cache[id] = value -} - -// Get retrieves the resource associated with the ID from the cache. -// Returns (nil, true) if not found in the cache. -func (rc *ResourceCacheMap) Get(id object.ObjMetadata) ResourceStatus { - rc.mu.RLock() - defer rc.mu.RUnlock() - - obj, found := rc.cache[id] - if klog.V(6).Enabled() { - if found { - klog.V(6).Infof("resource cache hit: %s", id) - } else { - klog.V(6).Infof("resource cache miss: %s", id) - } - } - if !found { - return ResourceStatus{ - Resource: nil, - Status: status.UnknownStatus, - StatusMessage: "resource not cached", - } - } - return obj -} - -// Remove the resource associated with the ID from the cache. -func (rc *ResourceCacheMap) Remove(id object.ObjMetadata) { - rc.mu.Lock() - defer rc.mu.Unlock() - - delete(rc.cache, id) -} - -// Clear the cache. -func (rc *ResourceCacheMap) Clear() { - rc.mu.Lock() - defer rc.mu.Unlock() - - rc.cache = make(map[object.ObjMetadata]ResourceStatus) -} diff --git a/pkg/apply/common_test.go b/pkg/apply/common_test.go deleted file mode 100644 index 55e1d388..00000000 --- a/pkg/apply/common_test.go +++ /dev/null @@ -1,479 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package apply - -import ( - "bytes" - "context" - "fmt" - "io" - "net/http" - "regexp" - "testing" - - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/pkg/jsonpath" - pollevent "github.com/fluxcd/cli-utils/pkg/kstatus/polling/event" - "github.com/fluxcd/cli-utils/pkg/kstatus/watcher" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/cli-runtime/pkg/resource" - dynamicfake "k8s.io/client-go/dynamic/fake" - "k8s.io/client-go/rest/fake" - clienttesting "k8s.io/client-go/testing" - "k8s.io/klog/v2" - cmdtesting "k8s.io/kubectl/pkg/cmd/testing" - "k8s.io/kubectl/pkg/scheme" -) - -type inventoryInfo struct { - name string - namespace string - id string - set object.ObjMetadataSet -} - -func (i inventoryInfo) toUnstructured() *unstructured.Unstructured { - invMap := make(map[string]interface{}) - for _, objMeta := range i.set { - invMap[objMeta.String()] = "" - } - - return &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "v1", - "kind": "ConfigMap", - "metadata": map[string]interface{}{ - "name": i.name, - "namespace": i.namespace, - "labels": map[string]interface{}{ - common.InventoryLabel: i.id, - }, - }, - "data": invMap, - }, - } -} - -func (i inventoryInfo) toWrapped() inventory.Info { - return inventory.WrapInventoryInfoObj(i.toUnstructured()) -} - -func newTestApplier( - t *testing.T, - invInfo inventoryInfo, - resources object.UnstructuredSet, - clusterObjs object.UnstructuredSet, - statusWatcher watcher.StatusWatcher, -) *Applier { - tf := newTestFactory(t, invInfo, resources, clusterObjs) - defer tf.Cleanup() - - infoHelper := &fakeInfoHelper{ - factory: tf, - } - - invClient := newTestInventory(t, tf) - - applier, err := NewApplierBuilder(). - WithFactory(tf). - WithInventoryClient(invClient). - WithStatusWatcher(statusWatcher). - Build() - require.NoError(t, err) - - // Inject the fakeInfoHelper to allow generating Info - // objects that use the FakeRESTClient as the UnstructuredClient. - applier.infoHelper = infoHelper - - return applier -} - -func newTestDestroyer( - t *testing.T, - invInfo inventoryInfo, - clusterObjs object.UnstructuredSet, - statusWatcher watcher.StatusWatcher, -) *Destroyer { - tf := newTestFactory(t, invInfo, object.UnstructuredSet{}, clusterObjs) - defer tf.Cleanup() - - invClient := newTestInventory(t, tf) - - destroyer, err := NewDestroyerBuilder(). - WithFactory(tf). - WithInventoryClient(invClient). - Build() - require.NoError(t, err) - destroyer.statusWatcher = statusWatcher - - return destroyer -} - -func newTestInventory( - t *testing.T, - tf *cmdtesting.TestFactory, -) inventory.Client { - // Use an Client with a fakeInfoHelper to allow generating Info - // objects that use the FakeRESTClient as the UnstructuredClient. - invClient, err := inventory.ClusterClientFactory{StatusPolicy: inventory.StatusPolicyAll}.NewClient(tf) - require.NoError(t, err) - return invClient -} - -func newTestFactory( - t *testing.T, - invInfo inventoryInfo, - resourceSet object.UnstructuredSet, - clusterObjs object.UnstructuredSet, -) *cmdtesting.TestFactory { - tf := cmdtesting.NewTestFactory().WithNamespace(invInfo.namespace) - - mapper, err := tf.ToRESTMapper() - require.NoError(t, err) - - objMap := make(map[object.ObjMetadata]resourceInfo) - for _, r := range resourceSet { - objMeta := object.UnstructuredToObjMetadata(r) - objMap[objMeta] = resourceInfo{ - resource: r, - exists: false, - } - } - for _, r := range clusterObjs { - objMeta := object.UnstructuredToObjMetadata(r) - objMap[objMeta] = resourceInfo{ - resource: r, - exists: true, - } - } - var objs []resourceInfo - for _, obj := range objMap { - objs = append(objs, obj) - } - - handlers := []handler{ - &nsHandler{}, - &genericHandler{ - resources: objs, - mapper: mapper, - }, - } - - tf.UnstructuredClient = newFakeRESTClient(t, handlers) - tf.FakeDynamicClient = fakeDynamicClient(t, mapper, invInfo, objs...) - - return tf -} - -type resourceInfo struct { - resource *unstructured.Unstructured - exists bool -} - -// newFakeRESTClient creates a new client that uses a set of handlers to -// determine how to handle requests. For every request it will iterate through -// the handlers until it can find one that knows how to handle the request. -// This is to keep the main structure of the fake client manageable while still -// allowing different behavior for different testcases. -func newFakeRESTClient(t *testing.T, handlers []handler) *fake.RESTClient { - return &fake.RESTClient{ - NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, - Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { - klog.V(5).Infof("FakeRESTClient: handling %s request for %q", req.Method, req.URL) - for _, h := range handlers { - resp, handled, err := h.handle(t, req) - if err != nil { - t.Fatalf("unexpected error: %v", err) - return nil, nil - } - if handled { - return resp, nil - } - } - t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) - return nil, nil - }), - } -} - -// The handler interface allows different testcases to provide -// special handling of requests. It also allows a single handler -// to keep state between a set of related requests instead of keeping -// a single large event handler. -type handler interface { - handle(t *testing.T, req *http.Request) (*http.Response, bool, error) -} - -// genericHandler provides a simple handler for resources that can -// be fetched and updated. It will simply return the given resource -// when asked for and accept patch requests. -type genericHandler struct { - resources []resourceInfo - mapper meta.RESTMapper -} - -func (g *genericHandler) handle(t *testing.T, req *http.Request) (*http.Response, bool, error) { - klog.V(5).Infof("genericHandler: handling %s request for %q", req.Method, req.URL) - for _, r := range g.resources { - gvk := r.resource.GroupVersionKind() - mapping, err := g.mapper.RESTMapping(gvk.GroupKind(), gvk.Version) - if err != nil { - return nil, false, err - } - var allPath string - if mapping.Scope == meta.RESTScopeNamespace { - allPath = fmt.Sprintf("/namespaces/%s/%s", r.resource.GetNamespace(), mapping.Resource.Resource) - } else { - allPath = fmt.Sprintf("/%s", mapping.Resource.Resource) - } - singlePath := allPath + "/" + r.resource.GetName() - - if req.URL.Path == singlePath && req.Method == http.MethodGet { - if r.exists { - bodyRC := io.NopCloser(bytes.NewReader(toJSONBytes(t, r.resource))) - return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, true, nil - } - return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.StringBody("")}, true, nil - } - - if req.URL.Path == singlePath && req.Method == http.MethodPatch { - bodyRC := io.NopCloser(bytes.NewReader(toJSONBytes(t, r.resource))) - return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, true, nil - } - - if req.URL.Path == singlePath && req.Method == http.MethodDelete { - if r.exists { - bodyRC := io.NopCloser(bytes.NewReader(toJSONBytes(t, r.resource))) - return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, true, nil - } - - // We're not testing DeletePropagationOrphan, so StatusOK should be - // safe. Otherwise, the status might be StatusAccepted. - // https://github.com/kubernetes/apiserver/blob/v0.22.2/pkg/endpoints/handlers/delete.go#L140 - status := http.StatusOK - - // Return Status object, if resource doesn't exist. - result := &metav1.Status{ - Status: metav1.StatusSuccess, - Code: int32(status), - Details: &metav1.StatusDetails{ - Name: r.resource.GetName(), - Kind: r.resource.GetKind(), - }, - } - bodyRC := io.NopCloser(bytes.NewReader(toJSONBytes(t, result))) - return &http.Response{StatusCode: status, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, true, nil - } - - if req.URL.Path == allPath && req.Method == http.MethodPost { - bodyRC := io.NopCloser(bytes.NewReader(toJSONBytes(t, r.resource))) - return &http.Response{StatusCode: http.StatusCreated, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, true, nil - } - } - return nil, false, nil -} - -func newInventoryReactor(invInfo inventoryInfo) *inventoryReactor { - return &inventoryReactor{ - inventoryObj: invInfo.toUnstructured(), - } -} - -type inventoryReactor struct { - inventoryObj *unstructured.Unstructured -} - -func (ir *inventoryReactor) updateFakeDynamicClient(fdc *dynamicfake.FakeDynamicClient) { - fdc.PrependReactor("create", "configmaps", func(action clienttesting.Action) (bool, runtime.Object, error) { - obj := *action.(clienttesting.CreateAction).GetObject().(*unstructured.Unstructured) - ir.inventoryObj = &obj - return true, ir.inventoryObj.DeepCopy(), nil - }) - fdc.PrependReactor("list", "configmaps", func(action clienttesting.Action) (bool, runtime.Object, error) { - uList := &unstructured.UnstructuredList{ - Items: []unstructured.Unstructured{}, - } - if ir.inventoryObj != nil { - uList.Items = append(uList.Items, *ir.inventoryObj.DeepCopy()) - } - return true, uList, nil - }) - fdc.PrependReactor("get", "configmaps", func(action clienttesting.Action) (bool, runtime.Object, error) { - return true, ir.inventoryObj.DeepCopy(), nil - }) - fdc.PrependReactor("update", "configmaps", func(action clienttesting.Action) (bool, runtime.Object, error) { - obj := *action.(clienttesting.UpdateAction).GetObject().(*unstructured.Unstructured) - ir.inventoryObj = &obj - return true, ir.inventoryObj.DeepCopy(), nil - }) -} - -// nsHandler can handle requests for a namespace. It will behave as if -// every requested namespace exists. It simply fetches the name of the requested -// namespace from the url and creates a new namespace type with the provided -// name for the response. -type nsHandler struct{} - -var ( - nsPathRegex = regexp.MustCompile(`/api/v1/namespaces/([^/]+)`) -) - -func (n *nsHandler) handle(t *testing.T, req *http.Request) (*http.Response, bool, error) { - match := nsPathRegex.FindStringSubmatch(req.URL.Path) - if req.Method == http.MethodGet && match != nil { - nsName := match[1] - ns := v1.Namespace{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "v1", - Kind: "Namespace", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: nsName, - }, - } - bodyRC := io.NopCloser(bytes.NewReader(toJSONBytes(t, &ns))) - return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, true, nil - } - return nil, false, nil -} - -type fakeWatcher struct { - start chan struct{} - events []pollevent.Event -} - -func newFakeWatcher(statusEvents []pollevent.Event) *fakeWatcher { - return &fakeWatcher{ - events: statusEvents, - start: make(chan struct{}), - } -} - -// Start events being sent on the status channel -func (f *fakeWatcher) Start() { - close(f.start) -} - -func (f *fakeWatcher) Watch(ctx context.Context, _ object.ObjMetadataSet, _ watcher.Options) <-chan pollevent.Event { - eventChannel := make(chan pollevent.Event) - go func() { - defer close(eventChannel) - // send sync event immediately - eventChannel <- pollevent.Event{Type: pollevent.SyncEvent} - // wait until started to send the events - <-f.start - for _, f := range f.events { - eventChannel <- f - } - // wait until cancelled to close the event channel and exit - <-ctx.Done() - }() - return eventChannel -} - -type fakeInfoHelper struct { - factory *cmdtesting.TestFactory -} - -// TODO(mortent): This has too much code in common with the -// infoHelper implementation. We need to find a better way to structure -// this. -func (f *fakeInfoHelper) UpdateInfo(info *resource.Info) error { - mapper, err := f.factory.ToRESTMapper() - if err != nil { - return err - } - gvk := info.Object.GetObjectKind().GroupVersionKind() - mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version) - if err != nil { - return err - } - info.Mapping = mapping - - c, err := f.getClient(gvk.GroupVersion()) - if err != nil { - return err - } - info.Client = c - return nil -} - -func (f *fakeInfoHelper) BuildInfo(obj *unstructured.Unstructured) (*resource.Info, error) { - info := &resource.Info{ - Name: obj.GetName(), - Namespace: obj.GetNamespace(), - Source: "unstructured", - Object: obj, - } - err := f.UpdateInfo(info) - return info, err -} - -func (f *fakeInfoHelper) getClient(gv schema.GroupVersion) (resource.RESTClient, error) { - if f.factory.UnstructuredClientForMappingFunc != nil { - return f.factory.UnstructuredClientForMappingFunc(gv) - } - if f.factory.UnstructuredClient != nil { - return f.factory.UnstructuredClient, nil - } - return f.factory.Client, nil -} - -// fakeDynamicClient returns a fake dynamic client. -func fakeDynamicClient(t *testing.T, mapper meta.RESTMapper, invInfo inventoryInfo, objs ...resourceInfo) *dynamicfake.FakeDynamicClient { - fakeClient := dynamicfake.NewSimpleDynamicClient(scheme.Scheme) - - invReactor := newInventoryReactor(invInfo) - invReactor.updateFakeDynamicClient(fakeClient) - - for i := range objs { - obj := objs[i] - gvk := obj.resource.GroupVersionKind() - mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version) - if !assert.NoError(t, err) { - t.FailNow() - } - r := mapping.Resource.Resource - fakeClient.PrependReactor("get", r, func(clienttesting.Action) (bool, runtime.Object, error) { - if obj.exists { - return true, obj.resource, nil - } - return false, nil, nil - }) - fakeClient.PrependReactor("delete", r, func(clienttesting.Action) (bool, runtime.Object, error) { - return true, nil, nil - }) - } - - return fakeClient -} - -func toJSONBytes(t *testing.T, obj runtime.Object) []byte { - objBytes, err := runtime.Encode(unstructured.NewJSONFallbackEncoder(codec), obj) - if !assert.NoError(t, err) { - t.Fatal(err) - } - return objBytes -} - -type JSONPathSetter struct { - Path string - Value interface{} -} - -func (jps JSONPathSetter) Mutate(u *unstructured.Unstructured) { - _, err := jsonpath.Set(u.Object, jps.Path, jps.Value) - if err != nil { - panic(fmt.Sprintf("failed to mutate unstructured object: %v", err)) - } -} diff --git a/pkg/apply/destroyer.go b/pkg/apply/destroyer.go deleted file mode 100644 index 388480c8..00000000 --- a/pkg/apply/destroyer.go +++ /dev/null @@ -1,196 +0,0 @@ -// Copyright 2019 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package apply - -import ( - "context" - "fmt" - "time" - - "github.com/fluxcd/cli-utils/pkg/apis/actuation" - "github.com/fluxcd/cli-utils/pkg/apply/cache" - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/apply/filter" - "github.com/fluxcd/cli-utils/pkg/apply/info" - "github.com/fluxcd/cli-utils/pkg/apply/prune" - "github.com/fluxcd/cli-utils/pkg/apply/solver" - "github.com/fluxcd/cli-utils/pkg/apply/taskrunner" - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/pkg/kstatus/watcher" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/object/validation" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/discovery" - "k8s.io/client-go/dynamic" - "k8s.io/klog/v2" -) - -// Destroyer performs the step of grabbing all the previous inventory objects and -// prune them. This also deletes all the previous inventory objects -type Destroyer struct { - pruner *prune.Pruner - statusWatcher watcher.StatusWatcher - invClient inventory.Client - mapper meta.RESTMapper - client dynamic.Interface - openAPIGetter discovery.OpenAPISchemaInterface - infoHelper info.Helper -} - -type DestroyerOptions struct { - // InventoryPolicy defines the inventory policy of apply. - InventoryPolicy inventory.Policy - - // DryRunStrategy defines whether changes should actually be performed, - // or if it is just talk and no action. - DryRunStrategy common.DryRunStrategy - - // DeleteTimeout defines how long we should wait for resources - // to be fully deleted. - DeleteTimeout time.Duration - - // DeletePropagationPolicy defines the deletion propagation policy - // that should be used. If this is not provided, the default is to - // use the Background policy. - DeletePropagationPolicy metav1.DeletionPropagation - - // EmitStatusEvents defines whether status events should be - // emitted on the eventChannel to the caller. - EmitStatusEvents bool - - // ValidationPolicy defines how to handle invalid objects. - ValidationPolicy validation.Policy -} - -func setDestroyerDefaults(o *DestroyerOptions) { - if o.DeletePropagationPolicy == "" { - o.DeletePropagationPolicy = metav1.DeletePropagationBackground - } -} - -// Run performs the destroy step. Passes the inventory object. This -// happens asynchronously on progress and any errors are reported -// back on the event channel. -func (d *Destroyer) Run(ctx context.Context, invInfo inventory.Info, options DestroyerOptions) <-chan event.Event { - eventChannel := make(chan event.Event) - setDestroyerDefaults(&options) - go func() { - defer close(eventChannel) - // Retrieve the objects to be deleted from the cluster. Second parameter is empty - // because no local objects returns all inventory objects for deletion. - emptyLocalObjs := object.UnstructuredSet{} - deleteObjs, err := d.pruner.GetPruneObjs(invInfo, emptyLocalObjs, prune.Options{ - DryRunStrategy: options.DryRunStrategy, - }) - if err != nil { - handleError(eventChannel, err) - return - } - - // Validate the resources to make sure we catch those problems early - // before anything has been updated in the cluster. - vCollector := &validation.Collector{} - validator := &validation.Validator{ - Collector: vCollector, - Mapper: d.mapper, - } - validator.Validate(deleteObjs) - - // Build a TaskContext for passing info between tasks - resourceCache := cache.NewResourceCacheMap() - taskContext := taskrunner.NewTaskContext(eventChannel, resourceCache) - - klog.V(4).Infoln("destroyer building task queue...") - deleteFilters := []filter.ValidationFilter{ - filter.PreventRemoveFilter{}, - filter.InventoryPolicyPruneFilter{ - Inv: invInfo, - InvPolicy: options.InventoryPolicy, - }, - filter.DependencyFilter{ - TaskContext: taskContext, - ActuationStrategy: actuation.ActuationStrategyDelete, - DryRunStrategy: options.DryRunStrategy, - }, - } - taskBuilder := &solver.TaskQueueBuilder{ - Pruner: d.pruner, - DynamicClient: d.client, - OpenAPIGetter: d.openAPIGetter, - InfoHelper: d.infoHelper, - Mapper: d.mapper, - InvClient: d.invClient, - Collector: vCollector, - PruneFilters: deleteFilters, - } - opts := solver.Options{ - Destroy: true, - Prune: true, - DryRunStrategy: options.DryRunStrategy, - PrunePropagationPolicy: options.DeletePropagationPolicy, - PruneTimeout: options.DeleteTimeout, - InventoryPolicy: options.InventoryPolicy, - } - - // Build the ordered set of tasks to execute. - taskQueue := taskBuilder. - WithPruneObjects(deleteObjs). - WithInventory(invInfo). - Build(taskContext, opts) - - klog.V(4).Infof("validation errors: %d", len(vCollector.Errors)) - klog.V(4).Infof("invalid objects: %d", len(vCollector.InvalidIds)) - - // Handle validation errors - switch options.ValidationPolicy { - case validation.ExitEarly: - err = vCollector.ToError() - if err != nil { - handleError(eventChannel, err) - return - } - case validation.SkipInvalid: - for _, err := range vCollector.Errors { - handleValidationError(eventChannel, err) - } - default: - handleError(eventChannel, fmt.Errorf("invalid ValidationPolicy: %q", options.ValidationPolicy)) - return - } - - // Register invalid objects to be retained in the inventory, if present. - for _, id := range vCollector.InvalidIds { - taskContext.AddInvalidObject(id) - } - - // Send event to inform the caller about the resources that - // will be pruned. - eventChannel <- event.Event{ - Type: event.InitType, - InitEvent: event.InitEvent{ - ActionGroups: taskQueue.ToActionGroups(), - }, - } - // Create a new TaskStatusRunner to execute the taskQueue. - klog.V(4).Infoln("destroyer building TaskStatusRunner...") - deleteIDs := object.UnstructuredSetToObjMetadataSet(deleteObjs) - statusWatcher := d.statusWatcher - // Disable watcher for dry runs - if opts.DryRunStrategy.ClientOrServerDryRun() { - statusWatcher = watcher.BlindStatusWatcher{} - } - runner := taskrunner.NewTaskStatusRunner(deleteIDs, statusWatcher) - klog.V(4).Infoln("destroyer running TaskStatusRunner...") - err = runner.Run(ctx, taskContext, taskQueue.ToChannel(), taskrunner.Options{ - EmitStatusEvents: options.EmitStatusEvents, - }) - if err != nil { - handleError(eventChannel, err) - return - } - }() - return eventChannel -} diff --git a/pkg/apply/destroyer_builder.go b/pkg/apply/destroyer_builder.go deleted file mode 100644 index 37ddb0db..00000000 --- a/pkg/apply/destroyer_builder.go +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2022 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package apply - -import ( - "github.com/fluxcd/cli-utils/pkg/apply/info" - "github.com/fluxcd/cli-utils/pkg/apply/prune" - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/pkg/kstatus/watcher" - "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/cli-runtime/pkg/resource" - "k8s.io/client-go/discovery" - "k8s.io/client-go/dynamic" - "k8s.io/client-go/rest" - "k8s.io/kubectl/pkg/cmd/util" -) - -type DestroyerBuilder struct { - commonBuilder -} - -// NewDestroyerBuilder returns a new DestroyerBuilder. -func NewDestroyerBuilder() *DestroyerBuilder { - return &DestroyerBuilder{ - // Defaults, if any, go here. - } -} - -func (b *DestroyerBuilder) Build() (*Destroyer, error) { - bx, err := b.finalize() - if err != nil { - return nil, err - } - return &Destroyer{ - pruner: &prune.Pruner{ - InvClient: bx.invClient, - Client: bx.client, - Mapper: bx.mapper, - }, - statusWatcher: bx.statusWatcher, - invClient: bx.invClient, - mapper: bx.mapper, - client: bx.client, - openAPIGetter: bx.discoClient, - infoHelper: info.NewHelper(bx.mapper, bx.unstructuredClientForMapping), - }, nil -} - -func (b *DestroyerBuilder) WithFactory(factory util.Factory) *DestroyerBuilder { - b.factory = factory - return b -} - -func (b *DestroyerBuilder) WithInventoryClient(invClient inventory.Client) *DestroyerBuilder { - b.invClient = invClient - return b -} - -func (b *DestroyerBuilder) WithDynamicClient(client dynamic.Interface) *DestroyerBuilder { - b.client = client - return b -} - -func (b *DestroyerBuilder) WithDiscoveryClient(discoClient discovery.CachedDiscoveryInterface) *DestroyerBuilder { - b.discoClient = discoClient - return b -} - -func (b *DestroyerBuilder) WithRestMapper(mapper meta.RESTMapper) *DestroyerBuilder { - b.mapper = mapper - return b -} - -func (b *DestroyerBuilder) WithRestConfig(restConfig *rest.Config) *DestroyerBuilder { - b.restConfig = restConfig - return b -} - -func (b *DestroyerBuilder) WithUnstructuredClientForMapping(unstructuredClientForMapping func(*meta.RESTMapping) (resource.RESTClient, error)) *DestroyerBuilder { - b.unstructuredClientForMapping = unstructuredClientForMapping - return b -} - -func (b *DestroyerBuilder) WithStatusWatcher(statusWatcher watcher.StatusWatcher) *DestroyerBuilder { - b.statusWatcher = statusWatcher - return b -} diff --git a/pkg/apply/destroyer_test.go b/pkg/apply/destroyer_test.go deleted file mode 100644 index 5f6e1493..00000000 --- a/pkg/apply/destroyer_test.go +++ /dev/null @@ -1,395 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package apply - -import ( - "context" - "sync" - "testing" - "time" - - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/inventory" - pollevent "github.com/fluxcd/cli-utils/pkg/kstatus/polling/event" - "github.com/fluxcd/cli-utils/pkg/kstatus/status" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/testutil" - "github.com/stretchr/testify/assert" -) - -func TestDestroyerCancel(t *testing.T) { - testCases := map[string]struct { - // inventory input to destroyer - invInfo inventoryInfo - // objects in the cluster - clusterObjs object.UnstructuredSet - // options input to destroyer.Run - options DestroyerOptions - // timeout for destroyer.Run - runTimeout time.Duration - // timeout for the test - testTimeout time.Duration - // fake input events from the status poller - statusEvents []pollevent.Event - // expected output status events (async) - expectedStatusEvents []testutil.ExpEvent - // expected output events - expectedEvents []testutil.ExpEvent - // true if runTimeout is expected to have caused cancellation - expectRunTimeout bool - }{ - "cancelled by caller while waiting for deletion": { - expectRunTimeout: true, - runTimeout: 2 * time.Second, - testTimeout: 30 * time.Second, - invInfo: inventoryInfo{ - name: "abc-123", - namespace: "test", - id: "test", - set: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["deployment"]), - }, - }, - clusterObjs: object.UnstructuredSet{ - testutil.Unstructured(t, resources["deployment"], testutil.AddOwningInv(t, "test")), - }, - options: DestroyerOptions{ - EmitStatusEvents: true, - // DeleteTimeout needs to block long enough to cancel the run, - // otherwise the WaitTask is skipped. - DeleteTimeout: 1 * time.Minute, - }, - statusEvents: []pollevent.Event{ - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - Status: status.InProgressStatus, - Resource: testutil.Unstructured(t, resources["deployment"], testutil.AddOwningInv(t, "test")), - }, - }, - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - Status: status.InProgressStatus, - Resource: testutil.Unstructured(t, resources["deployment"], testutil.AddOwningInv(t, "test")), - }, - }, - // Resource never becomes NotFound, blocking destroyer.Run from exiting - }, - expectedStatusEvents: []testutil.ExpEvent{ - { - EventType: event.StatusType, - StatusEvent: &testutil.ExpStatusEvent{ - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - Status: status.InProgressStatus, - }, - }, - }, - expectedEvents: []testutil.ExpEvent{ - { - // InitTask - EventType: event.InitType, - InitEvent: &testutil.ExpInitEvent{}, - }, - { - // PruneTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.DeleteAction, - GroupName: "prune-0", - Type: event.Started, - }, - }, - { - // Delete Deployment - EventType: event.DeleteType, - DeleteEvent: &testutil.ExpDeleteEvent{ - GroupName: "prune-0", - Status: event.DeleteSuccessful, - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - Error: nil, - }, - }, - { - // PruneTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.DeleteAction, - GroupName: "prune-0", - Type: event.Finished, - }, - }, - { - // WaitTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Started, - }, - }, - { - // Deployment reconcile pending. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcilePending, - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - }, - }, - // Deployment never becomes NotFound. - // WaitTask is expected to be cancelled before DeleteTimeout. - { - // WaitTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Finished, // TODO: add Cancelled event type - }, - }, - // Inventory cannot be deleted, because the objects still exist, - // even tho they've been deleted (ex: blocked by finalizer). - { - // Error - EventType: event.ErrorType, - ErrorEvent: &testutil.ExpErrorEvent{ - Err: context.DeadlineExceeded, - }, - }, - }, - }, - "completed with timeout": { - expectRunTimeout: false, - runTimeout: 10 * time.Second, - testTimeout: 30 * time.Second, - invInfo: inventoryInfo{ - name: "abc-123", - namespace: "test", - id: "test", - set: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["deployment"]), - }, - }, - clusterObjs: object.UnstructuredSet{ - testutil.Unstructured(t, resources["deployment"], testutil.AddOwningInv(t, "test")), - }, - options: DestroyerOptions{ - EmitStatusEvents: true, - // DeleteTimeout needs to block long enough for completion - DeleteTimeout: 1 * time.Minute, - }, - statusEvents: []pollevent.Event{ - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - Status: status.InProgressStatus, - Resource: testutil.Unstructured(t, resources["deployment"], testutil.AddOwningInv(t, "test")), - }, - }, - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - Status: status.NotFoundStatus, - }, - }, - // Resource becoming NotFound should unblock destroyer.Run WaitTask - }, - expectedStatusEvents: []testutil.ExpEvent{ - { - EventType: event.StatusType, - StatusEvent: &testutil.ExpStatusEvent{ - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - Status: status.InProgressStatus, - }, - }, - { - EventType: event.StatusType, - StatusEvent: &testutil.ExpStatusEvent{ - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - Status: status.NotFoundStatus, - }, - }, - }, - expectedEvents: []testutil.ExpEvent{ - { - // InitTask - EventType: event.InitType, - InitEvent: &testutil.ExpInitEvent{}, - }, - { - // PruneTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.DeleteAction, - GroupName: "prune-0", - Type: event.Started, - }, - }, - { - // Delete Deployment - EventType: event.DeleteType, - DeleteEvent: &testutil.ExpDeleteEvent{ - GroupName: "prune-0", - Status: event.DeleteSuccessful, - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - Error: nil, - }, - }, - { - // PruneTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.DeleteAction, - GroupName: "prune-0", - Type: event.Finished, - }, - }, - { - // WaitTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Started, - }, - }, - // Wait events sorted Pending > Successful (see pkg/testutil) - { - // Deployment reconcile pending. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcilePending, - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - }, - }, - { - // Deployment confirmed NotFound. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcileSuccessful, - Identifier: testutil.ToIdentifier(t, resources["deployment"]), - }, - }, - { - // WaitTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Finished, - }, - }, - { - // DeleteInvTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-delete-or-update-0", - Type: event.Started, - }, - }, - { - // DeleteInvTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-delete-or-update-0", - Type: event.Finished, - }, - }, - }, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - statusWatcher := newFakeWatcher(tc.statusEvents) - - invInfo := tc.invInfo.toWrapped() - - destroyer := newTestDestroyer(t, - tc.invInfo, - // Add the inventory to the cluster (to allow deletion) - append(tc.clusterObjs, inventory.InvInfoToConfigMap(invInfo)), - statusWatcher, - ) - - // Context for Destroyer.Run - runCtx, runCancel := context.WithTimeout(context.Background(), tc.runTimeout) - defer runCancel() // cleanup - - // Context for this test (in case Destroyer.Run never closes the event channel) - testCtx, testCancel := context.WithTimeout(context.Background(), tc.testTimeout) - defer testCancel() // cleanup - - eventChannel := destroyer.Run(runCtx, invInfo, tc.options) - - // only start poller once per run - var once sync.Once - var events []event.Event - - loop: - for { - select { - case <-testCtx.Done(): - // Test timed out - runCancel() - t.Errorf("Destroyer.Run failed to respond to cancellation (expected: %s, timeout: %s)", tc.runTimeout, tc.testTimeout) - break loop - - case e, ok := <-eventChannel: - if !ok { - // Event channel closed - testCancel() - break loop - } - events = append(events, e) - - if e.Type == event.ActionGroupType && - e.ActionGroupEvent.Action == event.WaitAction { - once.Do(func() { - // Start sending status events after waiting starts - statusWatcher.Start() - }) - } - } - } - - // Convert events to test events for comparison - receivedEvents := testutil.EventsToExpEvents(events) - - // Validate & remove expected status events - for _, e := range tc.expectedStatusEvents { - var removed int - receivedEvents, removed = testutil.RemoveEqualEvents(receivedEvents, e) - if removed < 1 { - t.Errorf("Expected status event not received: %#v", e) - } - } - - // sort to allow comparison of multiple wait events - testutil.SortExpEvents(receivedEvents) - - // Validate the rest of the events - testutil.AssertEqual(t, tc.expectedEvents, receivedEvents, - "Actual events (%d) do not match expected events (%d)", - len(receivedEvents), len(tc.expectedEvents)) - - // Validate that the expected timeout was the cause of the run completion. - // just in case something else cancelled the run - if tc.expectRunTimeout { - assert.Equal(t, context.DeadlineExceeded, runCtx.Err(), "Destroyer.Run exited, but not by expected timeout") - } else { - assert.Nil(t, runCtx.Err(), "Destroyer.Run exited, but not by expected timeout") - } - }) - } -} diff --git a/pkg/apply/error/error.go b/pkg/apply/error/error.go deleted file mode 100644 index 514a9ed1..00000000 --- a/pkg/apply/error/error.go +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2019 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 -package error - -type UnknownTypeError struct { - err error -} - -func (e *UnknownTypeError) Error() string { - return e.err.Error() -} - -func NewUnknownTypeError(err error) *UnknownTypeError { - return &UnknownTypeError{err: err} -} - -type ApplyRunError struct { - err error -} - -func (e *ApplyRunError) Error() string { - return e.err.Error() -} - -func NewApplyRunError(err error) *ApplyRunError { - return &ApplyRunError{err: err} -} - -type InitializeApplyOptionError struct { - err error -} - -func (e *InitializeApplyOptionError) Error() string { - return e.err.Error() -} - -func NewInitializeApplyOptionError(err error) *InitializeApplyOptionError { - return &InitializeApplyOptionError{err: err} -} diff --git a/pkg/apply/event/actiongroupeventstatus_string.go b/pkg/apply/event/actiongroupeventstatus_string.go deleted file mode 100644 index aa1738a9..00000000 --- a/pkg/apply/event/actiongroupeventstatus_string.go +++ /dev/null @@ -1,24 +0,0 @@ -// Code generated by "stringer -type=ActionGroupEventStatus"; DO NOT EDIT. - -package event - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[Started-0] - _ = x[Finished-1] -} - -const _ActionGroupEventStatus_name = "StartedFinished" - -var _ActionGroupEventStatus_index = [...]uint8{0, 7, 15} - -func (i ActionGroupEventStatus) String() string { - if i < 0 || i >= ActionGroupEventStatus(len(_ActionGroupEventStatus_index)-1) { - return "ActionGroupEventStatus(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _ActionGroupEventStatus_name[_ActionGroupEventStatus_index[i]:_ActionGroupEventStatus_index[i+1]] -} diff --git a/pkg/apply/event/applyeventstatus_string.go b/pkg/apply/event/applyeventstatus_string.go deleted file mode 100644 index 64646f2e..00000000 --- a/pkg/apply/event/applyeventstatus_string.go +++ /dev/null @@ -1,26 +0,0 @@ -// Code generated by "stringer -type=ApplyEventStatus -linecomment"; DO NOT EDIT. - -package event - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[ApplyPending-0] - _ = x[ApplySuccessful-1] - _ = x[ApplySkipped-2] - _ = x[ApplyFailed-3] -} - -const _ApplyEventStatus_name = "PendingSuccessfulSkippedFailed" - -var _ApplyEventStatus_index = [...]uint8{0, 7, 17, 24, 30} - -func (i ApplyEventStatus) String() string { - if i < 0 || i >= ApplyEventStatus(len(_ApplyEventStatus_index)-1) { - return "ApplyEventStatus(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _ApplyEventStatus_name[_ApplyEventStatus_index[i]:_ApplyEventStatus_index[i+1]] -} diff --git a/pkg/apply/event/deleteeventstatus_string.go b/pkg/apply/event/deleteeventstatus_string.go deleted file mode 100644 index b65eef9e..00000000 --- a/pkg/apply/event/deleteeventstatus_string.go +++ /dev/null @@ -1,26 +0,0 @@ -// Code generated by "stringer -type=DeleteEventStatus -linecomment"; DO NOT EDIT. - -package event - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[DeletePending-0] - _ = x[DeleteSuccessful-1] - _ = x[DeleteSkipped-2] - _ = x[DeleteFailed-3] -} - -const _DeleteEventStatus_name = "PendingSuccessfulSkippedFailed" - -var _DeleteEventStatus_index = [...]uint8{0, 7, 17, 24, 30} - -func (i DeleteEventStatus) String() string { - if i < 0 || i >= DeleteEventStatus(len(_DeleteEventStatus_index)-1) { - return "DeleteEventStatus(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _DeleteEventStatus_name[_DeleteEventStatus_index[i]:_DeleteEventStatus_index[i+1]] -} diff --git a/pkg/apply/event/event.go b/pkg/apply/event/event.go deleted file mode 100644 index 9890ce4a..00000000 --- a/pkg/apply/event/event.go +++ /dev/null @@ -1,325 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package event - -import ( - "fmt" - "strings" - - pollevent "github.com/fluxcd/cli-utils/pkg/kstatus/polling/event" - "github.com/fluxcd/cli-utils/pkg/object" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -// Type determines the type of events that are available. -// -//go:generate stringer -type=Type -type Type int - -const ( - InitType Type = iota - ErrorType - ActionGroupType - ApplyType - StatusType - PruneType - DeleteType - WaitType - ValidationType -) - -// Event is the type of the objects that will be returned through -// the channel that is returned from a call to Run. It contains -// information about progress and errors encountered during -// the process of doing apply, waiting for status and doing a prune. -type Event struct { - // Type is the type of event. - Type Type - - // InitEvent contains information about which resources will - // be applied/pruned. - InitEvent InitEvent - - // ErrorEvent contains information about any errors encountered. - ErrorEvent ErrorEvent - - // ActionGroupEvent contains information about the progression of tasks - // to apply, prune, and destroy resources, and tasks that involves waiting - // for a set of resources to reach a specific state. - ActionGroupEvent ActionGroupEvent - - // ApplyEvent contains information about progress pertaining to - // applying a resource to the cluster. - ApplyEvent ApplyEvent - - // StatusEvents contains information about the status of one of - // the applied resources. - StatusEvent StatusEvent - - // PruneEvent contains information about objects that have been - // pruned. - PruneEvent PruneEvent - - // DeleteEvent contains information about object that have been - // deleted. - DeleteEvent DeleteEvent - - // WaitEvent contains information about any errors encountered in a WaitTask. - WaitEvent WaitEvent - - // ValidationEvent contains information about validation errors. - ValidationEvent ValidationEvent -} - -// String returns a string suitable for logging -func (e Event) String() string { - var sb strings.Builder - switch e.Type { - case InitType: - sb.WriteString(e.InitEvent.String()) - case ErrorType: - sb.WriteString(e.ErrorEvent.String()) - case ActionGroupType: - sb.WriteString(e.ActionGroupEvent.String()) - case ApplyType: - sb.WriteString(e.ApplyEvent.String()) - case StatusType: - sb.WriteString(e.StatusEvent.String()) - case PruneType: - sb.WriteString(e.PruneEvent.String()) - case DeleteType: - sb.WriteString(e.DeleteEvent.String()) - case WaitType: - sb.WriteString(e.WaitEvent.String()) - case ValidationType: - sb.WriteString(e.ValidationEvent.String()) - } - return sb.String() -} - -type InitEvent struct { - ActionGroups ActionGroupList -} - -// String returns a string suitable for logging -func (ie InitEvent) String() string { - return fmt.Sprintf("InitEvent{ ActionGroups: %s }", ie.ActionGroups) -} - -//go:generate stringer -type=ResourceAction -linecomment -type ResourceAction int - -const ( - ApplyAction ResourceAction = iota // Apply - PruneAction // Prune - DeleteAction // Delete - WaitAction // Wait - InventoryAction // Inventory -) - -type ActionGroupList []ActionGroup - -// String returns a string suitable for logging -func (agl ActionGroupList) String() string { - var sb strings.Builder - sb.WriteString("[ ") - for i, ag := range agl { - if i > 0 { - sb.WriteString(", ") - } - sb.WriteString("[ ") - sb.WriteString(ag.String()) - sb.WriteString(" ]") - } - sb.WriteString(" ]") - return sb.String() -} - -type ActionGroup struct { - Name string - Action ResourceAction - Identifiers object.ObjMetadataSet -} - -// String returns a string suitable for logging -func (ag ActionGroup) String() string { - return fmt.Sprintf("ActionGroup{ Name: %q, Action: %q, Identifiers: %s }", - ag.Name, ag.Action, ag.Identifiers) -} - -type ErrorEvent struct { - Err error -} - -// String returns a string suitable for logging -func (ee ErrorEvent) String() string { - return fmt.Sprintf("ErrorEvent{ Err: %q }", ee.Err.Error()) -} - -//go:generate stringer -type=WaitEventStatus -linecomment -type WaitEventStatus int - -const ( - ReconcilePending WaitEventStatus = iota // Pending - ReconcileSuccessful // Successful - ReconcileSkipped // Skipped - ReconcileTimeout // Timeout - ReconcileFailed // Failed -) - -type WaitEvent struct { - GroupName string - Identifier object.ObjMetadata - Status WaitEventStatus -} - -// String returns a string suitable for logging -func (we WaitEvent) String() string { - return fmt.Sprintf("WaitEvent{ GroupName: %q, Status: %q, Identifier: %q }", - we.GroupName, we.Status, we.Identifier) -} - -//go:generate stringer -type=ActionGroupEventStatus -type ActionGroupEventStatus int - -const ( - Started ActionGroupEventStatus = iota - Finished -) - -type ActionGroupEvent struct { - GroupName string - Action ResourceAction - Status ActionGroupEventStatus -} - -// String returns a string suitable for logging -func (age ActionGroupEvent) String() string { - return fmt.Sprintf("ActionGroupEvent{ GroupName: %q, Action: %q, Type: %q }", - age.GroupName, age.Action, age.Status) -} - -//go:generate stringer -type=ApplyEventStatus -linecomment -type ApplyEventStatus int - -const ( - ApplyPending ApplyEventStatus = iota // Pending - ApplySuccessful // Successful - ApplySkipped // Skipped - ApplyFailed // Failed -) - -type ApplyEvent struct { - GroupName string - Identifier object.ObjMetadata - Status ApplyEventStatus - Resource *unstructured.Unstructured - Error error -} - -// String returns a string suitable for logging -func (ae ApplyEvent) String() string { - if ae.Error != nil { - return fmt.Sprintf("ApplyEvent{ GroupName: %q, Status: %q, Identifier: %q, Error: %q }", - ae.GroupName, ae.Status, ae.Identifier, ae.Error) - } - return fmt.Sprintf("ApplyEvent{ GroupName: %q, Status: %q, Identifier: %q }", - ae.GroupName, ae.Status, ae.Identifier) -} - -type StatusEvent struct { - Identifier object.ObjMetadata - PollResourceInfo *pollevent.ResourceStatus - Resource *unstructured.Unstructured - Error error -} - -// String returns a string suitable for logging -func (se StatusEvent) String() string { - status := "nil" - gen := int64(0) - if se.PollResourceInfo != nil { - status = se.PollResourceInfo.Status.String() - if se.PollResourceInfo.Resource != nil { - gen = se.PollResourceInfo.Resource.GetGeneration() - } - } - if se.Error != nil { - return fmt.Sprintf("StatusEvent{ Status: %q, Generation: %d, Identifier: %q, Error: %q }", - status, gen, se.Identifier, se.Error) - } - return fmt.Sprintf("StatusEvent{ Status: %q, Generation: %d, Identifier: %q }", - status, gen, se.Identifier) -} - -//go:generate stringer -type=PruneEventStatus -linecomment -type PruneEventStatus int - -const ( - PrunePending PruneEventStatus = iota // Pending - PruneSuccessful // Successful - PruneSkipped // Skipped - PruneFailed // Failed -) - -type PruneEvent struct { - GroupName string - Identifier object.ObjMetadata - Status PruneEventStatus - Object *unstructured.Unstructured - Error error -} - -// String returns a string suitable for logging -func (pe PruneEvent) String() string { - if pe.Error != nil { - return fmt.Sprintf("PruneEvent{ GroupName: %q, Status: %q, Identifier: %q, Error: %q }", - pe.GroupName, pe.Status, pe.Identifier, pe.Error) - } - return fmt.Sprintf("PruneEvent{ GroupName: %q, Status: %q, Identifier: %q }", - pe.GroupName, pe.Status, pe.Identifier) -} - -//go:generate stringer -type=DeleteEventStatus -linecomment -type DeleteEventStatus int - -const ( - DeletePending DeleteEventStatus = iota // Pending - DeleteSuccessful // Successful - DeleteSkipped // Skipped - DeleteFailed // Failed -) - -type DeleteEvent struct { - GroupName string - Identifier object.ObjMetadata - Status DeleteEventStatus - Object *unstructured.Unstructured - Error error -} - -// String returns a string suitable for logging -func (de DeleteEvent) String() string { - if de.Error != nil { - return fmt.Sprintf("DeleteEvent{ GroupName: %q, Status: %q, Identifier: %q, Error: %q }", - de.GroupName, de.Status, de.Identifier, de.Error) - } - return fmt.Sprintf("DeleteEvent{ GroupName: %q, Status: %q, Identifier: %q }", - de.GroupName, de.Status, de.Identifier) -} - -type ValidationEvent struct { - Identifiers object.ObjMetadataSet - Error error -} - -// String returns a string suitable for logging -func (ve ValidationEvent) String() string { - if ve.Error != nil { - return fmt.Sprintf("ValidationEvent{ Identifiers: %+v, Error: %q }", - ve.Identifiers, ve.Error) - } - return fmt.Sprintf("ValidationEvent{ Identifiers: %+v }", - ve.Identifiers) -} diff --git a/pkg/apply/event/pruneeventstatus_string.go b/pkg/apply/event/pruneeventstatus_string.go deleted file mode 100644 index 36a468df..00000000 --- a/pkg/apply/event/pruneeventstatus_string.go +++ /dev/null @@ -1,26 +0,0 @@ -// Code generated by "stringer -type=PruneEventStatus -linecomment"; DO NOT EDIT. - -package event - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[PrunePending-0] - _ = x[PruneSuccessful-1] - _ = x[PruneSkipped-2] - _ = x[PruneFailed-3] -} - -const _PruneEventStatus_name = "PendingSuccessfulSkippedFailed" - -var _PruneEventStatus_index = [...]uint8{0, 7, 17, 24, 30} - -func (i PruneEventStatus) String() string { - if i < 0 || i >= PruneEventStatus(len(_PruneEventStatus_index)-1) { - return "PruneEventStatus(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _PruneEventStatus_name[_PruneEventStatus_index[i]:_PruneEventStatus_index[i+1]] -} diff --git a/pkg/apply/event/resourceaction_string.go b/pkg/apply/event/resourceaction_string.go deleted file mode 100644 index ff00de4c..00000000 --- a/pkg/apply/event/resourceaction_string.go +++ /dev/null @@ -1,27 +0,0 @@ -// Code generated by "stringer -type=ResourceAction -linecomment"; DO NOT EDIT. - -package event - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[ApplyAction-0] - _ = x[PruneAction-1] - _ = x[DeleteAction-2] - _ = x[WaitAction-3] - _ = x[InventoryAction-4] -} - -const _ResourceAction_name = "ApplyPruneDeleteWaitInventory" - -var _ResourceAction_index = [...]uint8{0, 5, 10, 16, 20, 29} - -func (i ResourceAction) String() string { - if i < 0 || i >= ResourceAction(len(_ResourceAction_index)-1) { - return "ResourceAction(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _ResourceAction_name[_ResourceAction_index[i]:_ResourceAction_index[i+1]] -} diff --git a/pkg/apply/event/type_string.go b/pkg/apply/event/type_string.go deleted file mode 100644 index 27253240..00000000 --- a/pkg/apply/event/type_string.go +++ /dev/null @@ -1,31 +0,0 @@ -// Code generated by "stringer -type=Type"; DO NOT EDIT. - -package event - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[InitType-0] - _ = x[ErrorType-1] - _ = x[ActionGroupType-2] - _ = x[ApplyType-3] - _ = x[StatusType-4] - _ = x[PruneType-5] - _ = x[DeleteType-6] - _ = x[WaitType-7] - _ = x[ValidationType-8] -} - -const _Type_name = "InitTypeErrorTypeActionGroupTypeApplyTypeStatusTypePruneTypeDeleteTypeWaitTypeValidationType" - -var _Type_index = [...]uint8{0, 8, 17, 32, 41, 51, 60, 70, 78, 92} - -func (i Type) String() string { - if i < 0 || i >= Type(len(_Type_index)-1) { - return "Type(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _Type_name[_Type_index[i]:_Type_index[i+1]] -} diff --git a/pkg/apply/event/waiteventstatus_string.go b/pkg/apply/event/waiteventstatus_string.go deleted file mode 100644 index b512f571..00000000 --- a/pkg/apply/event/waiteventstatus_string.go +++ /dev/null @@ -1,27 +0,0 @@ -// Code generated by "stringer -type=WaitEventStatus -linecomment"; DO NOT EDIT. - -package event - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[ReconcilePending-0] - _ = x[ReconcileSuccessful-1] - _ = x[ReconcileSkipped-2] - _ = x[ReconcileTimeout-3] - _ = x[ReconcileFailed-4] -} - -const _WaitEventStatus_name = "PendingSuccessfulSkippedTimeoutFailed" - -var _WaitEventStatus_index = [...]uint8{0, 7, 17, 24, 31, 37} - -func (i WaitEventStatus) String() string { - if i < 0 || i >= WaitEventStatus(len(_WaitEventStatus_index)-1) { - return "WaitEventStatus(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _WaitEventStatus_name[_WaitEventStatus_index[i]:_WaitEventStatus_index[i+1]] -} diff --git a/pkg/apply/filter/current-uids-filter.go b/pkg/apply/filter/current-uids-filter.go deleted file mode 100644 index 8a6edba8..00000000 --- a/pkg/apply/filter/current-uids-filter.go +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package filter - -import ( - "fmt" - - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/sets" -) - -// CurrentUIDFilter implements ValidationFilter interface to determine -// if an object should not be pruned (deleted) because it has recently -// been applied. -type CurrentUIDFilter struct { - CurrentUIDs sets.String // nolint:staticcheck -} - -// Name returns a filter identifier for logging. -func (cuf CurrentUIDFilter) Name() string { - return "CurrentUIDFilter" -} - -// Filter returns a ApplyPreventedDeletionError if the object prune/delete -// should be skipped. -func (cuf CurrentUIDFilter) Filter(obj *unstructured.Unstructured) error { - uid := obj.GetUID() - if cuf.CurrentUIDs.Has(string(uid)) { - return &ApplyPreventedDeletionError{UID: uid} - } - return nil -} - -type ApplyPreventedDeletionError struct { - UID types.UID -} - -func (e *ApplyPreventedDeletionError) Error() string { - return fmt.Sprintf("object just applied (UID: %q)", e.UID) -} - -func (e *ApplyPreventedDeletionError) Is(err error) bool { - if err == nil { - return false - } - tErr, ok := err.(*ApplyPreventedDeletionError) - if !ok { - return false - } - return e.UID == tErr.UID -} diff --git a/pkg/apply/filter/current-uids-filter_test.go b/pkg/apply/filter/current-uids-filter_test.go deleted file mode 100644 index 6a4b69ef..00000000 --- a/pkg/apply/filter/current-uids-filter_test.go +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2019 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package filter - -import ( - "testing" - - "github.com/fluxcd/cli-utils/pkg/testutil" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/sets" -) - -func TestCurrentUIDFilter(t *testing.T) { - tests := map[string]struct { - filterUIDs sets.String // nolint:staticcheck - objUID string - expectedError error - }{ - "Empty filter UIDs, object is not filtered": { - filterUIDs: sets.NewString(), - objUID: "bar", - }, - "Empty object UID, object is not filtered": { - filterUIDs: sets.NewString("foo"), - objUID: "", - }, - "Object UID not in filter UID set, object is not filtered": { - filterUIDs: sets.NewString("foo", "baz"), - objUID: "bar", - }, - "Object UID is in filter UID set, object is filtered": { - filterUIDs: sets.NewString("foo"), - objUID: "foo", - expectedError: &ApplyPreventedDeletionError{UID: "foo"}, - }, - "Object UID is among several filter UIDs, object is filtered": { - filterUIDs: sets.NewString("foo", "bar", "baz"), - objUID: "foo", - expectedError: &ApplyPreventedDeletionError{UID: "foo"}, - }, - } - - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - filter := CurrentUIDFilter{ - CurrentUIDs: tc.filterUIDs, - } - obj := defaultObj.DeepCopy() - obj.SetUID(types.UID(tc.objUID)) - err := filter.Filter(obj) - testutil.AssertEqual(t, tc.expectedError, err) - }) - } -} diff --git a/pkg/apply/filter/dependency-filter.go b/pkg/apply/filter/dependency-filter.go deleted file mode 100644 index 6ec1cf13..00000000 --- a/pkg/apply/filter/dependency-filter.go +++ /dev/null @@ -1,261 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package filter - -import ( - "fmt" - "strings" - - "github.com/fluxcd/cli-utils/pkg/apis/actuation" - "github.com/fluxcd/cli-utils/pkg/apply/taskrunner" - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/object" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -//go:generate stringer -type=Relationship -linecomment -type Relationship int - -const ( - RelationshipDependent Relationship = iota // Dependent - RelationshipDependency // Dependency -) - -//go:generate stringer -type=Phase -linecomment -type Phase int - -const ( - PhaseActuation Phase = iota // Actuation - PhaseReconcile // Reconcile -) - -// DependencyFilter implements ValidationFilter interface to determine if an -// object can be applied or deleted based on the status of it's dependencies. -type DependencyFilter struct { - TaskContext *taskrunner.TaskContext - ActuationStrategy actuation.ActuationStrategy - DryRunStrategy common.DryRunStrategy -} - -const DependencyFilterName = "DependencyFilter" - -// Name returns the name of the filter for logs and events. -func (dnrf DependencyFilter) Name() string { - return DependencyFilterName -} - -// Filter returns an error if the specified object should be skipped because at -// least one of its dependencies is Not Found or Not Reconciled. -// Typed Errors: -// - DependencyPreventedActuationError -// - DependencyActuationMismatchError -func (dnrf DependencyFilter) Filter(obj *unstructured.Unstructured) error { - id := object.UnstructuredToObjMetadata(obj) - - switch dnrf.ActuationStrategy { - case actuation.ActuationStrategyApply: - // For apply, check dependencies (outgoing) - for _, depID := range dnrf.TaskContext.Graph().Dependencies(id) { - err := dnrf.filterByRelationship(id, depID, RelationshipDependency) - if err != nil { - return err - } - } - case actuation.ActuationStrategyDelete: - // For delete, check dependents (incoming) - for _, depID := range dnrf.TaskContext.Graph().Dependents(id) { - err := dnrf.filterByRelationship(id, depID, RelationshipDependent) - if err != nil { - return err - } - } - default: - return NewFatalError(fmt.Errorf("invalid actuation strategy: %q", dnrf.ActuationStrategy)) - } - return nil -} - -func (dnrf DependencyFilter) filterByRelationship(aID, bID object.ObjMetadata, relationship Relationship) error { - // Dependency on an invalid object is considered an invalid dependency, making both objects invalid. - // For applies: don't prematurely apply something that depends on something that hasn't been applied (because invalid). - // For deletes: don't prematurely delete something that depends on something that hasn't been deleted (because invalid). - // These can't be caught be subsequent checks, because invalid objects aren't in the inventory. - if dnrf.TaskContext.IsInvalidObject(bID) { - // Should have been caught in validation - return NewFatalError(fmt.Errorf("invalid %s: %s", - strings.ToLower(relationship.String()), - bID)) - } - - status, found := dnrf.TaskContext.InventoryManager().ObjectStatus(bID) - if !found { - // Status is registered during planning. - // So if status is not found, the object is external (NYI) or invalid. - // Should have been caught in validation. - return NewFatalError(fmt.Errorf("unknown %s actuation strategy: %s", - strings.ToLower(relationship.String()), bID)) - } - - // Dependencies must have the same actuation strategy. - // If there is a mismatch, skip both. - if status.Strategy != dnrf.ActuationStrategy { - // Skip! - return &DependencyActuationMismatchError{ - Object: aID, - Strategy: dnrf.ActuationStrategy, - Relationship: relationship, - Relation: bID, - RelationStrategy: status.Strategy, - } - } - - switch status.Actuation { - case actuation.ActuationPending: - // If actuation is still pending, dependency sorting is probably broken. - return NewFatalError(fmt.Errorf("premature %s: %s %s actuation %s: %s", - strings.ToLower(dnrf.ActuationStrategy.String()), - strings.ToLower(relationship.String()), - strings.ToLower(status.Strategy.String()), - strings.ToLower(status.Actuation.String()), - bID)) - case actuation.ActuationSkipped, actuation.ActuationFailed: - // Skip! - return &DependencyPreventedActuationError{ - Object: aID, - Strategy: dnrf.ActuationStrategy, - Relationship: relationship, - Relation: bID, - RelationPhase: PhaseActuation, - RelationActuationStatus: status.Actuation, - RelationReconcileStatus: status.Reconcile, - } - case actuation.ActuationSucceeded: - // Don't skip! - default: - // Should never happen - return NewFatalError(fmt.Errorf("invalid %s actuation status %q: %s", - strings.ToLower(relationship.String()), - strings.ToLower(status.Actuation.String()), - bID)) - } - - // DryRun skips WaitTasks, so reconcile status can be ignored - if dnrf.DryRunStrategy.ClientOrServerDryRun() { - // Don't skip! - return nil - } - - switch status.Reconcile { - case actuation.ReconcilePending: - // If reconcile is still pending, dependency sorting is probably broken. - return NewFatalError(fmt.Errorf("premature %s: %s %s reconcile %s: %s", - strings.ToLower(dnrf.ActuationStrategy.String()), - strings.ToLower(relationship.String()), - strings.ToLower(status.Strategy.String()), - strings.ToLower(status.Reconcile.String()), - bID)) - case actuation.ReconcileSkipped, actuation.ReconcileFailed, actuation.ReconcileTimeout: - // Skip! - return &DependencyPreventedActuationError{ - Object: aID, - Strategy: dnrf.ActuationStrategy, - Relationship: relationship, - Relation: bID, - RelationPhase: PhaseReconcile, - RelationActuationStatus: status.Actuation, - RelationReconcileStatus: status.Reconcile, - } - case actuation.ReconcileSucceeded: - // Don't skip! - default: - // Should never happen - return NewFatalError(fmt.Errorf("invalid %s reconcile status %q: %s", - strings.ToLower(relationship.String()), - strings.ToLower(status.Reconcile.String()), - bID)) - } - - // Don't skip! - return nil -} - -type DependencyPreventedActuationError struct { - Object object.ObjMetadata - Strategy actuation.ActuationStrategy - Relationship Relationship - - Relation object.ObjMetadata - RelationPhase Phase - RelationActuationStatus actuation.ActuationStatus - RelationReconcileStatus actuation.ReconcileStatus -} - -func (e *DependencyPreventedActuationError) Error() string { - switch e.RelationPhase { - case PhaseActuation: - return fmt.Sprintf("%s %s %s %s: %s", - strings.ToLower(e.Relationship.String()), - strings.ToLower(e.Strategy.String()), - strings.ToLower(e.RelationPhase.String()), - strings.ToLower(e.RelationActuationStatus.String()), - e.Relation) - case PhaseReconcile: - return fmt.Sprintf("%s %s %s %s: %s", - strings.ToLower(e.Relationship.String()), - strings.ToLower(e.Strategy.String()), - strings.ToLower(e.RelationPhase.String()), - strings.ToLower(e.RelationReconcileStatus.String()), - e.Relation) - default: - return fmt.Sprintf("invalid phase: %s", e.RelationPhase) - } -} - -func (e *DependencyPreventedActuationError) Is(err error) bool { - if err == nil { - return false - } - tErr, ok := err.(*DependencyPreventedActuationError) - if !ok { - return false - } - return e.Object == tErr.Object && - e.Strategy == tErr.Strategy && - e.Relationship == tErr.Relationship && - e.Relation == tErr.Relation && - e.RelationPhase == tErr.RelationPhase && - e.RelationActuationStatus == tErr.RelationActuationStatus && - e.RelationReconcileStatus == tErr.RelationReconcileStatus -} - -type DependencyActuationMismatchError struct { - Object object.ObjMetadata - Strategy actuation.ActuationStrategy - Relationship Relationship - - Relation object.ObjMetadata - RelationStrategy actuation.ActuationStrategy -} - -func (e *DependencyActuationMismatchError) Error() string { - return fmt.Sprintf("%s scheduled for %s: %s", - strings.ToLower(e.Relationship.String()), - strings.ToLower(e.RelationStrategy.String()), - e.Relation) -} - -func (e *DependencyActuationMismatchError) Is(err error) bool { - if err == nil { - return false - } - tErr, ok := err.(*DependencyActuationMismatchError) - if !ok { - return false - } - return e.Object == tErr.Object && - e.Strategy == tErr.Strategy && - e.Relationship == tErr.Relationship && - e.Relation == tErr.Relation && - e.RelationStrategy == tErr.RelationStrategy -} diff --git a/pkg/apply/filter/dependency-filter_test.go b/pkg/apply/filter/dependency-filter_test.go deleted file mode 100644 index 24dd8a18..00000000 --- a/pkg/apply/filter/dependency-filter_test.go +++ /dev/null @@ -1,549 +0,0 @@ -// Copyright 2019 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package filter - -import ( - "fmt" - "testing" - - "github.com/fluxcd/cli-utils/pkg/apis/actuation" - "github.com/fluxcd/cli-utils/pkg/apply/taskrunner" - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/testutil" - "k8s.io/apimachinery/pkg/runtime/schema" -) - -var idInvalid = object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Kind: "", // required - }, - Name: "invalid", // required -} - -var idA = object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "group-a", - Kind: "kind-a", - }, - Name: "name-a", - Namespace: "namespace-a", -} - -var idB = object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "group-b", - Kind: "kind-b", - }, - Name: "name-b", - Namespace: "namespace-b", -} - -func TestDependencyFilter(t *testing.T) { - tests := map[string]struct { - dryRunStrategy common.DryRunStrategy - actuationStrategy actuation.ActuationStrategy - contextSetup func(*taskrunner.TaskContext) - id object.ObjMetadata - expectedError error - }{ - "apply A (no deps)": { - actuationStrategy: actuation.ActuationStrategyApply, - contextSetup: func(taskContext *taskrunner.TaskContext) { - taskContext.Graph().AddVertex(idA) - taskContext.InventoryManager().AddPendingApply(idA) - }, - id: idA, - expectedError: nil, - }, - "apply A (A -> B) when B is invalid": { - actuationStrategy: actuation.ActuationStrategyApply, - contextSetup: func(taskContext *taskrunner.TaskContext) { - taskContext.Graph().AddVertex(idA) - taskContext.Graph().AddVertex(idInvalid) - taskContext.Graph().AddEdge(idA, idInvalid) - taskContext.InventoryManager().AddPendingApply(idA) - taskContext.AddInvalidObject(idInvalid) - }, - id: idA, - expectedError: testutil.EqualError( - NewFatalError(fmt.Errorf("invalid dependency: %s", idInvalid)), - ), - }, - "apply A (A -> B) before B is applied": { - actuationStrategy: actuation.ActuationStrategyApply, - contextSetup: func(taskContext *taskrunner.TaskContext) { - taskContext.Graph().AddVertex(idA) - taskContext.Graph().AddVertex(idB) - taskContext.Graph().AddEdge(idA, idB) - taskContext.InventoryManager().AddPendingApply(idA) - taskContext.InventoryManager().AddPendingApply(idB) - }, - id: idA, - expectedError: testutil.EqualError( - NewFatalError(fmt.Errorf("premature apply: dependency apply actuation pending: %s", idB)), - ), - }, - "apply A (A -> B) before B is reconciled": { - actuationStrategy: actuation.ActuationStrategyApply, - contextSetup: func(taskContext *taskrunner.TaskContext) { - taskContext.Graph().AddVertex(idA) - taskContext.Graph().AddVertex(idB) - taskContext.Graph().AddEdge(idA, idB) - taskContext.InventoryManager().AddPendingApply(idA) - taskContext.InventoryManager().SetObjectStatus(actuation.ObjectStatus{ - ObjectReference: inventory.ObjectReferenceFromObjMetadata(idB), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationSucceeded, - Reconcile: actuation.ReconcilePending, - }) - }, - id: idA, - expectedError: testutil.EqualError( - NewFatalError(fmt.Errorf("premature apply: dependency apply reconcile pending: %s", idB)), - ), - }, - "apply A (A -> B) after B is reconciled": { - actuationStrategy: actuation.ActuationStrategyApply, - contextSetup: func(taskContext *taskrunner.TaskContext) { - taskContext.Graph().AddVertex(idA) - taskContext.Graph().AddVertex(idB) - taskContext.Graph().AddEdge(idA, idB) - taskContext.InventoryManager().AddPendingApply(idA) - taskContext.InventoryManager().SetObjectStatus(actuation.ObjectStatus{ - ObjectReference: inventory.ObjectReferenceFromObjMetadata(idB), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationSucceeded, - Reconcile: actuation.ReconcileSucceeded, - }) - }, - id: idA, - expectedError: nil, - }, - "apply A (A -> B) after B apply failed": { - actuationStrategy: actuation.ActuationStrategyApply, - contextSetup: func(taskContext *taskrunner.TaskContext) { - taskContext.Graph().AddVertex(idA) - taskContext.Graph().AddVertex(idB) - taskContext.Graph().AddEdge(idA, idB) - taskContext.InventoryManager().AddPendingApply(idA) - taskContext.InventoryManager().SetObjectStatus(actuation.ObjectStatus{ - ObjectReference: inventory.ObjectReferenceFromObjMetadata(idB), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationFailed, - Reconcile: actuation.ReconcilePending, - }) - }, - id: idA, - expectedError: &DependencyPreventedActuationError{ - Object: idA, - Strategy: actuation.ActuationStrategyApply, - Relationship: RelationshipDependency, - Relation: idB, - RelationPhase: PhaseActuation, - RelationActuationStatus: actuation.ActuationFailed, - RelationReconcileStatus: actuation.ReconcilePending, - }, - }, - "apply A (A -> B) after B apply skipped": { - actuationStrategy: actuation.ActuationStrategyApply, - contextSetup: func(taskContext *taskrunner.TaskContext) { - taskContext.Graph().AddVertex(idA) - taskContext.Graph().AddVertex(idB) - taskContext.Graph().AddEdge(idA, idB) - taskContext.InventoryManager().AddPendingApply(idA) - taskContext.InventoryManager().SetObjectStatus(actuation.ObjectStatus{ - ObjectReference: inventory.ObjectReferenceFromObjMetadata(idB), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationSkipped, - Reconcile: actuation.ReconcileSkipped, - }) - }, - id: idA, - expectedError: &DependencyPreventedActuationError{ - Object: idA, - Strategy: actuation.ActuationStrategyApply, - Relationship: RelationshipDependency, - Relation: idB, - RelationPhase: PhaseActuation, - RelationActuationStatus: actuation.ActuationSkipped, - RelationReconcileStatus: actuation.ReconcileSkipped, - }, - }, - "apply A (A -> B) after B reconcile failed": { - actuationStrategy: actuation.ActuationStrategyApply, - contextSetup: func(taskContext *taskrunner.TaskContext) { - taskContext.Graph().AddVertex(idA) - taskContext.Graph().AddVertex(idB) - taskContext.Graph().AddEdge(idA, idB) - taskContext.InventoryManager().AddPendingApply(idA) - taskContext.InventoryManager().SetObjectStatus(actuation.ObjectStatus{ - ObjectReference: inventory.ObjectReferenceFromObjMetadata(idB), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationSucceeded, - Reconcile: actuation.ReconcileFailed, - }) - }, - id: idA, - expectedError: &DependencyPreventedActuationError{ - Object: idA, - Strategy: actuation.ActuationStrategyApply, - Relationship: RelationshipDependency, - Relation: idB, - RelationPhase: PhaseReconcile, - RelationActuationStatus: actuation.ActuationSucceeded, - RelationReconcileStatus: actuation.ReconcileFailed, - }, - }, - "apply A (A -> B) after B reconcile timeout": { - actuationStrategy: actuation.ActuationStrategyApply, - contextSetup: func(taskContext *taskrunner.TaskContext) { - taskContext.Graph().AddVertex(idA) - taskContext.Graph().AddVertex(idB) - taskContext.Graph().AddEdge(idA, idB) - taskContext.InventoryManager().AddPendingApply(idA) - taskContext.InventoryManager().SetObjectStatus(actuation.ObjectStatus{ - ObjectReference: inventory.ObjectReferenceFromObjMetadata(idB), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationSucceeded, - Reconcile: actuation.ReconcileTimeout, - }) - }, - id: idA, - expectedError: &DependencyPreventedActuationError{ - Object: idA, - Strategy: actuation.ActuationStrategyApply, - Relationship: RelationshipDependency, - Relation: idB, - RelationPhase: PhaseReconcile, - RelationActuationStatus: actuation.ActuationSucceeded, - RelationReconcileStatus: actuation.ReconcileTimeout, - }, - }, - // artificial use case: reconcile should only be skipped if apply failed or was skipped - "apply A (A -> B) after B reconcile skipped": { - actuationStrategy: actuation.ActuationStrategyApply, - contextSetup: func(taskContext *taskrunner.TaskContext) { - taskContext.Graph().AddVertex(idA) - taskContext.Graph().AddVertex(idB) - taskContext.Graph().AddEdge(idA, idB) - taskContext.InventoryManager().AddPendingApply(idA) - taskContext.InventoryManager().SetObjectStatus(actuation.ObjectStatus{ - ObjectReference: inventory.ObjectReferenceFromObjMetadata(idB), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationSucceeded, - Reconcile: actuation.ReconcileSkipped, - }) - }, - id: idA, - expectedError: &DependencyPreventedActuationError{ - Object: idA, - Strategy: actuation.ActuationStrategyApply, - Relationship: RelationshipDependency, - Relation: idB, - RelationPhase: PhaseReconcile, - RelationActuationStatus: actuation.ActuationSucceeded, - RelationReconcileStatus: actuation.ReconcileSkipped, - }, - }, - "apply A (A -> B) when B delete pending": { - actuationStrategy: actuation.ActuationStrategyApply, - contextSetup: func(taskContext *taskrunner.TaskContext) { - taskContext.Graph().AddVertex(idA) - taskContext.Graph().AddVertex(idB) - taskContext.Graph().AddEdge(idA, idB) - taskContext.InventoryManager().AddPendingApply(idA) - taskContext.InventoryManager().AddPendingDelete(idB) - }, - id: idA, - expectedError: &DependencyActuationMismatchError{ - Object: idA, - Strategy: actuation.ActuationStrategyApply, - Relationship: RelationshipDependency, - Relation: idB, - RelationStrategy: actuation.ActuationStrategyDelete, - }, - }, - "delete B (no deps)": { - actuationStrategy: actuation.ActuationStrategyDelete, - contextSetup: func(taskContext *taskrunner.TaskContext) { - taskContext.Graph().AddVertex(idB) - taskContext.InventoryManager().AddPendingDelete(idB) - }, - id: idB, - expectedError: nil, - }, - "delete B (A -> B) when A is invalid": { - actuationStrategy: actuation.ActuationStrategyDelete, - contextSetup: func(taskContext *taskrunner.TaskContext) { - taskContext.Graph().AddVertex(idInvalid) - taskContext.Graph().AddVertex(idB) - taskContext.Graph().AddEdge(idInvalid, idB) - taskContext.InventoryManager().AddPendingDelete(idB) - taskContext.AddInvalidObject(idInvalid) - }, - id: idB, - expectedError: testutil.EqualError( - NewFatalError(fmt.Errorf("invalid dependent: %s", idInvalid)), - ), - }, - "delete B (A -> B) before A is deleted": { - actuationStrategy: actuation.ActuationStrategyDelete, - contextSetup: func(taskContext *taskrunner.TaskContext) { - taskContext.Graph().AddVertex(idA) - taskContext.Graph().AddVertex(idB) - taskContext.Graph().AddEdge(idA, idB) - taskContext.InventoryManager().AddPendingDelete(idB) - taskContext.InventoryManager().AddPendingDelete(idA) - }, - id: idB, - expectedError: testutil.EqualError( - NewFatalError(fmt.Errorf("premature delete: dependent delete actuation pending: %s", idA)), - ), - }, - "delete B (A -> B) before A is reconciled": { - actuationStrategy: actuation.ActuationStrategyDelete, - contextSetup: func(taskContext *taskrunner.TaskContext) { - taskContext.Graph().AddVertex(idA) - taskContext.Graph().AddVertex(idB) - taskContext.Graph().AddEdge(idA, idB) - taskContext.InventoryManager().AddPendingDelete(idB) - taskContext.InventoryManager().SetObjectStatus(actuation.ObjectStatus{ - ObjectReference: inventory.ObjectReferenceFromObjMetadata(idA), - Strategy: actuation.ActuationStrategyDelete, - Actuation: actuation.ActuationSucceeded, - Reconcile: actuation.ReconcilePending, - }) - }, - id: idB, - expectedError: testutil.EqualError( - NewFatalError(fmt.Errorf("premature delete: dependent delete reconcile pending: %s", idA)), - ), - }, - "delete B (A -> B) after A is reconciled": { - actuationStrategy: actuation.ActuationStrategyDelete, - contextSetup: func(taskContext *taskrunner.TaskContext) { - taskContext.Graph().AddVertex(idA) - taskContext.Graph().AddVertex(idB) - taskContext.Graph().AddEdge(idA, idB) - taskContext.InventoryManager().AddPendingDelete(idB) - taskContext.InventoryManager().SetObjectStatus(actuation.ObjectStatus{ - ObjectReference: inventory.ObjectReferenceFromObjMetadata(idA), - Strategy: actuation.ActuationStrategyDelete, - Actuation: actuation.ActuationSucceeded, - Reconcile: actuation.ReconcileSucceeded, - }) - }, - id: idB, - expectedError: nil, - }, - "delete B (A -> B) after A delete failed": { - actuationStrategy: actuation.ActuationStrategyDelete, - contextSetup: func(taskContext *taskrunner.TaskContext) { - taskContext.Graph().AddVertex(idA) - taskContext.Graph().AddVertex(idB) - taskContext.Graph().AddEdge(idA, idB) - taskContext.InventoryManager().AddPendingDelete(idB) - taskContext.InventoryManager().SetObjectStatus(actuation.ObjectStatus{ - ObjectReference: inventory.ObjectReferenceFromObjMetadata(idA), - Strategy: actuation.ActuationStrategyDelete, - Actuation: actuation.ActuationFailed, - Reconcile: actuation.ReconcilePending, - }) - }, - id: idB, - expectedError: &DependencyPreventedActuationError{ - Object: idB, - Strategy: actuation.ActuationStrategyDelete, - Relationship: RelationshipDependent, - Relation: idA, - RelationPhase: PhaseActuation, - RelationActuationStatus: actuation.ActuationFailed, - RelationReconcileStatus: actuation.ReconcilePending, - }, - }, - "delete B (A -> B) after A delete skipped": { - actuationStrategy: actuation.ActuationStrategyDelete, - contextSetup: func(taskContext *taskrunner.TaskContext) { - taskContext.Graph().AddVertex(idA) - taskContext.Graph().AddVertex(idB) - taskContext.Graph().AddEdge(idA, idB) - taskContext.InventoryManager().AddPendingDelete(idB) - taskContext.InventoryManager().SetObjectStatus(actuation.ObjectStatus{ - ObjectReference: inventory.ObjectReferenceFromObjMetadata(idA), - Strategy: actuation.ActuationStrategyDelete, - Actuation: actuation.ActuationSkipped, - Reconcile: actuation.ReconcileSkipped, - }) - }, - id: idB, - expectedError: &DependencyPreventedActuationError{ - Object: idB, - Strategy: actuation.ActuationStrategyDelete, - Relationship: RelationshipDependent, - Relation: idA, - RelationPhase: PhaseActuation, - RelationActuationStatus: actuation.ActuationSkipped, - RelationReconcileStatus: actuation.ReconcileSkipped, - }, - }, - // artificial use case: delete reconcile can't fail, only timeout - "delete B (A -> B) after A reconcile failed": { - actuationStrategy: actuation.ActuationStrategyDelete, - contextSetup: func(taskContext *taskrunner.TaskContext) { - taskContext.Graph().AddVertex(idA) - taskContext.Graph().AddVertex(idB) - taskContext.Graph().AddEdge(idA, idB) - taskContext.InventoryManager().AddPendingDelete(idB) - taskContext.InventoryManager().SetObjectStatus(actuation.ObjectStatus{ - ObjectReference: inventory.ObjectReferenceFromObjMetadata(idA), - Strategy: actuation.ActuationStrategyDelete, - Actuation: actuation.ActuationSucceeded, - Reconcile: actuation.ReconcileFailed, - }) - }, - id: idB, - expectedError: &DependencyPreventedActuationError{ - Object: idB, - Strategy: actuation.ActuationStrategyDelete, - Relationship: RelationshipDependent, - Relation: idA, - RelationPhase: PhaseReconcile, - RelationActuationStatus: actuation.ActuationSucceeded, - RelationReconcileStatus: actuation.ReconcileFailed, - }, - }, - "delete B (A -> B) after A reconcile timeout": { - actuationStrategy: actuation.ActuationStrategyDelete, - contextSetup: func(taskContext *taskrunner.TaskContext) { - taskContext.Graph().AddVertex(idA) - taskContext.Graph().AddVertex(idB) - taskContext.Graph().AddEdge(idA, idB) - taskContext.InventoryManager().AddPendingDelete(idB) - taskContext.InventoryManager().SetObjectStatus(actuation.ObjectStatus{ - ObjectReference: inventory.ObjectReferenceFromObjMetadata(idA), - Strategy: actuation.ActuationStrategyDelete, - Actuation: actuation.ActuationSucceeded, - Reconcile: actuation.ReconcileTimeout, - }) - }, - id: idB, - expectedError: &DependencyPreventedActuationError{ - Object: idB, - Strategy: actuation.ActuationStrategyDelete, - Relationship: RelationshipDependent, - Relation: idA, - RelationPhase: PhaseReconcile, - RelationActuationStatus: actuation.ActuationSucceeded, - RelationReconcileStatus: actuation.ReconcileTimeout, - }, - }, - // artificial use case: reconcile should only be skipped if delete failed or was skipped - "delete B (A -> B) after A reconcile skipped": { - actuationStrategy: actuation.ActuationStrategyDelete, - contextSetup: func(taskContext *taskrunner.TaskContext) { - taskContext.Graph().AddVertex(idA) - taskContext.Graph().AddVertex(idB) - taskContext.Graph().AddEdge(idA, idB) - taskContext.InventoryManager().AddPendingDelete(idB) - taskContext.InventoryManager().SetObjectStatus(actuation.ObjectStatus{ - ObjectReference: inventory.ObjectReferenceFromObjMetadata(idA), - Strategy: actuation.ActuationStrategyDelete, - Actuation: actuation.ActuationSucceeded, - Reconcile: actuation.ReconcileSkipped, - }) - }, - id: idB, - expectedError: &DependencyPreventedActuationError{ - Object: idB, - Strategy: actuation.ActuationStrategyDelete, - Relationship: RelationshipDependent, - Relation: idA, - RelationPhase: PhaseReconcile, - RelationActuationStatus: actuation.ActuationSucceeded, - RelationReconcileStatus: actuation.ReconcileSkipped, - }, - }, - "delete B (A -> B) when A apply succeeded": { - actuationStrategy: actuation.ActuationStrategyDelete, - contextSetup: func(taskContext *taskrunner.TaskContext) { - taskContext.Graph().AddVertex(idA) - taskContext.Graph().AddVertex(idB) - taskContext.Graph().AddEdge(idA, idB) - taskContext.InventoryManager().AddPendingDelete(idB) - taskContext.InventoryManager().SetObjectStatus(actuation.ObjectStatus{ - ObjectReference: inventory.ObjectReferenceFromObjMetadata(idA), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationSucceeded, - Reconcile: actuation.ReconcileSucceeded, - }) - }, - id: idB, - expectedError: &DependencyActuationMismatchError{ - Object: idB, - Strategy: actuation.ActuationStrategyDelete, - Relationship: RelationshipDependent, - Relation: idA, - RelationStrategy: actuation.ActuationStrategyApply, - }, - }, - "DryRun: apply A (A -> B) when B apply reconcile pending": { - dryRunStrategy: common.DryRunClient, - actuationStrategy: actuation.ActuationStrategyApply, - contextSetup: func(taskContext *taskrunner.TaskContext) { - taskContext.Graph().AddVertex(idA) - taskContext.Graph().AddVertex(idB) - taskContext.Graph().AddEdge(idA, idB) - taskContext.InventoryManager().AddPendingApply(idA) - taskContext.InventoryManager().SetObjectStatus(actuation.ObjectStatus{ - ObjectReference: inventory.ObjectReferenceFromObjMetadata(idB), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationSucceeded, - Reconcile: actuation.ReconcilePending, - }) - }, - id: idA, - expectedError: nil, - }, - "DryRun: delete B (A -> B) when A delete reconcile pending": { - dryRunStrategy: common.DryRunClient, - actuationStrategy: actuation.ActuationStrategyDelete, - contextSetup: func(taskContext *taskrunner.TaskContext) { - taskContext.Graph().AddVertex(idA) - taskContext.Graph().AddVertex(idB) - taskContext.Graph().AddEdge(idA, idB) - taskContext.InventoryManager().AddPendingDelete(idB) - taskContext.InventoryManager().SetObjectStatus(actuation.ObjectStatus{ - ObjectReference: inventory.ObjectReferenceFromObjMetadata(idA), - Strategy: actuation.ActuationStrategyDelete, - Actuation: actuation.ActuationSucceeded, - Reconcile: actuation.ReconcilePending, - }) - }, - id: idB, - expectedError: nil, - }, - } - - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - taskContext := taskrunner.NewTaskContext(nil, nil) - tc.contextSetup(taskContext) - - filter := DependencyFilter{ - TaskContext: taskContext, - ActuationStrategy: tc.actuationStrategy, - DryRunStrategy: tc.dryRunStrategy, - } - obj := defaultObj.DeepCopy() - obj.SetGroupVersionKind(tc.id.GroupKind.WithVersion("v1")) - obj.SetName(tc.id.Name) - obj.SetNamespace(tc.id.Namespace) - - err := filter.Filter(obj) - testutil.AssertEqual(t, tc.expectedError, err) - }) - } -} diff --git a/pkg/apply/filter/fatal_error.go b/pkg/apply/filter/fatal_error.go deleted file mode 100644 index 146cab83..00000000 --- a/pkg/apply/filter/fatal_error.go +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package filter - -// FatalError is a wrapper for filters to indicate an error is unrecoverable, -// not just a reason to skip actuation. -type FatalError struct { - Err error -} - -func NewFatalError(err error) *FatalError { - return &FatalError{Err: err} -} - -func (e *FatalError) Error() string { - return e.Err.Error() -} - -func (e *FatalError) Is(err error) bool { - if err == nil { - return false - } - tErr, ok := err.(*FatalError) - if !ok { - return false - } - return e.Err == tErr.Err -} diff --git a/pkg/apply/filter/filter.go b/pkg/apply/filter/filter.go deleted file mode 100644 index 7e1f4cb1..00000000 --- a/pkg/apply/filter/filter.go +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package filter - -import ( - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -// ValidationFilter interface decouples apply/prune validation -// from the concrete structs used for validation. The apply/prune -// functionality will run validation filters to remove objects -// which should not be applied or pruned. -type ValidationFilter interface { - // Name returns a filter name (usually for logging). - Name() string - // Filter returns an error if validation fails, indicating that actuation - // should be skipped for this object. - Filter(obj *unstructured.Unstructured) error -} diff --git a/pkg/apply/filter/inventory-policy-apply-filter.go b/pkg/apply/filter/inventory-policy-apply-filter.go deleted file mode 100644 index 6804937c..00000000 --- a/pkg/apply/filter/inventory-policy-apply-filter.go +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package filter - -import ( - "context" - "fmt" - - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/pkg/object" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/client-go/dynamic" -) - -// InventoryPolicyApplyFilter implements ValidationFilter interface to determine -// if an object should be applied based on the cluster object's inventory id, -// the id for the inventory object, and the inventory policy. -type InventoryPolicyApplyFilter struct { - Client dynamic.Interface - Mapper meta.RESTMapper - Inv inventory.Info - InvPolicy inventory.Policy -} - -// Name returns a filter identifier for logging. -func (ipaf InventoryPolicyApplyFilter) Name() string { - return "InventoryPolicyApplyFilter" -} - -// Filter returns an inventory.PolicyPreventedActuationError if the object -// apply should be skipped. -func (ipaf InventoryPolicyApplyFilter) Filter(obj *unstructured.Unstructured) error { - // optimization to avoid unnecessary API calls - if ipaf.InvPolicy == inventory.PolicyAdoptAll { - return nil - } - // Object must be retrieved from the cluster to get the inventory id. - clusterObj, err := ipaf.getObject(object.UnstructuredToObjMetadata(obj)) - if err != nil { - if apierrors.IsNotFound(err) { - // This simply means the object hasn't been created yet. - return nil - } - return NewFatalError(fmt.Errorf("failed to get current object from cluster: %w", err)) - } - _, err = inventory.CanApply(ipaf.Inv, clusterObj, ipaf.InvPolicy) - if err != nil { - return err - } - return nil -} - -// getObject retrieves the passed object from the cluster, or an error if one occurred. -func (ipaf InventoryPolicyApplyFilter) getObject(id object.ObjMetadata) (*unstructured.Unstructured, error) { - mapping, err := ipaf.Mapper.RESTMapping(id.GroupKind) - if err != nil { - return nil, err - } - namespacedClient, err := ipaf.Client.Resource(mapping.Resource).Namespace(id.Namespace), nil - if err != nil { - return nil, err - } - return namespacedClient.Get(context.TODO(), id.Name, metav1.GetOptions{}) -} diff --git a/pkg/apply/filter/inventory-policy-apply-filter_test.go b/pkg/apply/filter/inventory-policy-apply-filter_test.go deleted file mode 100644 index 0a26e189..00000000 --- a/pkg/apply/filter/inventory-policy-apply-filter_test.go +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package filter - -import ( - "testing" - - "github.com/fluxcd/cli-utils/pkg/apis/actuation" - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/pkg/testutil" - "k8s.io/apimachinery/pkg/api/meta/testrestmapper" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - dynamicfake "k8s.io/client-go/dynamic/fake" - "k8s.io/kubectl/pkg/scheme" -) - -var invObjTemplate = &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "v1", - "kind": "ConfigMap", - "metadata": map[string]interface{}{ - "name": "inventory-name", - "namespace": "inventory-namespace", - }, - }, -} - -func TestInventoryPolicyApplyFilter(t *testing.T) { - tests := map[string]struct { - inventoryID string - objInventoryID string - policy inventory.Policy - expectedError error - }{ - "inventory and object ids match, not filtered": { - inventoryID: "foo", - objInventoryID: "foo", - policy: inventory.PolicyMustMatch, - }, - "inventory and object ids match and adopt, not filtered": { - inventoryID: "foo", - objInventoryID: "foo", - policy: inventory.PolicyAdoptIfNoInventory, - }, - "inventory and object ids do no match and policy must match, filtered and error": { - inventoryID: "foo", - objInventoryID: "bar", - policy: inventory.PolicyMustMatch, - expectedError: &inventory.PolicyPreventedActuationError{ - Strategy: actuation.ActuationStrategyApply, - Policy: inventory.PolicyMustMatch, - Status: inventory.NoMatch, - }, - }, - "inventory and object ids do no match and adopt if no inventory, filtered and error": { - inventoryID: "foo", - objInventoryID: "bar", - policy: inventory.PolicyAdoptIfNoInventory, - expectedError: &inventory.PolicyPreventedActuationError{ - Strategy: actuation.ActuationStrategyApply, - Policy: inventory.PolicyAdoptIfNoInventory, - Status: inventory.NoMatch, - }, - }, - "inventory and object ids do no match and adopt all, not filtered": { - inventoryID: "foo", - objInventoryID: "bar", - policy: inventory.PolicyAdoptAll, - }, - "object id empty and adopt all, not filtered": { - inventoryID: "foo", - objInventoryID: "", - policy: inventory.PolicyAdoptAll, - }, - "object id empty and policy must match, filtered and error": { - inventoryID: "foo", - objInventoryID: "", - policy: inventory.PolicyMustMatch, - expectedError: &inventory.PolicyPreventedActuationError{ - Strategy: actuation.ActuationStrategyApply, - Policy: inventory.PolicyMustMatch, - Status: inventory.NoMatch, - }, - }, - } - - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - obj := defaultObj.DeepCopy() - objIDAnnotation := map[string]string{ - "config.k8s.io/owning-inventory": tc.objInventoryID, - } - obj.SetAnnotations(objIDAnnotation) - invIDLabel := map[string]string{ - common.InventoryLabel: tc.inventoryID, - } - invObj := invObjTemplate.DeepCopy() - invObj.SetLabels(invIDLabel) - filter := InventoryPolicyApplyFilter{ - Client: dynamicfake.NewSimpleDynamicClient(scheme.Scheme, obj), - Mapper: testrestmapper.TestOnlyStaticRESTMapper(scheme.Scheme, - scheme.Scheme.PrioritizedVersionsAllGroups()...), - Inv: inventory.WrapInventoryInfoObj(invObj), - InvPolicy: tc.policy, - } - err := filter.Filter(obj) - testutil.AssertEqual(t, tc.expectedError, err) - }) - } -} diff --git a/pkg/apply/filter/inventory-policy-prune-filter.go b/pkg/apply/filter/inventory-policy-prune-filter.go deleted file mode 100644 index 3c5f1e24..00000000 --- a/pkg/apply/filter/inventory-policy-prune-filter.go +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package filter - -import ( - "github.com/fluxcd/cli-utils/pkg/inventory" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -// InventoryPolicyPruneFilter implements ValidationFilter interface to determine -// if an object should be pruned (deleted) because of the InventoryPolicy -// and if the objects owning inventory identifier matchs the inventory id. -type InventoryPolicyPruneFilter struct { - Inv inventory.Info - InvPolicy inventory.Policy -} - -// Name returns a filter identifier for logging. -func (ipf InventoryPolicyPruneFilter) Name() string { - return "InventoryPolicyFilter" -} - -// Filter returns an inventory.PolicyPreventedActuationError if the object -// prune/delete should be skipped. -func (ipf InventoryPolicyPruneFilter) Filter(obj *unstructured.Unstructured) error { - _, err := inventory.CanPrune(ipf.Inv, obj, ipf.InvPolicy) - if err != nil { - return err - } - return nil -} diff --git a/pkg/apply/filter/inventory-policy-prune-filter_test.go b/pkg/apply/filter/inventory-policy-prune-filter_test.go deleted file mode 100644 index 47d04749..00000000 --- a/pkg/apply/filter/inventory-policy-prune-filter_test.go +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright 2019 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package filter - -import ( - "testing" - - "github.com/fluxcd/cli-utils/pkg/apis/actuation" - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/pkg/testutil" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -var inventoryObj = &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "v1", - "kind": "ConfigMap", - "metadata": map[string]interface{}{ - "name": "inventory-name", - "namespace": "inventory-namespace", - }, - }, -} - -func TestInventoryPolicyPruneFilter(t *testing.T) { - tests := map[string]struct { - inventoryID string - objInventoryID string - policy inventory.Policy - expectedError error - }{ - "inventory and object ids match, not filtered": { - inventoryID: "foo", - objInventoryID: "foo", - policy: inventory.PolicyMustMatch, - }, - "inventory and object ids match and adopt, not filtered": { - inventoryID: "foo", - objInventoryID: "foo", - policy: inventory.PolicyAdoptIfNoInventory, - }, - "inventory and object ids do no match and policy must match, filtered": { - inventoryID: "foo", - objInventoryID: "bar", - policy: inventory.PolicyMustMatch, - expectedError: &inventory.PolicyPreventedActuationError{ - Strategy: actuation.ActuationStrategyDelete, - Policy: inventory.PolicyMustMatch, - Status: inventory.NoMatch, - }, - }, - "inventory and object ids do no match and adopt if no inventory, filtered": { - inventoryID: "foo", - objInventoryID: "bar", - policy: inventory.PolicyAdoptIfNoInventory, - expectedError: &inventory.PolicyPreventedActuationError{ - Strategy: actuation.ActuationStrategyDelete, - Policy: inventory.PolicyAdoptIfNoInventory, - Status: inventory.NoMatch, - }, - }, - "inventory and object ids do no match and adopt all, not filtered": { - inventoryID: "foo", - objInventoryID: "bar", - policy: inventory.PolicyAdoptAll, - }, - "object id empty and adopt all, not filtered": { - inventoryID: "foo", - objInventoryID: "", - policy: inventory.PolicyAdoptAll, - }, - "object id empty and policy must match, filtered": { - inventoryID: "foo", - objInventoryID: "", - policy: inventory.PolicyMustMatch, - expectedError: &inventory.PolicyPreventedActuationError{ - Strategy: actuation.ActuationStrategyDelete, - Policy: inventory.PolicyMustMatch, - Status: inventory.NoMatch, - }, - }, - } - - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - invIDLabel := map[string]string{ - common.InventoryLabel: tc.inventoryID, - } - invObj := inventoryObj.DeepCopy() - invObj.SetLabels(invIDLabel) - filter := InventoryPolicyPruneFilter{ - Inv: inventory.WrapInventoryInfoObj(invObj), - InvPolicy: tc.policy, - } - objIDAnnotation := map[string]string{ - "config.k8s.io/owning-inventory": tc.objInventoryID, - } - obj := defaultObj.DeepCopy() - obj.SetAnnotations(objIDAnnotation) - err := filter.Filter(obj) - testutil.AssertEqual(t, tc.expectedError, err) - }) - } -} diff --git a/pkg/apply/filter/local-namespaces-filter.go b/pkg/apply/filter/local-namespaces-filter.go deleted file mode 100644 index d57989bc..00000000 --- a/pkg/apply/filter/local-namespaces-filter.go +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package filter - -import ( - "fmt" - - "github.com/fluxcd/cli-utils/pkg/object" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/util/sets" -) - -var ( - namespaceGK = schema.GroupKind{Group: "", Kind: "Namespace"} -) - -// LocalNamespacesFilter encapsulates the set of namespaces -// that are currently in use. Used to ensure we do not delete -// namespaces with currently applied objects in them. -type LocalNamespacesFilter struct { - LocalNamespaces sets.String // nolint:staticcheck -} - -// Name returns a filter identifier for logging. -func (lnf LocalNamespacesFilter) Name() string { - return "LocalNamespacesFilter" -} - -// Filter returns a NamespaceInUseError if the object prune/delete should be -// skipped. -func (lnf LocalNamespacesFilter) Filter(obj *unstructured.Unstructured) error { - id := object.UnstructuredToObjMetadata(obj) - if id.GroupKind == namespaceGK && - lnf.LocalNamespaces.Has(id.Name) { - return &NamespaceInUseError{ - Namespace: id.Name, - } - } - return nil -} - -type NamespaceInUseError struct { - Namespace string -} - -func (e *NamespaceInUseError) Error() string { - return fmt.Sprintf("namespace still in use: %s", e.Namespace) -} - -func (e *NamespaceInUseError) Is(err error) bool { - if err == nil { - return false - } - tErr, ok := err.(*NamespaceInUseError) - if !ok { - return false - } - return e.Namespace == tErr.Namespace -} diff --git a/pkg/apply/filter/local-namespaces-filter_test.go b/pkg/apply/filter/local-namespaces-filter_test.go deleted file mode 100644 index 4b850cdd..00000000 --- a/pkg/apply/filter/local-namespaces-filter_test.go +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2019 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package filter - -import ( - "testing" - - "github.com/fluxcd/cli-utils/pkg/testutil" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/util/sets" -) - -var testNamespace = &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "v1", - "kind": "Namespace", - "metadata": map[string]interface{}{ - "name": "test-namespace", - }, - }, -} - -func TestLocalNamespacesFilter(t *testing.T) { - tests := map[string]struct { - localNamespaces sets.String // nolint:staticcheck - namespace string - expectedError error - }{ - "No local namespaces, namespace is not filtered": { - localNamespaces: sets.NewString(), - namespace: "test-namespace", - }, - "Namespace not in local namespaces, namespace is not filtered": { - localNamespaces: sets.NewString("foo", "bar"), - namespace: "test-namespace", - }, - "Namespace is in local namespaces, namespace is filtered": { - localNamespaces: sets.NewString("foo", "test-namespace", "bar"), - namespace: "test-namespace", - expectedError: &NamespaceInUseError{ - Namespace: "test-namespace", - }, - }, - } - - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - filter := LocalNamespacesFilter{ - LocalNamespaces: tc.localNamespaces, - } - obj := testNamespace.DeepCopy() - obj.SetName(tc.namespace) - err := filter.Filter(obj) - testutil.AssertEqual(t, tc.expectedError, err) - }) - } -} diff --git a/pkg/apply/filter/phase_string.go b/pkg/apply/filter/phase_string.go deleted file mode 100644 index dd8d5685..00000000 --- a/pkg/apply/filter/phase_string.go +++ /dev/null @@ -1,24 +0,0 @@ -// Code generated by "stringer -type=Phase -linecomment"; DO NOT EDIT. - -package filter - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[PhaseActuation-0] - _ = x[PhaseReconcile-1] -} - -const _Phase_name = "ActuationReconcile" - -var _Phase_index = [...]uint8{0, 9, 18} - -func (i Phase) String() string { - if i < 0 || i >= Phase(len(_Phase_index)-1) { - return "Phase(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _Phase_name[_Phase_index[i]:_Phase_index[i+1]] -} diff --git a/pkg/apply/filter/prevent-remove-filter.go b/pkg/apply/filter/prevent-remove-filter.go deleted file mode 100644 index 1811babb..00000000 --- a/pkg/apply/filter/prevent-remove-filter.go +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package filter - -import ( - "fmt" - - "github.com/fluxcd/cli-utils/pkg/common" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -// PreventRemoveFilter implements ValidationFilter interface to determine -// if an object should not be pruned (deleted) because of a -// "prevent remove" annotation. -type PreventRemoveFilter struct{} - -const PreventRemoveFilterName = "PreventRemoveFilter" - -// Name returns the preferred name for the filter. Usually -// used for logging. -func (prf PreventRemoveFilter) Name() string { - return PreventRemoveFilterName -} - -// Filter returns a AnnotationPreventedDeletionError if the object prune/delete -// should be skipped. -func (prf PreventRemoveFilter) Filter(obj *unstructured.Unstructured) error { - for annotation, value := range obj.GetAnnotations() { - if common.NoDeletion(annotation, value) { - return &AnnotationPreventedDeletionError{ - Annotation: annotation, - Value: value, - } - } - } - return nil -} - -type AnnotationPreventedDeletionError struct { - Annotation string - Value string -} - -func (e *AnnotationPreventedDeletionError) Error() string { - return fmt.Sprintf("annotation prevents deletion (%q: %q)", e.Annotation, e.Value) -} - -func (e *AnnotationPreventedDeletionError) Is(err error) bool { - if err == nil { - return false - } - tErr, ok := err.(*AnnotationPreventedDeletionError) - if !ok { - return false - } - return e.Annotation == tErr.Annotation && - e.Value == tErr.Value -} diff --git a/pkg/apply/filter/prevent-remove-filter_test.go b/pkg/apply/filter/prevent-remove-filter_test.go deleted file mode 100644 index fa3e920f..00000000 --- a/pkg/apply/filter/prevent-remove-filter_test.go +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright 2019 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package filter - -import ( - "testing" - - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/testutil" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -var defaultObj = &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "v1", - "kind": "Pod", - "metadata": map[string]interface{}{ - "name": "pod-name", - "namespace": "test-namespace", - }, - }, -} - -func TestPreventDeleteAnnotation(t *testing.T) { - tests := map[string]struct { - annotations map[string]string - expectedError error - }{ - "Nil map returns false": { - annotations: nil, - }, - "Empty map returns false": { - annotations: map[string]string{}, - }, - "Wrong annotation key/value is false": { - annotations: map[string]string{ - "foo": "bar", - }, - }, - "Annotation key without value is false": { - annotations: map[string]string{ - common.OnRemoveAnnotation: "bar", - }, - }, - "Annotation key and value is true": { - annotations: map[string]string{ - common.OnRemoveAnnotation: common.OnRemoveKeep, - }, - expectedError: &AnnotationPreventedDeletionError{ - Annotation: common.OnRemoveAnnotation, - Value: common.OnRemoveKeep, - }, - }, - "Annotation key client.lifecycle.config.k8s.io/deletion without value is false": { - annotations: map[string]string{ - common.LifecycleDeleteAnnotation: "any", - }, - }, - "Annotation key client.lifecycle.config.k8s.io/deletion and value is true": { - annotations: map[string]string{ - common.LifecycleDeleteAnnotation: common.PreventDeletion, - }, - expectedError: &AnnotationPreventedDeletionError{ - Annotation: common.LifecycleDeleteAnnotation, - Value: common.PreventDeletion, - }, - }, - } - - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - filter := PreventRemoveFilter{} - obj := defaultObj.DeepCopy() - obj.SetAnnotations(tc.annotations) - err := filter.Filter(obj) - testutil.AssertEqual(t, tc.expectedError, err) - }) - } -} diff --git a/pkg/apply/filter/relationship_string.go b/pkg/apply/filter/relationship_string.go deleted file mode 100644 index 0d66ff30..00000000 --- a/pkg/apply/filter/relationship_string.go +++ /dev/null @@ -1,24 +0,0 @@ -// Code generated by "stringer -type=Relationship -linecomment"; DO NOT EDIT. - -package filter - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[RelationshipDependent-0] - _ = x[RelationshipDependency-1] -} - -const _Relationship_name = "DependentDependency" - -var _Relationship_index = [...]uint8{0, 9, 19} - -func (i Relationship) String() string { - if i < 0 || i >= Relationship(len(_Relationship_index)-1) { - return "Relationship(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _Relationship_name[_Relationship_index[i]:_Relationship_index[i+1]] -} diff --git a/pkg/apply/info/helper.go b/pkg/apply/info/helper.go deleted file mode 100644 index aa212e94..00000000 --- a/pkg/apply/info/helper.go +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package info - -import ( - "github.com/fluxcd/cli-utils/pkg/object" - "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/cli-runtime/pkg/resource" -) - -// Helper provides functions for interacting with Info objects. -type Helper interface { - // UpdateInfo sets the mapping and client for the provided Info - // object. This must be called at a time when all needed resource - // types are available in the RESTMapper. - UpdateInfo(info *resource.Info) error - - BuildInfo(obj *unstructured.Unstructured) (*resource.Info, error) -} - -func NewHelper(mapper meta.RESTMapper, unstructuredClientForMapping func(*meta.RESTMapping) (resource.RESTClient, error)) Helper { - return &helper{ - mapper: mapper, - unstructuredClientForMapping: unstructuredClientForMapping, - } -} - -type helper struct { - mapper meta.RESTMapper - unstructuredClientForMapping func(*meta.RESTMapping) (resource.RESTClient, error) -} - -func (ih *helper) UpdateInfo(info *resource.Info) error { - gvk := info.Object.GetObjectKind().GroupVersionKind() - mapping, err := ih.mapper.RESTMapping(gvk.GroupKind(), gvk.Version) - if err != nil { - return err - } - info.Mapping = mapping - - c, err := ih.unstructuredClientForMapping(mapping) - if err != nil { - return err - } - info.Client = c - return nil -} - -func (ih *helper) BuildInfo(obj *unstructured.Unstructured) (*resource.Info, error) { - info, err := object.UnstructuredToInfo(obj) - if err != nil { - return nil, err - } - err = ih.UpdateInfo(info) - return info, err -} diff --git a/pkg/apply/main_test.go b/pkg/apply/main_test.go deleted file mode 100644 index 260936aa..00000000 --- a/pkg/apply/main_test.go +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package apply - -import ( - "os" - "testing" - - "k8s.io/klog/v2" -) - -// TestMain executes the tests for this package, with optional logging. -// To see all logs, use: -// go test github.com/fluxcd/cli-utils/pkg/apply -v -args -v=5 -func TestMain(m *testing.M) { - klog.InitFlags(nil) - os.Exit(m.Run()) -} diff --git a/pkg/apply/mutator/apply_time_mutator.go b/pkg/apply/mutator/apply_time_mutator.go deleted file mode 100644 index f85fff39..00000000 --- a/pkg/apply/mutator/apply_time_mutator.go +++ /dev/null @@ -1,295 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package mutator - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "strings" - - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/client-go/dynamic" - "k8s.io/klog/v2" - - "github.com/fluxcd/cli-utils/pkg/apply/cache" - "github.com/fluxcd/cli-utils/pkg/jsonpath" - "github.com/fluxcd/cli-utils/pkg/kstatus/status" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/object/mutation" -) - -// ApplyTimeMutator mutates an object by injecting values specified by the -// apply-time-mutation annotation. -// The optional ResourceCache will be used to speed up source object lookups, -// if specified. -// Implements the Mutator interface -type ApplyTimeMutator struct { - Client dynamic.Interface - Mapper meta.RESTMapper - ResourceCache cache.ResourceCache -} - -// Name returns a mutator identifier for logging. -func (atm *ApplyTimeMutator) Name() string { - return "ApplyTimeMutator" -} - -// Mutate parses the apply-time-mutation annotation and loops through the -// substitutions, applying each of them to the supplied target object. -// Returns true with a reason, if mutation was performed. -func (atm *ApplyTimeMutator) Mutate(ctx context.Context, obj *unstructured.Unstructured) (bool, string, error) { - mutated := false - reason := "" - - targetRef := mutation.ResourceReferenceFromUnstructured(obj) - - if !mutation.HasAnnotation(obj) { - return mutated, reason, nil - } - - subs, err := mutation.ReadAnnotation(obj) - if err != nil { - return mutated, reason, fmt.Errorf("failed to read annotation in object (%s): %w", targetRef, err) - } - - klog.V(4).Infof("target object: %s", targetRef) - klog.V(7).Infof("target object YAML:\n%s", object.YamlStringer{O: obj}) - - // validate no self-references - // Early validation to avoid GETs, but won't catch sources with implicit namespace. - for _, sub := range subs { - if targetRef.Equal(sub.SourceRef) { - return mutated, reason, fmt.Errorf("invalid self-reference (%s)", sub.SourceRef) - } - } - - for _, sub := range subs { - sourceRef := sub.SourceRef - - // lookup REST mapping - sourceMapping, err := atm.getMapping(sourceRef) - if err != nil { - return mutated, reason, fmt.Errorf("failed to identify source object mapping (%s): %w", sourceRef, err) - } - - // Default source namespace to target namesapce, if namespace-scoped - if sourceRef.Namespace == "" && sourceMapping.Scope.Name() == meta.RESTScopeNameNamespace { - sourceRef.Namespace = targetRef.Namespace - } - - // validate no self-references - // Re-check to catch sources with implicit namespace. - if targetRef.Equal(sub.SourceRef) { - return mutated, reason, fmt.Errorf("invalid self-reference (%s)", sub.SourceRef) - } - - // lookup source object from cache or cluster - sourceObj, err := atm.getObject(ctx, sourceMapping, sourceRef) - if err != nil { - return mutated, reason, fmt.Errorf("failed to get source object (%s): %w", sourceRef, err) - } - - klog.V(4).Infof("source object: %s", sourceRef) - klog.V(7).Infof("source object YAML:\n%s", object.YamlStringer{O: sourceObj}) - - // lookup target field in target object - targetValue, _, err := readFieldValue(obj, sub.TargetPath) - if err != nil { - return mutated, reason, fmt.Errorf("failed to read field (%s) from target object (%s): %w", sub.TargetPath, targetRef, err) - } - - // lookup source field in source object - sourceValue, found, err := readFieldValue(sourceObj, sub.SourcePath) - if err != nil { - return mutated, reason, fmt.Errorf("failed to read field (%s) from source object (%s): %w", sub.SourcePath, sourceRef, err) - } - if !found { - return mutated, reason, fmt.Errorf("source field (%s) not present in source object (%s)", sub.SourcePath, sourceRef) - } - - var newValue interface{} - if sub.Token == "" { - // token not specified, replace the entire target value with the source value - newValue = sourceValue - } else { - // token specified, substitute token for source field value in target field value - targetValueString, ok := targetValue.(string) - if !ok { - return mutated, reason, fmt.Errorf("token is specified, but target field value is %T, expected string", targetValue) - } - - sourceValueString, err := valueToString(sourceValue) - if err != nil { - return mutated, reason, fmt.Errorf("failed to stringify source field value (%s): %w", targetRef, err) - } - - // Substitute token for source field value, if present. - // If not present, do nothing. This is common on updates. - newValue = strings.ReplaceAll(targetValueString, sub.Token, sourceValueString) - } - - klog.V(5).Infof("substitution: targetRef=(%s), sourceRef=(%s): sourceValue=(%v), token=(%s), oldTargetValue=(%v), newTargetValue=(%v)", - targetRef, sourceRef, sourceValue, sub.Token, targetValue, newValue) - - // update target field in target object - err = writeFieldValue(obj, sub.TargetPath, newValue) - if err != nil { - return mutated, reason, fmt.Errorf("failed to set field in target object (%s): %w", targetRef, err) - } - - mutated = true - reason = fmt.Sprintf("object contained annotation: %s", mutation.Annotation) - } - - if mutated { - klog.V(4).Infof("mutated target object: %s", targetRef) - klog.V(7).Infof("mutated target object YAML:\n%s", object.YamlStringer{O: obj}) - } - - return mutated, reason, nil -} - -func (atm *ApplyTimeMutator) getMapping(ref mutation.ResourceReference) (*meta.RESTMapping, error) { - // lookup object using group api version, if specified - sourceGvk := ref.GroupVersionKind() - var mapping *meta.RESTMapping - var err error - if sourceGvk.Version != "" { - mapping, err = atm.Mapper.RESTMapping(sourceGvk.GroupKind(), sourceGvk.Version) - } else { - mapping, err = atm.Mapper.RESTMapping(sourceGvk.GroupKind()) - } - if err != nil { - return nil, err - } - return mapping, nil -} - -// getObject returns a cached object, if cached and cache exists, otherwise -// the object is retrieved from the cluster. -func (atm *ApplyTimeMutator) getObject(ctx context.Context, mapping *meta.RESTMapping, ref mutation.ResourceReference) (*unstructured.Unstructured, error) { - // validate source object - if ref.Name == "" { - return nil, fmt.Errorf("invalid source object: empty name") - } - if ref.Kind == "" { - return nil, fmt.Errorf("invalid source object: empty kind") - } - id := ref.ToObjMetadata() - - // get object from cache - if atm.ResourceCache != nil { - result := atm.ResourceCache.Get(id) - // Use the cached version, if current/reconciled. - // Otherwise, get it from the cluster. - if result.Resource != nil && result.Status == status.CurrentStatus { - return result.Resource, nil - } - } - - // get object from cluster - namespacedClient := atm.Client.Resource(mapping.Resource).Namespace(ref.Namespace) - obj, err := namespacedClient.Get(ctx, ref.Name, metav1.GetOptions{}) - if err != nil && !apierrors.IsNotFound(err) { - // Skip NotFound so the cache gets updated. - return nil, fmt.Errorf("failed to retrieve object from cluster: %w", err) - } - - // add object to cache - if atm.ResourceCache != nil { - // If it's not cached or not current, update the cache. - // This will add external objects to the cache, - // but the user won't get status events for them. - atm.ResourceCache.Put(id, computeStatus(obj)) - } - - if err != nil { - // NotFound - return nil, fmt.Errorf("object not found: %w", err) - } - - return obj, nil -} - -// computeStatus compares the spec to the status and returns the result. -func computeStatus(obj *unstructured.Unstructured) cache.ResourceStatus { - if obj == nil { - return cache.ResourceStatus{ - Resource: obj, - Status: status.NotFoundStatus, - StatusMessage: "Object not found", - } - } - result, err := status.Compute(obj) - if err != nil { - if klog.V(3).Enabled() { - ref := mutation.ResourceReferenceFromUnstructured(obj) - klog.Infof("failed to compute object status (%s): %d", ref, err) - } - return cache.ResourceStatus{ - Resource: obj, - Status: status.UnknownStatus, - //StatusMessage: fmt.Sprintf("Failed to compute status: %s", err), - } - } - return cache.ResourceStatus{ - Resource: obj, - Status: result.Status, - StatusMessage: result.Message, - } -} - -func readFieldValue(obj *unstructured.Unstructured, path string) (interface{}, bool, error) { - if path == "" { - return nil, false, errors.New("empty path expression") - } - - values, err := jsonpath.Get(obj.Object, path) - if err != nil { - return nil, false, err - } - if len(values) != 1 { - return nil, false, fmt.Errorf("expected 1 match, but found %d)", len(values)) - } - return values[0], true, nil -} - -func writeFieldValue(obj *unstructured.Unstructured, path string, value interface{}) error { - if path == "" { - return errors.New("empty path expression") - } - - found, err := jsonpath.Set(obj.Object, path, value) - if err != nil { - return err - } - if found != 1 { - return fmt.Errorf("expected 1 match, but found %d)", found) - } - return nil -} - -// valueToString converts an interface{} to a string, formatting as json for -// maps, lists. Designed to handle yaml/json/krm primitives. -func valueToString(value interface{}) (string, error) { - var valueString string - switch valueTyped := value.(type) { - case string: - valueString = valueTyped - case int, int32, int64, float32, float64, bool: - valueString = fmt.Sprintf("%v", valueTyped) - default: - jsonBytes, err := json.Marshal(valueTyped) - if err != nil { - return "", fmt.Errorf("failed to marshal value to json: %#v", value) - } - valueString = string(jsonBytes) - } - return valueString, nil -} diff --git a/pkg/apply/mutator/apply_time_mutator_test.go b/pkg/apply/mutator/apply_time_mutator_test.go deleted file mode 100644 index 046fbc9e..00000000 --- a/pkg/apply/mutator/apply_time_mutator_test.go +++ /dev/null @@ -1,718 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package mutator - -import ( - "context" - "testing" - - "github.com/fluxcd/cli-utils/pkg/apply/cache" - ktestutil "github.com/fluxcd/cli-utils/pkg/kstatus/polling/testutil" - "github.com/fluxcd/cli-utils/pkg/object" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/meta/testrestmapper" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/dynamic" - "k8s.io/client-go/dynamic/fake" - "k8s.io/kubectl/pkg/scheme" - - // Using gopkg.in/yaml.v3 instead of sigs.k8s.io/yaml on purpose. - // yaml.v3 correctly parses ints: - // https://github.com/kubernetes-sigs/yaml/issues/45 - "gopkg.in/yaml.v3" - - "github.com/stretchr/testify/require" -) - -var expectedReason = "object contained annotation: config.kubernetes.io/apply-time-mutation" - -var pod1y = ` -apiVersion: v1 -kind: Pod -metadata: - name: pod-name - namespace: pod-namespace - annotations: - config.kubernetes.io/apply-time-mutation: | - - sourceRef: - group: networking.k8s.io - kind: Ingress - name: ingress1-name - namespace: ingress-namespace - sourcePath: $.spec.rules[0].http.paths[0].backend.service.port.number - targetPath: $.spec.containers[0].env[0].value - token: ${service-port} -spec: - containers: - - name: app - image: example:1.0 - ports: - - containerPort: 80 - env: - - name: SERVICE_PORT - value: ${service-port} -` - -var ingress1y = ` -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: ingress1-name - namespace: ingress-namespace - annotations: - nginx.ingress.kubernetes.io/rewrite-target: / -spec: - rules: - - http: - paths: - - path: /old - pathType: Prefix - backend: - service: - name: old - port: - number: 80 -` - -var pod2y = ` -apiVersion: v1 -kind: Pod -metadata: - name: pod-name - namespace: pod-namespace - annotations: - config.kubernetes.io/apply-time-mutation: | - - sourceRef: - group: networking.k8s.io - kind: Ingress - name: ingress1-name - namespace: ingress-namespace - sourcePath: $.spec.rules[?(@.http)].http.paths[?(@.path=="/old")].backend.service.port.number - targetPath: $.spec.containers[?(@.name=="app")].env[?(@.name=="SERVICE_PORT")].value - token: ${service-port} - - sourceRef: - group: networking.k8s.io - kind: Ingress - name: ingress1-name - namespace: ingress-namespace - sourcePath: $.spec.rules[?(@.http)].http.paths[?(@.path=="/old")].backend.service.name - targetPath: $.spec.containers[?(@.name=="app")].env[?(@.name=="SERVICE_NAME")].value -spec: - containers: - - name: app - image: example:1.0 - ports: - - containerPort: 80 - env: - - name: SERVICE_PORT - value: ${service-port} - - name: SERVICE_NAME - value: "" # field must exist to be mutated -` - -var pod3y = ` -apiVersion: v1 -kind: Pod -metadata: - name: pod-name - namespace: pod-namespace - annotations: - config.kubernetes.io/apply-time-mutation: | - - sourceRef: - kind: ConfigMap - name: map1-name - namespace: map-namespace - sourcePath: $.data.image - targetPath: $.spec.containers[?(@.name=="app")].image - token: ${app-image} - - sourceRef: - kind: ConfigMap - name: map1-name - namespace: map-namespace - sourcePath: $.data.version - targetPath: $.spec.containers[?(@.name=="app")].image - token: ${app-version} - - sourceRef: - group: networking.k8s.io - kind: Ingress - name: ingress1-name - namespace: ingress-namespace - sourcePath: $.spec.rules[?(@.http)].http.paths[?(@.path=="/old")].backend.service.port.number - targetPath: $.spec.containers[?(@.name=="app")].env[?(@.name=="SERVICE_PORT")].value - token: ${service-port} -spec: - containers: - - name: app - image: ${app-image}:${app-version} - ports: - - containerPort: 80 - env: - - name: SERVICE_PORT - value: ${service-port} -` - -var configmap1y = ` -apiVersion: v1 -kind: ConfigMap -metadata: - name: map1-name - namespace: map-namespace -data: - image: traefik/whoami - version: "1.0" -` - -var configmap2y = ` -apiVersion: v1 -kind: ConfigMap -metadata: - name: map2-name - namespace: map-namespace - annotations: - config.kubernetes.io/apply-time-mutation: | - - sourceRef: - kind: ConfigMap - name: map1-name - namespace: map-namespace - sourcePath: $.data - targetPath: $.data.json - token: ${map-data-json} -data: - json: "[{\"Ï€\":3.14},${map-data-json}]" -` - -// invalid -var configmap3y = ` -apiVersion: v1 -kind: ConfigMap -metadata: - name: map3-name - namespace: map-namespace - annotations: - config.kubernetes.io/apply-time-mutation: "not a valid substitution list" -data: {} -` - -// self-reference -var configmap4y = ` -apiVersion: v1 -kind: ConfigMap -metadata: - name: map4-name - namespace: map-namespace - annotations: - config.kubernetes.io/apply-time-mutation: | - - sourceRef: - kind: ConfigMap - name: map4-name - namespace: map-namespace - sourcePath: $.data - targetPath: $.data -data: - movie: inception - slogan: we need to go deeper -` - -var ingress2y = ` -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: ingress2-name - namespace: ingress-namespace - annotations: - nginx.ingress.kubernetes.io/rewrite-target: / - config.kubernetes.io/apply-time-mutation: | - - sourceRef: - apiVersion: networking.k8s.io/v1 - kind: Ingress - name: ingress1-name - namespace: ingress-namespace - sourcePath: $.spec.rules[0].http.paths[?(@.path=="/old")] - targetPath: $.spec.rules[0].http.paths[(@.length-1)] -spec: - rules: - - http: - paths: - - path: /new - pathType: Prefix - backend: - service: - name: new - port: - number: 80 - - {} # field must exist to be mutated -` - -var joinedPathsYaml = ` -- path: /new - pathType: Prefix - backend: - service: - name: new - port: - number: 80 -- path: /old - pathType: Prefix - backend: - service: - name: old - port: - number: 80 -` - -var service1y = ` -apiVersion: v1 -kind: Service -metadata: - name: service1-name - namespace: service1-namespace - annotations: - config.kubernetes.io/apply-time-mutation: | - - sourceRef: - group: apps - kind: Deployment - name: deployment1-name - namespace: deployment1-namespace - sourcePath: $.spec.template.spec.containers[?(@.name=="tcp-handler")].ports[0].containerPort - targetPath: $.spec.ports[?(@.protocol=="TCP" && @.port==80)].targetPort - - sourceRef: - group: apps - kind: Deployment - name: deployment1-name - namespace: deployment1-namespace - sourcePath: $.spec.template.spec.containers[?(@.name=="udp-handler")].ports[0].containerPort - targetPath: $.spec.ports[?(@.protocol=="UDP" && @.port==80)].targetPort -spec: - selector: - app: MyApp - ports: - - protocol: TCP - port: 80 - targetPort: 0 # field must exist to be mutated - - protocol: TCP - port: 443 - targetPort: 443 - - protocol: UDP - port: 80 - targetPort: 0 # field must exist to be mutated -` - -var deployment1y = ` -apiVersion: apps/v1 -kind: Deployment -metadata: - name: deployment1-name - namespace: deployment1-namespace -spec: - selector: - matchLabels: - app: example - replicas: 2 - template: - metadata: - labels: - app: example - spec: - containers: - - name: tcp-handler - image: example-tcp - ports: - - containerPort: 8080 - - name: udp-handler - image: example-udp - ports: - - containerPort: 8081 -` - -var clusterrole1y = ` -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: example-role - labels: - domain: example.com -rules: -- apiGroups: [""] - resources: ["pods"] - verbs: ["get", "watch", "list"] -` - -var clusterrolebinding1y = ` -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: read-secrets - annotations: - config.kubernetes.io/apply-time-mutation: | - - sourceRef: - apiVersion: rbac.authorization.k8s.io/v1 - kind: ClusterRole - name: example-role - sourcePath: $.metadata.labels.domain - targetPath: $.subjects[0].name - token: ${domain} -subjects: -- kind: User - name: "bob@${domain}" - apiGroup: rbac.authorization.k8s.io -roleRef: - kind: ClusterRole - name: secret-reader - apiGroup: rbac.authorization.k8s.io -` - -type nestedFieldValue struct { - Field []interface{} - Value interface{} -} - -func TestMutate(t *testing.T) { - pod1 := ktestutil.YamlToUnstructured(t, pod1y) - ingress1 := ktestutil.YamlToUnstructured(t, ingress1y) - pod2 := ktestutil.YamlToUnstructured(t, pod2y) - pod3 := ktestutil.YamlToUnstructured(t, pod3y) - configmap1 := ktestutil.YamlToUnstructured(t, configmap1y) - configmap2 := ktestutil.YamlToUnstructured(t, configmap2y) - configmap3 := ktestutil.YamlToUnstructured(t, configmap3y) - configmap4 := ktestutil.YamlToUnstructured(t, configmap4y) - ingress2 := ktestutil.YamlToUnstructured(t, ingress2y) - service1 := ktestutil.YamlToUnstructured(t, service1y) - deployment1 := ktestutil.YamlToUnstructured(t, deployment1y) - clusterrole1 := ktestutil.YamlToUnstructured(t, clusterrole1y) - clusterrolebinding1 := ktestutil.YamlToUnstructured(t, clusterrolebinding1y) - - joinedPaths := make([]interface{}, 0) - err := yaml.Unmarshal([]byte(joinedPathsYaml), &joinedPaths) - if err != nil { - t.Fatalf("error parsing yaml: %v", err) - } - - tests := map[string]struct { - target *unstructured.Unstructured - sources []*unstructured.Unstructured - cache cache.ResourceCache - mutated bool - reason string - errMsg string - expected []nestedFieldValue - }{ - "no annotation": { - target: configmap1, - mutated: false, - reason: "", - }, - "invalid annotation": { - target: configmap3, - mutated: false, - reason: "", - // exact error message isn't very important. Feel free to update if the error text changes. - errMsg: `failed to read annotation in object (v1/namespaces/map-namespace/ConfigMap/map3-name): ` + - `invalid "config.kubernetes.io/apply-time-mutation" annotation: ` + - `error unmarshaling JSON: ` + - `while decoding JSON: ` + - `json: cannot unmarshal string into Go value of type mutation.ApplyTimeMutation`, - }, - "invalid self-reference": { - target: configmap4, - mutated: false, - reason: "", - // exact error message isn't very important. Feel free to update if the error text changes. - errMsg: `invalid self-reference (/namespaces/map-namespace/ConfigMap/map4-name)`, - }, - "missing source": { - target: pod1, - mutated: false, - reason: "", - // exact error message isn't very important. Feel free to update if the error text changes. - errMsg: `failed to get source object (networking.k8s.io/namespaces/ingress-namespace/Ingress/ingress1-name): ` + - `object not found: ` + - `ingresses.networking.k8s.io "ingress1-name" not found`, - }, - "pod env var string from ingress port int": { - target: pod1, - sources: []*unstructured.Unstructured{ingress1}, - mutated: true, - reason: expectedReason, - expected: []nestedFieldValue{ - { - Field: []interface{}{"spec", "containers", 0, "env", 0, "value"}, - Value: "80", // must be string, not int - }, - }, - }, - "two subs, one source, no token, missing target field, field selector": { - target: pod2, - sources: []*unstructured.Unstructured{ingress1, ingress1}, // twice, because not cached - mutated: true, - reason: expectedReason, - expected: []nestedFieldValue{ - { - Field: []interface{}{"spec", "containers", 0, "env", 0, "value"}, - Value: "80", // must be string, not int - }, - { - Field: []interface{}{"spec", "containers", 0, "env", 1, "value"}, - Value: "old", - }, - }, - }, - "two subs, one source, no token, missing target field, field selector (cached)": { - target: pod2, - sources: []*unstructured.Unstructured{ingress1}, // only once, because cached - cache: cache.NewResourceCacheMap(), - mutated: true, - reason: expectedReason, - expected: []nestedFieldValue{ - { - Field: []interface{}{"spec", "containers", 0, "env", 0, "value"}, - Value: "80", // must be string, not int - }, - { - Field: []interface{}{"spec", "containers", 0, "env", 1, "value"}, - Value: "old", - }, - }, - }, - "three subs, two sources, two tokens in the same target field, float string": { - target: pod3, - sources: []*unstructured.Unstructured{configmap1, configmap1, ingress1}, // repeats, because not cached - mutated: true, - reason: expectedReason, - expected: []nestedFieldValue{ - { - Field: []interface{}{"spec", "containers", 0, "env", 0, "value"}, - Value: "80", // must be string, not int - }, - { - Field: []interface{}{"spec", "containers", 0, "image"}, - Value: "traefik/whoami:1.0", // make sure float string isn't trucated to "1" - }, - }, - }, - "three subs, two sources, two tokens in the same target field, float string (cached)": { - target: pod3, - sources: []*unstructured.Unstructured{configmap1, ingress1}, // no repeats, because cached - cache: cache.NewResourceCacheMap(), - mutated: true, - reason: expectedReason, - expected: []nestedFieldValue{ - { - Field: []interface{}{"spec", "containers", 0, "env", 0, "value"}, - Value: "80", // must be string, not int - }, - { - Field: []interface{}{"spec", "containers", 0, "image"}, - Value: "traefik/whoami:1.0", // make sure float string isn't trucated to "1" - }, - }, - }, - "map to json string": { - target: configmap2, - sources: []*unstructured.Unstructured{configmap1}, - mutated: true, - reason: expectedReason, - expected: []nestedFieldValue{ - { - Field: []interface{}{"data", "json"}, - Value: `[{"Ï€":3.14},{"image":"traefik/whoami","version":"1.0"}]`, // string, not object - }, - }, - }, - "map to map, array append": { - target: ingress2, - sources: []*unstructured.Unstructured{ingress1}, - mutated: true, - reason: expectedReason, - expected: []nestedFieldValue{ - { - Field: []interface{}{"spec", "rules", 0, "http", "paths"}, - Value: joinedPaths, // object, not string - }, - }, - }, - "multi-field selector": { - target: service1, - sources: []*unstructured.Unstructured{deployment1, deployment1}, // repeats, because not cached - mutated: true, - reason: expectedReason, - expected: []nestedFieldValue{ - { - Field: []interface{}{"spec", "ports", 0, "targetPort"}, - Value: 8080, - }, - { - Field: []interface{}{"spec", "ports", 2, "targetPort"}, - Value: 8081, - }, - }, - }, - "cluster-scoped": { - target: clusterrolebinding1, - sources: []*unstructured.Unstructured{clusterrole1}, - mutated: true, - reason: expectedReason, - expected: []nestedFieldValue{ - { - Field: []interface{}{"subjects", 0, "name"}, - Value: "bob@example.com", - }, - }, - }, - } - - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - getChan := make(chan unstructured.Unstructured) - - mutator := &ApplyTimeMutator{ - Client: &fakeDynamicClient{ - resourceInterfaceFunc: newFakeNamespaceClientFunc(getChan), - }, - Mapper: testrestmapper.TestOnlyStaticRESTMapper( - scheme.Scheme, - scheme.Scheme.PrioritizedVersionsAllGroups()..., - ), - ResourceCache: tc.cache, // optional! - } - - // send sources when GET is called - sources := tc.sources - go func() { - defer close(getChan) - for _, source := range sources { - getChan <- *source - } - }() - - mutated, reason, err := mutator.Mutate(context.TODO(), tc.target) - if tc.errMsg != "" { - require.EqualError(t, err, tc.errMsg) - } else { - require.NoError(t, err) - } - require.Equal(t, tc.mutated, mutated, "unexpected mutated bool") - require.Equal(t, tc.reason, reason, "unexpected mutated reason") - - for _, efv := range tc.expected { - received, found, err := object.NestedField(tc.target.Object, efv.Field...) - require.NoError(t, err) - require.True(t, found, "target field not found") - require.Equal(t, efv.Value, received, "unexpected target field value") - } - }) - } -} - -func TestValueToString(t *testing.T) { - tests := map[string]struct { - value interface{} - expected string - }{ - "int": { - value: 1, - expected: "1", - }, - "float": { - value: 1.2345, - expected: "1.2345", - }, - "string": { - value: "nothing to see", - expected: "nothing to see", - }, - "bool": { - value: false, - expected: "false", - }, - "interface map": { - value: map[string]interface{}{ - "apiVersion": "v1", - "kind": "Pod", - "metadata": map[string]interface{}{ - "name": "pod-name", - "namespace": "test-namespace", - }, - }, - expected: `{"apiVersion":"v1","kind":"Pod","metadata":{"name":"pod-name","namespace":"test-namespace"}}`, - }, - "interface list": { - value: []interface{}{ - "x", - map[string]interface{}{ - "?": nil, - }, - 0, - }, - expected: `["x",{"?":null},0]`, - }, - "string list": { - value: []string{ - "x", - "y", - "z", - }, - expected: `["x","y","z"]`, - }, - } - - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - received, err := valueToString(tc.value) - require.NoError(t, err) - require.Equal(t, tc.expected, received, "unexpected result") - }) - } -} - -// fakeNamespaceClient wraps ResourceInterface, overwriting the Get func. -type fakeNamespaceClient struct { - dynamic.ResourceInterface - resource schema.GroupVersionResource - namespace string - getChan <-chan unstructured.Unstructured -} - -func newFakeNamespaceClientFunc(getChan <-chan unstructured.Unstructured) func(resource schema.GroupVersionResource, namespace string) dynamic.ResourceInterface { - innerGetChan := getChan - return func(resource schema.GroupVersionResource, namespace string) dynamic.ResourceInterface { - return &fakeNamespaceClient{ - resource: resource, - namespace: namespace, - getChan: innerGetChan, - } - } -} - -func (c *fakeNamespaceClient) Get(ctx context.Context, name string, options metav1.GetOptions, subresources ...string) (*unstructured.Unstructured, error) { - obj, open := <-c.getChan - if !open { - return nil, apierrors.NewNotFound(c.resource.GroupResource(), name) - } - return &obj, nil -} - -// fakeDynamicClient accepts always returns the same client, just with a different -type fakeDynamicClient struct { - resourceInterfaceFunc func(resource schema.GroupVersionResource, namespace string) dynamic.ResourceInterface -} - -func (c *fakeDynamicClient) Resource(resource schema.GroupVersionResource) dynamic.NamespaceableResourceInterface { - return &fakeDynamicResourceClient{ - resourceInterfaceFunc: c.resourceInterfaceFunc, - NamespaceableResourceInterface: fake.NewSimpleDynamicClient(scheme.Scheme).Resource(resource), - resource: resource, - } -} - -type fakeDynamicResourceClient struct { - dynamic.NamespaceableResourceInterface - resourceInterfaceFunc func(resource schema.GroupVersionResource, namespace string) dynamic.ResourceInterface - resource schema.GroupVersionResource -} - -func (c *fakeDynamicResourceClient) Namespace(ns string) dynamic.ResourceInterface { - return c.resourceInterfaceFunc(c.resource, ns) -} diff --git a/pkg/apply/mutator/main_test.go b/pkg/apply/mutator/main_test.go deleted file mode 100644 index 76d842fd..00000000 --- a/pkg/apply/mutator/main_test.go +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package mutator - -import ( - "os" - "testing" - - "k8s.io/klog/v2" -) - -// TestMain executes the tests for this package, with optional logging. -// To see all logs, use: -// go test github.com/fluxcd/cli-utils/pkg/kyq -v -args -v=5 -func TestMain(m *testing.M) { - klog.InitFlags(nil) - os.Exit(m.Run()) -} diff --git a/pkg/apply/mutator/mutator.go b/pkg/apply/mutator/mutator.go deleted file mode 100644 index 0512a30a..00000000 --- a/pkg/apply/mutator/mutator.go +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package mutator - -import ( - "context" - - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -// Interface decouples apply-time-mutation -// from the concrete structs used for applying. -type Interface interface { - // Name returns a filter name (usually for logging). - Name() string - // Mutate returns true if the object was mutated. - // This allows the mutator to decide if mutation is needed. - // If mutated, a reason string is returned. - // If an error happens during mutation, it is returned. - Mutate(ctx context.Context, obj *unstructured.Unstructured) (bool, string, error) -} diff --git a/pkg/apply/poller/poller.go b/pkg/apply/poller/poller.go deleted file mode 100644 index fbdc86e5..00000000 --- a/pkg/apply/poller/poller.go +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package poller - -import ( - "context" - - "github.com/fluxcd/cli-utils/pkg/kstatus/polling" - pollevent "github.com/fluxcd/cli-utils/pkg/kstatus/polling/event" - "github.com/fluxcd/cli-utils/pkg/object" -) - -// Poller defines the interface the applier needs to poll for status of resources. -// The context is the preferred way to shut down the poller. -// The identifiers defines the resources which the poller should poll and -// compute status for. -// The options allows callers to override some of the settings of the poller, -// like the polling frequency and the caching strategy. -type Poller interface { - Poll(ctx context.Context, identifiers object.ObjMetadataSet, options polling.PollOptions) <-chan pollevent.Event -} diff --git a/pkg/apply/prune/event-factory.go b/pkg/apply/prune/event-factory.go deleted file mode 100644 index 5d3c6c5e..00000000 --- a/pkg/apply/prune/event-factory.go +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 -// - -package prune - -import ( - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/object" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -// EventFactory is an abstract interface describing functions to generate -// events for pruning or deleting. -type EventFactory interface { - CreateSuccessEvent(obj *unstructured.Unstructured) event.Event - CreateSkippedEvent(obj *unstructured.Unstructured, err error) event.Event - CreateFailedEvent(id object.ObjMetadata, err error) event.Event -} - -// CreateEventFactory returns the correct concrete version of -// an EventFactory based on the passed boolean. -func CreateEventFactory(isDelete bool, groupName string) EventFactory { - if isDelete { - return DeleteEventFactory{ - groupName: groupName, - } - } - return PruneEventFactory{ - groupName: groupName, - } -} - -// PruneEventFactory implements EventFactory interface as a concrete -// representation of for prune events. -// -//nolint:revive // stuttering ok because Prune is a type of PruneEvent -type PruneEventFactory struct { - groupName string -} - -func (pef PruneEventFactory) CreateSuccessEvent(obj *unstructured.Unstructured) event.Event { - return event.Event{ - Type: event.PruneType, - PruneEvent: event.PruneEvent{ - GroupName: pef.groupName, - Status: event.PruneSuccessful, - Object: obj, - Identifier: object.UnstructuredToObjMetadata(obj), - }, - } -} - -func (pef PruneEventFactory) CreateSkippedEvent(obj *unstructured.Unstructured, err error) event.Event { - return event.Event{ - Type: event.PruneType, - PruneEvent: event.PruneEvent{ - GroupName: pef.groupName, - Status: event.PruneSkipped, - Object: obj, - Identifier: object.UnstructuredToObjMetadata(obj), - Error: err, - }, - } -} - -func (pef PruneEventFactory) CreateFailedEvent(id object.ObjMetadata, err error) event.Event { - return event.Event{ - Type: event.PruneType, - PruneEvent: event.PruneEvent{ - GroupName: pef.groupName, - Status: event.PruneFailed, - Identifier: id, - Error: err, - }, - } -} - -// DeleteEventFactory implements EventFactory interface as a concrete -// representation of for delete events. -type DeleteEventFactory struct { - groupName string -} - -func (def DeleteEventFactory) CreateSuccessEvent(obj *unstructured.Unstructured) event.Event { - return event.Event{ - Type: event.DeleteType, - DeleteEvent: event.DeleteEvent{ - GroupName: def.groupName, - Status: event.DeleteSuccessful, - Object: obj, - Identifier: object.UnstructuredToObjMetadata(obj), - }, - } -} - -func (def DeleteEventFactory) CreateSkippedEvent(obj *unstructured.Unstructured, err error) event.Event { - return event.Event{ - Type: event.DeleteType, - DeleteEvent: event.DeleteEvent{ - GroupName: def.groupName, - Status: event.DeleteSkipped, - Object: obj, - Identifier: object.UnstructuredToObjMetadata(obj), - Error: err, - }, - } -} - -func (def DeleteEventFactory) CreateFailedEvent(id object.ObjMetadata, err error) event.Event { - return event.Event{ - Type: event.DeleteType, - DeleteEvent: event.DeleteEvent{ - GroupName: def.groupName, - Status: event.DeleteFailed, - Identifier: id, - Error: err, - }, - } -} diff --git a/pkg/apply/prune/event-factory_test.go b/pkg/apply/prune/event-factory_test.go deleted file mode 100644 index 9252d5ae..00000000 --- a/pkg/apply/prune/event-factory_test.go +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright 2019 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package prune - -import ( - "fmt" - "testing" - - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/object" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -func TestEventFactory(t *testing.T) { - tests := map[string]struct { - destroy bool - obj *unstructured.Unstructured - skippedErr error - failedErr error - expectedType event.Type - }{ - "prune events": { - destroy: false, - obj: pod, - skippedErr: fmt.Errorf("fake reason"), - expectedType: event.PruneType, - }, - "delete events": { - destroy: true, - obj: pdb, - skippedErr: fmt.Errorf("fake reason"), - expectedType: event.DeleteType, - }, - } - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - id := object.UnstructuredToObjMetadata(tc.obj) - eventFactory := CreateEventFactory(tc.destroy, "task-0") - // Validate the "success" event" - actualEvent := eventFactory.CreateSuccessEvent(tc.obj) - if tc.expectedType != actualEvent.Type { - t.Errorf("success event expected type (%s), got (%s)", - tc.expectedType, actualEvent.Type) - } - var actualObj *unstructured.Unstructured - var err error - if tc.expectedType == event.PruneType { - if event.PruneSuccessful != actualEvent.PruneEvent.Status { - t.Errorf("success event expected status (PruneSuccessful), got (%s)", - actualEvent.PruneEvent.Status) - } - actualObj = actualEvent.PruneEvent.Object - err = actualEvent.PruneEvent.Error - } else { - if event.DeleteSuccessful != actualEvent.DeleteEvent.Status { - t.Errorf("success event expected status (DeleteSuccessful), got (%s)", - actualEvent.DeleteEvent.Status) - } - actualObj = actualEvent.DeleteEvent.Object - err = actualEvent.DeleteEvent.Error - } - if tc.obj != actualObj { - t.Errorf("expected event object (%v), got (%v)", tc.obj, actualObj) - } - if err != nil { - t.Errorf("success event expected nil error, got (%s)", err) - } - // Validate the "skipped" event" - actualEvent = eventFactory.CreateSkippedEvent(tc.obj, tc.skippedErr) - if tc.expectedType != actualEvent.Type { - t.Errorf("skipped event expected type (%s), got (%s)", - tc.expectedType, actualEvent.Type) - } - if tc.expectedType == event.PruneType { - if event.PruneSkipped != actualEvent.PruneEvent.Status { - t.Errorf("skipped event expected status (PruneSkipped), got (%s)", - actualEvent.PruneEvent.Status) - } - actualObj = actualEvent.PruneEvent.Object - err = actualEvent.PruneEvent.Error - } else { - if event.DeleteSkipped != actualEvent.DeleteEvent.Status { - t.Errorf("skipped event expected status (DeleteSkipped), got (%s)", - actualEvent.DeleteEvent.Status) - } - actualObj = actualEvent.DeleteEvent.Object - err = actualEvent.DeleteEvent.Error - } - if tc.obj != actualObj { - t.Errorf("expected event object (%v), got (%v)", tc.obj, actualObj) - } - if tc.skippedErr != err { - t.Errorf("skipped event expected error (%s), got (%s)", tc.skippedErr, err) - } - // Validate the "failed" event" - actualEvent = eventFactory.CreateFailedEvent(id, tc.failedErr) - if tc.expectedType != actualEvent.Type { - t.Errorf("failed event expected type (%s), got (%s)", - tc.expectedType, actualEvent.Type) - } - if tc.expectedType != actualEvent.Type { - t.Errorf("failed event expected type (%s), got (%s)", - tc.expectedType, actualEvent.Type) - } - if tc.expectedType == event.PruneType { - err = actualEvent.PruneEvent.Error - } else { - err = actualEvent.DeleteEvent.Error - } - if tc.failedErr != err { - t.Errorf("failed event expected error (%s), got (%s)", tc.failedErr, err) - } - }) - } -} diff --git a/pkg/apply/prune/main_test.go b/pkg/apply/prune/main_test.go deleted file mode 100644 index d8f92883..00000000 --- a/pkg/apply/prune/main_test.go +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package prune - -import ( - "os" - "testing" - - "k8s.io/klog/v2" -) - -// TestMain executes the tests for this package, with optional logging. -// To see all logs, use: -// go test github.com/fluxcd/cli-utils/pkg/apply/prune -v -args -v=5 -func TestMain(m *testing.M) { - klog.InitFlags(nil) - os.Exit(m.Run()) -} diff --git a/pkg/apply/prune/prune.go b/pkg/apply/prune/prune.go deleted file mode 100644 index 746a649a..00000000 --- a/pkg/apply/prune/prune.go +++ /dev/null @@ -1,289 +0,0 @@ -// Copyright 2019 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 -// -// Prune functionality deletes previously applied objects -// which are subsequently omitted in further apply operations. -// This functionality relies on "inventory" objects to store -// object metadata for each apply operation. This file defines -// PruneOptions to encapsulate information necessary to -// calculate the prune set, and to delete the objects in -// this prune set. - -package prune - -import ( - "context" - "errors" - - "github.com/fluxcd/cli-utils/pkg/apply/filter" - "github.com/fluxcd/cli-utils/pkg/apply/taskrunner" - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/pkg/object" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/client-go/dynamic" - "k8s.io/klog/v2" - "k8s.io/kubectl/pkg/cmd/util" -) - -// Pruner implements GetPruneObjs to calculate which objects to prune and Prune -// to delete them. -type Pruner struct { - InvClient inventory.Client - Client dynamic.Interface - Mapper meta.RESTMapper -} - -// NewPruner returns a new Pruner. -// Returns an error if dependency injection fails using the factory. -func NewPruner(factory util.Factory, invClient inventory.Client) (*Pruner, error) { - // Client/Builder fields from the Factory. - client, err := factory.DynamicClient() - if err != nil { - return nil, err - } - mapper, err := factory.ToRESTMapper() - if err != nil { - return nil, err - } - return &Pruner{ - InvClient: invClient, - Client: client, - Mapper: mapper, - }, nil -} - -// Options defines a set of parameters that can be used to tune -// the behavior of the pruner. -type Options struct { - // DryRunStrategy defines whether objects should actually be pruned or if - // we should just print what would happen without actually doing it. - DryRunStrategy common.DryRunStrategy - - PropagationPolicy metav1.DeletionPropagation - - // True if we are destroying, which deletes the inventory object - // as well (possibly) the inventory namespace. - Destroy bool -} - -// Prune deletes the set of passed objects. A prune skip/failure is -// captured in the TaskContext, so we do not lose track of these -// objects from the inventory. The passed prune filters are used to -// determine if permission exists to delete the object. An example -// of a prune filter is PreventDeleteFilter, which checks if an -// annotation exists on the object to ensure the objects is not -// deleted (e.g. a PersistentVolume that we do no want to -// automatically prune/delete). -// -// Parameters: -// -// objs - objects to prune (delete) -// pruneFilters - list of filters for deletion permission -// taskContext - task for apply/prune -// taskName - name of the parent task group, for events -// opts - options for dry-run -func (p *Pruner) Prune( - objs object.UnstructuredSet, - pruneFilters []filter.ValidationFilter, - taskContext *taskrunner.TaskContext, - taskName string, - opts Options, -) error { - eventFactory := CreateEventFactory(opts.Destroy, taskName) - // Iterate through objects to prune (delete). If an object is not pruned - // and we need to keep it in the inventory, we must capture the prune failure. - for _, obj := range objs { - id := object.UnstructuredToObjMetadata(obj) - klog.V(5).Infof("evaluating prune filters (object: %q)", id) - - // UID will change if the object is deleted and re-created. - uid := obj.GetUID() - if uid == "" { - err := object.NotFound([]interface{}{"metadata", "uid"}, "") - if klog.V(4).Enabled() { - // only log event emitted errors if the verbosity > 4 - klog.Errorf("prune uid lookup errored (object: %s): %v", id, err) - } - taskContext.SendEvent(eventFactory.CreateFailedEvent(id, err)) - taskContext.InventoryManager().AddFailedDelete(id) - continue - } - - // Check filters to see if we're prevented from pruning/deleting object. - var filterErr error - for _, pruneFilter := range pruneFilters { - klog.V(6).Infof("prune filter evaluating (filter: %s, object: %s)", pruneFilter.Name(), id) - filterErr = pruneFilter.Filter(obj) - if filterErr != nil { - var fatalErr *filter.FatalError - if errors.As(filterErr, &fatalErr) { - if klog.V(4).Enabled() { - // only log event emitted errors if the verbosity > 4 - klog.Errorf("prune filter errored (filter: %s, object: %s): %v", pruneFilter.Name(), id, fatalErr.Err) - } - taskContext.SendEvent(eventFactory.CreateFailedEvent(id, fatalErr.Err)) - taskContext.InventoryManager().AddFailedDelete(id) - break - } - klog.V(4).Infof("prune filtered (filter: %s, object: %s): %v", pruneFilter.Name(), id, filterErr) - - // Remove the inventory annotation if deletion was prevented. - // This abandons the object so it won't be pruned by future applier runs. - var abandonErr *filter.AnnotationPreventedDeletionError - if errors.As(filterErr, &abandonErr) { - if !opts.DryRunStrategy.ClientOrServerDryRun() { - var err error - obj, err = p.removeInventoryAnnotation(obj) - if err != nil { - if klog.V(4).Enabled() { - // only log event emitted errors if the verbosity > 4 - klog.Errorf("error removing annotation (object: %q, annotation: %q): %v", id, inventory.OwningInventoryKey, err) - } - taskContext.SendEvent(eventFactory.CreateFailedEvent(id, err)) - taskContext.InventoryManager().AddFailedDelete(id) - break - } - // Inventory annotation was successfully removed from the object. - // Register for removal from the inventory. - taskContext.AddAbandonedObject(id) - } - } - - // Remove the object from inventory if it was determined that the object should not be pruned, - // because it had recently been applied. This probably means that the object is in the inventory - // more than one time with a different group (e.g. kind Ingress and apiGroups networking.k8s.io & extensions) - // due to being cohabitated: https://github.com/kubernetes/kubernetes/blob/v1.25.0/pkg/kubeapiserver/default_storage_factory_builder.go#L124-L131 - var deleteAfterApplyErr *filter.ApplyPreventedDeletionError - if errors.As(filterErr, &deleteAfterApplyErr) { - if !opts.DryRunStrategy.ClientOrServerDryRun() { - // Register for removal from the inventory. - taskContext.AddAbandonedObject(id) - } - } - - taskContext.SendEvent(eventFactory.CreateSkippedEvent(obj, filterErr)) - taskContext.InventoryManager().AddSkippedDelete(id) - break - } - } - if filterErr != nil { - continue - } - - // Filters passed--actually delete object if not dry run. - if !opts.DryRunStrategy.ClientOrServerDryRun() { - klog.V(4).Infof("deleting object (object: %q)", id) - err := p.deleteObject(id, metav1.DeleteOptions{ - // Only delete the resource if it hasn't already been deleted - // and recreated since the last GET. Otherwise error. - Preconditions: &metav1.Preconditions{ - UID: &uid, - }, - PropagationPolicy: &opts.PropagationPolicy, - }) - if err != nil { - if apierrors.IsNotFound(err) { - klog.Warningf("error deleting object (object: %q): object not found: object may have been deleted asynchronously by another client", id) - // treat this as successful idempotent deletion - } else { - if klog.V(4).Enabled() { - // only log event emitted errors if the verbosity > 4 - klog.Errorf("error deleting object (object: %q): %v", id, err) - } - taskContext.SendEvent(eventFactory.CreateFailedEvent(id, err)) - taskContext.InventoryManager().AddFailedDelete(id) - continue - } - } - } - taskContext.InventoryManager().AddSuccessfulDelete(id, obj.GetUID()) - taskContext.SendEvent(eventFactory.CreateSuccessEvent(obj)) - } - return nil -} - -// removeInventoryAnnotation removes the `config.k8s.io/owning-inventory` annotation from pruneObj. -func (p *Pruner) removeInventoryAnnotation(obj *unstructured.Unstructured) (*unstructured.Unstructured, error) { - // Make a copy of the input object to avoid modifying the input. - // This prevents race conditions when writing to the underlying map. - obj = obj.DeepCopy() - id := object.UnstructuredToObjMetadata(obj) - annotations := obj.GetAnnotations() - if annotations != nil { - if _, ok := annotations[inventory.OwningInventoryKey]; ok { - klog.V(4).Infof("removing annotation (object: %q, annotation: %q)", id, inventory.OwningInventoryKey) - delete(annotations, inventory.OwningInventoryKey) - obj.SetAnnotations(annotations) - namespacedClient, err := p.namespacedClient(id) - if err != nil { - return obj, err - } - _, err = namespacedClient.Update(context.TODO(), obj, metav1.UpdateOptions{}) - return obj, err - } - } - return obj, nil -} - -// GetPruneObjs calculates the set of prune objects, and retrieves them -// from the cluster. Set of prune objects equals the set of inventory -// objects minus the set of currently applied objects. Returns an error -// if one occurs. -func (p *Pruner) GetPruneObjs( - inv inventory.Info, - objs object.UnstructuredSet, - opts Options, -) (object.UnstructuredSet, error) { - ids := object.UnstructuredSetToObjMetadataSet(objs) - invIDs, err := p.InvClient.GetClusterObjs(inv) - if err != nil { - return nil, err - } - // only return objects that were in the inventory but not in the object set - ids = invIDs.Diff(ids) - objs = object.UnstructuredSet{} - for _, id := range ids { - pruneObj, err := p.getObject(id) - if err != nil { - if meta.IsNoMatchError(err) { - klog.V(4).Infof("skip pruning (object: %q): resource type not registered", id) - continue - } - if apierrors.IsNotFound(err) { - klog.V(4).Infof("skip pruning (object: %q): resource not found", id) - continue - } - return nil, err - } - objs = append(objs, pruneObj) - } - return objs, nil -} - -func (p *Pruner) getObject(id object.ObjMetadata) (*unstructured.Unstructured, error) { - namespacedClient, err := p.namespacedClient(id) - if err != nil { - return nil, err - } - return namespacedClient.Get(context.TODO(), id.Name, metav1.GetOptions{}) -} - -func (p *Pruner) deleteObject(id object.ObjMetadata, opts metav1.DeleteOptions) error { - namespacedClient, err := p.namespacedClient(id) - if err != nil { - return err - } - return namespacedClient.Delete(context.TODO(), id.Name, opts) -} - -func (p *Pruner) namespacedClient(id object.ObjMetadata) (dynamic.ResourceInterface, error) { - mapping, err := p.Mapper.RESTMapping(id.GroupKind) - if err != nil { - return nil, err - } - return p.Client.Resource(mapping.Resource).Namespace(id.Namespace), nil -} diff --git a/pkg/apply/prune/prune_test.go b/pkg/apply/prune/prune_test.go deleted file mode 100644 index 73cf6850..00000000 --- a/pkg/apply/prune/prune_test.go +++ /dev/null @@ -1,1012 +0,0 @@ -// Copyright 2019 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package prune - -import ( - "context" - "fmt" - "strings" - "testing" - - "github.com/fluxcd/cli-utils/pkg/apply/cache" - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/apply/filter" - "github.com/fluxcd/cli-utils/pkg/apply/taskrunner" - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/testutil" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/apimachinery/pkg/api/meta/testrestmapper" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/util/sets" - "k8s.io/client-go/dynamic" - "k8s.io/client-go/dynamic/fake" - "k8s.io/kubectl/pkg/scheme" -) - -var testNamespace = "test-inventory-namespace" -var inventoryObjName = "test-inventory-obj" -var podName = "pod-1" -var pdbName = "pdb" - -var testInventoryLabel = "test-app-label" - -var inventoryObj = &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "v1", - "kind": "ConfigMap", - "metadata": map[string]interface{}{ - "name": inventoryObjName, - "namespace": testNamespace, - "labels": map[string]interface{}{ - common.InventoryLabel: testInventoryLabel, - }, - }, - }, -} - -var namespace = &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "v1", - "kind": "Namespace", - "metadata": map[string]interface{}{ - "name": testNamespace, - "uid": "uid-namespace", - "annotations": map[string]interface{}{ - "config.k8s.io/owning-inventory": testInventoryLabel, - }, - }, - }, -} - -var pod = &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "v1", - "kind": "Pod", - "metadata": map[string]interface{}{ - "name": podName, - "namespace": testNamespace, - "uid": "pod-uid", - "annotations": map[string]interface{}{ - "config.k8s.io/owning-inventory": testInventoryLabel, - }, - }, - }, -} - -var pdb = &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "policy/v1beta1", - "kind": "PodDisruptionBudget", - "metadata": map[string]interface{}{ - "name": pdbName, - "namespace": testNamespace, - "uid": "uid2", - "annotations": map[string]interface{}{ - "config.k8s.io/owning-inventory": testInventoryLabel, - }, - }, - }, -} - -var pdbDeleteFailure = &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "policy/v1beta1", - "kind": "PodDisruptionBudget", - "metadata": map[string]interface{}{ - "name": pdbName + "delete-failure", - "namespace": testNamespace, - "uid": "uid2", - "annotations": map[string]interface{}{ - "config.k8s.io/owning-inventory": testInventoryLabel, - }, - }, - }, -} - -var crontabCRManifest = ` -apiVersion: "stable.example.com/v1" -kind: CronTab -metadata: - name: cron-tab-01 - namespace: test-namespace -` - -// Returns a inventory object with the inventory set from -// the passed "children". -func createInventoryInfo(children ...*unstructured.Unstructured) inventory.Info { - inventoryObjCopy := inventoryObj.DeepCopy() - wrappedInv := inventory.WrapInventoryObj(inventoryObjCopy) - objs := object.UnstructuredSetToObjMetadataSet(children) - if err := wrappedInv.Store(objs, nil); err != nil { - return nil - } - obj, err := wrappedInv.GetObject() - if err != nil { - return nil - } - return inventory.WrapInventoryInfoObj(obj) -} - -// podDeletionPrevention object contains the "on-remove:keep" lifecycle directive. -var podDeletionPrevention = &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "v1", - "kind": "Pod", - "metadata": map[string]interface{}{ - "name": "test-prevent-delete", - "namespace": testNamespace, - "annotations": map[string]interface{}{ - common.OnRemoveAnnotation: common.OnRemoveKeep, - inventory.OwningInventoryKey: testInventoryLabel, - }, - "uid": "prevent-delete", - }, - }, -} - -var pdbDeletePreventionManifest = ` -apiVersion: "policy/v1beta1" -kind: PodDisruptionBudget -metadata: - name: pdb-delete-prevention - namespace: test-namespace - uid: uid2 - annotations: - client.lifecycle.config.k8s.io/deletion: detach - config.k8s.io/owning-inventory: test-app-label -` - -// Options with different dry-run values. -var ( - defaultOptions = Options{ - DryRunStrategy: common.DryRunNone, - PropagationPolicy: metav1.DeletePropagationBackground, - } - defaultOptionsDestroy = Options{ - DryRunStrategy: common.DryRunNone, - PropagationPolicy: metav1.DeletePropagationBackground, - Destroy: true, - } - clientDryRunOptions = Options{ - DryRunStrategy: common.DryRunClient, - PropagationPolicy: metav1.DeletePropagationBackground, - } -) - -func TestPrune(t *testing.T) { - tests := map[string]struct { - clusterObjs []*unstructured.Unstructured - pruneObjs []*unstructured.Unstructured - pruneFilters []filter.ValidationFilter - options Options - expectedEvents []event.Event - expectedSkipped object.ObjMetadataSet - expectedFailed object.ObjMetadataSet - expectedAbandoned object.ObjMetadataSet - }{ - "No pruned objects; no prune/delete events": { - clusterObjs: []*unstructured.Unstructured{}, - pruneObjs: []*unstructured.Unstructured{}, - options: defaultOptions, - expectedEvents: nil, - }, - "One successfully pruned object": { - clusterObjs: []*unstructured.Unstructured{pod}, - pruneObjs: []*unstructured.Unstructured{pod}, - options: defaultOptions, - expectedEvents: []event.Event{ - { - Type: event.PruneType, - PruneEvent: event.PruneEvent{ - Identifier: object.UnstructuredToObjMetadata(pod), - Status: event.PruneSuccessful, - Object: pod, - }, - }, - }, - }, - "Multiple successfully pruned object": { - clusterObjs: []*unstructured.Unstructured{pod, pdb, namespace}, - pruneObjs: []*unstructured.Unstructured{pod, pdb, namespace}, - options: defaultOptions, - expectedEvents: []event.Event{ - { - Type: event.PruneType, - PruneEvent: event.PruneEvent{ - Identifier: object.UnstructuredToObjMetadata(pod), - Status: event.PruneSuccessful, - Object: pod, - }, - }, - { - Type: event.PruneType, - PruneEvent: event.PruneEvent{ - Identifier: object.UnstructuredToObjMetadata(pdb), - Status: event.PruneSuccessful, - Object: pdb, - }, - }, - { - Type: event.PruneType, - PruneEvent: event.PruneEvent{ - Identifier: object.UnstructuredToObjMetadata(namespace), - Status: event.PruneSuccessful, - Object: namespace, - }, - }, - }, - }, - "One successfully deleted object": { - clusterObjs: []*unstructured.Unstructured{pod}, - pruneObjs: []*unstructured.Unstructured{pod}, - options: defaultOptionsDestroy, - expectedEvents: []event.Event{ - { - Type: event.DeleteType, - DeleteEvent: event.DeleteEvent{ - Identifier: object.UnstructuredToObjMetadata(pod), - Status: event.DeleteSuccessful, - Object: pod, - }, - }, - }, - }, - "Multiple successfully deleted objects": { - clusterObjs: []*unstructured.Unstructured{pod, pdb, namespace}, - pruneObjs: []*unstructured.Unstructured{pod, pdb, namespace}, - options: defaultOptionsDestroy, - expectedEvents: []event.Event{ - { - Type: event.DeleteType, - DeleteEvent: event.DeleteEvent{ - Identifier: object.UnstructuredToObjMetadata(pod), - Status: event.DeleteSuccessful, - Object: pod, - }, - }, - { - Type: event.DeleteType, - DeleteEvent: event.DeleteEvent{ - Identifier: object.UnstructuredToObjMetadata(pdb), - Status: event.DeleteSuccessful, - Object: pdb, - }, - }, - { - Type: event.DeleteType, - DeleteEvent: event.DeleteEvent{ - Identifier: object.UnstructuredToObjMetadata(namespace), - Status: event.DeleteSuccessful, - Object: namespace, - }, - }, - }, - }, - "Client dry run still pruned event": { - clusterObjs: []*unstructured.Unstructured{pod}, - pruneObjs: []*unstructured.Unstructured{pod}, - options: clientDryRunOptions, - expectedEvents: []event.Event{ - { - Type: event.PruneType, - PruneEvent: event.PruneEvent{ - Identifier: object.UnstructuredToObjMetadata(pod), - Status: event.PruneSuccessful, - Object: pod, - }, - }, - }, - }, - "Server dry run still deleted event": { - clusterObjs: []*unstructured.Unstructured{pod}, - pruneObjs: []*unstructured.Unstructured{pod}, - options: Options{ - DryRunStrategy: common.DryRunServer, - PropagationPolicy: metav1.DeletePropagationBackground, - Destroy: true, - }, - expectedEvents: []event.Event{ - { - Type: event.DeleteType, - DeleteEvent: event.DeleteEvent{ - Identifier: object.UnstructuredToObjMetadata(pod), - Status: event.DeleteSuccessful, - Object: pod, - }, - }, - }, - }, - "UID match means prune skipped and object abandoned": { - clusterObjs: []*unstructured.Unstructured{pod}, - pruneObjs: []*unstructured.Unstructured{pod}, - pruneFilters: []filter.ValidationFilter{ - filter.CurrentUIDFilter{ - // Add pod UID to set of current UIDs - CurrentUIDs: sets.NewString("pod-uid"), - }, - }, - options: defaultOptions, - expectedEvents: []event.Event{ - { - Type: event.PruneType, - PruneEvent: event.PruneEvent{ - Identifier: object.UnstructuredToObjMetadata(pod), - Status: event.PruneSkipped, - Object: pod, - Error: testutil.EqualError(&filter.ApplyPreventedDeletionError{ - UID: "pod-uid", - }), - }, - }, - }, - expectedSkipped: object.ObjMetadataSet{ - object.UnstructuredToObjMetadata(pod), - }, - expectedAbandoned: object.ObjMetadataSet{ - object.UnstructuredToObjMetadata(pod), - }, - }, - "UID match for only one object one pruned, one skipped and abandoned": { - clusterObjs: []*unstructured.Unstructured{pod, pdb}, - pruneObjs: []*unstructured.Unstructured{pod, pdb}, - pruneFilters: []filter.ValidationFilter{ - filter.CurrentUIDFilter{ - // Add pod UID to set of current UIDs - CurrentUIDs: sets.NewString("pod-uid"), - }, - }, - options: defaultOptions, - expectedEvents: []event.Event{ - { - Type: event.PruneType, - PruneEvent: event.PruneEvent{ - Identifier: object.UnstructuredToObjMetadata(pod), - Status: event.PruneSkipped, - Object: pod, - Error: testutil.EqualError(&filter.ApplyPreventedDeletionError{ - UID: "pod-uid", - }), - }, - }, - { - Type: event.PruneType, - PruneEvent: event.PruneEvent{ - Identifier: object.UnstructuredToObjMetadata(pdb), - Status: event.PruneSuccessful, - Object: pdb, - }, - }, - }, - expectedSkipped: object.ObjMetadataSet{ - object.UnstructuredToObjMetadata(pod), - }, - expectedAbandoned: object.ObjMetadataSet{ - object.UnstructuredToObjMetadata(pod), - }, - }, - "Prevent delete annotation equals prune skipped": { - clusterObjs: []*unstructured.Unstructured{ - podDeletionPrevention, - testutil.Unstructured(t, pdbDeletePreventionManifest), - }, - pruneObjs: []*unstructured.Unstructured{ - podDeletionPrevention, - testutil.Unstructured(t, pdbDeletePreventionManifest), - }, - pruneFilters: []filter.ValidationFilter{filter.PreventRemoveFilter{}}, - options: defaultOptions, - expectedEvents: []event.Event{ - { - Type: event.PruneType, - PruneEvent: event.PruneEvent{ - Identifier: object.UnstructuredToObjMetadata(podDeletionPrevention), - Status: event.PruneSkipped, - Object: testutil.Mutate(podDeletionPrevention.DeepCopy(), - testutil.DeleteOwningInv(t, testInventoryLabel)), - Error: testutil.EqualError(&filter.AnnotationPreventedDeletionError{ - Annotation: common.OnRemoveAnnotation, - Value: common.OnRemoveKeep, - }), - }, - }, - { - Type: event.PruneType, - PruneEvent: event.PruneEvent{ - Identifier: testutil.ToIdentifier(t, pdbDeletePreventionManifest), - Status: event.PruneSkipped, - Object: testutil.Unstructured(t, pdbDeletePreventionManifest, - testutil.DeleteOwningInv(t, testInventoryLabel)), - Error: testutil.EqualError(&filter.AnnotationPreventedDeletionError{ - Annotation: common.LifecycleDeleteAnnotation, - Value: common.PreventDeletion, - }), - }, - }, - }, - expectedSkipped: object.ObjMetadataSet{ - object.UnstructuredToObjMetadata(podDeletionPrevention), - testutil.ToIdentifier(t, pdbDeletePreventionManifest), - }, - expectedAbandoned: object.ObjMetadataSet{ - object.UnstructuredToObjMetadata(podDeletionPrevention), - testutil.ToIdentifier(t, pdbDeletePreventionManifest), - }, - }, - "Prevent delete annotation equals delete skipped": { - clusterObjs: []*unstructured.Unstructured{ - podDeletionPrevention, - testutil.Unstructured(t, pdbDeletePreventionManifest), - }, - pruneObjs: []*unstructured.Unstructured{ - podDeletionPrevention, - testutil.Unstructured(t, pdbDeletePreventionManifest), - }, - pruneFilters: []filter.ValidationFilter{filter.PreventRemoveFilter{}}, - options: defaultOptionsDestroy, - expectedEvents: []event.Event{ - { - Type: event.DeleteType, - DeleteEvent: event.DeleteEvent{ - Identifier: object.UnstructuredToObjMetadata(podDeletionPrevention), - Status: event.DeleteSkipped, - Object: testutil.Mutate(podDeletionPrevention.DeepCopy(), - testutil.DeleteOwningInv(t, testInventoryLabel)), - Error: testutil.EqualError(&filter.AnnotationPreventedDeletionError{ - Annotation: common.OnRemoveAnnotation, - Value: common.OnRemoveKeep, - }), - }, - }, - { - Type: event.DeleteType, - DeleteEvent: event.DeleteEvent{ - Identifier: testutil.ToIdentifier(t, pdbDeletePreventionManifest), - Status: event.DeleteSkipped, - Object: testutil.Unstructured(t, pdbDeletePreventionManifest, - testutil.DeleteOwningInv(t, testInventoryLabel)), - Error: testutil.EqualError(&filter.AnnotationPreventedDeletionError{ - Annotation: common.LifecycleDeleteAnnotation, - Value: common.PreventDeletion, - }), - }, - }, - }, - expectedSkipped: object.ObjMetadataSet{ - object.UnstructuredToObjMetadata(podDeletionPrevention), - testutil.ToIdentifier(t, pdbDeletePreventionManifest), - }, - expectedAbandoned: object.ObjMetadataSet{ - object.UnstructuredToObjMetadata(podDeletionPrevention), - testutil.ToIdentifier(t, pdbDeletePreventionManifest), - }, - }, - "Prevent delete annotation, one skipped, one pruned": { - clusterObjs: []*unstructured.Unstructured{podDeletionPrevention, pod}, - pruneObjs: []*unstructured.Unstructured{podDeletionPrevention, pod}, - pruneFilters: []filter.ValidationFilter{filter.PreventRemoveFilter{}}, - options: defaultOptions, - expectedEvents: []event.Event{ - { - Type: event.PruneType, - PruneEvent: event.PruneEvent{ - Identifier: object.UnstructuredToObjMetadata(podDeletionPrevention), - Status: event.PruneSkipped, - Object: testutil.Mutate(podDeletionPrevention.DeepCopy(), - testutil.DeleteOwningInv(t, testInventoryLabel)), - Error: testutil.EqualError(&filter.AnnotationPreventedDeletionError{ - Annotation: common.OnRemoveAnnotation, - Value: common.OnRemoveKeep, - }), - }, - }, - { - Type: event.PruneType, - PruneEvent: event.PruneEvent{ - Status: event.PruneSuccessful, - Identifier: object.UnstructuredToObjMetadata(pod), - Object: pod, - }, - }, - }, - expectedSkipped: object.ObjMetadataSet{ - object.UnstructuredToObjMetadata(podDeletionPrevention), - }, - expectedAbandoned: object.ObjMetadataSet{ - object.UnstructuredToObjMetadata(podDeletionPrevention), - }, - }, - "Namespace prune skipped": { - clusterObjs: []*unstructured.Unstructured{namespace}, - pruneObjs: []*unstructured.Unstructured{namespace}, - pruneFilters: []filter.ValidationFilter{ - filter.LocalNamespacesFilter{ - LocalNamespaces: sets.NewString(namespace.GetName()), - }, - }, - options: defaultOptions, - expectedEvents: []event.Event{ - { - Type: event.PruneType, - PruneEvent: event.PruneEvent{ - Identifier: object.UnstructuredToObjMetadata(namespace), - Status: event.PruneSkipped, - Object: namespace, - Error: testutil.EqualError(&filter.NamespaceInUseError{ - Namespace: namespace.GetName(), - }), - }, - }, - }, - expectedSkipped: object.ObjMetadataSet{ - object.UnstructuredToObjMetadata(namespace), - }, - }, - "Deletion of already deleted object": { - clusterObjs: []*unstructured.Unstructured{}, - pruneObjs: []*unstructured.Unstructured{pod}, - options: defaultOptionsDestroy, - expectedEvents: []event.Event{ - { - Type: event.DeleteType, - DeleteEvent: event.DeleteEvent{ - Identifier: object.UnstructuredToObjMetadata(pod), - Status: event.DeleteSuccessful, - Object: pod, - }, - }, - }, - }, - } - - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - // Set up the fake dynamic client to recognize all objects, and the RESTMapper. - clusterObjs := make([]runtime.Object, 0, len(tc.clusterObjs)) - for _, obj := range tc.clusterObjs { - clusterObjs = append(clusterObjs, obj) - } - pruneIDs := object.UnstructuredSetToObjMetadataSet(tc.pruneObjs) - po := Pruner{ - InvClient: inventory.NewFakeClient(pruneIDs), - Client: fake.NewSimpleDynamicClient(scheme.Scheme, clusterObjs...), - Mapper: testrestmapper.TestOnlyStaticRESTMapper(scheme.Scheme, - scheme.Scheme.PrioritizedVersionsAllGroups()...), - } - // The event channel can not block; make sure its bigger than all - // the events that can be put on it. - eventChannel := make(chan event.Event, len(tc.pruneObjs)+1) - resourceCache := cache.NewResourceCacheMap() - taskContext := taskrunner.NewTaskContext(eventChannel, resourceCache) - taskName := "test-0" - err := func() error { - defer close(eventChannel) - // Run the prune and validate. - return po.Prune(tc.pruneObjs, tc.pruneFilters, taskContext, taskName, tc.options) - }() - - if err != nil { - t.Fatalf("Unexpected error during Prune(): %#v", err) - } - var actualEvents []event.Event - for e := range eventChannel { - actualEvents = append(actualEvents, e) - } - // Inject expected GroupName for event comparison - for i := range tc.expectedEvents { - switch tc.expectedEvents[i].Type { - case event.ApplyType: - tc.expectedEvents[i].ApplyEvent.GroupName = taskName - case event.DeleteType: - tc.expectedEvents[i].DeleteEvent.GroupName = taskName - case event.PruneType: - tc.expectedEvents[i].PruneEvent.GroupName = taskName - } - } - // Validate the expected/actual events - testutil.AssertEqual(t, tc.expectedEvents, actualEvents) - - im := taskContext.InventoryManager() - - // validate record of failed prunes - for _, id := range tc.expectedFailed { - assert.Truef(t, im.IsFailedDelete(id), "Prune() should mark object as failed: %s", id) - } - for _, id := range pruneIDs.Diff(tc.expectedFailed) { - assert.Falsef(t, im.IsFailedDelete(id), "Prune() should NOT mark object as failed: %s", id) - } - // validate record of skipped prunes - for _, id := range tc.expectedSkipped { - assert.Truef(t, im.IsSkippedDelete(id), "Prune() should mark object as skipped: %s", id) - } - for _, id := range pruneIDs.Diff(tc.expectedSkipped) { - assert.Falsef(t, im.IsSkippedDelete(id), "Prune() should NOT mark object as skipped: %s", id) - } - // validate record of abandoned objects - for _, id := range tc.expectedAbandoned { - assert.Truef(t, taskContext.IsAbandonedObject(id), "Prune() should mark object as abandoned: %s", id) - } - for _, id := range pruneIDs.Diff(tc.expectedAbandoned) { - assert.Falsef(t, taskContext.IsAbandonedObject(id), "Prune() should NOT mark object as abandoned: %s", id) - } - }) - } -} - -func TestPruneDeletionPrevention(t *testing.T) { - tests := map[string]struct { - pruneObj *unstructured.Unstructured - options Options - }{ - "an object with the cli-utils.sigs.k8s.io/on-remove annotation (prune)": { - pruneObj: podDeletionPrevention, - options: defaultOptions, - }, - "an object with the cli-utils.sigs.k8s.io/on-remove annotation (destroy)": { - pruneObj: podDeletionPrevention, - options: defaultOptionsDestroy, - }, - "an object with the client.lifecycle.config.k8s.io/deletion annotation (prune)": { - pruneObj: testutil.Unstructured(t, pdbDeletePreventionManifest), - options: defaultOptions, - }, - "an object with the client.lifecycle.config.k8s.io/deletion annotation (destroy)": { - pruneObj: testutil.Unstructured(t, pdbDeletePreventionManifest), - options: defaultOptionsDestroy, - }, - } - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - pruneID := object.UnstructuredToObjMetadata(tc.pruneObj) - po := Pruner{ - InvClient: inventory.NewFakeClient(object.ObjMetadataSet{pruneID}), - Client: fake.NewSimpleDynamicClient(scheme.Scheme, tc.pruneObj), - Mapper: testrestmapper.TestOnlyStaticRESTMapper(scheme.Scheme, - scheme.Scheme.PrioritizedVersionsAllGroups()...), - } - // The event channel can not block; make sure its bigger than all - // the events that can be put on it. - eventChannel := make(chan event.Event, 2) - resourceCache := cache.NewResourceCacheMap() - taskContext := taskrunner.NewTaskContext(eventChannel, resourceCache) - err := func() error { - defer close(eventChannel) - // Run the prune and validate. - return po.Prune([]*unstructured.Unstructured{tc.pruneObj}, []filter.ValidationFilter{filter.PreventRemoveFilter{}}, taskContext, "test-0", tc.options) - }() - require.NoError(t, err) - - // verify that the object no longer has the annotation - obj, err := po.getObject(pruneID) - require.NoError(t, err) - - for annotation := range obj.GetAnnotations() { - if annotation == inventory.OwningInventoryKey { - t.Errorf("Prune() should remove the %s annotation", inventory.OwningInventoryKey) - break - } - } - - im := taskContext.InventoryManager() - - assert.Truef(t, taskContext.IsAbandonedObject(pruneID), "Prune() should mark object as abandoned") - assert.Truef(t, im.IsSkippedDelete(pruneID), "Prune() should mark object as skipped") - assert.Falsef(t, im.IsFailedDelete(pruneID), "Prune() should NOT mark object as failed") - }) - } -} - -// failureNamespaceClient wrappers around a namespaceClient with the overwriting to Get and Delete functions. -type failureNamespaceClient struct { - dynamic.ResourceInterface -} - -var _ dynamic.ResourceInterface = &failureNamespaceClient{} - -func (c *failureNamespaceClient) Delete(ctx context.Context, name string, options metav1.DeleteOptions, subresources ...string) error { - if strings.Contains(name, "delete-failure") { - return fmt.Errorf("expected delete error") - } - return nil -} - -func (c *failureNamespaceClient) Get(ctx context.Context, name string, options metav1.GetOptions, subresources ...string) (*unstructured.Unstructured, error) { - if strings.Contains(name, "get-failure") { - return nil, fmt.Errorf("expected get error") - } - return pdb, nil -} - -func TestPruneWithErrors(t *testing.T) { - tests := map[string]struct { - pruneObjs []*unstructured.Unstructured - destroy bool - expectedEvents []testutil.ExpEvent - }{ - "Prune delete failure": { - pruneObjs: []*unstructured.Unstructured{pdbDeleteFailure}, - expectedEvents: []testutil.ExpEvent{ - { - EventType: event.PruneType, - PruneEvent: &testutil.ExpPruneEvent{ - Identifier: object.UnstructuredToObjMetadata(pdbDeleteFailure), - Status: event.PruneFailed, - Error: fmt.Errorf("expected delete error"), - }, - }, - }, - }, - "Destroy delete failure": { - pruneObjs: []*unstructured.Unstructured{pdbDeleteFailure}, - destroy: true, - expectedEvents: []testutil.ExpEvent{ - { - EventType: event.DeleteType, - DeleteEvent: &testutil.ExpDeleteEvent{ - Identifier: object.UnstructuredToObjMetadata(pdbDeleteFailure), - Status: event.DeleteFailed, - Error: fmt.Errorf("expected delete error"), - }, - }, - }, - }, - } - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - pruneIDs := object.UnstructuredSetToObjMetadataSet(tc.pruneObjs) - po := Pruner{ - InvClient: inventory.NewFakeClient(pruneIDs), - // Set up the fake dynamic client to recognize all objects, and the RESTMapper. - Client: &fakeDynamicClient{ - resourceInterface: &failureNamespaceClient{}, - }, - Mapper: testrestmapper.TestOnlyStaticRESTMapper(scheme.Scheme, - scheme.Scheme.PrioritizedVersionsAllGroups()...), - } - // The event channel can not block; make sure its bigger than all - // the events that can be put on it. - eventChannel := make(chan event.Event, len(tc.pruneObjs)) - resourceCache := cache.NewResourceCacheMap() - taskContext := taskrunner.NewTaskContext(eventChannel, resourceCache) - err := func() error { - defer close(eventChannel) - var opts Options - if tc.destroy { - opts = defaultOptionsDestroy - } else { - opts = defaultOptions - } - // Run the prune and validate. - return po.Prune(tc.pruneObjs, []filter.ValidationFilter{}, taskContext, "test-0", opts) - }() - if err != nil { - t.Fatalf("Unexpected error during Prune(): %#v", err) - } - var actualEvents []event.Event - for e := range eventChannel { - actualEvents = append(actualEvents, e) - } - err = testutil.VerifyEvents(tc.expectedEvents, actualEvents) - assert.NoError(t, err) - }) - } -} - -func TestGetPruneObjs(t *testing.T) { - tests := map[string]struct { - localObjs []*unstructured.Unstructured - prevInventory []*unstructured.Unstructured - expectedObjs []*unstructured.Unstructured - }{ - "no local objects, no inventory equals no prune objs": { - localObjs: []*unstructured.Unstructured{}, - prevInventory: []*unstructured.Unstructured{}, - expectedObjs: []*unstructured.Unstructured{}, - }, - "local objects, no inventory equals no prune objs": { - localObjs: []*unstructured.Unstructured{pod, pdb, namespace}, - prevInventory: []*unstructured.Unstructured{}, - expectedObjs: []*unstructured.Unstructured{}, - }, - "no local objects, with inventory equals all prune objs": { - localObjs: []*unstructured.Unstructured{}, - prevInventory: []*unstructured.Unstructured{pod, pdb, namespace}, - expectedObjs: []*unstructured.Unstructured{pod, pdb, namespace}, - }, - "set difference equals one prune object": { - localObjs: []*unstructured.Unstructured{pod, pdb}, - prevInventory: []*unstructured.Unstructured{pdb, namespace}, - expectedObjs: []*unstructured.Unstructured{namespace}, - }, - "local and inventory the same equals no prune objects": { - localObjs: []*unstructured.Unstructured{pod, pdb}, - prevInventory: []*unstructured.Unstructured{pod, pdb}, - expectedObjs: []*unstructured.Unstructured{}, - }, - "two prune objects": { - localObjs: []*unstructured.Unstructured{pdb}, - prevInventory: []*unstructured.Unstructured{pod, pdb, namespace}, - expectedObjs: []*unstructured.Unstructured{pod, namespace}, - }, - "skip pruning objects whose resource types are unrecognized by the cluster": { - localObjs: []*unstructured.Unstructured{pdb}, - prevInventory: []*unstructured.Unstructured{testutil.Unstructured(t, crontabCRManifest), pdb, namespace}, - expectedObjs: []*unstructured.Unstructured{namespace}, - }, - "local objs, inventory disjoint means inventory is pruned": { - localObjs: []*unstructured.Unstructured{pdb}, - prevInventory: []*unstructured.Unstructured{pod, namespace}, - expectedObjs: []*unstructured.Unstructured{pod, namespace}, - }, - } - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - objs := make([]runtime.Object, 0, len(tc.prevInventory)) - for _, obj := range tc.prevInventory { - objs = append(objs, obj) - } - po := Pruner{ - InvClient: inventory.NewFakeClient(object.UnstructuredSetToObjMetadataSet(tc.prevInventory)), - Client: fake.NewSimpleDynamicClient(scheme.Scheme, objs...), - Mapper: testrestmapper.TestOnlyStaticRESTMapper(scheme.Scheme, - scheme.Scheme.PrioritizedVersionsAllGroups()...), - } - currentInventory := createInventoryInfo(tc.prevInventory...) - actualObjs, err := po.GetPruneObjs(currentInventory, tc.localObjs, Options{}) - if err != nil { - t.Fatalf("unexpected error %s returned", err) - } - if len(tc.expectedObjs) != len(actualObjs) { - t.Fatalf("expected %d prune objs, got %d", len(tc.expectedObjs), len(actualObjs)) - } - actualIDs := object.UnstructuredSetToObjMetadataSet(actualObjs) - expectedIDs := object.UnstructuredSetToObjMetadataSet(tc.expectedObjs) - if !object.ObjMetadataSetEquals(expectedIDs, actualIDs) { - t.Errorf("expected prune objects (%v), got (%v)", expectedIDs, actualIDs) - } - }) - } -} - -func TestGetObject_NoMatchError(t *testing.T) { - po := Pruner{ - Client: fake.NewSimpleDynamicClient(scheme.Scheme, pod, namespace), - Mapper: testrestmapper.TestOnlyStaticRESTMapper(scheme.Scheme, - scheme.Scheme.PrioritizedVersionsAllGroups()...), - } - _, err := po.getObject(testutil.ToIdentifier(t, crontabCRManifest)) - if err == nil { - t.Fatalf("expected GetObject() to return a NoKindMatchError, got nil") - } - if !meta.IsNoMatchError(err) { - t.Fatalf("expected GetObject() to return a NoKindMatchError, got %v", err) - } -} - -func TestGetObject_NotFoundError(t *testing.T) { - po := Pruner{ - Client: fake.NewSimpleDynamicClient(scheme.Scheme, pod, namespace), - Mapper: testrestmapper.TestOnlyStaticRESTMapper(scheme.Scheme, - scheme.Scheme.PrioritizedVersionsAllGroups()...), - } - id := object.UnstructuredToObjMetadata(pdb) - _, err := po.getObject(id) - if err == nil { - t.Fatalf("expected GetObject() to return a NotFound error, got nil") - } - if !apierrors.IsNotFound(err) { - t.Fatalf("expected GetObject() to return a NotFound error, got %v", err) - } -} - -func TestHandleDeletePrevention(t *testing.T) { - obj := testutil.Unstructured(t, pdbDeletePreventionManifest) - po := Pruner{ - Client: fake.NewSimpleDynamicClient(scheme.Scheme, obj, namespace), - Mapper: testrestmapper.TestOnlyStaticRESTMapper(scheme.Scheme, - scheme.Scheme.PrioritizedVersionsAllGroups()...), - } - var err error - obj, err = po.removeInventoryAnnotation(obj) - if err != nil { - t.Fatalf("unexpected error %s returned", err) - } - // Verify annotation removed from the local object - annotations := obj.GetAnnotations() - if annotations != nil { - if _, ok := annotations[inventory.OwningInventoryKey]; ok { - t.Fatalf("expected handleDeletePrevention() to remove the %q annotation", inventory.OwningInventoryKey) - } - } - - // Get the object from the cluster - obj, err = po.getObject(testutil.ToIdentifier(t, pdbDeletePreventionManifest)) - if err != nil { - t.Fatalf("unexpected error %s returned", err) - } - // Verify annotation removed from the remote object - annotations = obj.GetAnnotations() - if annotations != nil { - if _, ok := annotations[inventory.OwningInventoryKey]; ok { - t.Fatalf("expected handleDeletePrevention() to remove the %q annotation", inventory.OwningInventoryKey) - } - } -} - -type optionsCaptureNamespaceClient struct { - dynamic.ResourceInterface - options metav1.DeleteOptions -} - -var _ dynamic.ResourceInterface = &optionsCaptureNamespaceClient{} - -func (c *optionsCaptureNamespaceClient) Delete(_ context.Context, _ string, options metav1.DeleteOptions, _ ...string) error { - c.options = options - return nil -} - -func TestPrune_PropagationPolicy(t *testing.T) { - testCases := map[string]struct { - propagationPolicy metav1.DeletionPropagation - }{ - "background propagation policy": { - propagationPolicy: metav1.DeletePropagationBackground, - }, - "foreground propagation policy": { - propagationPolicy: metav1.DeletePropagationForeground, - }, - } - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - captureClient := &optionsCaptureNamespaceClient{} - po := Pruner{ - InvClient: inventory.NewFakeClient(object.ObjMetadataSet{}), - Client: &fakeDynamicClient{ - resourceInterface: captureClient, - }, - Mapper: testrestmapper.TestOnlyStaticRESTMapper(scheme.Scheme, - scheme.Scheme.PrioritizedVersionsAllGroups()...), - } - - eventChannel := make(chan event.Event, 1) - resourceCache := cache.NewResourceCacheMap() - taskContext := taskrunner.NewTaskContext(eventChannel, resourceCache) - err := po.Prune([]*unstructured.Unstructured{pdb}, []filter.ValidationFilter{}, taskContext, "test-0", Options{ - PropagationPolicy: tc.propagationPolicy, - }) - assert.NoError(t, err) - require.NotNil(t, captureClient.options.PropagationPolicy) - assert.Equal(t, tc.propagationPolicy, *captureClient.options.PropagationPolicy) - }) - } -} - -type fakeDynamicClient struct { - resourceInterface dynamic.ResourceInterface -} - -var _ dynamic.Interface = &fakeDynamicClient{} - -func (c *fakeDynamicClient) Resource(resource schema.GroupVersionResource) dynamic.NamespaceableResourceInterface { - return &fakeDynamicResourceClient{ - resourceInterface: c.resourceInterface, - NamespaceableResourceInterface: fake.NewSimpleDynamicClient(scheme.Scheme).Resource(resource), - } -} - -type fakeDynamicResourceClient struct { - dynamic.NamespaceableResourceInterface - resourceInterface dynamic.ResourceInterface -} - -func (c *fakeDynamicResourceClient) Namespace(ns string) dynamic.ResourceInterface { - return c.resourceInterface -} diff --git a/pkg/apply/solver/main_test.go b/pkg/apply/solver/main_test.go deleted file mode 100644 index b8f24fe0..00000000 --- a/pkg/apply/solver/main_test.go +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package solver - -import ( - "os" - "testing" - - "k8s.io/klog/v2" -) - -// TestMain executes the tests for this package, with optional logging. -// To see all logs, use: -// go test github.com/fluxcd/cli-utils/pkg/apply/solver -v -args -v=5 -func TestMain(m *testing.M) { - klog.InitFlags(nil) - os.Exit(m.Run()) -} diff --git a/pkg/apply/solver/solver.go b/pkg/apply/solver/solver.go deleted file mode 100644 index 08736e44..00000000 --- a/pkg/apply/solver/solver.go +++ /dev/null @@ -1,290 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -// The solver package is responsible for constructing a -// taskqueue based on the set of resources that should be -// applied. -// This involves setting up the appropriate sequence of -// apply, wait and prune tasks so any dependencies between -// resources doesn't cause a later apply operation to -// fail. -// Currently this package assumes that the resources have -// already been sorted in the appropriate order. We might -// want to consider moving the sorting functionality into -// this package. -package solver - -import ( - "fmt" - "time" - - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/apply/filter" - "github.com/fluxcd/cli-utils/pkg/apply/info" - "github.com/fluxcd/cli-utils/pkg/apply/mutator" - "github.com/fluxcd/cli-utils/pkg/apply/prune" - "github.com/fluxcd/cli-utils/pkg/apply/task" - "github.com/fluxcd/cli-utils/pkg/apply/taskrunner" - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/object/graph" - "github.com/fluxcd/cli-utils/pkg/object/validation" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/discovery" - "k8s.io/client-go/dynamic" - "k8s.io/klog/v2" -) - -type TaskQueueBuilder struct { - Pruner *prune.Pruner - DynamicClient dynamic.Interface - OpenAPIGetter discovery.OpenAPISchemaInterface - InfoHelper info.Helper - Mapper meta.RESTMapper - InvClient inventory.Client - // Collector is used to collect validation errors and invalid objects. - // Invalid objects will be filtered and not be injected into tasks. - Collector *validation.Collector - ApplyFilters []filter.ValidationFilter - ApplyMutators []mutator.Interface - PruneFilters []filter.ValidationFilter - - // The accumulated tasks and counter variables to name tasks. - applyCounter int - pruneCounter int - waitCounter int - - invInfo inventory.Info - applyObjs object.UnstructuredSet - pruneObjs object.UnstructuredSet -} - -type TaskQueue struct { - tasks []taskrunner.Task -} - -func (tq *TaskQueue) ToChannel() chan taskrunner.Task { - taskQueue := make(chan taskrunner.Task, len(tq.tasks)) - for _, t := range tq.tasks { - taskQueue <- t - } - return taskQueue -} - -func (tq *TaskQueue) ToActionGroups() []event.ActionGroup { - var ags []event.ActionGroup - - for _, t := range tq.tasks { - ags = append(ags, event.ActionGroup{ - Name: t.Name(), - Action: t.Action(), - Identifiers: t.Identifiers(), - }) - } - return ags -} - -type Options struct { - ServerSideOptions common.ServerSideOptions - ReconcileTimeout time.Duration - // True if we are destroying, which deletes the inventory object - // as well (possibly) the inventory namespace. - Destroy bool - // True if we're deleting prune objects - Prune bool - DryRunStrategy common.DryRunStrategy - PrunePropagationPolicy metav1.DeletionPropagation - PruneTimeout time.Duration - InventoryPolicy inventory.Policy -} - -// WithInventory sets the inventory info and returns the builder for chaining. -func (t *TaskQueueBuilder) WithInventory(inv inventory.Info) *TaskQueueBuilder { - t.invInfo = inv - return t -} - -// WithApplyObjects sets the apply objects and returns the builder for chaining. -func (t *TaskQueueBuilder) WithApplyObjects(applyObjs object.UnstructuredSet) *TaskQueueBuilder { - t.applyObjs = applyObjs - return t -} - -// WithPruneObjects sets the prune objects and returns the builder for chaining. -func (t *TaskQueueBuilder) WithPruneObjects(pruneObjs object.UnstructuredSet) *TaskQueueBuilder { - t.pruneObjs = pruneObjs - return t -} - -// Build returns the queue of tasks that have been created -func (t *TaskQueueBuilder) Build(taskContext *taskrunner.TaskContext, o Options) *TaskQueue { - var tasks []taskrunner.Task - - // reset counters - t.applyCounter = 0 - t.pruneCounter = 0 - t.waitCounter = 0 - - // Filter objects that failed earlier validation - applyObjs := t.Collector.FilterInvalidObjects(t.applyObjs) - pruneObjs := t.Collector.FilterInvalidObjects(t.pruneObjs) - - // Merge applyObjs & pruneObjs and graph them together. - // This detects implicit and explicit dependencies. - // Invalid dependency annotations will be treated as validation errors. - allObjs := make(object.UnstructuredSet, 0, len(applyObjs)+len(pruneObjs)) - allObjs = append(allObjs, applyObjs...) - allObjs = append(allObjs, pruneObjs...) - g, err := graph.DependencyGraph(allObjs) - if err != nil { - t.Collector.Collect(err) - } - // Store graph for use by DependencyFilter - taskContext.SetGraph(g) - // Sort objects into phases (apply order). - // Cycles will be treated as validation errors. - idSetList, err := g.Sort() - if err != nil { - t.Collector.Collect(err) - } - - // Filter objects with cycles or invalid dependency annotations - applyObjs = t.Collector.FilterInvalidObjects(applyObjs) - pruneObjs = t.Collector.FilterInvalidObjects(pruneObjs) - - if !o.Destroy { - // InvAddTask creates the inventory and adds any objects being applied - klog.V(2).Infof("adding inventory add task (%d objects)", len(applyObjs)) - tasks = append(tasks, &task.InvAddTask{ - TaskName: "inventory-add-0", - InvClient: t.InvClient, - InvInfo: t.invInfo, - Objects: applyObjs, - DryRun: o.DryRunStrategy, - }) - } - - if len(applyObjs) > 0 { - // Register actuation plan in the inventory - for _, id := range object.UnstructuredSetToObjMetadataSet(applyObjs) { - taskContext.InventoryManager().AddPendingApply(id) - } - - // Filter idSetList down to just apply objects - applySets := graph.HydrateSetList(idSetList, applyObjs) - - for _, applySet := range applySets { - tasks = append(tasks, - t.newApplyTask(applySet, t.ApplyFilters, t.ApplyMutators, o)) - // dry-run skips wait tasks - if !o.DryRunStrategy.ClientOrServerDryRun() { - applyIDs := object.UnstructuredSetToObjMetadataSet(applySet) - tasks = append(tasks, - t.newWaitTask(applyIDs, taskrunner.AllCurrent, o.ReconcileTimeout)) - } - } - } - - if o.Prune && len(pruneObjs) > 0 { - // Register actuation plan in the inventory - for _, id := range object.UnstructuredSetToObjMetadataSet(pruneObjs) { - taskContext.InventoryManager().AddPendingDelete(id) - } - - // Filter idSetList down to just prune objects - pruneSets := graph.HydrateSetList(idSetList, pruneObjs) - - // Reverse apply order to get prune order - graph.ReverseSetList(pruneSets) - - for _, pruneSet := range pruneSets { - tasks = append(tasks, - t.newPruneTask(pruneSet, t.PruneFilters, o)) - // dry-run skips wait tasks - if !o.DryRunStrategy.ClientOrServerDryRun() { - pruneIDs := object.UnstructuredSetToObjMetadataSet(pruneSet) - tasks = append(tasks, - t.newWaitTask(pruneIDs, taskrunner.AllNotFound, o.PruneTimeout)) - } - } - } - - prevInvIDs, _ := t.InvClient.GetClusterObjs(t.invInfo) - klog.V(2).Infoln("adding delete/update inventory task") - var taskName string - if o.Destroy { - taskName = "inventory-delete-or-update-0" - } else { - taskName = "inventory-set-0" - } - tasks = append(tasks, &task.DeleteOrUpdateInvTask{ - TaskName: taskName, - InvClient: t.InvClient, - InvInfo: t.invInfo, - PrevInventory: prevInvIDs, - DryRun: o.DryRunStrategy, - Destroy: o.Destroy, - }) - - return &TaskQueue{tasks: tasks} -} - -// AppendApplyTask appends a task to the task queue to apply the passed objects -// to the cluster. Returns a pointer to the Builder to chain function calls. -func (t *TaskQueueBuilder) newApplyTask(applyObjs object.UnstructuredSet, - applyFilters []filter.ValidationFilter, applyMutators []mutator.Interface, o Options) taskrunner.Task { - applyObjs = t.Collector.FilterInvalidObjects(applyObjs) - klog.V(2).Infof("adding apply task (%d objects)", len(applyObjs)) - task := &task.ApplyTask{ - TaskName: fmt.Sprintf("apply-%d", t.applyCounter), - Objects: applyObjs, - Filters: applyFilters, - Mutators: applyMutators, - ServerSideOptions: o.ServerSideOptions, - DryRunStrategy: o.DryRunStrategy, - DynamicClient: t.DynamicClient, - OpenAPIGetter: t.OpenAPIGetter, - InfoHelper: t.InfoHelper, - Mapper: t.Mapper, - } - t.applyCounter++ - return task -} - -// AppendWaitTask appends a task to wait on the passed objects to the task queue. -// Returns a pointer to the Builder to chain function calls. -func (t *TaskQueueBuilder) newWaitTask(waitIDs object.ObjMetadataSet, condition taskrunner.Condition, - waitTimeout time.Duration) taskrunner.Task { - waitIDs = t.Collector.FilterInvalidIds(waitIDs) - klog.V(2).Infoln("adding wait task") - task := taskrunner.NewWaitTask( - fmt.Sprintf("wait-%d", t.waitCounter), - waitIDs, - condition, - waitTimeout, - t.Mapper, - ) - t.waitCounter++ - return task -} - -// AppendPruneTask appends a task to delete objects from the cluster to the task queue. -// Returns a pointer to the Builder to chain function calls. -func (t *TaskQueueBuilder) newPruneTask(pruneObjs object.UnstructuredSet, - pruneFilters []filter.ValidationFilter, o Options) taskrunner.Task { - pruneObjs = t.Collector.FilterInvalidObjects(pruneObjs) - klog.V(2).Infof("adding prune task (%d objects)", len(pruneObjs)) - task := &task.PruneTask{ - TaskName: fmt.Sprintf("prune-%d", t.pruneCounter), - Objects: pruneObjs, - Filters: pruneFilters, - Pruner: t.Pruner, - PropagationPolicy: o.PrunePropagationPolicy, - DryRunStrategy: o.DryRunStrategy, - Destroy: o.Destroy, - } - t.pruneCounter++ - return task -} diff --git a/pkg/apply/solver/solver_test.go b/pkg/apply/solver/solver_test.go deleted file mode 100644 index 92805624..00000000 --- a/pkg/apply/solver/solver_test.go +++ /dev/null @@ -1,1904 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package solver - -import ( - "testing" - "time" - - "github.com/fluxcd/cli-utils/pkg/apis/actuation" - "github.com/fluxcd/cli-utils/pkg/apply/prune" - "github.com/fluxcd/cli-utils/pkg/apply/task" - "github.com/fluxcd/cli-utils/pkg/apply/taskrunner" - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/object/graph" - "github.com/fluxcd/cli-utils/pkg/object/validation" - "github.com/fluxcd/cli-utils/pkg/testutil" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - "github.com/stretchr/testify/assert" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -var ( - pruner = &prune.Pruner{} - resources = map[string]string{ - "pod": ` -kind: Pod -apiVersion: v1 -metadata: - name: test-pod - namespace: test-namespace -`, - "default-pod": ` -kind: Pod -apiVersion: v1 -metadata: - name: pod-in-default-namespace - namespace: default -`, - "deployment": ` -kind: Deployment -apiVersion: apps/v1 -metadata: - name: foo - namespace: test-namespace - uid: dep-uid - generation: 1 -spec: - replicas: 1 -`, - "secret": ` -kind: Secret -apiVersion: v1 -metadata: - name: secret - namespace: test-namespace - uid: secret-uid - generation: 1 -type: Opaque -spec: - foo: bar -`, - "namespace": ` -kind: Namespace -apiVersion: v1 -metadata: - name: test-namespace -`, - - "crd": ` -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: crontabs.stable.example.com -spec: - group: stable.example.com - versions: - - name: v1 - served: true - storage: true - scope: Namespaced - names: - plural: crontabs - singular: crontab - kind: CronTab -`, - "crontab1": ` -apiVersion: "stable.example.com/v1" -kind: CronTab -metadata: - name: cron-tab-01 - namespace: test-namespace -`, - "crontab2": ` -apiVersion: "stable.example.com/v1" -kind: CronTab -metadata: - name: cron-tab-02 - namespace: test-namespace -`, - } -) - -func newInvObject(name, namespace, inventoryID string) *unstructured.Unstructured { - return &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "v1", - "kind": "ConfigMap", - "metadata": map[string]interface{}{ - "name": name, - "namespace": namespace, - "labels": map[string]interface{}{ - common.InventoryLabel: inventoryID, - }, - }, - "data": map[string]string{}, - }, - } -} - -func TestTaskQueueBuilder_ApplyBuild(t *testing.T) { - // Use a custom Asserter to customize the comparison options - asserter := testutil.NewAsserter( - cmpopts.EquateErrors(), - waitTaskComparer(), - fakeClientComparer(), - inventoryInfoComparer(), - ) - - invInfo := inventory.WrapInventoryInfoObj(newInvObject( - "abc-123", "default", "test")) - - testCases := map[string]struct { - applyObjs []*unstructured.Unstructured - options Options - expectedTasks []taskrunner.Task - expectedError error - expectedStatus []actuation.ObjectStatus - }{ - "no resources, no apply or wait tasks": { - applyObjs: []*unstructured.Unstructured{}, - expectedTasks: []taskrunner.Task{ - &task.InvAddTask{ - TaskName: "inventory-add-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - Objects: object.UnstructuredSet{}, - }, - &task.DeleteOrUpdateInvTask{ - TaskName: "inventory-set-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - PrevInventory: object.ObjMetadataSet{}, - }, - }, - }, - "single resource, one apply task, one wait task": { - applyObjs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["deployment"]), - }, - expectedTasks: []taskrunner.Task{ - &task.InvAddTask{ - TaskName: "inventory-add-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - Objects: object.UnstructuredSet{ - testutil.Unstructured(t, resources["deployment"]), - }, - }, - &task.ApplyTask{ - TaskName: "apply-0", - Objects: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["deployment"]), - }, - }, - &taskrunner.WaitTask{ - TaskName: "wait-0", - Ids: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["deployment"]), - }, - Condition: taskrunner.AllCurrent, - }, - &task.DeleteOrUpdateInvTask{ - TaskName: "inventory-set-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - PrevInventory: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["deployment"]), - }, - }, - }, - expectedStatus: []actuation.ObjectStatus{ - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["deployment"]), - ), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - }, - }, - "multiple resource with no timeout": { - applyObjs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["deployment"]), - testutil.Unstructured(t, resources["secret"]), - }, - expectedTasks: []taskrunner.Task{ - &task.InvAddTask{ - TaskName: "inventory-add-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - Objects: object.UnstructuredSet{ - testutil.Unstructured(t, resources["deployment"]), - testutil.Unstructured(t, resources["secret"]), - }, - }, - &task.ApplyTask{ - TaskName: "apply-0", - Objects: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["deployment"]), - testutil.Unstructured(t, resources["secret"]), - }, - DryRunStrategy: common.DryRunNone, - }, - &taskrunner.WaitTask{ - TaskName: "wait-0", - Ids: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["deployment"]), - testutil.ToIdentifier(t, resources["secret"]), - }, - Condition: taskrunner.AllCurrent, - }, - &task.DeleteOrUpdateInvTask{ - TaskName: "inventory-set-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - PrevInventory: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["deployment"]), - testutil.ToIdentifier(t, resources["secret"]), - }, - }, - }, - expectedStatus: []actuation.ObjectStatus{ - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["deployment"]), - ), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["secret"]), - ), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - }, - }, - "multiple resources with reconcile timeout": { - applyObjs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["deployment"]), - testutil.Unstructured(t, resources["secret"]), - }, - options: Options{ - ReconcileTimeout: 1 * time.Minute, - }, - expectedTasks: []taskrunner.Task{ - &task.InvAddTask{ - TaskName: "inventory-add-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - Objects: object.UnstructuredSet{ - testutil.Unstructured(t, resources["secret"]), - testutil.Unstructured(t, resources["deployment"]), - }, - }, - &task.ApplyTask{ - TaskName: "apply-0", - Objects: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["secret"]), - testutil.Unstructured(t, resources["deployment"]), - }, - DryRunStrategy: common.DryRunNone, - }, - &taskrunner.WaitTask{ - TaskName: "wait-0", - Ids: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["secret"]), - testutil.ToIdentifier(t, resources["deployment"]), - }, - Condition: taskrunner.AllCurrent, - Timeout: 1 * time.Minute, - }, - &task.DeleteOrUpdateInvTask{ - TaskName: "inventory-set-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - PrevInventory: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["secret"]), - testutil.ToIdentifier(t, resources["deployment"]), - }, - }, - }, - expectedStatus: []actuation.ObjectStatus{ - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["deployment"]), - ), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["secret"]), - ), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - }, - }, - "multiple resources with reconcile timeout and dryrun": { - applyObjs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["deployment"]), - testutil.Unstructured(t, resources["secret"]), - }, - options: Options{ - ReconcileTimeout: time.Minute, - DryRunStrategy: common.DryRunClient, - }, - // No wait task, since it is dry run - expectedTasks: []taskrunner.Task{ - &task.InvAddTask{ - TaskName: "inventory-add-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - Objects: object.UnstructuredSet{ - testutil.Unstructured(t, resources["deployment"]), - testutil.Unstructured(t, resources["secret"]), - }, - DryRun: common.DryRunClient, - }, - &task.ApplyTask{ - TaskName: "apply-0", - Objects: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["deployment"]), - testutil.Unstructured(t, resources["secret"]), - }, - DryRunStrategy: common.DryRunClient, - }, - &task.DeleteOrUpdateInvTask{ - TaskName: "inventory-set-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - PrevInventory: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["deployment"]), - testutil.ToIdentifier(t, resources["secret"]), - }, - DryRun: common.DryRunClient, - }, - }, - expectedStatus: []actuation.ObjectStatus{ - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["deployment"]), - ), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["secret"]), - ), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - }, - }, - "multiple resources with reconcile timeout and server-dryrun": { - applyObjs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["pod"]), - testutil.Unstructured(t, resources["default-pod"]), - }, - options: Options{ - ReconcileTimeout: time.Minute, - DryRunStrategy: common.DryRunServer, - }, - // No wait task, since it is dry run - expectedTasks: []taskrunner.Task{ - &task.InvAddTask{ - TaskName: "inventory-add-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - Objects: object.UnstructuredSet{ - testutil.Unstructured(t, resources["pod"]), - testutil.Unstructured(t, resources["default-pod"]), - }, - DryRun: common.DryRunServer, - }, - &task.ApplyTask{ - TaskName: "apply-0", - Objects: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["pod"]), - testutil.Unstructured(t, resources["default-pod"]), - }, - DryRunStrategy: common.DryRunServer, - }, - &task.DeleteOrUpdateInvTask{ - TaskName: "inventory-set-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - PrevInventory: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["pod"]), - testutil.ToIdentifier(t, resources["default-pod"]), - }, - DryRun: common.DryRunServer, - }, - }, - expectedStatus: []actuation.ObjectStatus{ - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["pod"]), - ), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["default-pod"]), - ), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - }, - }, - "multiple resources including CRD": { - applyObjs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["crontab1"]), - testutil.Unstructured(t, resources["crd"]), - testutil.Unstructured(t, resources["crontab2"]), - }, - expectedTasks: []taskrunner.Task{ - &task.InvAddTask{ - TaskName: "inventory-add-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - Objects: object.UnstructuredSet{ - testutil.Unstructured(t, resources["crontab1"]), - testutil.Unstructured(t, resources["crd"]), - testutil.Unstructured(t, resources["crontab2"]), - }, - }, - &task.ApplyTask{ - TaskName: "apply-0", - Objects: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["crd"]), - }, - DryRunStrategy: common.DryRunNone, - }, - &taskrunner.WaitTask{ - TaskName: "wait-0", - Ids: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["crd"]), - }, - Condition: taskrunner.AllCurrent, - }, - &task.ApplyTask{ - TaskName: "apply-1", - Objects: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["crontab1"]), - testutil.Unstructured(t, resources["crontab2"]), - }, - DryRunStrategy: common.DryRunNone, - }, - &taskrunner.WaitTask{ - TaskName: "wait-1", - Ids: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["crontab1"]), - testutil.ToIdentifier(t, resources["crontab2"]), - }, - Condition: taskrunner.AllCurrent, - }, - &task.DeleteOrUpdateInvTask{ - TaskName: "inventory-set-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - PrevInventory: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["crontab1"]), - testutil.ToIdentifier(t, resources["crd"]), - testutil.ToIdentifier(t, resources["crontab2"]), - }, - }, - }, - expectedStatus: []actuation.ObjectStatus{ - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["crontab1"]), - ), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["crd"]), - ), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["crontab2"]), - ), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - }, - }, - "no wait with CRDs if it is a dryrun": { - applyObjs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["crontab1"]), - testutil.Unstructured(t, resources["crd"]), - testutil.Unstructured(t, resources["crontab2"]), - }, - options: Options{ - ReconcileTimeout: time.Minute, - DryRunStrategy: common.DryRunClient, - }, - expectedTasks: []taskrunner.Task{ - &task.InvAddTask{ - TaskName: "inventory-add-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - Objects: object.UnstructuredSet{ - testutil.Unstructured(t, resources["crontab1"]), - testutil.Unstructured(t, resources["crd"]), - testutil.Unstructured(t, resources["crontab2"]), - }, - DryRun: common.DryRunClient, - }, - &task.ApplyTask{ - TaskName: "apply-0", - Objects: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["crd"]), - }, - DryRunStrategy: common.DryRunClient, - }, - &task.ApplyTask{ - TaskName: "apply-1", - Objects: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["crontab1"]), - testutil.Unstructured(t, resources["crontab2"]), - }, - DryRunStrategy: common.DryRunClient, - }, - &task.DeleteOrUpdateInvTask{ - TaskName: "inventory-set-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - PrevInventory: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["crontab1"]), - testutil.ToIdentifier(t, resources["crd"]), - testutil.ToIdentifier(t, resources["crontab2"]), - }, - DryRun: common.DryRunClient, - }, - }, - expectedStatus: []actuation.ObjectStatus{ - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["crontab1"]), - ), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["crd"]), - ), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["crontab2"]), - ), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - }, - }, - "resources in namespace creates multiple apply tasks": { - applyObjs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["namespace"]), - testutil.Unstructured(t, resources["pod"]), - testutil.Unstructured(t, resources["secret"]), - }, - expectedTasks: []taskrunner.Task{ - &task.InvAddTask{ - TaskName: "inventory-add-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - Objects: object.UnstructuredSet{ - testutil.Unstructured(t, resources["namespace"]), - testutil.Unstructured(t, resources["pod"]), - testutil.Unstructured(t, resources["secret"]), - }, - }, - &task.ApplyTask{ - TaskName: "apply-0", - Objects: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["namespace"]), - }, - DryRunStrategy: common.DryRunNone, - }, - &taskrunner.WaitTask{ - TaskName: "wait-0", - Ids: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["namespace"]), - }, - Condition: taskrunner.AllCurrent, - }, - &task.ApplyTask{ - TaskName: "apply-1", - Objects: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["secret"]), - testutil.Unstructured(t, resources["pod"]), - }, - DryRunStrategy: common.DryRunNone, - }, - &taskrunner.WaitTask{ - TaskName: "wait-1", - Ids: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["secret"]), - testutil.ToIdentifier(t, resources["pod"]), - }, - Condition: taskrunner.AllCurrent, - }, - &task.DeleteOrUpdateInvTask{ - TaskName: "inventory-set-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - PrevInventory: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["namespace"]), - testutil.ToIdentifier(t, resources["pod"]), - testutil.ToIdentifier(t, resources["secret"]), - }, - }, - }, - expectedStatus: []actuation.ObjectStatus{ - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["namespace"]), - ), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["pod"]), - ), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["secret"]), - ), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - }, - }, - "deployment depends on secret creates multiple tasks": { - applyObjs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["deployment"], - testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), - testutil.Unstructured(t, resources["secret"]), - }, - expectedTasks: []taskrunner.Task{ - &task.InvAddTask{ - TaskName: "inventory-add-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - Objects: object.UnstructuredSet{ - testutil.Unstructured(t, resources["deployment"], - testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), - testutil.Unstructured(t, resources["secret"]), - }, - }, - &task.ApplyTask{ - TaskName: "apply-0", - Objects: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["secret"]), - }, - DryRunStrategy: common.DryRunNone, - }, - &taskrunner.WaitTask{ - TaskName: "wait-0", - Ids: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["secret"]), - }, - Condition: taskrunner.AllCurrent, - }, - &task.ApplyTask{ - TaskName: "apply-1", - Objects: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["deployment"], - testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), - }, - DryRunStrategy: common.DryRunNone, - }, - &taskrunner.WaitTask{ - TaskName: "wait-1", - Ids: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["deployment"]), - }, - Condition: taskrunner.AllCurrent, - }, - &task.DeleteOrUpdateInvTask{ - TaskName: "inventory-set-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - PrevInventory: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["deployment"]), - testutil.ToIdentifier(t, resources["secret"]), - }, - }, - }, - expectedStatus: []actuation.ObjectStatus{ - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["deployment"]), - ), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["secret"]), - ), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - }, - }, - "cyclic dependency returns error": { - applyObjs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["deployment"], - testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), - testutil.Unstructured(t, resources["secret"], - testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["deployment"]))), - }, - expectedTasks: []taskrunner.Task{}, - expectedError: validation.NewError( - graph.CyclicDependencyError{ - Edges: []graph.Edge{ - { - From: testutil.ToIdentifier(t, resources["secret"]), - To: testutil.ToIdentifier(t, resources["deployment"]), - }, - { - From: testutil.ToIdentifier(t, resources["deployment"]), - To: testutil.ToIdentifier(t, resources["secret"]), - }, - }, - }, - testutil.ToIdentifier(t, resources["secret"]), - testutil.ToIdentifier(t, resources["deployment"]), - ), - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - mapper := testutil.NewFakeRESTMapper() - // inject mapper for equality comparison - for _, t := range tc.expectedTasks { - switch typedTask := t.(type) { - case *task.ApplyTask: - typedTask.Mapper = mapper - case *taskrunner.WaitTask: - typedTask.Mapper = mapper - } - } - - applyIDs := object.UnstructuredSetToObjMetadataSet(tc.applyObjs) - fakeInvClient := inventory.NewFakeClient(applyIDs) - vCollector := &validation.Collector{} - tqb := TaskQueueBuilder{ - Pruner: pruner, - Mapper: mapper, - InvClient: fakeInvClient, - Collector: vCollector, - } - taskContext := taskrunner.NewTaskContext(nil, nil) - tq := tqb.WithInventory(invInfo). - WithApplyObjects(tc.applyObjs). - Build(taskContext, tc.options) - err := vCollector.ToError() - if tc.expectedError != nil { - assert.EqualError(t, err, tc.expectedError.Error()) - return - } - assert.NoError(t, err) - asserter.Equal(t, tc.expectedTasks, tq.tasks) - - actualStatus := taskContext.InventoryManager().Inventory().Status.Objects - testutil.AssertEqual(t, tc.expectedStatus, actualStatus) - }) - } -} - -func TestTaskQueueBuilder_PruneBuild(t *testing.T) { - // Use a custom Asserter to customize the comparison options - asserter := testutil.NewAsserter( - cmpopts.EquateErrors(), - waitTaskComparer(), - fakeClientComparer(), - inventoryInfoComparer(), - ) - - invInfo := inventory.WrapInventoryInfoObj(newInvObject( - "abc-123", "default", "test")) - - testCases := map[string]struct { - pruneObjs []*unstructured.Unstructured - options Options - expectedTasks []taskrunner.Task - expectedError error - expectedStatus []actuation.ObjectStatus - }{ - "no resources, no apply or prune tasks": { - pruneObjs: []*unstructured.Unstructured{}, - options: Options{Prune: true}, - expectedTasks: []taskrunner.Task{ - &task.InvAddTask{ - TaskName: "inventory-add-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - Objects: object.UnstructuredSet{}, - }, - &task.DeleteOrUpdateInvTask{ - TaskName: "inventory-set-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - PrevInventory: object.ObjMetadataSet{}, - }, - }, - }, - "single resource, one prune task, one wait task": { - pruneObjs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["default-pod"]), - }, - options: Options{Prune: true}, - expectedTasks: []taskrunner.Task{ - &task.InvAddTask{ - TaskName: "inventory-add-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - Objects: object.UnstructuredSet{}, - }, - &task.PruneTask{ - TaskName: "prune-0", - Objects: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["default-pod"]), - }, - }, - &taskrunner.WaitTask{ - TaskName: "wait-0", - Ids: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["default-pod"]), - }, - Condition: taskrunner.AllNotFound, - }, - &task.DeleteOrUpdateInvTask{ - TaskName: "inventory-set-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - PrevInventory: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["default-pod"]), - }, - }, - }, - expectedStatus: []actuation.ObjectStatus{ - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["default-pod"]), - ), - Strategy: actuation.ActuationStrategyDelete, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - }, - }, - "multiple resources, one prune task, one wait task": { - pruneObjs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["default-pod"]), - testutil.Unstructured(t, resources["pod"]), - }, - options: Options{Prune: true}, - expectedTasks: []taskrunner.Task{ - &task.InvAddTask{ - TaskName: "inventory-add-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - Objects: object.UnstructuredSet{}, - }, - &task.PruneTask{ - TaskName: "prune-0", - Objects: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["default-pod"]), - testutil.Unstructured(t, resources["pod"]), - }, - }, - &taskrunner.WaitTask{ - TaskName: "wait-0", - Ids: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["default-pod"]), - testutil.ToIdentifier(t, resources["pod"]), - }, - Condition: taskrunner.AllNotFound, - }, - &task.DeleteOrUpdateInvTask{ - TaskName: "inventory-set-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - PrevInventory: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["default-pod"]), - testutil.ToIdentifier(t, resources["pod"]), - }, - }, - }, - expectedStatus: []actuation.ObjectStatus{ - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["default-pod"]), - ), - Strategy: actuation.ActuationStrategyDelete, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["pod"]), - ), - Strategy: actuation.ActuationStrategyDelete, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - }, - }, - "dependent resources, two prune tasks, two wait tasks": { - pruneObjs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["pod"], - testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), - testutil.Unstructured(t, resources["secret"]), - }, - options: Options{Prune: true}, - // Opposite ordering when pruning/deleting - expectedTasks: []taskrunner.Task{ - &task.InvAddTask{ - TaskName: "inventory-add-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - Objects: object.UnstructuredSet{}, - }, - &task.PruneTask{ - TaskName: "prune-0", - Objects: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["pod"], - testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), - }, - }, - &taskrunner.WaitTask{ - TaskName: "wait-0", - Ids: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["pod"]), - }, - Condition: taskrunner.AllNotFound, - }, - &task.PruneTask{ - TaskName: "prune-1", - Objects: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["secret"]), - }, - }, - &taskrunner.WaitTask{ - TaskName: "wait-1", - Ids: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["secret"]), - }, - Condition: taskrunner.AllNotFound, - }, - &task.DeleteOrUpdateInvTask{ - TaskName: "inventory-set-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - PrevInventory: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["pod"]), - testutil.ToIdentifier(t, resources["secret"]), - }, - }, - }, - expectedStatus: []actuation.ObjectStatus{ - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["pod"]), - ), - Strategy: actuation.ActuationStrategyDelete, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["secret"]), - ), - Strategy: actuation.ActuationStrategyDelete, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - }, - }, - "single resource with prune timeout has wait task": { - pruneObjs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["pod"]), - }, - options: Options{ - Prune: true, - PruneTimeout: 3 * time.Minute, - }, - expectedTasks: []taskrunner.Task{ - &task.InvAddTask{ - TaskName: "inventory-add-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - Objects: object.UnstructuredSet{}, - }, - &task.PruneTask{ - TaskName: "prune-0", - Objects: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["pod"]), - }, - }, - &taskrunner.WaitTask{ - TaskName: "wait-0", - Ids: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["pod"]), - }, - Condition: taskrunner.AllNotFound, - Timeout: 3 * time.Minute, - }, - &task.DeleteOrUpdateInvTask{ - TaskName: "inventory-set-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - PrevInventory: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["pod"]), - }, - }, - }, - expectedStatus: []actuation.ObjectStatus{ - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["pod"]), - ), - Strategy: actuation.ActuationStrategyDelete, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - }, - }, - "multiple resources with prune timeout and server-dryrun": { - pruneObjs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["pod"]), - testutil.Unstructured(t, resources["default-pod"]), - }, - options: Options{ - PruneTimeout: time.Minute, - DryRunStrategy: common.DryRunServer, - Prune: true, - }, - // No wait task, since it is dry run - expectedTasks: []taskrunner.Task{ - &task.InvAddTask{ - TaskName: "inventory-add-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - Objects: object.UnstructuredSet{}, - DryRun: common.DryRunServer, - }, - &task.PruneTask{ - TaskName: "prune-0", - Objects: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["pod"]), - testutil.Unstructured(t, resources["default-pod"]), - }, - DryRunStrategy: common.DryRunServer, - }, - &task.DeleteOrUpdateInvTask{ - TaskName: "inventory-set-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - PrevInventory: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["pod"]), - testutil.ToIdentifier(t, resources["default-pod"]), - }, - DryRun: common.DryRunServer, - }, - }, - expectedStatus: []actuation.ObjectStatus{ - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["pod"]), - ), - Strategy: actuation.ActuationStrategyDelete, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["default-pod"]), - ), - Strategy: actuation.ActuationStrategyDelete, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - }, - }, - "multiple resources including CRD": { - pruneObjs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["crontab1"]), - testutil.Unstructured(t, resources["crd"]), - testutil.Unstructured(t, resources["crontab2"]), - }, - options: Options{Prune: true}, - // Opposite ordering when pruning/deleting. - expectedTasks: []taskrunner.Task{ - &task.InvAddTask{ - TaskName: "inventory-add-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - Objects: object.UnstructuredSet{}, - }, - &task.PruneTask{ - TaskName: "prune-0", - Objects: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["crontab1"]), - testutil.Unstructured(t, resources["crontab2"]), - }, - }, - &taskrunner.WaitTask{ - TaskName: "wait-0", - Ids: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["crontab1"]), - testutil.ToIdentifier(t, resources["crontab2"]), - }, - Condition: taskrunner.AllNotFound, - }, - &task.PruneTask{ - TaskName: "prune-1", - Objects: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["crd"]), - }, - }, - &taskrunner.WaitTask{ - TaskName: "wait-1", - Ids: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["crd"]), - }, - Condition: taskrunner.AllNotFound, - }, - &task.DeleteOrUpdateInvTask{ - TaskName: "inventory-set-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - PrevInventory: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["crontab1"]), - testutil.ToIdentifier(t, resources["crd"]), - testutil.ToIdentifier(t, resources["crontab2"]), - }, - }, - }, - expectedStatus: []actuation.ObjectStatus{ - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["crontab1"]), - ), - Strategy: actuation.ActuationStrategyDelete, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["crd"]), - ), - Strategy: actuation.ActuationStrategyDelete, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["crontab2"]), - ), - Strategy: actuation.ActuationStrategyDelete, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - }, - }, - "no wait with CRDs if it is a dryrun": { - pruneObjs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["crontab1"]), - testutil.Unstructured(t, resources["crd"]), - testutil.Unstructured(t, resources["crontab2"]), - }, - options: Options{ - ReconcileTimeout: time.Minute, - DryRunStrategy: common.DryRunClient, - Prune: true, - }, - expectedTasks: []taskrunner.Task{ - &task.InvAddTask{ - TaskName: "inventory-add-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - Objects: object.UnstructuredSet{}, - DryRun: common.DryRunClient, - }, - &task.PruneTask{ - TaskName: "prune-0", - Objects: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["crontab1"]), - testutil.Unstructured(t, resources["crontab2"]), - }, - DryRunStrategy: common.DryRunClient, - }, - &task.PruneTask{ - TaskName: "prune-1", - Objects: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["crd"]), - }, - DryRunStrategy: common.DryRunClient, - }, - &task.DeleteOrUpdateInvTask{ - TaskName: "inventory-set-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - PrevInventory: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["crontab1"]), - testutil.ToIdentifier(t, resources["crd"]), - testutil.ToIdentifier(t, resources["crontab2"]), - }, - DryRun: common.DryRunClient, - }, - }, - expectedStatus: []actuation.ObjectStatus{ - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["crontab1"]), - ), - Strategy: actuation.ActuationStrategyDelete, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["crd"]), - ), - Strategy: actuation.ActuationStrategyDelete, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["crontab2"]), - ), - Strategy: actuation.ActuationStrategyDelete, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - }, - }, - "resources in namespace creates multiple apply tasks": { - pruneObjs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["namespace"]), - testutil.Unstructured(t, resources["pod"]), - testutil.Unstructured(t, resources["secret"]), - }, - options: Options{Prune: true}, - expectedTasks: []taskrunner.Task{ - &task.InvAddTask{ - TaskName: "inventory-add-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - Objects: object.UnstructuredSet{}, - }, - &task.PruneTask{ - TaskName: "prune-0", - Objects: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["pod"]), - testutil.Unstructured(t, resources["secret"]), - }, - }, - &taskrunner.WaitTask{ - TaskName: "wait-0", - Ids: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["pod"]), - testutil.ToIdentifier(t, resources["secret"]), - }, - Condition: taskrunner.AllNotFound, - }, - &task.PruneTask{ - TaskName: "prune-1", - Objects: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["namespace"]), - }, - }, - &taskrunner.WaitTask{ - TaskName: "wait-1", - Ids: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["namespace"]), - }, - Condition: taskrunner.AllNotFound, - }, - &task.DeleteOrUpdateInvTask{ - TaskName: "inventory-set-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - PrevInventory: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["namespace"]), - testutil.ToIdentifier(t, resources["pod"]), - testutil.ToIdentifier(t, resources["secret"]), - }, - }, - }, - expectedStatus: []actuation.ObjectStatus{ - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["namespace"]), - ), - Strategy: actuation.ActuationStrategyDelete, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["pod"]), - ), - Strategy: actuation.ActuationStrategyDelete, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["secret"]), - ), - Strategy: actuation.ActuationStrategyDelete, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - }, - }, - "cyclic dependency": { - pruneObjs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["deployment"], - testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), - testutil.Unstructured(t, resources["secret"], - testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["deployment"]))), - }, - options: Options{Prune: true}, - expectedTasks: []taskrunner.Task{}, - expectedError: validation.NewError( - graph.CyclicDependencyError{ - Edges: []graph.Edge{ - { - From: testutil.ToIdentifier(t, resources["secret"]), - To: testutil.ToIdentifier(t, resources["deployment"]), - }, - { - From: testutil.ToIdentifier(t, resources["deployment"]), - To: testutil.ToIdentifier(t, resources["secret"]), - }, - }, - }, - testutil.ToIdentifier(t, resources["secret"]), - testutil.ToIdentifier(t, resources["deployment"]), - ), - }, - "cyclic dependency and valid": { - pruneObjs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["deployment"], - testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), - testutil.Unstructured(t, resources["secret"], - testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["deployment"]))), - testutil.Unstructured(t, resources["pod"]), - }, - options: Options{Prune: true}, - expectedTasks: []taskrunner.Task{ - &task.InvAddTask{ - TaskName: "inventory-add-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - Objects: object.UnstructuredSet{}, - }, - &task.PruneTask{ - TaskName: "prune-0", - Objects: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["pod"]), - }, - }, - taskrunner.NewWaitTask( - "wait-0", - object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["pod"]), - }, - taskrunner.AllCurrent, 1*time.Second, - testutil.NewFakeRESTMapper(), - ), - &task.DeleteOrUpdateInvTask{ - TaskName: "inventory-set-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - PrevInventory: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["pod"]), - }, - }, - }, - expectedError: validation.NewError( - graph.CyclicDependencyError{ - Edges: []graph.Edge{ - { - From: testutil.ToIdentifier(t, resources["secret"]), - To: testutil.ToIdentifier(t, resources["deployment"]), - }, - { - From: testutil.ToIdentifier(t, resources["deployment"]), - To: testutil.ToIdentifier(t, resources["secret"]), - }, - }, - }, - testutil.ToIdentifier(t, resources["secret"]), - testutil.ToIdentifier(t, resources["deployment"]), - ), - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - mapper := testutil.NewFakeRESTMapper() - // inject mapper & pruner for equality comparison - for _, t := range tc.expectedTasks { - switch typedTask := t.(type) { - case *task.PruneTask: - typedTask.Pruner = &prune.Pruner{} - case *taskrunner.WaitTask: - typedTask.Mapper = mapper - } - } - - pruneIDs := object.UnstructuredSetToObjMetadataSet(tc.pruneObjs) - fakeInvClient := inventory.NewFakeClient(pruneIDs) - vCollector := &validation.Collector{} - tqb := TaskQueueBuilder{ - Pruner: pruner, - Mapper: mapper, - InvClient: fakeInvClient, - Collector: vCollector, - } - taskContext := taskrunner.NewTaskContext(nil, nil) - tq := tqb.WithInventory(invInfo). - WithPruneObjects(tc.pruneObjs). - Build(taskContext, tc.options) - err := vCollector.ToError() - if tc.expectedError != nil { - assert.EqualError(t, err, tc.expectedError.Error()) - return - } - assert.NoError(t, err) - asserter.Equal(t, tc.expectedTasks, tq.tasks) - - actualStatus := taskContext.InventoryManager().Inventory().Status.Objects - testutil.AssertEqual(t, tc.expectedStatus, actualStatus) - }) - } -} - -func TestTaskQueueBuilder_ApplyPruneBuild(t *testing.T) { - // Use a custom Asserter to customize the comparison options - asserter := testutil.NewAsserter( - cmpopts.EquateErrors(), - waitTaskComparer(), - fakeClientComparer(), - inventoryInfoComparer(), - ) - - invInfo := inventory.WrapInventoryInfoObj(newInvObject( - "abc-123", "default", "test")) - - testCases := map[string]struct { - inventoryIDs object.ObjMetadataSet - applyObjs object.UnstructuredSet - pruneObjs object.UnstructuredSet - options Options - expectedTasks []taskrunner.Task - expectedError error - expectedStatus []actuation.ObjectStatus - }{ - "two resources, one apply, one prune": { - inventoryIDs: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["secret"]), - }, - applyObjs: object.UnstructuredSet{ - testutil.Unstructured(t, resources["deployment"]), - }, - pruneObjs: object.UnstructuredSet{ - testutil.Unstructured(t, resources["secret"]), - }, - options: Options{Prune: true}, - expectedTasks: []taskrunner.Task{ - &task.InvAddTask{ - TaskName: "inventory-add-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - Objects: object.UnstructuredSet{ - testutil.Unstructured(t, resources["deployment"]), - }, - }, - &task.ApplyTask{ - TaskName: "apply-0", - Objects: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["deployment"]), - }, - }, - &taskrunner.WaitTask{ - TaskName: "wait-0", - Ids: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["deployment"]), - }, - Condition: taskrunner.AllCurrent, - }, - &task.PruneTask{ - TaskName: "prune-0", - Objects: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["secret"]), - }, - }, - &taskrunner.WaitTask{ - TaskName: "wait-1", - Ids: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["secret"]), - }, - Condition: taskrunner.AllNotFound, - }, - &task.DeleteOrUpdateInvTask{ - TaskName: "inventory-set-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - PrevInventory: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["secret"]), - }, - }, - }, - expectedStatus: []actuation.ObjectStatus{ - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["deployment"]), - ), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["secret"]), - ), - Strategy: actuation.ActuationStrategyDelete, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - }, - }, - "prune disabled": { - inventoryIDs: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["secret"]), - }, - applyObjs: object.UnstructuredSet{ - testutil.Unstructured(t, resources["deployment"]), - }, - pruneObjs: object.UnstructuredSet{ - testutil.Unstructured(t, resources["secret"]), - }, - options: Options{Prune: false}, - expectedTasks: []taskrunner.Task{ - &task.InvAddTask{ - TaskName: "inventory-add-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - Objects: object.UnstructuredSet{ - testutil.Unstructured(t, resources["deployment"]), - }, - }, - &task.ApplyTask{ - TaskName: "apply-0", - Objects: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["deployment"]), - }, - }, - &taskrunner.WaitTask{ - TaskName: "wait-0", - Ids: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["deployment"]), - }, - Condition: taskrunner.AllCurrent, - }, - &task.DeleteOrUpdateInvTask{ - TaskName: "inventory-set-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - PrevInventory: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["secret"]), - }, - }, - }, - expectedStatus: []actuation.ObjectStatus{ - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["deployment"]), - ), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - }, - }, - // This use case returns in a task plan that would cause a dependency - // to be deleted. This is remediated by the DependencyFilter at - // apply-time, by skipping both the apply and prune. - // This test does not verify the DependencyFilter tho, just that the - // dependency was discovered between apply & prune objects. - "dependency: apply -> prune": { - inventoryIDs: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["secret"]), - }, - applyObjs: object.UnstructuredSet{ - testutil.Unstructured(t, resources["deployment"], - testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), - }, - pruneObjs: object.UnstructuredSet{ - testutil.Unstructured(t, resources["secret"]), - }, - options: Options{Prune: true}, - expectedTasks: []taskrunner.Task{ - &task.InvAddTask{ - TaskName: "inventory-add-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - Objects: object.UnstructuredSet{ - testutil.Unstructured(t, resources["deployment"], - testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), - }, - }, - &task.ApplyTask{ - TaskName: "apply-0", - Objects: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["deployment"], - testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), - }, - }, - &taskrunner.WaitTask{ - TaskName: "wait-0", - Ids: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["deployment"]), - }, - Condition: taskrunner.AllCurrent, - }, - &task.PruneTask{ - TaskName: "prune-0", - Objects: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["secret"]), - }, - }, - &taskrunner.WaitTask{ - TaskName: "wait-1", - Ids: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["secret"]), - }, - Condition: taskrunner.AllNotFound, - }, - &task.DeleteOrUpdateInvTask{ - TaskName: "inventory-set-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - PrevInventory: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["secret"]), - }, - }, - }, - expectedStatus: []actuation.ObjectStatus{ - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["deployment"]), - ), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["secret"]), - ), - Strategy: actuation.ActuationStrategyDelete, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - }, - }, - // This use case returns in a task plan that would cause a dependency - // to be applied. This is fine. - // This test just verifies that the dependency was discovered between - // prune & apply objects. - "dependency: prune -> apply": { - inventoryIDs: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["secret"]), - }, - applyObjs: object.UnstructuredSet{ - testutil.Unstructured(t, resources["deployment"]), - }, - pruneObjs: object.UnstructuredSet{ - testutil.Unstructured(t, resources["secret"], - testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["deployment"]))), - }, - options: Options{Prune: true}, - expectedTasks: []taskrunner.Task{ - &task.InvAddTask{ - TaskName: "inventory-add-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - Objects: object.UnstructuredSet{ - testutil.Unstructured(t, resources["deployment"]), - }, - }, - &task.ApplyTask{ - TaskName: "apply-0", - Objects: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["deployment"]), - }, - }, - &taskrunner.WaitTask{ - TaskName: "wait-0", - Ids: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["deployment"]), - }, - Condition: taskrunner.AllCurrent, - }, - &task.PruneTask{ - TaskName: "prune-0", - Objects: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["secret"], - testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["deployment"]))), - }, - }, - &taskrunner.WaitTask{ - TaskName: "wait-1", - Ids: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["secret"]), - }, - Condition: taskrunner.AllNotFound, - }, - &task.DeleteOrUpdateInvTask{ - TaskName: "inventory-set-0", - InvClient: &inventory.FakeClient{}, - InvInfo: invInfo, - PrevInventory: object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["secret"]), - }, - }, - }, - expectedStatus: []actuation.ObjectStatus{ - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["deployment"]), - ), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["secret"]), - ), - Strategy: actuation.ActuationStrategyDelete, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }, - }, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - mapper := testutil.NewFakeRESTMapper() - // inject mapper & pruner for equality comparison - for _, t := range tc.expectedTasks { - switch typedTask := t.(type) { - case *task.ApplyTask: - typedTask.Mapper = mapper - case *task.PruneTask: - typedTask.Pruner = &prune.Pruner{} - case *taskrunner.WaitTask: - typedTask.Mapper = mapper - } - } - - fakeInvClient := inventory.NewFakeClient(tc.inventoryIDs) - vCollector := &validation.Collector{} - tqb := TaskQueueBuilder{ - Pruner: pruner, - Mapper: mapper, - InvClient: fakeInvClient, - Collector: vCollector, - } - taskContext := taskrunner.NewTaskContext(nil, nil) - tq := tqb.WithInventory(invInfo). - WithApplyObjects(tc.applyObjs). - WithPruneObjects(tc.pruneObjs). - Build(taskContext, tc.options) - - err := vCollector.ToError() - if tc.expectedError != nil { - assert.EqualError(t, err, tc.expectedError.Error()) - return - } - assert.NoError(t, err) - - asserter.Equal(t, tc.expectedTasks, tq.tasks) - - actualStatus := taskContext.InventoryManager().Inventory().Status.Objects - testutil.AssertEqual(t, tc.expectedStatus, actualStatus) - }) - } -} - -// waitTaskComparer allows comparion of WaitTasks, ignoring private fields. -func waitTaskComparer() cmp.Option { - return cmp.Comparer(func(x, y *taskrunner.WaitTask) bool { - if x == nil { - return y == nil - } - if y == nil { - return false - } - return x.TaskName == y.TaskName && - x.Ids.Hash() == y.Ids.Hash() && // exact order match - x.Condition == y.Condition && - x.Timeout == y.Timeout && - cmp.Equal(x.Mapper, y.Mapper) - }) -} - -// fakeClientComparer allows comparion of inventory.FakeClient, ignoring objs. -func fakeClientComparer() cmp.Option { - return cmp.Comparer(func(x, y *inventory.FakeClient) bool { - if x == nil { - return y == nil - } - if y == nil { - return false - } - return true - }) -} - -// inventoryInfoComparer allows comparion of inventory.Info, ignoring impl. -func inventoryInfoComparer() cmp.Option { - return cmp.Comparer(func(x, y inventory.Info) bool { - return x.ID() == y.ID() && - x.Name() == y.Name() && - x.Namespace() == y.Namespace() && - x.Strategy() == y.Strategy() - }) -} diff --git a/pkg/apply/task/apply_task.go b/pkg/apply/task/apply_task.go deleted file mode 100644 index 58028073..00000000 --- a/pkg/apply/task/apply_task.go +++ /dev/null @@ -1,288 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package task - -import ( - "context" - "errors" - "fmt" - "io" - "strings" - - "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/sets" - "k8s.io/cli-runtime/pkg/genericclioptions" - "k8s.io/cli-runtime/pkg/resource" - "k8s.io/client-go/discovery" - "k8s.io/client-go/dynamic" - "k8s.io/klog/v2" - "k8s.io/kubectl/pkg/cmd/apply" - cmddelete "k8s.io/kubectl/pkg/cmd/delete" - - applyerror "github.com/fluxcd/cli-utils/pkg/apply/error" - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/apply/filter" - "github.com/fluxcd/cli-utils/pkg/apply/info" - "github.com/fluxcd/cli-utils/pkg/apply/mutator" - "github.com/fluxcd/cli-utils/pkg/apply/taskrunner" - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/object" -) - -// applyOptions defines the two key functions on the ApplyOptions -// struct that is used by the ApplyTask. -type applyOptions interface { - - // Run applies the resource set with the SetObjects function - // to the cluster. - Run() error - - // SetObjects sets the slice of resource (in the form form resourceInfo objects) - // that will be applied upon invoking the Run function. - SetObjects([]*resource.Info) -} - -// ApplyTask applies the given Objects to the cluster -// by using the ApplyOptions. -type ApplyTask struct { - TaskName string - - DynamicClient dynamic.Interface - OpenAPIGetter discovery.OpenAPISchemaInterface - InfoHelper info.Helper - Mapper meta.RESTMapper - Objects object.UnstructuredSet - Filters []filter.ValidationFilter - Mutators []mutator.Interface - DryRunStrategy common.DryRunStrategy - ServerSideOptions common.ServerSideOptions -} - -// applyOptionsFactoryFunc is a factory function for creating a new -// applyOptions implementation. Used to allow unit testing. -var applyOptionsFactoryFunc = newApplyOptions - -func (a *ApplyTask) Name() string { - return a.TaskName -} - -func (a *ApplyTask) Action() event.ResourceAction { - return event.ApplyAction -} - -func (a *ApplyTask) Identifiers() object.ObjMetadataSet { - return object.UnstructuredSetToObjMetadataSet(a.Objects) -} - -// Start creates a new goroutine that will invoke -// the Run function on the ApplyOptions to update -// the cluster. It will push a TaskResult on the taskChannel -// to signal to the taskrunner that the task has completed (or failed). -// It will also fetch the Generation from each of the applied resources -// after the Run function has completed. This information is then added -// to the taskContext. The generation is increased every time -// the desired state of a resource is changed. -func (a *ApplyTask) Start(taskContext *taskrunner.TaskContext) { - go func() { - // TODO: pipe Context through TaskContext - ctx := context.TODO() - objects := a.Objects - klog.V(2).Infof("apply task starting (name: %q, objects: %d)", - a.Name(), len(objects)) - for _, obj := range objects { - // Set the client and mapping fields on the provided - // info so they can be applied to the cluster. - info, err := a.InfoHelper.BuildInfo(obj) - // BuildInfo strips path annotations. - // Use modified object for filters, mutations, and events. - obj = info.Object.(*unstructured.Unstructured) - id := object.UnstructuredToObjMetadata(obj) - if err != nil { - err = applyerror.NewUnknownTypeError(err) - if klog.V(4).Enabled() { - // only log event emitted errors if the verbosity > 4 - klog.Errorf("apply task errored (object: %s): unable to convert obj to info: %v", id, err) - } - taskContext.SendEvent(a.createApplyFailedEvent(id, err)) - taskContext.InventoryManager().AddFailedApply(id) - continue - } - - // Check filters to see if we're prevented from applying. - var filterErr error - for _, applyFilter := range a.Filters { - klog.V(6).Infof("apply filter evaluating (filter: %s, object: %s)", applyFilter.Name(), id) - filterErr = applyFilter.Filter(obj) - if filterErr != nil { - var fatalErr *filter.FatalError - if errors.As(filterErr, &fatalErr) { - if klog.V(4).Enabled() { - // only log event emitted errors if the verbosity > 4 - klog.Errorf("apply filter errored (filter: %s, object: %s): %v", applyFilter.Name(), id, fatalErr.Err) - } - taskContext.SendEvent(a.createApplyFailedEvent(id, fatalErr)) - taskContext.InventoryManager().AddFailedApply(id) - break - } - klog.V(4).Infof("apply filtered (filter: %s, object: %s): %v", applyFilter.Name(), id, filterErr) - taskContext.SendEvent(a.createApplySkippedEvent(id, obj, filterErr)) - taskContext.InventoryManager().AddSkippedApply(id) - break - } - } - if filterErr != nil { - continue - } - - // Execute mutators, if any apply - err = a.mutate(ctx, obj) - if err != nil { - if klog.V(4).Enabled() { - // only log event emitted errors if the verbosity > 4 - klog.Errorf("apply mutation errored (object: %s): %v", id, err) - } - taskContext.SendEvent(a.createApplyFailedEvent(id, err)) - taskContext.InventoryManager().AddFailedApply(id) - continue - } - - // Create a new instance of the applyOptions interface and use it - // to apply the objects. - ao := applyOptionsFactoryFunc(a.Name(), taskContext.EventChannel(), - a.ServerSideOptions, a.DryRunStrategy, a.DynamicClient, a.OpenAPIGetter) - ao.SetObjects([]*resource.Info{info}) - klog.V(5).Infof("applying object: %v", id) - err = ao.Run() - if err != nil && a.ServerSideOptions.ServerSideApply && isAPIService(obj) && isStreamError(err) { - // Server-side Apply doesn't work with APIService before k8s 1.21 - // https://github.com/kubernetes/kubernetes/issues/89264 - // Thus APIService is handled specially using client-side apply. - err = a.clientSideApply(info, taskContext.EventChannel()) - } - if err != nil { - err = applyerror.NewApplyRunError(err) - if klog.V(4).Enabled() { - // only log event emitted errors if the verbosity > 4 - klog.Errorf("apply errored (object: %s): %v", id, err) - } - taskContext.SendEvent(a.createApplyFailedEvent(id, err)) - taskContext.InventoryManager().AddFailedApply(id) - } else if info.Object != nil { - acc, err := meta.Accessor(info.Object) - if err == nil { - uid := acc.GetUID() - gen := acc.GetGeneration() - taskContext.InventoryManager().AddSuccessfulApply(id, uid, gen) - } - } - } - a.sendTaskResult(taskContext) - }() -} - -func newApplyOptions(taskName string, eventChannel chan<- event.Event, serverSideOptions common.ServerSideOptions, - strategy common.DryRunStrategy, dynamicClient dynamic.Interface, - openAPIGetter discovery.OpenAPISchemaInterface) applyOptions { - emptyString := "" - return &apply.ApplyOptions{ - VisitedNamespaces: sets.New[string](), - VisitedUids: sets.New[types.UID](), - Overwrite: true, // Normally set in apply.NewApplyOptions - OpenAPIPatch: true, // Normally set in apply.NewApplyOptions - Recorder: genericclioptions.NoopRecorder{}, - IOStreams: genericclioptions.IOStreams{ - Out: io.Discard, - ErrOut: io.Discard, // TODO: Warning for no lastConfigurationAnnotation - // is printed directly to stderr in ApplyOptions. We - // should turn that into a warning on the event channel. - }, - // FilenameOptions are not needed since we don't use the ApplyOptions - // to read manifests. - DeleteOptions: &cmddelete.DeleteOptions{}, - PrintFlags: &genericclioptions.PrintFlags{ - OutputFormat: &emptyString, - }, - // Server-side apply if flag set or server-side dry run. - ServerSideApply: strategy.ServerDryRun() || serverSideOptions.ServerSideApply, - ForceConflicts: serverSideOptions.ForceConflicts, - FieldManager: serverSideOptions.FieldManager, - DryRunStrategy: strategy.Strategy(), - ToPrinter: (&KubectlPrinterAdapter{ - ch: eventChannel, - groupName: taskName, - }).toPrinterFunc(), - DynamicClient: dynamicClient, - } -} - -func (a *ApplyTask) sendTaskResult(taskContext *taskrunner.TaskContext) { - klog.V(2).Infof("apply task completing (name: %q)", a.Name()) - taskContext.TaskChannel() <- taskrunner.TaskResult{} -} - -// Cancel is not supported by the ApplyTask. -func (a *ApplyTask) Cancel(_ *taskrunner.TaskContext) {} - -// StatusUpdate is not supported by the ApplyTask. -func (a *ApplyTask) StatusUpdate(_ *taskrunner.TaskContext, _ object.ObjMetadata) {} - -// mutate loops through the mutator list and executes them on the object. -func (a *ApplyTask) mutate(ctx context.Context, obj *unstructured.Unstructured) error { - id := object.UnstructuredToObjMetadata(obj) - for _, mutator := range a.Mutators { - klog.V(6).Infof("apply mutator %s: %s", mutator.Name(), id) - mutated, reason, err := mutator.Mutate(ctx, obj) - if err != nil { - return fmt.Errorf("failed to mutate %q with %q: %w", id, mutator.Name(), err) - } - if mutated { - klog.V(4).Infof("resource mutated (mutator: %q, resource: %q, reason: %q)", mutator.Name(), id, reason) - } - } - return nil -} - -func (a *ApplyTask) createApplyFailedEvent(id object.ObjMetadata, err error) event.Event { - return event.Event{ - Type: event.ApplyType, - ApplyEvent: event.ApplyEvent{ - GroupName: a.Name(), - Identifier: id, - Status: event.ApplyFailed, - Error: err, - }, - } -} - -func (a *ApplyTask) createApplySkippedEvent(id object.ObjMetadata, resource *unstructured.Unstructured, err error) event.Event { - return event.Event{ - Type: event.ApplyType, - ApplyEvent: event.ApplyEvent{ - GroupName: a.Name(), - Identifier: id, - Status: event.ApplySkipped, - Resource: resource, - Error: err, - }, - } -} - -func isAPIService(obj *unstructured.Unstructured) bool { - gk := obj.GroupVersionKind().GroupKind() - return gk.Group == "apiregistration.k8s.io" && gk.Kind == "APIService" -} - -// isStreamError checks if the error is a StreamError. Since kubectl wraps the actual StreamError, -// we can't check the error type. -func isStreamError(err error) bool { - return strings.Contains(err.Error(), "stream error: stream ID ") -} - -func (a *ApplyTask) clientSideApply(info *resource.Info, eventChannel chan<- event.Event) error { - ao := applyOptionsFactoryFunc(a.Name(), eventChannel, common.ServerSideOptions{ServerSideApply: false}, a.DryRunStrategy, a.DynamicClient, a.OpenAPIGetter) - ao.SetObjects([]*resource.Info{info}) - return ao.Run() -} diff --git a/pkg/apply/task/apply_task_test.go b/pkg/apply/task/apply_task_test.go deleted file mode 100644 index 61eec2a0..00000000 --- a/pkg/apply/task/apply_task_test.go +++ /dev/null @@ -1,579 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package task - -import ( - "fmt" - "strings" - "sync" - "testing" - - "github.com/fluxcd/cli-utils/pkg/apply/cache" - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/apply/taskrunner" - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/testutil" - "github.com/stretchr/testify/assert" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/types" - "k8s.io/cli-runtime/pkg/resource" - "k8s.io/client-go/discovery" - "k8s.io/client-go/dynamic" -) - -type resourceInfo struct { - group string - apiVersion string - kind string - name string - namespace string - uid types.UID - generation int64 -} - -// Tests that the correct "applied" objects are sent -// to the TaskContext correctly, since these are the -// applied objects added to the final inventory. -func TestApplyTask_BasicAppliedObjects(t *testing.T) { - testCases := map[string]struct { - applied []resourceInfo - }{ - "apply single namespaced resource": { - applied: []resourceInfo{ - { - group: "apps", - apiVersion: "apps/v1", - kind: "Deployment", - name: "foo", - namespace: "default", - uid: types.UID("my-uid"), - generation: int64(42), - }, - }, - }, - "apply multiple clusterscoped resources": { - applied: []resourceInfo{ - { - group: "custom.io", - apiVersion: "custom.io/v1beta1", - kind: "Custom", - name: "bar", - uid: types.UID("uid-1"), - generation: int64(32), - }, - { - group: "custom2.io", - apiVersion: "custom2.io/v1", - kind: "Custom2", - name: "foo", - uid: types.UID("uid-2"), - generation: int64(1), - }, - }, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - eventChannel := make(chan event.Event) - defer close(eventChannel) - resourceCache := cache.NewResourceCacheMap() - taskContext := taskrunner.NewTaskContext(eventChannel, resourceCache) - - objs := toUnstructureds(tc.applied) - - oldAO := applyOptionsFactoryFunc - applyOptionsFactoryFunc = func(string, chan<- event.Event, common.ServerSideOptions, common.DryRunStrategy, - dynamic.Interface, discovery.OpenAPISchemaInterface) applyOptions { - return &fakeApplyOptions{} - } - defer func() { applyOptionsFactoryFunc = oldAO }() - - restMapper := testutil.NewFakeRESTMapper(schema.GroupVersionKind{ - Group: "apps", - Version: "v1", - Kind: "Deployment", - }, schema.GroupVersionKind{ - Group: "anothercustom.io", - Version: "v2", - Kind: "AnotherCustom", - }) - - applyTask := &ApplyTask{ - Objects: objs, - Mapper: restMapper, - InfoHelper: &fakeInfoHelper{}, - } - - applyTask.Start(taskContext) - <-taskContext.TaskChannel() - - // The applied resources should be stored in the TaskContext - // for the final inventory. - expectedIDs := object.UnstructuredSetToObjMetadataSet(objs) - actual := taskContext.InventoryManager().SuccessfulApplies() - if !actual.Equal(expectedIDs) { - t.Errorf("expected (%s) inventory resources, got (%s)", expectedIDs, actual) - } - - im := taskContext.InventoryManager() - - for _, id := range expectedIDs { - assert.Falsef(t, im.IsFailedApply(id), "ApplyTask should NOT mark object as failed: %s", id) - assert.Falsef(t, im.IsSkippedApply(id), "ApplyTask should NOT mark object as skipped: %s", id) - } - }) - } -} - -func TestApplyTask_FetchGeneration(t *testing.T) { - testCases := map[string]struct { - rss []resourceInfo - }{ - "single namespaced resource": { - rss: []resourceInfo{ - { - group: "apps", - apiVersion: "apps/v1", - kind: "Deployment", - name: "foo", - namespace: "default", - uid: types.UID("my-uid"), - generation: int64(42), - }, - }, - }, - "multiple clusterscoped resources": { - rss: []resourceInfo{ - { - group: "custom.io", - apiVersion: "custom.io/v1beta1", - kind: "Custom", - name: "bar", - uid: types.UID("uid-1"), - generation: int64(32), - }, - { - group: "custom2.io", - apiVersion: "custom2.io/v1", - kind: "Custom2", - name: "foo", - uid: types.UID("uid-2"), - generation: int64(1), - }, - }, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - eventChannel := make(chan event.Event) - defer close(eventChannel) - resourceCache := cache.NewResourceCacheMap() - taskContext := taskrunner.NewTaskContext(eventChannel, resourceCache) - - objs := toUnstructureds(tc.rss) - - oldAO := applyOptionsFactoryFunc - applyOptionsFactoryFunc = func(string, chan<- event.Event, common.ServerSideOptions, common.DryRunStrategy, - dynamic.Interface, discovery.OpenAPISchemaInterface) applyOptions { - return &fakeApplyOptions{} - } - defer func() { applyOptionsFactoryFunc = oldAO }() - applyTask := &ApplyTask{ - Objects: objs, - InfoHelper: &fakeInfoHelper{}, - } - applyTask.Start(taskContext) - - <-taskContext.TaskChannel() - - for _, info := range tc.rss { - id := object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: info.group, - Kind: info.kind, - }, - Name: info.name, - Namespace: info.namespace, - } - uid, _ := taskContext.InventoryManager().AppliedResourceUID(id) - assert.Equal(t, info.uid, uid) - - gen, _ := taskContext.InventoryManager().AppliedGeneration(id) - assert.Equal(t, info.generation, gen) - } - }) - } -} - -func TestApplyTask_DryRun(t *testing.T) { - testCases := map[string]struct { - objs []*unstructured.Unstructured - expectedObjects []object.ObjMetadata - expectedEvents []event.Event - }{ - "simple dry run": { - objs: []*unstructured.Unstructured{ - toUnstructured(map[string]interface{}{ - "apiVersion": "apps/v1", - "kind": "Deployment", - "metadata": map[string]interface{}{ - "name": "foo", - "namespace": "default", - }, - }), - }, - expectedObjects: []object.ObjMetadata{ - { - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - }, - Name: "foo", - Namespace: "default", - }, - }, - expectedEvents: []event.Event{}, - }, - "dry run with CRD and CR": { - objs: []*unstructured.Unstructured{ - toUnstructured(map[string]interface{}{ - "apiVersion": "apiextensions.k8s.io/v1", - "kind": "CustomResourceDefinition", - "metadata": map[string]interface{}{ - "name": "foo", - }, - "spec": map[string]interface{}{ - "group": "custom.io", - "names": map[string]interface{}{ - "kind": "Custom", - }, - "versions": []interface{}{ - map[string]interface{}{ - "name": "v1alpha1", - }, - }, - }, - }), - toUnstructured(map[string]interface{}{ - "apiVersion": "custom.io/v1alpha1", - "kind": "Custom", - "metadata": map[string]interface{}{ - "name": "bar", - }, - }), - }, - expectedObjects: []object.ObjMetadata{ - { - GroupKind: schema.GroupKind{ - Group: "custom.io", - Kind: "Custom", - }, - Name: "bar", - }, - }, - expectedEvents: []event.Event{}, - }, - } - - for tn, tc := range testCases { - for i := range common.Strategies { - drs := common.Strategies[i] - t.Run(tn, func(t *testing.T) { - eventChannel := make(chan event.Event) - resourceCache := cache.NewResourceCacheMap() - taskContext := taskrunner.NewTaskContext(eventChannel, resourceCache) - - restMapper := testutil.NewFakeRESTMapper(schema.GroupVersionKind{ - Group: "apps", - Version: "v1", - Kind: "Deployment", - }, schema.GroupVersionKind{ - Group: "anothercustom.io", - Version: "v2", - Kind: "AnotherCustom", - }) - - ao := &fakeApplyOptions{} - oldAO := applyOptionsFactoryFunc - applyOptionsFactoryFunc = func(string, chan<- event.Event, common.ServerSideOptions, common.DryRunStrategy, - dynamic.Interface, discovery.OpenAPISchemaInterface) applyOptions { - return ao - } - defer func() { applyOptionsFactoryFunc = oldAO }() - - applyTask := &ApplyTask{ - Objects: tc.objs, - InfoHelper: &fakeInfoHelper{}, - Mapper: restMapper, - DryRunStrategy: drs, - } - - var events []event.Event - var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() - for msg := range eventChannel { - events = append(events, msg) - } - }() - - applyTask.Start(taskContext) - <-taskContext.TaskChannel() - close(eventChannel) - wg.Wait() - - assert.Equal(t, len(tc.expectedObjects), len(ao.objects)) - for i, obj := range ao.objects { - actual, err := object.InfoToObjMeta(obj) - if err != nil { - continue - } - assert.Equal(t, tc.expectedObjects[i], actual) - } - - assert.Equal(t, len(tc.expectedEvents), len(events)) - for i, e := range events { - assert.Equal(t, tc.expectedEvents[i].Type, e.Type) - } - }) - } - } -} - -func TestApplyTaskWithError(t *testing.T) { - testCases := map[string]struct { - objs []*unstructured.Unstructured - expectedObjects object.ObjMetadataSet - expectedEvents []event.Event - expectedSkipped object.ObjMetadataSet - expectedFailed object.ObjMetadataSet - }{ - "some resources have apply error": { - objs: []*unstructured.Unstructured{ - toUnstructured(map[string]interface{}{ - "apiVersion": "apiextensions.k8s.io/v1", - "kind": "CustomResourceDefinition", - "metadata": map[string]interface{}{ - "name": "foo", - }, - "spec": map[string]interface{}{ - "group": "anothercustom.io", - "names": map[string]interface{}{ - "kind": "AnotherCustom", - }, - "versions": []interface{}{ - map[string]interface{}{ - "name": "v2", - }, - }, - }, - }), - toUnstructured(map[string]interface{}{ - "apiVersion": "anothercustom.io/v2", - "kind": "AnotherCustom", - "metadata": map[string]interface{}{ - "name": "bar", - "namespace": "barbar", - }, - }), - toUnstructured(map[string]interface{}{ - "apiVersion": "anothercustom.io/v2", - "kind": "AnotherCustom", - "metadata": map[string]interface{}{ - "name": "bar-with-failure", - "namespace": "barbar", - }, - }), - }, - expectedObjects: object.ObjMetadataSet{ - { - GroupKind: schema.GroupKind{ - Group: "apiextensions.k8s.io", - Kind: "CustomResourceDefinition", - }, - Name: "foo", - }, - { - GroupKind: schema.GroupKind{ - Group: "anothercustom.io", - Kind: "AnotherCustom", - }, - Name: "bar", - Namespace: "barbar", - }, - }, - expectedEvents: []event.Event{ - { - Type: event.ApplyType, - ApplyEvent: event.ApplyEvent{ - Status: event.ApplyFailed, - Error: fmt.Errorf("expected apply error"), - }, - }, - }, - expectedFailed: object.ObjMetadataSet{ - { - GroupKind: schema.GroupKind{ - Group: "anothercustom.io", - Kind: "AnotherCustom", - }, - Name: "bar-with-failure", - Namespace: "barbar", - }, - }, - }, - } - - for tn, tc := range testCases { - drs := common.DryRunNone - t.Run(tn, func(t *testing.T) { - eventChannel := make(chan event.Event) - resourceCache := cache.NewResourceCacheMap() - taskContext := taskrunner.NewTaskContext(eventChannel, resourceCache) - - restMapper := testutil.NewFakeRESTMapper(schema.GroupVersionKind{ - Group: "apps", - Version: "v1", - Kind: "Deployment", - }, schema.GroupVersionKind{ - Group: "anothercustom.io", - Version: "v2", - Kind: "AnotherCustom", - }) - - ao := &fakeApplyOptions{} - oldAO := applyOptionsFactoryFunc - applyOptionsFactoryFunc = func(string, chan<- event.Event, common.ServerSideOptions, common.DryRunStrategy, - dynamic.Interface, discovery.OpenAPISchemaInterface) applyOptions { - return ao - } - defer func() { applyOptionsFactoryFunc = oldAO }() - - applyTask := &ApplyTask{ - Objects: tc.objs, - InfoHelper: &fakeInfoHelper{}, - Mapper: restMapper, - DryRunStrategy: drs, - } - - var events []event.Event - var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() - for msg := range eventChannel { - events = append(events, msg) - } - }() - - applyTask.Start(taskContext) - <-taskContext.TaskChannel() - close(eventChannel) - wg.Wait() - - assert.Equal(t, len(tc.expectedObjects), len(ao.passedObjects)) - for i, obj := range ao.passedObjects { - actual, err := object.InfoToObjMeta(obj) - if err != nil { - continue - } - assert.Equal(t, tc.expectedObjects[i], actual) - } - - assert.Equal(t, len(tc.expectedEvents), len(events)) - for i, e := range events { - assert.Equal(t, tc.expectedEvents[i].Type, e.Type) - assert.Equal(t, tc.expectedEvents[i].ApplyEvent.Error.Error(), e.ApplyEvent.Error.Error()) - } - - applyIDs := object.UnstructuredSetToObjMetadataSet(tc.objs) - - im := taskContext.InventoryManager() - - // validate record of failed prunes - for _, id := range tc.expectedFailed { - assert.Truef(t, im.IsFailedApply(id), "ApplyTask should mark object as failed: %s", id) - } - for _, id := range applyIDs.Diff(tc.expectedFailed) { - assert.Falsef(t, im.IsFailedApply(id), "ApplyTask should NOT mark object as failed: %s", id) - } - // validate record of skipped prunes - for _, id := range tc.expectedSkipped { - assert.Truef(t, im.IsSkippedApply(id), "ApplyTask should mark object as skipped: %s", id) - } - for _, id := range applyIDs.Diff(tc.expectedSkipped) { - assert.Falsef(t, im.IsSkippedApply(id), "ApplyTask should NOT mark object as skipped: %s", id) - } - }) - } -} - -func toUnstructured(obj map[string]interface{}) *unstructured.Unstructured { - return &unstructured.Unstructured{ - Object: obj, - } -} - -func toUnstructureds(rss []resourceInfo) []*unstructured.Unstructured { - var objs []*unstructured.Unstructured - - for _, rs := range rss { - objs = append(objs, &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": rs.apiVersion, - "kind": rs.kind, - "metadata": map[string]interface{}{ - "name": rs.name, - "namespace": rs.namespace, - "uid": string(rs.uid), - "generation": rs.generation, - "annotations": map[string]interface{}{ - "config.k8s.io/owning-inventory": "id", - }, - }, - }, - }) - } - return objs -} - -type fakeApplyOptions struct { - objects []*resource.Info - passedObjects []*resource.Info -} - -func (f *fakeApplyOptions) Run() error { - var err error - for _, obj := range f.objects { - if strings.Contains(obj.Name, "failure") { - err = fmt.Errorf("expected apply error") - } else { - f.passedObjects = append(f.passedObjects, obj) - } - } - return err -} - -func (f *fakeApplyOptions) SetObjects(objects []*resource.Info) { - f.objects = objects -} - -type fakeInfoHelper struct{} - -func (f *fakeInfoHelper) UpdateInfo(*resource.Info) error { - return nil -} - -func (f *fakeInfoHelper) BuildInfos(objs []*unstructured.Unstructured) ([]*resource.Info, error) { - return object.UnstructuredsToInfos(objs) -} - -func (f *fakeInfoHelper) BuildInfo(obj *unstructured.Unstructured) (*resource.Info, error) { - return object.UnstructuredToInfo(obj) -} diff --git a/pkg/apply/task/delete_inv_task_test.go b/pkg/apply/task/delete_inv_task_test.go deleted file mode 100644 index a861a564..00000000 --- a/pkg/apply/task/delete_inv_task_test.go +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package task - -import ( - "testing" - - "github.com/fluxcd/cli-utils/pkg/apply/cache" - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/apply/taskrunner" - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/testutil" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/runtime/schema" -) - -func TestDeleteInvTask(t *testing.T) { - id1 := object.UnstructuredToObjMetadata(obj1) - id2 := object.UnstructuredToObjMetadata(obj2) - id3 := object.UnstructuredToObjMetadata(obj3) - testCases := map[string]struct { - prevInventory object.ObjMetadataSet - deletedObjs object.ObjMetadataSet - failedDeletes object.ObjMetadataSet - failedReconciles object.ObjMetadataSet - timeoutReconciles object.ObjMetadataSet - err error - isError bool - expectedObjs object.ObjMetadataSet - }{ - "no error case": { - prevInventory: object.ObjMetadataSet{id1, id2, id3}, - err: nil, - isError: false, - }, - "error is returned in result": { - err: apierrors.NewResourceExpired("unused message"), - isError: true, - }, - "inventory not found is not error and not returned": { - err: apierrors.NewNotFound(schema.GroupResource{Resource: "simples"}, - "unused-resource-name"), - isError: false, - }, - "inventory is updated instead of deleted in case of pruning failure": { - prevInventory: object.ObjMetadataSet{id1, id2, id3}, - failedDeletes: object.ObjMetadataSet{id1}, - err: nil, - isError: false, - expectedObjs: object.ObjMetadataSet{id1}, - }, - "inventory is updated instead of deleted in case of reconcile failure": { - prevInventory: object.ObjMetadataSet{id1, id2, id3}, - deletedObjs: object.ObjMetadataSet{id1, id2, id3}, - failedReconciles: object.ObjMetadataSet{id1}, - err: nil, - isError: false, - expectedObjs: object.ObjMetadataSet{id1}, - }, - "inventory is updated instead of deleted in case of reconcile timeout": { - prevInventory: object.ObjMetadataSet{id1, id2, id3}, - deletedObjs: object.ObjMetadataSet{id1, id2, id3}, - timeoutReconciles: object.ObjMetadataSet{id1}, - err: nil, - isError: false, - expectedObjs: object.ObjMetadataSet{id1}, - }, - "inventory is updated instead of deleted in case of pruning/reconcile failure": { - prevInventory: object.ObjMetadataSet{id1, id2, id3}, - deletedObjs: object.ObjMetadataSet{id1, id2, id3}, - failedReconciles: object.ObjMetadataSet{id1}, - failedDeletes: object.ObjMetadataSet{id2}, - err: nil, - isError: false, - expectedObjs: object.ObjMetadataSet{id1, id2}, - }, - } - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - client := inventory.NewFakeClient(object.ObjMetadataSet{}) - client.Err = tc.err - eventChannel := make(chan event.Event) - resourceCache := cache.NewResourceCacheMap() - context := taskrunner.NewTaskContext(eventChannel, resourceCache) - im := context.InventoryManager() - for _, deleteObj := range tc.deletedObjs { - im.AddSuccessfulDelete(deleteObj, "unused-uid") - } - for _, failedDelete := range tc.failedDeletes { - im.AddFailedDelete(failedDelete) - } - for _, failedReconcile := range tc.failedReconciles { - if err := im.SetFailedReconcile(failedReconcile); err != nil { - t.Fatal(err) - } - } - for _, timeoutReconcile := range tc.timeoutReconciles { - if err := im.SetTimeoutReconcile(timeoutReconcile); err != nil { - t.Fatal(err) - } - } - - task := DeleteOrUpdateInvTask{ - TaskName: taskName, - InvClient: client, - InvInfo: nil, - DryRun: common.DryRunNone, - PrevInventory: tc.prevInventory, - Destroy: true, - } - if taskName != task.Name() { - t.Errorf("expected task name (%s), got (%s)", taskName, task.Name()) - } - task.Start(context) - result := <-context.TaskChannel() - if tc.isError { - if tc.err != result.Err { - t.Errorf("running DeleteOrUpdateInvTask expected error (%s), got (%s)", tc.err, result.Err) - } - } else { - if result.Err != nil { - t.Errorf("unexpected error running DeleteOrUpdateInvTask: %s", result.Err) - } - } - actual, _ := client.GetClusterObjs(nil) - testutil.AssertEqual(t, tc.expectedObjs, actual, - "Actual cluster objects (%d) do not match expected cluster objects (%d)", - len(actual), len(tc.expectedObjs)) - }) - } -} diff --git a/pkg/apply/task/inv_add_task.go b/pkg/apply/task/inv_add_task.go deleted file mode 100644 index 1fb38b63..00000000 --- a/pkg/apply/task/inv_add_task.go +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package task - -import ( - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/apply/taskrunner" - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/pkg/object" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/klog/v2" -) - -var ( - namespaceGVKv1 = schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Namespace"} -) - -// InvAddTask encapsulates structures necessary to add/merge inventory -// into the cluster. The InvAddTask should add/merge inventory references -// before the actual object is applied. -type InvAddTask struct { - TaskName string - InvClient inventory.Client - InvInfo inventory.Info - Objects object.UnstructuredSet - DryRun common.DryRunStrategy -} - -func (i *InvAddTask) Name() string { - return i.TaskName -} - -func (i *InvAddTask) Action() event.ResourceAction { - return event.InventoryAction -} - -func (i *InvAddTask) Identifiers() object.ObjMetadataSet { - return object.UnstructuredSetToObjMetadataSet(i.Objects) -} - -// Start updates the inventory by merging the locally applied objects -// into the current inventory. -func (i *InvAddTask) Start(taskContext *taskrunner.TaskContext) { - go func() { - klog.V(2).Infof("inventory add task starting (name: %q)", i.Name()) - if err := inventory.ValidateNoInventory(i.Objects); err != nil { - i.sendTaskResult(taskContext, err) - return - } - // Ensures the namespace exists before applying the inventory object into it. - if invNamespace := inventoryNamespaceInSet(i.InvInfo, i.Objects); invNamespace != nil { - klog.V(4).Infof("applying inventory namespace %s", invNamespace.GetName()) - if err := i.InvClient.ApplyInventoryNamespace(invNamespace, i.DryRun); err != nil { - i.sendTaskResult(taskContext, err) - return - } - } - klog.V(4).Infof("merging %d local objects into inventory", len(i.Objects)) - currentObjs := object.UnstructuredSetToObjMetadataSet(i.Objects) - _, err := i.InvClient.Merge(i.InvInfo, currentObjs, i.DryRun) - i.sendTaskResult(taskContext, err) - }() -} - -// Cancel is not supported by the InvAddTask. -func (i *InvAddTask) Cancel(_ *taskrunner.TaskContext) {} - -// StatusUpdate is not supported by the InvAddTask. -func (i *InvAddTask) StatusUpdate(_ *taskrunner.TaskContext, _ object.ObjMetadata) {} - -// inventoryNamespaceInSet returns the the namespace the passed inventory -// object will be applied to, or nil if this namespace object does not exist -// in the passed slice "infos" or the inventory object is cluster-scoped. -func inventoryNamespaceInSet(inv inventory.Info, objs object.UnstructuredSet) *unstructured.Unstructured { - if inv == nil { - return nil - } - invNamespace := inv.Namespace() - - for _, obj := range objs { - gvk := obj.GetObjectKind().GroupVersionKind() - if gvk == namespaceGVKv1 && obj.GetName() == invNamespace { - inventory.AddInventoryIDAnnotation(obj, inv) - return obj - } - } - return nil -} - -func (i *InvAddTask) sendTaskResult(taskContext *taskrunner.TaskContext, err error) { - klog.V(2).Infof("inventory add task completing (name: %q)", i.Name()) - taskContext.TaskChannel() <- taskrunner.TaskResult{ - Err: err, - } -} diff --git a/pkg/apply/task/inv_add_task_test.go b/pkg/apply/task/inv_add_task_test.go deleted file mode 100644 index 83f6d65a..00000000 --- a/pkg/apply/task/inv_add_task_test.go +++ /dev/null @@ -1,196 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package task - -import ( - "testing" - - "github.com/fluxcd/cli-utils/pkg/apply/cache" - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/apply/taskrunner" - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/pkg/object" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -var namespace = "test-namespace" - -var inventoryObj = &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "v1", - "kind": "ConfigMap", - "metadata": map[string]interface{}{ - "name": "test-inventory-obj", - "namespace": namespace, - "labels": map[string]interface{}{ - common.InventoryLabel: "test-app-label", - }, - }, - }, -} - -var localInv = inventory.WrapInventoryInfoObj(inventoryObj) - -var obj1 = &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "v1", - "kind": "Pod", - "metadata": map[string]interface{}{ - "name": "obj1", - "namespace": namespace, - }, - }, -} - -var obj2 = &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "batch/v1", - "kind": "Job", - "metadata": map[string]interface{}{ - "name": "obj2", - "namespace": namespace, - }, - }, -} - -var obj3 = &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "apps/v1", - "kind": "Deployment", - "metadata": map[string]interface{}{ - "name": "obj3", - "namespace": "different-namespace", - }, - }, -} - -const taskName = "test-inventory-task" - -func TestInvAddTask(t *testing.T) { - id1 := object.UnstructuredToObjMetadata(obj1) - id2 := object.UnstructuredToObjMetadata(obj2) - id3 := object.UnstructuredToObjMetadata(obj3) - - tests := map[string]struct { - initialObjs object.ObjMetadataSet - applyObjs []*unstructured.Unstructured - expectedObjs object.ObjMetadataSet - }{ - "no initial inventory and no apply objects; no merged inventory": { - initialObjs: object.ObjMetadataSet{}, - applyObjs: []*unstructured.Unstructured{}, - expectedObjs: object.ObjMetadataSet{}, - }, - "no initial inventory, one apply object; one merged inventory": { - initialObjs: object.ObjMetadataSet{}, - applyObjs: []*unstructured.Unstructured{obj1}, - expectedObjs: object.ObjMetadataSet{id1}, - }, - "one initial inventory, no apply object; one merged inventory": { - initialObjs: object.ObjMetadataSet{id2}, - applyObjs: []*unstructured.Unstructured{}, - expectedObjs: object.ObjMetadataSet{id2}, - }, - "one initial inventory, one apply object; one merged inventory": { - initialObjs: object.ObjMetadataSet{id3}, - applyObjs: []*unstructured.Unstructured{obj3}, - expectedObjs: object.ObjMetadataSet{id3}, - }, - "three initial inventory, two same objects; three merged inventory": { - initialObjs: object.ObjMetadataSet{id1, id2, id3}, - applyObjs: []*unstructured.Unstructured{obj2, obj3}, - expectedObjs: object.ObjMetadataSet{id1, id2, id3}, - }, - } - - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - client := inventory.NewFakeClient(tc.initialObjs) - eventChannel := make(chan event.Event) - resourceCache := cache.NewResourceCacheMap() - context := taskrunner.NewTaskContext(eventChannel, resourceCache) - - task := InvAddTask{ - TaskName: taskName, - InvClient: client, - InvInfo: nil, - Objects: tc.applyObjs, - } - if taskName != task.Name() { - t.Errorf("expected task name (%s), got (%s)", taskName, task.Name()) - } - applyIDs := object.UnstructuredSetToObjMetadataSet(tc.applyObjs) - if !task.Identifiers().Equal(applyIDs) { - t.Errorf("expected task ids (%s), got (%s)", applyIDs, task.Identifiers()) - } - task.Start(context) - result := <-context.TaskChannel() - if result.Err != nil { - t.Errorf("unexpected error running InvAddTask: %s", result.Err) - } - actual, _ := client.GetClusterObjs(nil) - if !tc.expectedObjs.Equal(actual) { - t.Errorf("expected merged inventory (%s), got (%s)", tc.expectedObjs, actual) - } - }) - } -} - -func TestInventoryNamespaceInSet(t *testing.T) { - inventoryNamespace := createNamespace(namespace) - - tests := map[string]struct { - inv inventory.Info - objects []*unstructured.Unstructured - namespace *unstructured.Unstructured - }{ - "Nil inventory object, no resources returns nil namespace": { - inv: nil, - objects: []*unstructured.Unstructured{}, - namespace: nil, - }, - "Inventory object, but no resources returns nil namespace": { - inv: localInv, - objects: []*unstructured.Unstructured{}, - namespace: nil, - }, - "Inventory object, resources with no namespace returns nil namespace": { - inv: localInv, - objects: []*unstructured.Unstructured{obj1, obj2}, - namespace: nil, - }, - "Inventory object, different namespace returns nil namespace": { - inv: localInv, - objects: []*unstructured.Unstructured{createNamespace("foo")}, - namespace: nil, - }, - "Inventory object, inventory namespace returns inventory namespace": { - inv: localInv, - objects: []*unstructured.Unstructured{obj1, inventoryNamespace, obj3}, - namespace: inventoryNamespace, - }, - } - - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - actualNamespace := inventoryNamespaceInSet(tc.inv, tc.objects) - if tc.namespace != actualNamespace { - t.Fatalf("expected namespace (%v), got (%v)", tc.namespace, actualNamespace) - } - }) - } -} - -func createNamespace(ns string) *unstructured.Unstructured { - return &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "v1", - "kind": "Namespace", - "metadata": map[string]interface{}{ - "name": ns, - }, - }, - } -} diff --git a/pkg/apply/task/inv_set_task.go b/pkg/apply/task/inv_set_task.go deleted file mode 100644 index 56e159af..00000000 --- a/pkg/apply/task/inv_set_task.go +++ /dev/null @@ -1,195 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package task - -import ( - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/apply/taskrunner" - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/pkg/object" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/klog/v2" -) - -// DeleteOrUpdateInvTask encapsulates structures necessary to set the -// inventory references at the end of the apply/prune. -type DeleteOrUpdateInvTask struct { - TaskName string - InvClient inventory.Client - InvInfo inventory.Info - PrevInventory object.ObjMetadataSet - DryRun common.DryRunStrategy - // if Destroy is set, the inventory will be deleted if all objects were successfully pruned - Destroy bool -} - -func (i *DeleteOrUpdateInvTask) Name() string { - return i.TaskName -} - -func (i *DeleteOrUpdateInvTask) Action() event.ResourceAction { - return event.InventoryAction -} - -func (i *DeleteOrUpdateInvTask) Identifiers() object.ObjMetadataSet { - return object.ObjMetadataSet{} -} - -// Start will reconcile the state of the inventory, depending on the value of -// Destroy. -// -// If Destroy is set, the intent is to delete the inventory. The inventory will -// only be deleted if all prunes were successful (none failed/skipped). If any -// prunes were failed or skipped, the inventory will be updated. -// -// If Destroy is false, the inventory will be updated. -func (i *DeleteOrUpdateInvTask) Start(taskContext *taskrunner.TaskContext) { - go func() { - var err error - if i.Destroy && i.destroySuccessful(taskContext) { - err = i.deleteInventory() - } else { - err = i.updateInventory(taskContext) - } - taskContext.TaskChannel() <- taskrunner.TaskResult{Err: err} - }() -} - -// Cancel is not supported by the DeleteOrUpdateInvTask. -func (i *DeleteOrUpdateInvTask) Cancel(_ *taskrunner.TaskContext) {} - -// StatusUpdate is not supported by the DeleteOrUpdateInvTask. -func (i *DeleteOrUpdateInvTask) StatusUpdate(_ *taskrunner.TaskContext, _ object.ObjMetadata) {} - -// updateInventory sets (creates or replaces) the inventory. -// -// The guiding principal is that anything in the cluster should be in the -// inventory, unless it was explicitly abandoned. -// -// This task must run after all the apply and prune tasks have completed. -// -// Added objects: -// - Applied resources (successful) -// -// Retained objects: -// - Applied resources (filtered/skipped) -// - Applied resources (failed) -// - Deleted resources (filtered/skipped) that were not abandoned -// - Deleted resources (failed) -// - Abandoned resources (failed) -// -// Removed objects: -// - Deleted resources (successful) -// - Abandoned resources (successful) -func (i *DeleteOrUpdateInvTask) updateInventory(taskContext *taskrunner.TaskContext) error { - klog.V(2).Infof("inventory set task starting (name: %q)", i.TaskName) - invObjs := object.ObjMetadataSet{} - - // TODO: Just use InventoryManager.Store() - im := taskContext.InventoryManager() - - // If an object applied successfully, keep or add it to the inventory. - appliedObjs := im.SuccessfulApplies() - klog.V(4).Infof("set inventory %d successful applies", len(appliedObjs)) - invObjs = invObjs.Union(appliedObjs) - - // If an object failed to apply and was previously stored in the inventory, - // then keep it in the inventory so it can be applied/pruned next time. - // This will remove new resources that failed to apply from the inventory, - // because even tho they were added by InvAddTask, the PrevInventory - // represents the inventory before the pipeline has run. - applyFailures := i.PrevInventory.Intersection(im.FailedApplies()) - klog.V(4).Infof("keep in inventory %d failed applies", len(applyFailures)) - invObjs = invObjs.Union(applyFailures) - - // If an object skipped apply and was previously stored in the inventory, - // then keep it in the inventory so it can be applied/pruned next time. - // It's likely that all the skipped applies are already in the inventory, - // because the apply filters all currently depend on cluster state, - // but we're doing the intersection anyway just to be sure. - applySkips := i.PrevInventory.Intersection(im.SkippedApplies()) - klog.V(4).Infof("keep in inventory %d skipped applies", len(applySkips)) - invObjs = invObjs.Union(applySkips) - - // If an object failed to delete and was previously stored in the inventory, - // then keep it in the inventory so it can be applied/pruned next time. - // It's likely that all the delete failures are already in the inventory, - // because the set of resources to prune comes from the inventory, - // but we're doing the intersection anyway just to be sure. - pruneFailures := i.PrevInventory.Intersection(im.FailedDeletes()) - klog.V(4).Infof("set inventory %d failed prunes", len(pruneFailures)) - invObjs = invObjs.Union(pruneFailures) - - // If an object skipped delete and was previously stored in the inventory, - // then keep it in the inventory so it can be applied/pruned next time. - // It's likely that all the skipped deletes are already in the inventory, - // because the set of resources to prune comes from the inventory, - // but we're doing the intersection anyway just to be sure. - pruneSkips := i.PrevInventory.Intersection(im.SkippedDeletes()) - klog.V(4).Infof("keep in inventory %d skipped prunes", len(pruneSkips)) - invObjs = invObjs.Union(pruneSkips) - - // If an object failed to reconcile and was previously stored in the inventory, - // then keep it in the inventory so it can be waited on next time. - reconcileFailures := i.PrevInventory.Intersection(im.FailedReconciles()) - klog.V(4).Infof("set inventory %d failed reconciles", len(reconcileFailures)) - invObjs = invObjs.Union(reconcileFailures) - - // If an object timed out reconciling and was previously stored in the inventory, - // then keep it in the inventory so it can be waited on next time. - reconcileTimeouts := i.PrevInventory.Intersection(im.TimeoutReconciles()) - klog.V(4).Infof("keep in inventory %d timeout reconciles", len(reconcileTimeouts)) - invObjs = invObjs.Union(reconcileTimeouts) - - // If an object is abandoned, then remove it from the inventory. - abandonedObjects := taskContext.AbandonedObjects() - klog.V(4).Infof("remove from inventory %d abandoned objects", len(abandonedObjects)) - invObjs = invObjs.Diff(abandonedObjects) - - // If an object is invalid and was previously stored in the inventory, - // then keep it in the inventory so it can be applied/pruned next time. - invalidObjects := i.PrevInventory.Intersection(taskContext.InvalidObjects()) - klog.V(4).Infof("keep in inventory %d invalid objects", len(invalidObjects)) - invObjs = invObjs.Union(invalidObjects) - - klog.V(4).Infof("get the apply status for %d objects", len(invObjs)) - objStatus := taskContext.InventoryManager().Inventory().Status.Objects - - klog.V(4).Infof("set inventory %d total objects", len(invObjs)) - err := i.InvClient.Replace(i.InvInfo, invObjs, objStatus, i.DryRun) - - klog.V(2).Infof("inventory set task completing (name: %q)", i.TaskName) - return err -} - -// deleteInventory deletes the inventory object from the cluster. -func (i *DeleteOrUpdateInvTask) deleteInventory() error { - klog.V(2).Infof("delete inventory task starting (name: %q)", i.Name()) - err := i.InvClient.DeleteInventoryObj(i.InvInfo, i.DryRun) - // Not found is not error, since this means it was already deleted. - if apierrors.IsNotFound(err) { - err = nil - } - klog.V(2).Infof("delete inventory task completing (name: %q)", i.Name()) - return err -} - -// destroySuccessful returns true when destroy actuation and reconciliation was -// fully successful. When true, it's safe to delete the inventory. -func (i *DeleteOrUpdateInvTask) destroySuccessful(taskContext *taskrunner.TaskContext) bool { - // if any deletes failed, the Destroy is considered failed - if len(taskContext.InventoryManager().FailedDeletes()) > 0 { - return false - } - // if any reconciles failed, the Destroy is considered failed - if len(taskContext.InventoryManager().FailedReconciles()) > 0 { - return false - } - // if any reconciles timed out, the Destroy is considered failed - if len(taskContext.InventoryManager().TimeoutReconciles()) > 0 { - return false - } - return true -} diff --git a/pkg/apply/task/inv_set_task_test.go b/pkg/apply/task/inv_set_task_test.go deleted file mode 100644 index bfcd7ef8..00000000 --- a/pkg/apply/task/inv_set_task_test.go +++ /dev/null @@ -1,255 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package task - -import ( - "testing" - - "github.com/fluxcd/cli-utils/pkg/apply/cache" - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/apply/taskrunner" - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/testutil" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -var objInvalid = &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "apps/v1", - "kind": "Deployment", - }, -} - -func TestInvSetTask(t *testing.T) { - id1 := object.UnstructuredToObjMetadata(obj1) - id2 := object.UnstructuredToObjMetadata(obj2) - id3 := object.UnstructuredToObjMetadata(obj3) - idInvalid := object.UnstructuredToObjMetadata(objInvalid) - - tests := map[string]struct { - prevInventory object.ObjMetadataSet - appliedObjs object.ObjMetadataSet - deletedObjs object.ObjMetadataSet - failedApplies object.ObjMetadataSet - failedDeletes object.ObjMetadataSet - skippedApplies object.ObjMetadataSet - skippedDeletes object.ObjMetadataSet - failedReconciles object.ObjMetadataSet - timeoutReconciles object.ObjMetadataSet - abandonedObjs object.ObjMetadataSet - invalidObjs object.ObjMetadataSet - expectedObjs object.ObjMetadataSet - }{ - "no apply objs, no prune failures; no inventory": { - expectedObjs: object.ObjMetadataSet{}, - }, - "one apply objs, no prune failures; one inventory": { - appliedObjs: object.ObjMetadataSet{id1}, - expectedObjs: object.ObjMetadataSet{id1}, - }, - "no apply objs, one prune failure, in prev inventory; one inventory": { - prevInventory: object.ObjMetadataSet{id1}, - failedDeletes: object.ObjMetadataSet{id1}, - expectedObjs: object.ObjMetadataSet{id1}, - }, - "no apply objs, one prune failure, not in prev inventory; no inventory": { - // aritifical use case: prunes come from the inventory - failedDeletes: object.ObjMetadataSet{id1}, - expectedObjs: object.ObjMetadataSet{}, - }, - "one apply objs, one prune failures; one inventory": { - // aritifical use case: applies and prunes are mutually exclusive. - // Delete failure overwrites apply success in object status. - appliedObjs: object.ObjMetadataSet{id3}, - failedDeletes: object.ObjMetadataSet{id3}, - expectedObjs: object.ObjMetadataSet{}, - }, - "two apply objs, two prune failures; three inventory": { - // aritifical use case: applies and prunes are mutually exclusive - prevInventory: object.ObjMetadataSet{id2, id3}, - appliedObjs: object.ObjMetadataSet{id1, id2}, - failedDeletes: object.ObjMetadataSet{id2, id3}, - expectedObjs: object.ObjMetadataSet{id1, id2, id3}, - }, - "no apply objs, no apply failures, no prune failures; no inventory": { - failedApplies: object.ObjMetadataSet{id3}, - expectedObjs: object.ObjMetadataSet{}, - }, - "one apply failure not in prev inventory; no inventory": { - failedApplies: object.ObjMetadataSet{id3}, - expectedObjs: object.ObjMetadataSet{}, - }, - "one apply obj, one apply failure not in prev inventory; one inventory": { - appliedObjs: object.ObjMetadataSet{id2}, - failedApplies: object.ObjMetadataSet{id3}, - expectedObjs: object.ObjMetadataSet{id2}, - }, - "one apply obj, one apply failure in prev inventory; one inventory": { - appliedObjs: object.ObjMetadataSet{id2}, - failedApplies: object.ObjMetadataSet{id3}, - prevInventory: object.ObjMetadataSet{id3}, - expectedObjs: object.ObjMetadataSet{id2, id3}, - }, - "one apply obj, two apply failures with one in prev inventory; two inventory": { - appliedObjs: object.ObjMetadataSet{id2}, - failedApplies: object.ObjMetadataSet{id1, id3}, - prevInventory: object.ObjMetadataSet{id3}, - expectedObjs: object.ObjMetadataSet{id2, id3}, - }, - "three apply failures with two in prev inventory; two inventory": { - failedApplies: object.ObjMetadataSet{id1, id2, id3}, - prevInventory: object.ObjMetadataSet{id2, id3}, - expectedObjs: object.ObjMetadataSet{id2, id3}, - }, - "three apply failures with three in prev inventory; three inventory": { - failedApplies: object.ObjMetadataSet{id1, id2, id3}, - prevInventory: object.ObjMetadataSet{id2, id3, id1}, - expectedObjs: object.ObjMetadataSet{id2, id1, id3}, - }, - "one skipped apply from prev inventory; one inventory": { - prevInventory: object.ObjMetadataSet{id1}, - skippedApplies: object.ObjMetadataSet{id1}, - expectedObjs: object.ObjMetadataSet{id1}, - }, - "one skipped apply, no prev inventory; no inventory": { - skippedApplies: object.ObjMetadataSet{id1}, - expectedObjs: object.ObjMetadataSet{}, - }, - "one apply obj, one skipped apply, two prev inventory; two inventory": { - prevInventory: object.ObjMetadataSet{id1, id2}, - appliedObjs: object.ObjMetadataSet{id2}, - skippedApplies: object.ObjMetadataSet{id1}, - expectedObjs: object.ObjMetadataSet{id1, id2}, - }, - "one skipped delete from prev inventory; one inventory": { - prevInventory: object.ObjMetadataSet{id1}, - skippedDeletes: object.ObjMetadataSet{id1}, - expectedObjs: object.ObjMetadataSet{id1}, - }, - "one apply obj, one skipped delete, two prev inventory; two inventory": { - prevInventory: object.ObjMetadataSet{id1, id2}, - appliedObjs: object.ObjMetadataSet{id2}, - skippedDeletes: object.ObjMetadataSet{id1}, - expectedObjs: object.ObjMetadataSet{id1, id2}, - }, - "two apply obj, one abandoned, three in prev inventory; two inventory": { - prevInventory: object.ObjMetadataSet{id1, id2, id3}, - appliedObjs: object.ObjMetadataSet{id1, id2}, - abandonedObjs: object.ObjMetadataSet{id3}, - expectedObjs: object.ObjMetadataSet{id1, id2}, - }, - "two abandoned, two in prev inventory; no inventory": { - prevInventory: object.ObjMetadataSet{id2, id3}, - abandonedObjs: object.ObjMetadataSet{id2, id3}, - expectedObjs: object.ObjMetadataSet{}, - }, - "same obj skipped delete and abandoned, one in prev inventory; no inventory": { - prevInventory: object.ObjMetadataSet{id3}, - skippedDeletes: object.ObjMetadataSet{id3}, - abandonedObjs: object.ObjMetadataSet{id3}, - expectedObjs: object.ObjMetadataSet{}, - }, - "preserve invalid objects in the inventory": { - prevInventory: object.ObjMetadataSet{id3, idInvalid}, - appliedObjs: object.ObjMetadataSet{id3}, - invalidObjs: object.ObjMetadataSet{idInvalid}, - expectedObjs: object.ObjMetadataSet{id3, idInvalid}, - }, - "ignore invalid objects not in the inventory": { - prevInventory: object.ObjMetadataSet{id3}, - appliedObjs: object.ObjMetadataSet{id3}, - invalidObjs: object.ObjMetadataSet{idInvalid}, - expectedObjs: object.ObjMetadataSet{id3}, - }, - "applied object failed to reconcile": { - prevInventory: object.ObjMetadataSet{}, - appliedObjs: object.ObjMetadataSet{id3}, - failedReconciles: object.ObjMetadataSet{id3}, - expectedObjs: object.ObjMetadataSet{id3}, - }, - "deleted object failed to reconcile": { - prevInventory: object.ObjMetadataSet{id3}, - deletedObjs: object.ObjMetadataSet{id3}, - failedReconciles: object.ObjMetadataSet{id3}, - expectedObjs: object.ObjMetadataSet{id3}, - }, - "applied object timed out to reconcile": { - prevInventory: object.ObjMetadataSet{}, - appliedObjs: object.ObjMetadataSet{id3}, - timeoutReconciles: object.ObjMetadataSet{id3}, - expectedObjs: object.ObjMetadataSet{id3}, - }, - "deleted object timed out to reconcile": { - prevInventory: object.ObjMetadataSet{id3}, - deletedObjs: object.ObjMetadataSet{id3}, - timeoutReconciles: object.ObjMetadataSet{id3}, - expectedObjs: object.ObjMetadataSet{id3}, - }, - } - - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - client := inventory.NewFakeClient(object.ObjMetadataSet{}) - eventChannel := make(chan event.Event) - resourceCache := cache.NewResourceCacheMap() - context := taskrunner.NewTaskContext(eventChannel, resourceCache) - - task := DeleteOrUpdateInvTask{ - TaskName: taskName, - InvClient: client, - InvInfo: nil, - PrevInventory: tc.prevInventory, - Destroy: false, - } - im := context.InventoryManager() - for _, applyObj := range tc.appliedObjs { - im.AddSuccessfulApply(applyObj, "unusued-uid", int64(0)) - } - for _, deleteObj := range tc.deletedObjs { - im.AddSuccessfulDelete(deleteObj, "unused-uid") - } - for _, applyFailure := range tc.failedApplies { - im.AddFailedApply(applyFailure) - } - for _, pruneObj := range tc.failedDeletes { - im.AddFailedDelete(pruneObj) - } - for _, skippedApply := range tc.skippedApplies { - im.AddSkippedApply(skippedApply) - } - for _, skippedDelete := range tc.skippedDeletes { - im.AddSkippedDelete(skippedDelete) - } - for _, abandonedObj := range tc.abandonedObjs { - context.AddAbandonedObject(abandonedObj) - } - for _, invalidObj := range tc.invalidObjs { - context.AddInvalidObject(invalidObj) - } - for _, failedReconcile := range tc.failedReconciles { - if err := im.SetFailedReconcile(failedReconcile); err != nil { - t.Fatal(err) - } - } - for _, timeoutReconcile := range tc.timeoutReconciles { - if err := im.SetTimeoutReconcile(timeoutReconcile); err != nil { - t.Fatal(err) - } - } - if taskName != task.Name() { - t.Errorf("expected task name (%s), got (%s)", taskName, task.Name()) - } - task.Start(context) - result := <-context.TaskChannel() - if result.Err != nil { - t.Errorf("unexpected error running InvAddTask: %s", result.Err) - } - actual, _ := client.GetClusterObjs(nil) - testutil.AssertEqual(t, tc.expectedObjs, actual, - "Actual cluster objects (%d) do not match expected cluster objects (%d)", - len(actual), len(tc.expectedObjs)) - }) - } -} diff --git a/pkg/apply/task/printer_adapter.go b/pkg/apply/task/printer_adapter.go deleted file mode 100644 index c2c6b616..00000000 --- a/pkg/apply/task/printer_adapter.go +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright 2019 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package task - -import ( - "fmt" - "io" - - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/object" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/cli-runtime/pkg/printers" -) - -// KubectlPrinterAdapter is a workaround for capturing progress from -// ApplyOptions. ApplyOptions were originally meant to print progress -// directly using a configurable printer. The KubectlPrinterAdapter -// plugs into ApplyOptions as a ToPrinter function, but instead of -// printing the info, it emits it as an event on the provided channel. -type KubectlPrinterAdapter struct { - ch chan<- event.Event - groupName string -} - -// resourcePrinterImpl implements the ResourcePrinter interface. But -// instead of printing, it emits information on the provided channel. -type resourcePrinterImpl struct { - applyStatus event.ApplyEventStatus - ch chan<- event.Event - groupName string -} - -// PrintObj takes the provided object and operation and emits -// it on the channel. -func (r *resourcePrinterImpl) PrintObj(obj runtime.Object, _ io.Writer) error { - id, err := object.RuntimeToObjMeta(obj) - if err != nil { - return err - } - r.ch <- event.Event{ - Type: event.ApplyType, - ApplyEvent: event.ApplyEvent{ - GroupName: r.groupName, - Identifier: id, - Status: r.applyStatus, - Resource: obj.(*unstructured.Unstructured), - }, - } - return nil -} - -type toPrinterFunc func(string) (printers.ResourcePrinter, error) - -// toPrinterFunc returns a function of type toPrinterFunc. This -// is the type required by the ApplyOptions. -func (p *KubectlPrinterAdapter) toPrinterFunc() toPrinterFunc { - return func(operation string) (printers.ResourcePrinter, error) { - applyStatus, err := kubectlOperationToApplyStatus(operation) - return &resourcePrinterImpl{ - ch: p.ch, - applyStatus: applyStatus, - groupName: p.groupName, - }, err - } -} - -func kubectlOperationToApplyStatus(operation string) (event.ApplyEventStatus, error) { - switch operation { - case "serverside-applied": - return event.ApplySuccessful, nil - case "created": - return event.ApplySuccessful, nil - case "unchanged": - return event.ApplySuccessful, nil - case "configured": - return event.ApplySuccessful, nil - default: - return event.ApplyEventStatus(0), fmt.Errorf("unknown operation %s", operation) - } -} diff --git a/pkg/apply/task/printer_adapter_test.go b/pkg/apply/task/printer_adapter_test.go deleted file mode 100644 index 63c36715..00000000 --- a/pkg/apply/task/printer_adapter_test.go +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2019 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package task - -import ( - "bytes" - "sync" - "testing" - - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/stretchr/testify/assert" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -func TestKubectlPrinterAdapter(t *testing.T) { - ch := make(chan event.Event) - buffer := bytes.Buffer{} - operation := "serverside-applied" - - adapter := KubectlPrinterAdapter{ - ch: ch, - groupName: "test-0", - } - - toPrinterFunc := adapter.toPrinterFunc() - resourcePrinter, err := toPrinterFunc(operation) - assert.NoError(t, err) - - deployment := &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "apps/v1", - "kind": "Deployment", - "metadata": map[string]interface{}{ - "name": "name", - "namespace": "namespace", - }, - }, - } - - // Need to run this in a separate gorutine since go channels - // are blocking. - var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() - err = resourcePrinter.PrintObj(deployment, &buffer) - }() - msg := <-ch - wg.Wait() - - assert.NoError(t, err) - assert.Equal(t, event.ApplySuccessful, msg.ApplyEvent.Status) - assert.Equal(t, deployment, msg.ApplyEvent.Resource) -} diff --git a/pkg/apply/task/prune_task.go b/pkg/apply/task/prune_task.go deleted file mode 100644 index 77ffee16..00000000 --- a/pkg/apply/task/prune_task.go +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package task - -import ( - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/apply/filter" - "github.com/fluxcd/cli-utils/pkg/apply/prune" - "github.com/fluxcd/cli-utils/pkg/apply/taskrunner" - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/object" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/klog/v2" -) - -// PruneTask prunes objects from the cluster -// by using the PruneOptions. The provided Objects is the -// set of resources that have just been applied. -type PruneTask struct { - TaskName string - - Pruner *prune.Pruner - Objects object.UnstructuredSet - Filters []filter.ValidationFilter - DryRunStrategy common.DryRunStrategy - PropagationPolicy metav1.DeletionPropagation - // True if we are destroying, which deletes the inventory object - // as well (possibly) the inventory namespace. - Destroy bool -} - -func (p *PruneTask) Name() string { - return p.TaskName -} - -func (p *PruneTask) Action() event.ResourceAction { - action := event.PruneAction - if p.Destroy { - action = event.DeleteAction - } - return action -} - -func (p *PruneTask) Identifiers() object.ObjMetadataSet { - return object.UnstructuredSetToObjMetadataSet(p.Objects) -} - -// Start creates a new goroutine that will invoke -// the Run function on the PruneOptions to update -// the cluster. It will push a TaskResult on the taskChannel -// to signal to the taskrunner that the task has completed (or failed). -func (p *PruneTask) Start(taskContext *taskrunner.TaskContext) { - go func() { - klog.V(2).Infof("prune task starting (name: %q, objects: %d)", - p.Name(), len(p.Objects)) - // Create filter to prevent deletion of currently applied - // objects. Must be done here to wait for applied UIDs. - uidFilter := filter.CurrentUIDFilter{ - CurrentUIDs: taskContext.InventoryManager().AppliedResourceUIDs(), - } - p.Filters = append(p.Filters, uidFilter) - err := p.Pruner.Prune( - p.Objects, - p.Filters, - taskContext, - p.Name(), - prune.Options{ - DryRunStrategy: p.DryRunStrategy, - PropagationPolicy: p.PropagationPolicy, - Destroy: p.Destroy, - }, - ) - klog.V(2).Infof("prune task completing (name: %q)", p.Name()) - taskContext.TaskChannel() <- taskrunner.TaskResult{ - Err: err, - } - }() -} - -// Cancel is not supported by the PruneTask. -func (p *PruneTask) Cancel(_ *taskrunner.TaskContext) {} - -// StatusUpdate is not supported by the PruneTask. -func (p *PruneTask) StatusUpdate(_ *taskrunner.TaskContext, _ object.ObjMetadata) {} diff --git a/pkg/apply/task/send_event_task.go b/pkg/apply/task/send_event_task.go deleted file mode 100644 index 686fb697..00000000 --- a/pkg/apply/task/send_event_task.go +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package task - -import ( - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/apply/taskrunner" -) - -// SendEventTask is an implementation of the Task interface -// that will send the provided event on the eventChannel when -// executed. -type SendEventTask struct { - Event event.Event -} - -// Start start a separate goroutine that will send the -// event and then push a TaskResult on the taskChannel to -// signal to the taskrunner that the task is completed. -func (s *SendEventTask) Start(taskContext *taskrunner.TaskContext) { - go func() { - taskContext.SendEvent(s.Event) - taskContext.TaskChannel() <- taskrunner.TaskResult{} - }() -} - -// ClearTimeout doesn't do anything as SendEventTask doesn't support -// timeouts. -func (s *SendEventTask) ClearTimeout() {} diff --git a/pkg/apply/taskrunner/condition.go b/pkg/apply/taskrunner/condition.go deleted file mode 100644 index 1eb5d898..00000000 --- a/pkg/apply/taskrunner/condition.go +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package taskrunner - -import ( - "github.com/fluxcd/cli-utils/pkg/kstatus/status" - "github.com/fluxcd/cli-utils/pkg/object" -) - -// Condition is a type that defines the types of conditions -// which a WaitTask can use. -type Condition string - -const ( - // AllCurrent Condition means all the provided resources - // has reached (and remains in) the Current status. - AllCurrent Condition = "AllCurrent" - - // AllNotFound Condition means all the provided resources - // has reached the NotFound status, i.e. they are all deleted - // from the cluster. - AllNotFound Condition = "AllNotFound" -) - -// Meets returns true if the provided status meets the condition and -// false if it does not. -func (c Condition) Meets(s status.Status) bool { - switch c { - case AllCurrent: - return s == status.CurrentStatus - case AllNotFound: - return s == status.NotFoundStatus - default: - return false - } -} - -// conditionMet tests whether the provided Condition holds true for -// all resources in the list, according to the ResourceCache. -// Resources in the cache older that the applied generation are non-matches. -func conditionMet(taskContext *TaskContext, ids object.ObjMetadataSet, c Condition) bool { - switch c { - case AllCurrent: - return allMatchStatus(taskContext, ids, status.CurrentStatus) - case AllNotFound: - return allMatchStatus(taskContext, ids, status.NotFoundStatus) - default: - return noneMatchStatus(taskContext, ids, status.UnknownStatus) - } -} - -// allMatchStatus checks whether all of the resources provided have the provided status. -// Resources with older generations are considered non-matching. -func allMatchStatus(taskContext *TaskContext, ids object.ObjMetadataSet, s status.Status) bool { - for _, id := range ids { - cached := taskContext.ResourceCache().Get(id) - if cached.Status != s { - return false - } - - applyGen, _ := taskContext.InventoryManager().AppliedGeneration(id) // generation at apply time - cachedGen := int64(0) - if cached.Resource != nil { - cachedGen = cached.Resource.GetGeneration() - } - if cachedGen < applyGen { - // cache too old - return false - } - } - return true -} - -// allMatchStatus checks whether none of the resources provided have the provided status. -// Resources with older generations are considered matching. -func noneMatchStatus(taskContext *TaskContext, ids object.ObjMetadataSet, s status.Status) bool { - for _, id := range ids { - cached := taskContext.ResourceCache().Get(id) - if cached.Status == s { - return false - } - - applyGen, _ := taskContext.InventoryManager().AppliedGeneration(id) // generation at apply time - cachedGen := int64(0) - if cached.Resource != nil { - cachedGen = cached.Resource.GetGeneration() - } - if cachedGen < applyGen { - // cache too old - return false - } - } - return true -} diff --git a/pkg/apply/taskrunner/condition_test.go b/pkg/apply/taskrunner/condition_test.go deleted file mode 100644 index 7a19ccaf..00000000 --- a/pkg/apply/taskrunner/condition_test.go +++ /dev/null @@ -1,169 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package taskrunner - -import ( - "testing" - - "github.com/fluxcd/cli-utils/pkg/apply/cache" - ktestutil "github.com/fluxcd/cli-utils/pkg/kstatus/polling/testutil" - "github.com/fluxcd/cli-utils/pkg/kstatus/status" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/stretchr/testify/assert" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/types" -) - -var deployment1y = ` -apiVersion: apps/v1 -kind: Deployment -metadata: - name: Foo - namespace: default -spec: - replicas: 1 -status: - replicas: 1 - readyReplicas: 1 - updatedReplicas: 1 - availableReplicas: 1 - conditions: - - status: "True" - type: Available - - status: "True" - type: Ready -` - -var custom1y = ` -apiVersion: custom.io/v1alpha1 -kind: Custom -metadata: - name: Foo - namespace: default -spec: {} -status: -conditions: -- status: "False" - type: Ready -` - -// withGeneration returns a DeepCopy with .metadata.generation set. -func withGeneration(obj *unstructured.Unstructured, gen int64) *unstructured.Unstructured { - obj = obj.DeepCopy() - obj.SetGeneration(gen) - return obj -} - -func TestCollector_ConditionMet(t *testing.T) { - deployment1 := ktestutil.YamlToUnstructured(t, deployment1y) - deployment1Meta := object.UnstructuredToObjMetadata(deployment1) - custom1 := ktestutil.YamlToUnstructured(t, custom1y) - custom1Meta := object.UnstructuredToObjMetadata(custom1) - - testCases := map[string]struct { - cacheContents []cache.ResourceStatus - appliedGen map[object.ObjMetadata]int64 - ids object.ObjMetadataSet - condition Condition - expectedResult bool - }{ - "single resource with current status": { - cacheContents: []cache.ResourceStatus{ - { - Resource: withGeneration(deployment1, 42), - Status: status.CurrentStatus, - }, - }, - appliedGen: map[object.ObjMetadata]int64{ - deployment1Meta: 42, - }, - ids: object.ObjMetadataSet{ - deployment1Meta, - }, - condition: AllCurrent, - expectedResult: true, - }, - "single resource with current status and old generation": { - cacheContents: []cache.ResourceStatus{ - { - Resource: withGeneration(deployment1, 41), - Status: status.CurrentStatus, - }, - }, - appliedGen: map[object.ObjMetadata]int64{ - deployment1Meta: 42, - }, - ids: object.ObjMetadataSet{ - deployment1Meta, - }, - condition: AllCurrent, - expectedResult: false, - }, - "multiple resources not all current": { - cacheContents: []cache.ResourceStatus{ - { - Resource: withGeneration(deployment1, 42), - Status: status.InProgressStatus, - }, - { - Resource: withGeneration(custom1, 0), - Status: status.CurrentStatus, - }, - }, - appliedGen: map[object.ObjMetadata]int64{ - deployment1Meta: 42, - custom1Meta: 0, - }, - ids: object.ObjMetadataSet{ - deployment1Meta, - custom1Meta, - }, - condition: AllCurrent, - expectedResult: false, - }, - "multiple resources single with old generation": { - cacheContents: []cache.ResourceStatus{ - { - Resource: withGeneration(deployment1, 42), - Status: status.CurrentStatus, - }, - { - Resource: withGeneration(custom1, 4), - Status: status.CurrentStatus, - }, - }, - appliedGen: map[object.ObjMetadata]int64{ - deployment1Meta: 42, - custom1Meta: 5, - }, - ids: object.ObjMetadataSet{ - deployment1Meta, - custom1Meta, - }, - condition: AllCurrent, - expectedResult: false, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - resourceCache := cache.NewResourceCacheMap() - if tc.cacheContents != nil { - resourceCache.Load(tc.cacheContents...) - } - - taskContext := NewTaskContext(nil, resourceCache) - - if tc.appliedGen != nil { - for id, gen := range tc.appliedGen { - taskContext.InventoryManager().AddSuccessfulApply(id, types.UID("unused"), gen) - } - } - - res := conditionMet(taskContext, tc.ids, tc.condition) - - assert.Equal(t, tc.expectedResult, res) - }) - } -} diff --git a/pkg/apply/taskrunner/context.go b/pkg/apply/taskrunner/context.go deleted file mode 100644 index 5459ade2..00000000 --- a/pkg/apply/taskrunner/context.go +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package taskrunner - -import ( - "github.com/fluxcd/cli-utils/pkg/apply/cache" - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/object/graph" - "k8s.io/klog/v2" -) - -// NewTaskContext returns a new TaskContext -func NewTaskContext(eventChannel chan event.Event, resourceCache cache.ResourceCache) *TaskContext { - return &TaskContext{ - taskChannel: make(chan TaskResult), - eventChannel: eventChannel, - resourceCache: resourceCache, - inventoryManager: inventory.NewManager(), - abandonedObjects: make(map[object.ObjMetadata]struct{}), - invalidObjects: make(map[object.ObjMetadata]struct{}), - graph: graph.New(), - } -} - -// TaskContext defines a context that is passed between all -// the tasks that is in a taskqueue. -type TaskContext struct { - taskChannel chan TaskResult - eventChannel chan event.Event - resourceCache cache.ResourceCache - inventoryManager *inventory.Manager - abandonedObjects map[object.ObjMetadata]struct{} - invalidObjects map[object.ObjMetadata]struct{} - graph *graph.Graph -} - -func (tc *TaskContext) TaskChannel() chan TaskResult { - return tc.taskChannel -} - -func (tc *TaskContext) EventChannel() chan event.Event { - return tc.eventChannel -} - -func (tc *TaskContext) ResourceCache() cache.ResourceCache { - return tc.resourceCache -} - -func (tc *TaskContext) InventoryManager() *inventory.Manager { - return tc.inventoryManager -} - -func (tc *TaskContext) Graph() *graph.Graph { - return tc.graph -} - -func (tc *TaskContext) SetGraph(g *graph.Graph) { - tc.graph = g -} - -// SendEvent sends an event on the event channel -func (tc *TaskContext) SendEvent(e event.Event) { - klog.V(3).Infof("Sending event: %v", e) - tc.eventChannel <- e -} - -// IsAbandonedObject returns true if the object is abandoned -func (tc *TaskContext) IsAbandonedObject(id object.ObjMetadata) bool { - _, found := tc.abandonedObjects[id] - return found -} - -// AddAbandonedObject registers that the object is abandoned -func (tc *TaskContext) AddAbandonedObject(id object.ObjMetadata) { - tc.abandonedObjects[id] = struct{}{} -} - -// AbandonedObjects returns all the abandoned objects -func (tc *TaskContext) AbandonedObjects() object.ObjMetadataSet { - return object.ObjMetadataSetFromMap(tc.abandonedObjects) -} - -// IsInvalidObject returns true if the object is abandoned -func (tc *TaskContext) IsInvalidObject(id object.ObjMetadata) bool { - _, found := tc.invalidObjects[id] - return found -} - -// AddInvalidObject registers that the object is abandoned -func (tc *TaskContext) AddInvalidObject(id object.ObjMetadata) { - tc.invalidObjects[id] = struct{}{} -} - -// InvalidObjects returns all the abandoned objects -func (tc *TaskContext) InvalidObjects() object.ObjMetadataSet { - return object.ObjMetadataSetFromMap(tc.invalidObjects) -} diff --git a/pkg/apply/taskrunner/main_test.go b/pkg/apply/taskrunner/main_test.go deleted file mode 100644 index b06b1384..00000000 --- a/pkg/apply/taskrunner/main_test.go +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package taskrunner - -import ( - "os" - "testing" - - "k8s.io/klog/v2" -) - -// TestMain executes the tests for this package, with optional logging. -// To see all logs, use: -// go test github.com/fluxcd/cli-utils/pkg/apply/taskrunner -v -args -v=5 -func TestMain(m *testing.M) { - klog.InitFlags(nil) - os.Exit(m.Run()) -} diff --git a/pkg/apply/taskrunner/runner.go b/pkg/apply/taskrunner/runner.go deleted file mode 100644 index e3a9b02a..00000000 --- a/pkg/apply/taskrunner/runner.go +++ /dev/null @@ -1,260 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package taskrunner - -import ( - "context" - "fmt" - - "github.com/fluxcd/cli-utils/pkg/apply/cache" - "github.com/fluxcd/cli-utils/pkg/apply/event" - pollevent "github.com/fluxcd/cli-utils/pkg/kstatus/polling/event" - "github.com/fluxcd/cli-utils/pkg/kstatus/watcher" - "github.com/fluxcd/cli-utils/pkg/object" - "k8s.io/klog/v2" -) - -// NewTaskStatusRunner returns a new TaskStatusRunner. -func NewTaskStatusRunner(identifiers object.ObjMetadataSet, statusWatcher watcher.StatusWatcher) *TaskStatusRunner { - return &TaskStatusRunner{ - Identifiers: identifiers, - StatusWatcher: statusWatcher, - } -} - -// TaskStatusRunner is a taskRunner that executes a set of -// tasks while at the same time uses the statusPoller to -// keep track of the status of the resources. -type TaskStatusRunner struct { - Identifiers object.ObjMetadataSet - StatusWatcher watcher.StatusWatcher -} - -// Options defines properties that is passed along to -// the statusPoller. -type Options struct { - EmitStatusEvents bool - // RESTScopeStrategy specifies which strategy to use when listing and - // watching resources. By default, the strategy is selected automatically. - WatcherRESTScopeStrategy watcher.RESTScopeStrategy -} - -// Run executes the tasks in the taskqueue, with the statusPoller running in the -// background. -// -// The tasks run in a loop where a single goroutine will process events from -// three different channels. -// - taskQueue is read to allow updating the task queue at runtime. -// - statusChannel is read to allow updates to the resource cache and triggering -// validation of wait conditions. -// - eventChannel is written to with events based on status updates, if -// emitStatusEvents is true. -func (tsr *TaskStatusRunner) Run( - ctx context.Context, - taskContext *TaskContext, - taskQueue chan Task, - opts Options, -) error { - // Give the poller its own context and run it in the background. - // If taskStatusRunner.Run is cancelled, baseRunner.run will exit early, - // causing the poller to be cancelled. - statusCtx, cancelFunc := context.WithCancel(context.Background()) - statusChannel := tsr.StatusWatcher.Watch(statusCtx, tsr.Identifiers, watcher.Options{ - RESTScopeStrategy: opts.WatcherRESTScopeStrategy, - }) - - // complete stops the statusPoller, drains the statusChannel, and returns - // the provided error. - // Run this before returning! - // Avoid using defer, otherwise the statusPoller will hang. It needs to be - // drained synchronously before return, instead of asynchronously after. - complete := func(err error) error { - klog.V(7).Info("Runner cancelled status watcher") - cancelFunc() - for statusEvent := range statusChannel { - klog.V(7).Infof("Runner ignored status event: %v", statusEvent) - } - return err - } - - // Wait until the StatusWatcher is sychronized to start the first task. - var currentTask Task - done := false - - // abort is used to signal that something has failed, and - // the task processing should end as soon as is possible. Only - // wait tasks can be interrupted, so for all other tasks we need - // to wait for the currently running one to finish before we can - // exit. - abort := false - var abortReason error - - // We do this so we can set the doneCh to a nil channel after - // it has been closed. This is needed to avoid a busy loop. - doneCh := ctx.Done() - - for { - select { - // This processes status events from a channel, most likely - // driven by the StatusPoller. All normal resource status update - // events are passed through to the eventChannel. This means - // that listeners of the eventChannel will get updates on status - // even while other tasks (like apply tasks) are running. - case statusEvent, ok := <-statusChannel: - // If the statusChannel has closed or we are preparing - // to abort the task processing, we just ignore all - // statusEvents. - // TODO(mortent): Check if a closed statusChannel might - // create a busy loop here. - if !ok { - continue - } - - if abort { - klog.V(7).Infof("Runner ignored status event: %v", statusEvent) - continue - } - klog.V(7).Infof("Runner received status event: %v", statusEvent) - - // An error event on the statusChannel means the StatusWatcher - // has encountered a problem so it can't continue. This means - // the statusChannel will be closed soon. - if statusEvent.Type == pollevent.ErrorEvent { - abort = true - abortReason = fmt.Errorf("polling for status failed: %v", - statusEvent.Error) - if currentTask != nil { - currentTask.Cancel(taskContext) - } else { - // tasks not started yet - abort now - return complete(abortReason) - } - continue - } - - // The StatusWatcher is synchronized. - // Tasks may commence! - if statusEvent.Type == pollevent.SyncEvent { - // Find and start the first task in the queue. - currentTask, done = nextTask(taskQueue, taskContext) - if done { - return complete(nil) - } - continue - } - - if opts.EmitStatusEvents { - // Forward all normal events to the eventChannel - taskContext.SendEvent(event.Event{ - Type: event.StatusType, - StatusEvent: event.StatusEvent{ - Identifier: statusEvent.Resource.Identifier, - PollResourceInfo: statusEvent.Resource, - Resource: statusEvent.Resource.Resource, - Error: statusEvent.Error, - }, - }) - } - - id := statusEvent.Resource.Identifier - - // Update the cache to track the latest resource spec & status. - // Status is computed from the resource on-demand. - // Warning: Resource may be nil! - taskContext.ResourceCache().Put(id, cache.ResourceStatus{ - Resource: statusEvent.Resource.Resource, - Status: statusEvent.Resource.Status, - StatusMessage: statusEvent.Resource.Message, - }) - - // send a status update to the running task, but only if the status - // has changed and the task is tracking the object. - if currentTask != nil { - if currentTask.Identifiers().Contains(id) { - currentTask.StatusUpdate(taskContext, id) - } - } - // A message on the taskChannel means that the current task - // has either completed or failed. - // If it has failed, we return the error. - // If the abort flag is true, which means something - // else has gone wrong and we are waiting for the current task to - // finish, we exit. - // If everything is ok, we fetch and start the next task. - case msg := <-taskContext.TaskChannel(): - taskContext.SendEvent(event.Event{ - Type: event.ActionGroupType, - ActionGroupEvent: event.ActionGroupEvent{ - GroupName: currentTask.Name(), - Action: currentTask.Action(), - Status: event.Finished, - }, - }) - if msg.Err != nil { - return complete( - fmt.Errorf("task failed (action: %q, name: %q): %w", - currentTask.Action(), currentTask.Name(), msg.Err)) - } - if abort { - return complete(abortReason) - } - currentTask, done = nextTask(taskQueue, taskContext) - // If there are no more tasks, we are done. So just - // return. - if done { - return complete(nil) - } - // The doneCh will be closed if the passed in context is cancelled. - // If so, we just set the abort flag and wait for the currently running - // task to complete before we exit. - case <-doneCh: - doneCh = nil // Set doneCh to nil so we don't enter a busy loop. - abort = true - abortReason = ctx.Err() // always non-nil when doneCh is closed - klog.V(7).Infof("Runner aborting: %v", abortReason) - if currentTask != nil { - currentTask.Cancel(taskContext) - } else { - // tasks not started yet - abort now - return complete(abortReason) - } - } - } -} - -// nextTask fetches the latest task from the taskQueue and -// starts it. If the taskQueue is empty, it the second -// return value will be true. -func nextTask(taskQueue chan Task, taskContext *TaskContext) (Task, bool) { - var tsk Task - select { - // If there is any tasks left in the queue, this - // case statement will be executed. - case t := <-taskQueue: - tsk = t - default: - // Only happens when the channel is empty. - return nil, true - } - - taskContext.SendEvent(event.Event{ - Type: event.ActionGroupType, - ActionGroupEvent: event.ActionGroupEvent{ - GroupName: tsk.Name(), - Action: tsk.Action(), - Status: event.Started, - }, - }) - - tsk.Start(taskContext) - - return tsk, false -} - -// TaskResult is the type returned from tasks once they have completed -// or failed. If it has failed or timed out, the Err property will be -// set. -type TaskResult struct { - Err error -} diff --git a/pkg/apply/taskrunner/runner_test.go b/pkg/apply/taskrunner/runner_test.go deleted file mode 100644 index 6ee78027..00000000 --- a/pkg/apply/taskrunner/runner_test.go +++ /dev/null @@ -1,575 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package taskrunner - -import ( - "context" - "fmt" - "sync" - "testing" - "time" - - "github.com/fluxcd/cli-utils/pkg/apply/cache" - "github.com/fluxcd/cli-utils/pkg/apply/event" - pollevent "github.com/fluxcd/cli-utils/pkg/kstatus/polling/event" - "github.com/fluxcd/cli-utils/pkg/kstatus/status" - "github.com/fluxcd/cli-utils/pkg/kstatus/watcher" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/testutil" - "github.com/stretchr/testify/assert" - "k8s.io/apimachinery/pkg/runtime/schema" -) - -var ( - depID = object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - }, - Namespace: "default", - Name: "dep", - } - cmID = object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "", - Kind: "ConfigMap", - }, - Namespace: "default", - Name: "cm", - } -) - -func TestBaseRunner(t *testing.T) { - testCases := map[string]struct { - tasks []Task - statusEventsDelay time.Duration - statusEvents []pollevent.Event - expectedEventTypes []event.Type - expectedWaitEvents []event.WaitEvent - }{ - "wait task runs until condition is met": { - tasks: []Task{ - &fakeApplyTask{ - resultEvent: event.Event{ - Type: event.ApplyType, - }, - duration: 3 * time.Second, - }, - NewWaitTask("wait", object.ObjMetadataSet{depID, cmID}, AllCurrent, - 1*time.Minute, testutil.NewFakeRESTMapper()), - &fakeApplyTask{ - resultEvent: event.Event{ - Type: event.PruneType, - }, - duration: 2 * time.Second, - }, - }, - statusEventsDelay: 5 * time.Second, - statusEvents: []pollevent.Event{ - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: cmID, - Status: status.CurrentStatus, - }, - }, - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: depID, - Status: status.CurrentStatus, - }, - }, - }, - expectedEventTypes: []event.Type{ - event.ActionGroupType, - event.ApplyType, - event.ActionGroupType, - event.ActionGroupType, - event.WaitType, // deployment pending - event.WaitType, // configmap pending - event.StatusType, // configmap current - event.WaitType, // configmap reconciled - event.StatusType, // deployment current - event.WaitType, // deployment reconciled - event.ActionGroupType, - event.ActionGroupType, - event.PruneType, - event.ActionGroupType, - }, - expectedWaitEvents: []event.WaitEvent{ - { - GroupName: "wait", - Identifier: depID, - Status: event.ReconcilePending, - }, - { - GroupName: "wait", - Identifier: cmID, - Status: event.ReconcilePending, - }, - { - GroupName: "wait", - Identifier: cmID, - Status: event.ReconcileSuccessful, - }, - { - GroupName: "wait", - Identifier: depID, - Status: event.ReconcileSuccessful, - }, - }, - }, - "wait task times out eventually (Unknown)": { - tasks: []Task{ - NewWaitTask("wait", object.ObjMetadataSet{depID, cmID}, AllCurrent, - 2*time.Second, testutil.NewFakeRESTMapper()), - }, - statusEventsDelay: time.Second, - statusEvents: []pollevent.Event{ - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: cmID, - Status: status.CurrentStatus, - }, - }, - }, - expectedEventTypes: []event.Type{ - event.ActionGroupType, - event.WaitType, // configmap pending - event.WaitType, // deployment pending - event.StatusType, // configmap current - event.WaitType, // configmap reconciled - event.WaitType, // deployment timeout error - event.ActionGroupType, - }, - expectedWaitEvents: []event.WaitEvent{ - { - GroupName: "wait", - Identifier: depID, - Status: event.ReconcilePending, - }, - { - GroupName: "wait", - Identifier: cmID, - Status: event.ReconcilePending, - }, - { - GroupName: "wait", - Identifier: cmID, - Status: event.ReconcileSuccessful, - }, - { - GroupName: "wait", - Identifier: depID, - Status: event.ReconcileTimeout, - }, - }, - }, - "wait task times out eventually (InProgress)": { - tasks: []Task{ - NewWaitTask("wait", object.ObjMetadataSet{depID, cmID}, AllCurrent, - 2*time.Second, testutil.NewFakeRESTMapper()), - }, - statusEventsDelay: time.Second, - statusEvents: []pollevent.Event{ - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: cmID, - Status: status.CurrentStatus, - }, - }, - { - Type: pollevent.ResourceUpdateEvent, - Resource: &pollevent.ResourceStatus{ - Identifier: depID, - Status: status.InProgressStatus, - }, - }, - }, - expectedEventTypes: []event.Type{ - event.ActionGroupType, - event.WaitType, // configmap pending - event.WaitType, // deployment pending - event.StatusType, // configmap current - event.WaitType, // configmap reconciled - event.StatusType, // deployment inprogress - event.WaitType, // deployment timeout error - event.ActionGroupType, - }, - expectedWaitEvents: []event.WaitEvent{ - { - GroupName: "wait", - Identifier: depID, - Status: event.ReconcilePending, - }, - { - GroupName: "wait", - Identifier: cmID, - Status: event.ReconcilePending, - }, - { - GroupName: "wait", - Identifier: cmID, - Status: event.ReconcileSuccessful, - }, - { - GroupName: "wait", - Identifier: depID, - Status: event.ReconcileTimeout, - }, - }, - }, - "tasks run in order": { - tasks: []Task{ - &fakeApplyTask{ - resultEvent: event.Event{ - Type: event.ApplyType, - }, - duration: 1 * time.Second, - }, - &fakeApplyTask{ - resultEvent: event.Event{ - Type: event.PruneType, - }, - duration: 1 * time.Second, - }, - &fakeApplyTask{ - resultEvent: event.Event{ - Type: event.ApplyType, - }, - duration: 1 * time.Second, - }, - &fakeApplyTask{ - resultEvent: event.Event{ - Type: event.PruneType, - }, - duration: 1 * time.Second, - }, - }, - statusEventsDelay: 1 * time.Second, - statusEvents: []pollevent.Event{}, - expectedEventTypes: []event.Type{ - event.ActionGroupType, - event.ApplyType, - event.ActionGroupType, - event.ActionGroupType, - event.PruneType, - event.ActionGroupType, - event.ActionGroupType, - event.ApplyType, - event.ActionGroupType, - event.ActionGroupType, - event.PruneType, - event.ActionGroupType, - }, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - taskQueue := make(chan Task, len(tc.tasks)) - for _, tsk := range tc.tasks { - taskQueue <- tsk - } - - ids := object.ObjMetadataSet{} // unused by fake statusWatcher - statusWatcher := newFakeWatcher(tc.statusEvents) - eventChannel := make(chan event.Event) - resourceCache := cache.NewResourceCacheMap() - taskContext := NewTaskContext(eventChannel, resourceCache) - runner := NewTaskStatusRunner(ids, statusWatcher) - - // Use a WaitGroup to make sure changes in the goroutines - // are visible to the main goroutine. - var wg sync.WaitGroup - - statusChannel := make(chan pollevent.Event) - wg.Add(1) - go func() { - defer wg.Done() - - time.Sleep(tc.statusEventsDelay) - statusWatcher.Start() - }() - - var events []event.Event - wg.Add(1) - go func() { - defer wg.Done() - - for msg := range eventChannel { - events = append(events, msg) - } - }() - - opts := Options{EmitStatusEvents: true} - ctx := context.Background() - err := runner.Run(ctx, taskContext, taskQueue, opts) - close(statusChannel) - close(eventChannel) - wg.Wait() - - assert.NoError(t, err) - - if want, got := len(tc.expectedEventTypes), len(events); want != got { - t.Errorf("expected %d events, but got %d", want, got) - } - var waitEvents []event.WaitEvent - for i, e := range events { - expectedEventType := tc.expectedEventTypes[i] - if want, got := expectedEventType, e.Type; want != got { - t.Errorf("expected event type %s, but got %s", - want, got) - } - if e.Type == event.WaitType { - waitEvents = append(waitEvents, e.WaitEvent) - } - } - assert.Equal(t, tc.expectedWaitEvents, waitEvents) - }) - } -} - -func TestBaseRunnerCancellation(t *testing.T) { - testError := fmt.Errorf("this is a test error") - - testCases := map[string]struct { - tasks []Task - statusEventsDelay time.Duration - statusEvents []pollevent.Event - contextTimeout time.Duration - expectedError error - expectedEventTypes []event.Type - }{ - "cancellation while custom task is running": { - tasks: []Task{ - &fakeApplyTask{ - resultEvent: event.Event{ - Type: event.ApplyType, - }, - duration: 4 * time.Second, - }, - &fakeApplyTask{ - resultEvent: event.Event{ - Type: event.PruneType, - }, - duration: 2 * time.Second, - }, - }, - contextTimeout: 2 * time.Second, - expectedError: context.DeadlineExceeded, - expectedEventTypes: []event.Type{ - event.ActionGroupType, - event.ApplyType, - event.ActionGroupType, - }, - }, - "cancellation while wait task is running": { - tasks: []Task{ - NewWaitTask("wait", object.ObjMetadataSet{depID}, AllCurrent, - 20*time.Second, testutil.NewFakeRESTMapper()), - &fakeApplyTask{ - resultEvent: event.Event{ - Type: event.PruneType, - }, - duration: 2 * time.Second, - }, - }, - contextTimeout: 2 * time.Second, - expectedError: context.DeadlineExceeded, - expectedEventTypes: []event.Type{ - event.ActionGroupType, - event.WaitType, // pending - event.ActionGroupType, - }, - }, - "error while custom task is running": { - tasks: []Task{ - &fakeApplyTask{ - name: "apply-0", - resultEvent: event.Event{ - Type: event.ApplyType, - }, - duration: 2 * time.Second, - err: testError, - }, - &fakeApplyTask{ - name: "prune-0", - resultEvent: event.Event{ - Type: event.PruneType, - }, - duration: 2 * time.Second, - }, - }, - contextTimeout: 30 * time.Second, - expectedError: fmt.Errorf(`task failed (action: "Apply", name: "apply-0"): %w`, testError), - expectedEventTypes: []event.Type{ - event.ActionGroupType, - event.ApplyType, - event.ActionGroupType, - }, - }, - "error from status watcher while wait task is running": { - tasks: []Task{ - NewWaitTask("wait", object.ObjMetadataSet{depID}, AllCurrent, - 20*time.Second, testutil.NewFakeRESTMapper()), - &fakeApplyTask{ - resultEvent: event.Event{ - Type: event.PruneType, - }, - duration: 2 * time.Second, - }, - }, - statusEventsDelay: 2 * time.Second, - statusEvents: []pollevent.Event{ - { - Type: pollevent.ErrorEvent, - Error: testError, - }, - }, - contextTimeout: 30 * time.Second, - expectedError: fmt.Errorf("polling for status failed: %w", testError), - expectedEventTypes: []event.Type{ - event.ActionGroupType, - event.WaitType, // pending - event.ActionGroupType, - }, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - taskQueue := make(chan Task, len(tc.tasks)) - for _, tsk := range tc.tasks { - taskQueue <- tsk - } - - ids := object.ObjMetadataSet{} // unused by fake statusWatcher - statusWatcher := newFakeWatcher(tc.statusEvents) - eventChannel := make(chan event.Event) - resourceCache := cache.NewResourceCacheMap() - taskContext := NewTaskContext(eventChannel, resourceCache) - runner := NewTaskStatusRunner(ids, statusWatcher) - - // Use a WaitGroup to make sure changes in the goroutines - // are visible to the main goroutine. - var wg sync.WaitGroup - - statusChannel := make(chan pollevent.Event) - wg.Add(1) - go func() { - defer wg.Done() - - time.Sleep(tc.statusEventsDelay) - statusWatcher.Start() - }() - - var events []event.Event - wg.Add(1) - go func() { - defer wg.Done() - - for msg := range eventChannel { - events = append(events, msg) - } - }() - - ctx, cancel := context.WithTimeout(context.Background(), tc.contextTimeout) - defer cancel() - - opts := Options{EmitStatusEvents: true} - err := runner.Run(ctx, taskContext, taskQueue, opts) - close(statusChannel) - close(eventChannel) - wg.Wait() - - if tc.expectedError != nil { - assert.EqualError(t, err, tc.expectedError.Error()) - } else { - assert.NoError(t, err) - } - - if want, got := len(tc.expectedEventTypes), len(events); want != got { - t.Errorf("expected %d events, but got %d", want, got) - } - for i, e := range events { - expectedEventType := tc.expectedEventTypes[i] - if want, got := expectedEventType, e.Type; want != got { - t.Errorf("expected event type %s, but got %s", - want, got) - } - } - }) - } -} - -type fakeApplyTask struct { - name string - resultEvent event.Event - duration time.Duration - err error -} - -func (f *fakeApplyTask) Name() string { - return f.name -} - -func (f *fakeApplyTask) Action() event.ResourceAction { - return event.ApplyAction -} - -func (f *fakeApplyTask) Identifiers() object.ObjMetadataSet { - return object.ObjMetadataSet{} -} - -func (f *fakeApplyTask) Start(taskContext *TaskContext) { - go func() { - <-time.NewTimer(f.duration).C - taskContext.SendEvent(f.resultEvent) - taskContext.TaskChannel() <- TaskResult{ - Err: f.err, - } - }() -} - -func (f *fakeApplyTask) Cancel(_ *TaskContext) {} - -func (f *fakeApplyTask) StatusUpdate(_ *TaskContext, _ object.ObjMetadata) {} - -type fakeWatcher struct { - start chan struct{} - events []pollevent.Event -} - -func newFakeWatcher(statusEvents []pollevent.Event) *fakeWatcher { - return &fakeWatcher{ - events: statusEvents, - start: make(chan struct{}), - } -} - -// Start events being sent on the status channel -func (f *fakeWatcher) Start() { - close(f.start) -} - -func (f *fakeWatcher) Watch(ctx context.Context, _ object.ObjMetadataSet, _ watcher.Options) <-chan pollevent.Event { - eventChannel := make(chan pollevent.Event) - go func() { - defer close(eventChannel) - // send sync event immediately - eventChannel <- pollevent.Event{Type: pollevent.SyncEvent} - // wait until started to send the events - <-f.start - for _, f := range f.events { - eventChannel <- f - } - // wait until cancelled to close the event channel and exit - <-ctx.Done() - }() - return eventChannel -} diff --git a/pkg/apply/taskrunner/task.go b/pkg/apply/taskrunner/task.go deleted file mode 100644 index f2aefb89..00000000 --- a/pkg/apply/taskrunner/task.go +++ /dev/null @@ -1,431 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package taskrunner - -import ( - "context" - "fmt" - "sync" - "time" - - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/kstatus/status" - "github.com/fluxcd/cli-utils/pkg/object" - "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/types" - "k8s.io/klog/v2" -) - -var ( - crdGK = schema.GroupKind{Group: "apiextensions.k8s.io", Kind: "CustomResourceDefinition"} -) - -// Task is the interface that must be implemented by -// all tasks that will be executed by the taskrunner. -type Task interface { - Name() string - Action() event.ResourceAction - Identifiers() object.ObjMetadataSet - Start(*TaskContext) - StatusUpdate(*TaskContext, object.ObjMetadata) - Cancel(*TaskContext) -} - -// NewWaitTask creates a new wait task where we will wait until -// the resources specifies by ids all meet the specified condition. -func NewWaitTask(name string, ids object.ObjMetadataSet, cond Condition, timeout time.Duration, mapper meta.RESTMapper) *WaitTask { - return &WaitTask{ - TaskName: name, - Ids: ids, - Condition: cond, - Timeout: timeout, - Mapper: mapper, - } -} - -// WaitTask is an implementation of the Task interface that is used -// to wait for a set of resources (identified by a slice of ObjMetadata) -// will all meet the condition specified. It also specifies a timeout -// for how long we are willing to wait for this to happen. -// Unlike other implementations of the Task interface, the wait task -// is handled in a special way to the taskrunner and is a part of the core -// package. -type WaitTask struct { - // TaskName allows providing a name for the task. - TaskName string - // Ids is the full list of resources that we are waiting for. - Ids object.ObjMetadataSet //nolint:revive - // Condition defines the status we want all resources to reach - Condition Condition - // Timeout defines how long we are willing to wait for the condition - // to be met. - Timeout time.Duration - // Mapper is the RESTMapper to update after CRDs have been reconciled - Mapper meta.RESTMapper - // cancelFunc is a function that will cancel the timeout timer - // on the task. - cancelFunc context.CancelFunc - // pending is the set of resources that we are still waiting for. - pending object.ObjMetadataSet - // failed is the set of resources that we are waiting for, but is considered - // failed, i.e. unlikely to successfully reconcile. - failed object.ObjMetadataSet - // mu protects the pending ObjMetadataSet - mu sync.RWMutex -} - -func (w *WaitTask) Name() string { - return w.TaskName -} - -func (w *WaitTask) Action() event.ResourceAction { - return event.WaitAction -} - -func (w *WaitTask) Identifiers() object.ObjMetadataSet { - return w.Ids -} - -// Start kicks off the task. For the wait task, this just means -// setting up the timeout timer. -func (w *WaitTask) Start(taskContext *TaskContext) { - klog.V(2).Infof("wait task starting (name: %q, objects: %d)", - w.Name(), len(w.Ids)) - - // TODO: inherit context from task runner, passed through the TaskContext - ctx := context.Background() - - // use a context wrapper to handle complete/cancel/timeout - if w.Timeout > 0 { - ctx, w.cancelFunc = context.WithTimeout(ctx, w.Timeout) - } else { - ctx, w.cancelFunc = context.WithCancel(ctx) - } - - w.startInner(taskContext) - - // A goroutine to handle ending the WaitTask. - go func() { - // Block until complete/cancel/timeout - <-ctx.Done() - // Err is always non-nil when Done channel is closed. - err := ctx.Err() - - klog.V(2).Infof("wait task completing (name: %q,): %v", w.TaskName, err) - - switch err { - case context.Canceled: - // happy path - cancelled or completed (not considered an error) - case context.DeadlineExceeded: - // timed out - w.sendTimeoutEvents(taskContext) - } - - // Update RESTMapper to pick up new custom resource types - w.updateRESTMapper(taskContext) - - // Done here. signal completion to the task runner - taskContext.TaskChannel() <- TaskResult{} - }() -} - -func (w *WaitTask) sendEvent(taskContext *TaskContext, id object.ObjMetadata, status event.WaitEventStatus) { - taskContext.SendEvent(event.Event{ - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: w.Name(), - Identifier: id, - Status: status, - }, - }) -} - -// startInner sends initial pending, skipped, an reconciled events. -// If all objects are reconciled or skipped, cancelFunc is called. -// The pending set is write locked during execution of startInner. -func (w *WaitTask) startInner(taskContext *TaskContext) { - w.mu.Lock() - defer w.mu.Unlock() - - klog.V(3).Infof("wait task progress: %d/%d", 0, len(w.Ids)) - - pending := object.ObjMetadataSet{} - for _, id := range w.Ids { - switch { - case w.skipped(taskContext, id): - err := taskContext.InventoryManager().SetSkippedReconcile(id) - if err != nil { - // Object never applied or deleted! - klog.Errorf("Failed to mark object as skipped reconcile: %v", err) - } - w.sendEvent(taskContext, id, event.ReconcileSkipped) - case w.changedUID(taskContext, id): - // replaced - w.handleChangedUID(taskContext, id) - case w.reconciledByID(taskContext, id): - err := taskContext.InventoryManager().SetSuccessfulReconcile(id) - if err != nil { - // Object never applied or deleted! - klog.Errorf("Failed to mark object as successful reconcile: %v", err) - } - w.sendEvent(taskContext, id, event.ReconcileSuccessful) - default: - err := taskContext.InventoryManager().SetPendingReconcile(id) - if err != nil { - // Object never applied or deleted! - klog.Errorf("Failed to mark object as pending reconcile: %v", err) - } - pending = append(pending, id) - w.sendEvent(taskContext, id, event.ReconcilePending) - } - } - w.pending = pending - - klog.V(3).Infof("wait task progress: %d/%d", len(w.Ids)-len(w.pending), len(w.Ids)) - - if len(pending) == 0 { - // all reconciled - clear pending and exit - klog.V(3).Infof("all objects reconciled or skipped (name: %q)", w.TaskName) - w.cancelFunc() - } -} - -// sendTimeoutEvents sends a timeout event for every remaining pending object -// The pending set is read locked during execution of sendTimeoutEvents. -func (w *WaitTask) sendTimeoutEvents(taskContext *TaskContext) { - w.mu.RLock() - defer w.mu.RUnlock() - - for _, id := range w.pending { - err := taskContext.InventoryManager().SetTimeoutReconcile(id) - if err != nil { - // Object never applied or deleted! - klog.Errorf("Failed to mark object as pending reconcile: %v", err) - } - w.sendEvent(taskContext, id, event.ReconcileTimeout) - } -} - -// reconciledByID checks whether the condition set in the task is currently met -// for the specified object given the status of resource in the cache. -func (w *WaitTask) reconciledByID(taskContext *TaskContext, id object.ObjMetadata) bool { - return conditionMet(taskContext, object.ObjMetadataSet{id}, w.Condition) -} - -// skipped returns true if the object failed or was skipped by a preceding -// apply/delete/prune task. -func (w *WaitTask) skipped(taskContext *TaskContext, id object.ObjMetadata) bool { - im := taskContext.InventoryManager() - if w.Condition == AllCurrent && - im.IsFailedApply(id) || im.IsSkippedApply(id) { - return true - } - if w.Condition == AllNotFound && - im.IsFailedDelete(id) || im.IsSkippedDelete(id) { - return true - } - return false -} - -// failedByID returns true if the resource is failed. -func (w *WaitTask) failedByID(taskContext *TaskContext, id object.ObjMetadata) bool { - cached := taskContext.ResourceCache().Get(id) - return cached.Status == status.FailedStatus -} - -// changedUID returns true if the UID of the object has changed since it was -// applied or deleted. This indicates that the object was deleted and recreated. -func (w *WaitTask) changedUID(taskContext *TaskContext, id object.ObjMetadata) bool { - var oldUID, newUID types.UID - - // Get the uid from the ApplyTask/PruneTask - taskObj, found := taskContext.InventoryManager().ObjectStatus(id) - if !found { - klog.Errorf("Unknown object UID from InventoryManager: %v", id) - return false - } - oldUID = taskObj.UID - if oldUID == "" { - // All objects should have been given a UID by the apiserver - klog.Errorf("Empty object UID from InventoryManager: %v", id) - return false - } - - // Get the uid from the StatusPoller - pollerObj := taskContext.ResourceCache().Get(id) - if pollerObj.Resource == nil { - switch pollerObj.Status { - case status.UnknownStatus: - // Resource is expected to be nil when Unknown. - case status.NotFoundStatus: - // Resource is expected to be nil when NotFound. - // K8s DELETE API doesn't always return an object. - default: - // For all other statuses, nil Resource is probably a bug. - klog.Errorf("Unknown object UID from ResourceCache (status: %v): %v", pollerObj.Status, id) - } - return false - } - newUID = pollerObj.Resource.GetUID() - if newUID == "" { - // All objects should have been given a UID by the apiserver - klog.Errorf("Empty object UID from ResourceCache (status: %v): %v", pollerObj.Status, id) - return false - } - - return (oldUID != newUID) -} - -// handleChangedUID updates the object status and sends an event -func (w *WaitTask) handleChangedUID(taskContext *TaskContext, id object.ObjMetadata) { - switch w.Condition { - case AllNotFound: - // Object recreated by another actor after deletion. - // Treat as success. - klog.Infof("UID change detected: deleted object have been recreated: marking reconcile successful: %v", id) - err := taskContext.InventoryManager().SetSuccessfulReconcile(id) - if err != nil { - // Object never applied or deleted! - klog.Errorf("Failed to mark object as successful reconcile: %v", err) - } - w.sendEvent(taskContext, id, event.ReconcileSuccessful) - case AllCurrent: - // Object deleted and recreated by another actor after apply. - // Treat as failure (unverifiable). - klog.Infof("UID change detected: applied object has been deleted and recreated: marking reconcile failed: %v", id) - err := taskContext.InventoryManager().SetFailedReconcile(id) - if err != nil { - // Object never applied or deleted! - klog.Errorf("Failed to mark object as failed reconcile: %v", err) - } - w.sendEvent(taskContext, id, event.ReconcileFailed) - default: - panic(fmt.Sprintf("Invalid wait condition: %v", w.Condition)) - } -} - -// Cancel exits early with a timeout error -func (w *WaitTask) Cancel(_ *TaskContext) { - w.cancelFunc() -} - -// StatusUpdate records objects status updates and sends WaitEvents. -// If all objects are reconciled or skipped, cancelFunc is called. -// The pending set is write locked during execution of StatusUpdate. -func (w *WaitTask) StatusUpdate(taskContext *TaskContext, id object.ObjMetadata) { - w.mu.Lock() - defer w.mu.Unlock() - - if klog.V(5).Enabled() { - status := taskContext.ResourceCache().Get(id).Status - klog.Infof("status update (object: %q, status: %q)", id, status) - } - - switch { - case w.pending.Contains(id): - switch { - case w.changedUID(taskContext, id): - // replaced - w.handleChangedUID(taskContext, id) - w.pending = w.pending.Remove(id) - case w.reconciledByID(taskContext, id): - // reconciled - remove from pending & send event - err := taskContext.InventoryManager().SetSuccessfulReconcile(id) - if err != nil { - // Object never applied or deleted! - klog.Errorf("Failed to mark object as successful reconcile: %v", err) - } - w.pending = w.pending.Remove(id) - w.sendEvent(taskContext, id, event.ReconcileSuccessful) - case w.failedByID(taskContext, id): - // failed - remove from pending & send event - err := taskContext.InventoryManager().SetFailedReconcile(id) - if err != nil { - // Object never applied or deleted! - klog.Errorf("Failed to mark object as failed reconcile: %v", err) - } - w.pending = w.pending.Remove(id) - w.failed = append(w.failed, id) - w.sendEvent(taskContext, id, event.ReconcileFailed) - // default - still pending - } - case !w.Ids.Contains(id): - // not in wait group - ignore - return - case w.skipped(taskContext, id): - // skipped - ignore - return - case w.failed.Contains(id): - // If a failed resource becomes current before other - // resources have completed/timed out, we consider it - // current. - if w.reconciledByID(taskContext, id) { - // reconciled - remove from pending & send event - err := taskContext.InventoryManager().SetSuccessfulReconcile(id) - if err != nil { - // Object never applied or deleted! - klog.Errorf("Failed to mark object as successful reconcile: %v", err) - } - w.failed = w.failed.Remove(id) - w.sendEvent(taskContext, id, event.ReconcileSuccessful) - } else if !w.failedByID(taskContext, id) { - // If a resource is no longer reported as Failed and is not Reconciled, - // they should just go back to InProgress. - err := taskContext.InventoryManager().SetPendingReconcile(id) - if err != nil { - // Object never applied or deleted! - klog.Errorf("Failed to mark object as pending reconcile: %v", err) - } - w.failed = w.failed.Remove(id) - w.pending = append(w.pending, id) - w.sendEvent(taskContext, id, event.ReconcilePending) - } - // else - still failed - default: - // reconciled - check if unreconciled - if !w.reconciledByID(taskContext, id) { - // unreconciled - add to pending & send event - err := taskContext.InventoryManager().SetPendingReconcile(id) - if err != nil { - // Object never applied or deleted! - klog.Errorf("Failed to mark object as pending reconcile: %v", err) - } - w.pending = append(w.pending, id) - w.sendEvent(taskContext, id, event.ReconcilePending) - } - // else - still reconciled - } - - klog.V(3).Infof("wait task progress: %d/%d", len(w.Ids)-len(w.pending), len(w.Ids)) - - // If we no longer have any pending resources, the WaitTask - // can be completed. - if len(w.pending) == 0 { - // all reconciled, so exit - klog.V(3).Infof("all objects reconciled or skipped (name: %q)", w.TaskName) - w.cancelFunc() - } -} - -// updateRESTMapper resets the RESTMapper if CRDs were applied, so that new -// resource types can be applied by subsequent tasks. -// TODO: find a way to add/remove mappers without resetting the entire mapper -// Resetting the mapper requires all CRDs to be queried again. -func (w *WaitTask) updateRESTMapper(taskContext *TaskContext) { - foundCRD := false - for _, id := range w.Ids { - if id.GroupKind == crdGK && !w.skipped(taskContext, id) { - foundCRD = true - break - } - } - if !foundCRD { - // no update required - return - } - - klog.V(3).Infof("Resetting RESTMapper") - meta.MaybeResetRESTMapper(w.Mapper) -} diff --git a/pkg/apply/taskrunner/task_test.go b/pkg/apply/taskrunner/task_test.go deleted file mode 100644 index c46c61f3..00000000 --- a/pkg/apply/taskrunner/task_test.go +++ /dev/null @@ -1,1405 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package taskrunner - -import ( - "testing" - "time" - - "github.com/fluxcd/cli-utils/pkg/apis/actuation" - "github.com/fluxcd/cli-utils/pkg/apply/cache" - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/pkg/kstatus/status" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/testutil" - "github.com/stretchr/testify/assert" -) - -var testDeployment1YAML = ` -apiVersion: apps/v1 -kind: Deployment -metadata: - name: a - namespace: default - uid: dep-uid-a - generation: 1 -spec: - replicas: 1 -` - -var testDeployment2YAML = ` -apiVersion: v1 -kind: Deployment -metadata: - name: b - namespace: default - uid: dep-uid-b - generation: 1 -spec: - replicas: 2 -` - -var testDeployment3YAML = ` -apiVersion: v1 -kind: Deployment -metadata: - name: c - namespace: default - uid: dep-uid-c - generation: 1 -spec: - replicas: 3 -` - -var testDeployment4YAML = ` -apiVersion: v1 -kind: Deployment -metadata: - name: d - namespace: default - uid: dep-uid-d - generation: 1 -spec: - replicas: 4 -` - -func TestWaitTask_CompleteEventually(t *testing.T) { - testDeployment1ID := testutil.ToIdentifier(t, testDeployment1YAML) - testDeployment1 := testutil.Unstructured(t, testDeployment1YAML) - testDeployment2ID := testutil.ToIdentifier(t, testDeployment2YAML) - testDeployment2 := testutil.Unstructured(t, testDeployment2YAML) - testDeployment3ID := testutil.ToIdentifier(t, testDeployment3YAML) - testDeployment4ID := testutil.ToIdentifier(t, testDeployment4YAML) - ids := object.ObjMetadataSet{ - testDeployment1ID, - testDeployment2ID, - testDeployment3ID, - testDeployment4ID, - } - waitTimeout := 2 * time.Second - taskName := "wait-1" - task := NewWaitTask(taskName, ids, AllCurrent, - waitTimeout, testutil.NewFakeRESTMapper()) - - eventChannel := make(chan event.Event) - resourceCache := cache.NewResourceCacheMap() - taskContext := NewTaskContext(eventChannel, resourceCache) - defer close(eventChannel) - - // Update metadata on successfully applied objects - testDeployment1.SetUID("a") - testDeployment1.SetGeneration(1) - testDeployment2.SetUID("b") - testDeployment2.SetGeneration(1) - - // mark deployment 1 & 2 as apply succeeded - taskContext.InventoryManager().AddSuccessfulApply(testDeployment1ID, - testDeployment1.GetUID(), testDeployment1.GetGeneration()) - taskContext.InventoryManager().AddSuccessfulApply(testDeployment2ID, - testDeployment2.GetUID(), testDeployment2.GetGeneration()) - - // mark deployment 3 as apply failed - taskContext.InventoryManager().AddFailedApply(testDeployment3ID) - - // mark deployment 4 as apply skipped - taskContext.InventoryManager().AddSkippedApply(testDeployment4ID) - - // run task async, to let the test collect events - go func() { - // start the task - task.Start(taskContext) - - // mark deployment1 as Current - resourceCache.Put(testDeployment1ID, cache.ResourceStatus{ - Resource: testDeployment1, - Status: status.CurrentStatus, - }) - // tell the WaitTask deployment1 has new status - task.StatusUpdate(taskContext, testDeployment1ID) - - // mark deployment2 as InProgress - resourceCache.Put(testDeployment2ID, cache.ResourceStatus{ - Resource: testDeployment2, - Status: status.InProgressStatus, - }) - // tell the WaitTask deployment2 has new status - task.StatusUpdate(taskContext, testDeployment2ID) - - // mark deployment2 as Current - resourceCache.Put(testDeployment2ID, cache.ResourceStatus{ - Resource: testDeployment2, - Status: status.CurrentStatus, - }) - // tell the WaitTask deployment2 has new status - task.StatusUpdate(taskContext, testDeployment2ID) - }() - - // wait for task result - timer := time.NewTimer(5 * time.Second) - receivedEvents := []event.Event{} -loop: - for { - select { - case e := <-taskContext.EventChannel(): - receivedEvents = append(receivedEvents, e) - case res := <-taskContext.TaskChannel(): - timer.Stop() - assert.NoError(t, res.Err) - break loop - case <-timer.C: - t.Fatalf("timed out waiting for TaskResult") - } - } - - // Expect an event for every object (sorted). - expectedEvents := []event.Event{ - // skipped/reconciled/pending events first, in the order provided to the WaitTask - // deployment1 pending - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: taskName, - Identifier: testDeployment1ID, - Status: event.ReconcilePending, - }, - }, - // deployment2 pending - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: taskName, - Identifier: testDeployment2ID, - Status: event.ReconcilePending, - }, - }, - // deployment3 skipped - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: taskName, - Identifier: testDeployment3ID, - Status: event.ReconcileSkipped, - }, - }, - // deployment4 skipped - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: taskName, - Identifier: testDeployment4ID, - Status: event.ReconcileSkipped, - }, - }, - // current events next, in the order of status updates - // deployment1 current - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: taskName, - Identifier: testDeployment1ID, - Status: event.ReconcileSuccessful, - }, - }, - // deployment2 current - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: taskName, - Identifier: testDeployment2ID, - Status: event.ReconcileSuccessful, - }, - }, - } - testutil.AssertEqual(t, expectedEvents, receivedEvents, - "Actual events (%d) do not match expected events (%d)", - len(receivedEvents), len(expectedEvents)) - - expectedInventory := actuation.Inventory{ - Status: actuation.InventoryStatus{ - Objects: []actuation.ObjectStatus{ - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata(testDeployment1ID), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationSucceeded, - Reconcile: actuation.ReconcileSucceeded, - UID: testDeployment1.GetUID(), - Generation: testDeployment1.GetGeneration(), - }, - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata(testDeployment2ID), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationSucceeded, - Reconcile: actuation.ReconcileSucceeded, - UID: testDeployment2.GetUID(), - Generation: testDeployment2.GetGeneration(), - }, - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata(testDeployment3ID), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationFailed, - Reconcile: actuation.ReconcileSkipped, - }, - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata(testDeployment4ID), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationSkipped, - Reconcile: actuation.ReconcileSkipped, - }, - }, - }, - } - testutil.AssertEqual(t, &expectedInventory, taskContext.InventoryManager().Inventory()) -} - -func TestWaitTask_Timeout(t *testing.T) { - testDeployment1ID := testutil.ToIdentifier(t, testDeployment1YAML) - testDeployment1 := testutil.Unstructured(t, testDeployment1YAML) - testDeployment2ID := testutil.ToIdentifier(t, testDeployment2YAML) - testDeployment2 := testutil.Unstructured(t, testDeployment2YAML) - testDeployment3ID := testutil.ToIdentifier(t, testDeployment3YAML) - testDeployment4ID := testutil.ToIdentifier(t, testDeployment4YAML) - ids := object.ObjMetadataSet{ - testDeployment1ID, - testDeployment2ID, - testDeployment3ID, - testDeployment4ID, - } - waitTimeout := 2 * time.Second - taskName := "wait-2" - task := NewWaitTask(taskName, ids, AllCurrent, - waitTimeout, testutil.NewFakeRESTMapper()) - - eventChannel := make(chan event.Event) - resourceCache := cache.NewResourceCacheMap() - taskContext := NewTaskContext(eventChannel, resourceCache) - defer close(eventChannel) - - // Update metadata on successfully applied objects - testDeployment1.SetUID("a") - testDeployment1.SetGeneration(1) - testDeployment2.SetUID("b") - testDeployment2.SetGeneration(1) - - // mark deployment 1 & 2 as apply succeeded - taskContext.InventoryManager().AddSuccessfulApply(testDeployment1ID, - testDeployment1.GetUID(), testDeployment1.GetGeneration()) - taskContext.InventoryManager().AddSuccessfulApply(testDeployment2ID, - testDeployment2.GetUID(), testDeployment2.GetGeneration()) - - // mark deployment 3 as apply failed - taskContext.InventoryManager().AddFailedApply(testDeployment3ID) - - // mark deployment 4 as apply skipped - taskContext.InventoryManager().AddSkippedApply(testDeployment4ID) - - // run task async, to let the test collect events - go func() { - // start the task - task.Start(taskContext) - // mark deployment1 as Current - resourceCache.Put(testDeployment1ID, cache.ResourceStatus{ - Resource: testDeployment1, - Status: status.CurrentStatus, - }) - // tell the WaitTask deployment1 has new status - task.StatusUpdate(taskContext, testDeployment1ID) - }() - - // wait for task result - timer := time.NewTimer(5 * time.Second) - receivedEvents := []event.Event{} -loop: - for { - select { - case e := <-taskContext.EventChannel(): - receivedEvents = append(receivedEvents, e) - case res := <-taskContext.TaskChannel(): - timer.Stop() - assert.NoError(t, res.Err) - break loop - case <-timer.C: - t.Fatalf("timed out waiting for TaskResult") - } - } - - // Expect an event for every object (sorted). - expectedEvents := []event.Event{ - // skipped/reconciled/pending events first, in the order provided to the WaitTask - // deployment1 pending - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: taskName, - Identifier: testDeployment1ID, - Status: event.ReconcilePending, - }, - }, - // deployment2 pending - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: taskName, - Identifier: testDeployment2ID, - Status: event.ReconcilePending, - }, - }, - // deployment3 skipped - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: taskName, - Identifier: testDeployment3ID, - Status: event.ReconcileSkipped, - }, - }, - // deployment4 skipped - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: taskName, - Identifier: testDeployment4ID, - Status: event.ReconcileSkipped, - }, - }, - // current events next, in the order of status updates - // deployment1 current - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: taskName, - Identifier: testDeployment1ID, - Status: event.ReconcileSuccessful, - }, - }, - // timeout events last, in the order provided to the WaitTask - // deployment2 timeout - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: taskName, - Identifier: testDeployment2ID, - Status: event.ReconcileTimeout, - }, - }, - } - testutil.AssertEqual(t, expectedEvents, receivedEvents, - "Actual events (%d) do not match expected events (%d)", - len(receivedEvents), len(expectedEvents)) - - expectedInventory := actuation.Inventory{ - Status: actuation.InventoryStatus{ - Objects: []actuation.ObjectStatus{ - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata(testDeployment1ID), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationSucceeded, - Reconcile: actuation.ReconcileSucceeded, - UID: testDeployment1.GetUID(), - Generation: testDeployment1.GetGeneration(), - }, - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata(testDeployment2ID), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationSucceeded, - Reconcile: actuation.ReconcileTimeout, - UID: testDeployment2.GetUID(), - Generation: testDeployment2.GetGeneration(), - }, - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata(testDeployment3ID), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationFailed, - Reconcile: actuation.ReconcileSkipped, - }, - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata(testDeployment4ID), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationSkipped, - Reconcile: actuation.ReconcileSkipped, - }, - }, - }, - } - testutil.AssertEqual(t, &expectedInventory, taskContext.InventoryManager().Inventory()) -} - -func TestWaitTask_StartAndComplete(t *testing.T) { - testDeploymentID := testutil.ToIdentifier(t, testDeployment1YAML) - testDeployment := testutil.Unstructured(t, testDeployment1YAML) - ids := object.ObjMetadataSet{ - testDeploymentID, - } - waitTimeout := 2 * time.Second - taskName := "wait-3" - task := NewWaitTask(taskName, ids, AllCurrent, - waitTimeout, testutil.NewFakeRESTMapper()) - - eventChannel := make(chan event.Event) - resourceCache := cache.NewResourceCacheMap() - taskContext := NewTaskContext(eventChannel, resourceCache) - defer close(eventChannel) - - // Update metadata on successfully applied objects - testDeployment.SetUID("a") - testDeployment.SetGeneration(1) - - // mark deployment as apply succeeded - taskContext.InventoryManager().AddSuccessfulApply(testDeploymentID, - testDeployment.GetUID(), testDeployment.GetGeneration()) - - // mark the deployment as Current before starting - resourceCache.Put(testDeploymentID, cache.ResourceStatus{ - Resource: testDeployment, - Status: status.CurrentStatus, - }) - - // run task async, to let the test collect events - go func() { - // start the task - task.Start(taskContext) - }() - - // wait for first task result - timer := time.NewTimer(5 * time.Second) - receivedEvents := []event.Event{} -loop: - for { - select { - case e := <-taskContext.EventChannel(): - receivedEvents = append(receivedEvents, e) - case res := <-taskContext.TaskChannel(): - timer.Stop() - assert.NoError(t, res.Err) - break loop - case <-timer.C: - t.Fatalf("timed out waiting for TaskResult") - } - } - - expectedEvents := []event.Event{ - // deployment1 current (no pending event when Current before start) - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: taskName, - Identifier: testDeploymentID, - Status: event.ReconcileSuccessful, - }, - }, - } - testutil.AssertEqual(t, expectedEvents, receivedEvents, - "Actual events (%d) do not match expected events (%d)", - len(receivedEvents), len(expectedEvents)) - - expectedInventory := actuation.Inventory{ - Status: actuation.InventoryStatus{ - Objects: []actuation.ObjectStatus{ - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata(testDeploymentID), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationSucceeded, - Reconcile: actuation.ReconcileSucceeded, - UID: testDeployment.GetUID(), - Generation: testDeployment.GetGeneration(), - }, - }, - }, - } - testutil.AssertEqual(t, &expectedInventory, taskContext.InventoryManager().Inventory()) -} - -func TestWaitTask_Cancel(t *testing.T) { - testDeploymentID := testutil.ToIdentifier(t, testDeployment1YAML) - testDeployment := testutil.Unstructured(t, testDeployment1YAML) - ids := object.ObjMetadataSet{ - testDeploymentID, - } - waitTimeout := 5 * time.Second - taskName := "wait-4" - task := NewWaitTask(taskName, ids, AllCurrent, - waitTimeout, testutil.NewFakeRESTMapper()) - - eventChannel := make(chan event.Event) - resourceCache := cache.NewResourceCacheMap() - taskContext := NewTaskContext(eventChannel, resourceCache) - defer close(eventChannel) - - // Update metadata on successfully applied objects - testDeployment.SetUID("a") - testDeployment.SetGeneration(1) - - // mark deployment as apply succeeded - taskContext.InventoryManager().AddSuccessfulApply(testDeploymentID, - testDeployment.GetUID(), testDeployment.GetGeneration()) - - // run task async, to let the test collect events - go func() { - // start the task - task.Start(taskContext) - - // wait a bit - time.Sleep(1 * time.Second) - - // cancel immediately (simulate context cancel from baseRunner) - task.Cancel(taskContext) - }() - - // wait for first task result - timer := time.NewTimer(10 * time.Second) - receivedEvents := []event.Event{} -loop: - for { - select { - case e := <-taskContext.EventChannel(): - receivedEvents = append(receivedEvents, e) - case res := <-taskContext.TaskChannel(): - timer.Stop() - assert.NoError(t, res.Err) - break loop - case <-timer.C: - t.Fatalf("timed out waiting for TaskResult") - } - } - - // no timeout events sent on cancel - expectedEvents := []event.Event{ - // skipped/reconciled/pending events first, in the order provided to the WaitTask - // deployment1 pending - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: taskName, - Identifier: testDeploymentID, - Status: event.ReconcilePending, - }, - }, - } - testutil.AssertEqual(t, expectedEvents, receivedEvents, - "Actual events (%d) do not match expected events (%d)", - len(receivedEvents), len(expectedEvents)) - - expectedInventory := actuation.Inventory{ - Status: actuation.InventoryStatus{ - Objects: []actuation.ObjectStatus{ - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata(testDeploymentID), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationSucceeded, - Reconcile: actuation.ReconcilePending, - UID: testDeployment.GetUID(), - Generation: testDeployment.GetGeneration(), - }, - }, - }, - } - testutil.AssertEqual(t, &expectedInventory, taskContext.InventoryManager().Inventory()) -} - -func TestWaitTask_SingleTaskResult(t *testing.T) { - testDeploymentID := testutil.ToIdentifier(t, testDeployment1YAML) - testDeployment := testutil.Unstructured(t, testDeployment1YAML) - ids := object.ObjMetadataSet{ - testDeploymentID, - } - waitTimeout := 3 * time.Second - taskName := "wait-5" - task := NewWaitTask(taskName, ids, AllCurrent, - waitTimeout, testutil.NewFakeRESTMapper()) - - // buffer events, because they're sent by StatusUpdate - eventChannel := make(chan event.Event, 10) - resourceCache := cache.NewResourceCacheMap() - taskContext := NewTaskContext(eventChannel, resourceCache) - defer close(eventChannel) - - // Update metadata on successfully applied objects - testDeployment.SetUID("a") - testDeployment.SetGeneration(1) - - // mark deployment as apply succeeded - taskContext.InventoryManager().AddSuccessfulApply(testDeploymentID, - testDeployment.GetUID(), testDeployment.GetGeneration()) - - // run task async, to let the test collect events - go func() { - // start the task - task.Start(taskContext) - - // wait a bit - time.Sleep(1 * time.Second) - - // mark the deployment as Current - resourceCache.Put(testDeploymentID, cache.ResourceStatus{ - Resource: withGeneration(testDeployment, 1), - Status: status.CurrentStatus, - }) - - // send multiple status updates - for i := 0; i < 10; i++ { - task.StatusUpdate(taskContext, testDeploymentID) - } - }() - - // wait for timeout - timer := time.NewTimer(5 * time.Second) - receivedEvents := []event.Event{} - receivedResults := []TaskResult{} -loop: - for { - select { - case e := <-taskContext.EventChannel(): - receivedEvents = append(receivedEvents, e) - case res := <-taskContext.TaskChannel(): - receivedResults = append(receivedResults, res) - case <-timer.C: - break loop - } - } - - expectedEvents := []event.Event{ - // skipped/reconciled/pending events first, in the order provided to the WaitTask - // deployment1 pending - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: taskName, - Identifier: testDeploymentID, - Status: event.ReconcilePending, - }, - }, - // deployment1 reconciled - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: taskName, - Identifier: testDeploymentID, - Status: event.ReconcileSuccessful, - }, - }, - } - testutil.AssertEqual(t, expectedEvents, receivedEvents, - "Actual events (%d) do not match expected events (%d)", - len(receivedEvents), len(expectedEvents)) - - expectedResults := []TaskResult{ - {}, // Empty result means success - } - assert.Equal(t, expectedResults, receivedResults) - - expectedInventory := actuation.Inventory{ - Status: actuation.InventoryStatus{ - Objects: []actuation.ObjectStatus{ - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata(testDeploymentID), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationSucceeded, - Reconcile: actuation.ReconcileSucceeded, - UID: testDeployment.GetUID(), - Generation: testDeployment.GetGeneration(), - }, - }, - }, - } - testutil.AssertEqual(t, &expectedInventory, taskContext.InventoryManager().Inventory()) -} - -func TestWaitTask_Failed(t *testing.T) { - taskName := "wait-6" - testDeployment1ID := testutil.ToIdentifier(t, testDeployment1YAML) - testDeployment1 := testutil.Unstructured(t, testDeployment1YAML) - testDeployment2ID := testutil.ToIdentifier(t, testDeployment2YAML) - testDeployment2 := testutil.Unstructured(t, testDeployment2YAML) - - // Update metadata on successfully applied objects - testDeployment1.SetUID("a") - testDeployment1.SetGeneration(1) - testDeployment2.SetUID("b") - testDeployment2.SetGeneration(1) - - testCases := map[string]struct { - configureTaskContextFunc func(taskContext *TaskContext) - eventsFunc func(*cache.ResourceCacheMap, *WaitTask, *TaskContext) - waitTimeout time.Duration - expectedEvents []event.Event - expectedInventory *actuation.Inventory - }{ - "continue on failed if others InProgress": { - configureTaskContextFunc: func(taskContext *TaskContext) { - // mark deployment as apply succeeded - taskContext.InventoryManager().AddSuccessfulApply(testDeployment1ID, - testDeployment1.GetUID(), testDeployment1.GetGeneration()) - taskContext.InventoryManager().AddSuccessfulApply(testDeployment2ID, - testDeployment2.GetUID(), testDeployment2.GetGeneration()) - }, - eventsFunc: func(resourceCache *cache.ResourceCacheMap, task *WaitTask, taskContext *TaskContext) { - resourceCache.Put(testDeployment1ID, cache.ResourceStatus{ - Resource: testDeployment1, - Status: status.FailedStatus, - }) - task.StatusUpdate(taskContext, testDeployment1ID) - - resourceCache.Put(testDeployment2ID, cache.ResourceStatus{ - Resource: testDeployment2, - Status: status.InProgressStatus, - }) - task.StatusUpdate(taskContext, testDeployment2ID) - - resourceCache.Put(testDeployment2ID, cache.ResourceStatus{ - Resource: testDeployment2, - Status: status.CurrentStatus, - }) - task.StatusUpdate(taskContext, testDeployment2ID) - }, - waitTimeout: 2 * time.Second, - expectedEvents: []event.Event{ - // deployment1 pending - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: taskName, - Identifier: testDeployment1ID, - Status: event.ReconcilePending, - }, - }, - // deployment2 pending - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: taskName, - Identifier: testDeployment2ID, - Status: event.ReconcilePending, - }, - }, - // deployment1 is failed - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: taskName, - Identifier: testDeployment1ID, - Status: event.ReconcileFailed, - }, - }, - // deployment2 current - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: taskName, - Identifier: testDeployment2ID, - Status: event.ReconcileSuccessful, - }, - }, - }, - expectedInventory: &actuation.Inventory{ - Status: actuation.InventoryStatus{ - Objects: []actuation.ObjectStatus{ - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata(testDeployment1ID), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationSucceeded, - Reconcile: actuation.ReconcileFailed, - UID: testDeployment1.GetUID(), - Generation: testDeployment1.GetGeneration(), - }, - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata(testDeployment2ID), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationSucceeded, - Reconcile: actuation.ReconcileSucceeded, - UID: testDeployment2.GetUID(), - Generation: testDeployment2.GetGeneration(), - }, - }, - }, - }, - }, - "complete wait task is last resource becomes failed": { - configureTaskContextFunc: func(taskContext *TaskContext) { - // mark deployment as apply succeeded - taskContext.InventoryManager().AddSuccessfulApply(testDeployment1ID, - testDeployment1.GetUID(), testDeployment1.GetGeneration()) - taskContext.InventoryManager().AddSuccessfulApply(testDeployment2ID, - testDeployment2.GetUID(), testDeployment2.GetGeneration()) - }, - eventsFunc: func(resourceCache *cache.ResourceCacheMap, task *WaitTask, taskContext *TaskContext) { - resourceCache.Put(testDeployment2ID, cache.ResourceStatus{ - Resource: testDeployment2, - Status: status.CurrentStatus, - }) - task.StatusUpdate(taskContext, testDeployment2ID) - - resourceCache.Put(testDeployment1ID, cache.ResourceStatus{ - Resource: testDeployment1, - Status: status.FailedStatus, - }) - task.StatusUpdate(taskContext, testDeployment1ID) - }, - waitTimeout: 2 * time.Second, - expectedEvents: []event.Event{ - // deployment1 pending - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: taskName, - Identifier: testDeployment1ID, - Status: event.ReconcilePending, - }, - }, - // deployment2 pending - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: taskName, - Identifier: testDeployment2ID, - Status: event.ReconcilePending, - }, - }, - // deployment2 current - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: taskName, - Identifier: testDeployment2ID, - Status: event.ReconcileSuccessful, - }, - }, - // deployment1 is failed - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: taskName, - Identifier: testDeployment1ID, - Status: event.ReconcileFailed, - }, - }, - }, - expectedInventory: &actuation.Inventory{ - Status: actuation.InventoryStatus{ - Objects: []actuation.ObjectStatus{ - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata(testDeployment1ID), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationSucceeded, - Reconcile: actuation.ReconcileFailed, - UID: testDeployment1.GetUID(), - Generation: testDeployment1.GetGeneration(), - }, - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata(testDeployment2ID), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationSucceeded, - Reconcile: actuation.ReconcileSucceeded, - UID: testDeployment2.GetUID(), - Generation: testDeployment2.GetGeneration(), - }, - }, - }, - }, - }, - "failed resource can become current": { - configureTaskContextFunc: func(taskContext *TaskContext) { - // mark deployment as apply succeeded - taskContext.InventoryManager().AddSuccessfulApply(testDeployment1ID, - testDeployment1.GetUID(), testDeployment1.GetGeneration()) - taskContext.InventoryManager().AddSuccessfulApply(testDeployment2ID, - testDeployment2.GetUID(), testDeployment2.GetGeneration()) - }, - eventsFunc: func(resourceCache *cache.ResourceCacheMap, task *WaitTask, taskContext *TaskContext) { - resourceCache.Put(testDeployment1ID, cache.ResourceStatus{ - Resource: testDeployment1, - Status: status.FailedStatus, - }) - task.StatusUpdate(taskContext, testDeployment1ID) - - resourceCache.Put(testDeployment1ID, cache.ResourceStatus{ - Resource: testDeployment1, - Status: status.CurrentStatus, - }) - task.StatusUpdate(taskContext, testDeployment1ID) - - resourceCache.Put(testDeployment2ID, cache.ResourceStatus{ - Resource: testDeployment2, - Status: status.CurrentStatus, - }) - task.StatusUpdate(taskContext, testDeployment2ID) - }, - waitTimeout: 2 * time.Second, - expectedEvents: []event.Event{ - // deployment1 pending - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: taskName, - Identifier: testDeployment1ID, - Status: event.ReconcilePending, - }, - }, - // deployment2 pending - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: taskName, - Identifier: testDeployment2ID, - Status: event.ReconcilePending, - }, - }, - // deployment1 is failed - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: taskName, - Identifier: testDeployment1ID, - Status: event.ReconcileFailed, - }, - }, - // deployment1 is current - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: taskName, - Identifier: testDeployment1ID, - Status: event.ReconcileSuccessful, - }, - }, - // deployment2 current - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: taskName, - Identifier: testDeployment2ID, - Status: event.ReconcileSuccessful, - }, - }, - }, - expectedInventory: &actuation.Inventory{ - Status: actuation.InventoryStatus{ - Objects: []actuation.ObjectStatus{ - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata(testDeployment1ID), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationSucceeded, - Reconcile: actuation.ReconcileSucceeded, - UID: testDeployment1.GetUID(), - Generation: testDeployment1.GetGeneration(), - }, - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata(testDeployment2ID), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationSucceeded, - Reconcile: actuation.ReconcileSucceeded, - UID: testDeployment2.GetUID(), - Generation: testDeployment2.GetGeneration(), - }, - }, - }, - }, - }, - "failed resource can become InProgress": { - configureTaskContextFunc: func(taskContext *TaskContext) { - // mark deployment as apply succeeded - taskContext.InventoryManager().AddSuccessfulApply(testDeployment1ID, - testDeployment1.GetUID(), testDeployment1.GetGeneration()) - taskContext.InventoryManager().AddSuccessfulApply(testDeployment2ID, - testDeployment2.GetUID(), testDeployment2.GetGeneration()) - }, - eventsFunc: func(resourceCache *cache.ResourceCacheMap, task *WaitTask, taskContext *TaskContext) { - resourceCache.Put(testDeployment1ID, cache.ResourceStatus{ - Resource: testDeployment1, - Status: status.FailedStatus, - }) - task.StatusUpdate(taskContext, testDeployment1ID) - - resourceCache.Put(testDeployment1ID, cache.ResourceStatus{ - Resource: testDeployment1, - Status: status.InProgressStatus, - }) - task.StatusUpdate(taskContext, testDeployment1ID) - - resourceCache.Put(testDeployment2ID, cache.ResourceStatus{ - Resource: testDeployment2, - Status: status.CurrentStatus, - }) - task.StatusUpdate(taskContext, testDeployment2ID) - }, - waitTimeout: 2 * time.Second, - expectedEvents: []event.Event{ - // deployment1 pending - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: taskName, - Identifier: testDeployment1ID, - Status: event.ReconcilePending, - }, - }, - // deployment2 pending - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: taskName, - Identifier: testDeployment2ID, - Status: event.ReconcilePending, - }, - }, - // deployment1 is failed - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: taskName, - Identifier: testDeployment1ID, - Status: event.ReconcileFailed, - }, - }, - // deployment1 pending - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: taskName, - Identifier: testDeployment1ID, - Status: event.ReconcilePending, - }, - }, - // deployment2 current - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: taskName, - Identifier: testDeployment2ID, - Status: event.ReconcileSuccessful, - }, - }, - // deployment1 timed out - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: taskName, - Identifier: testDeployment1ID, - Status: event.ReconcileTimeout, - }, - }, - }, - expectedInventory: &actuation.Inventory{ - Status: actuation.InventoryStatus{ - Objects: []actuation.ObjectStatus{ - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata(testDeployment1ID), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationSucceeded, - Reconcile: actuation.ReconcileTimeout, - UID: testDeployment1.GetUID(), - Generation: testDeployment1.GetGeneration(), - }, - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata(testDeployment2ID), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationSucceeded, - Reconcile: actuation.ReconcileSucceeded, - UID: testDeployment2.GetUID(), - Generation: testDeployment2.GetGeneration(), - }, - }, - }, - }, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - ids := object.ObjMetadataSet{ - testDeployment1ID, - testDeployment2ID, - } - task := NewWaitTask(taskName, ids, AllCurrent, - tc.waitTimeout, testutil.NewFakeRESTMapper()) - - eventChannel := make(chan event.Event) - resourceCache := cache.NewResourceCacheMap() - taskContext := NewTaskContext(eventChannel, resourceCache) - defer close(eventChannel) - - tc.configureTaskContextFunc(taskContext) - - // run task async, to let the test collect events - go func() { - // start the task - task.Start(taskContext) - - tc.eventsFunc(resourceCache, task, taskContext) - }() - - // wait for task result - timer := time.NewTimer(5 * time.Second) - receivedEvents := []event.Event{} - loop: - for { - select { - case e := <-taskContext.EventChannel(): - receivedEvents = append(receivedEvents, e) - case res := <-taskContext.TaskChannel(): - timer.Stop() - assert.NoError(t, res.Err) - break loop - case <-timer.C: - t.Fatalf("timed out waiting for TaskResult") - } - } - - testutil.AssertEqual(t, tc.expectedEvents, receivedEvents, - "Actual events (%d) do not match expected events (%d)", - len(receivedEvents), len(tc.expectedEvents)) - - testutil.AssertEqual(t, tc.expectedInventory, taskContext.InventoryManager().Inventory()) - }) - } -} - -func TestWaitTask_UIDChanged(t *testing.T) { - taskName := "wait-7" - testDeployment1ID := testutil.ToIdentifier(t, testDeployment1YAML) - testDeployment1 := testutil.Unstructured(t, testDeployment1YAML) - testDeployment2ID := testutil.ToIdentifier(t, testDeployment2YAML) - testDeployment2 := testutil.Unstructured(t, testDeployment2YAML) - - // Update metadata on successfully applied objects - testDeployment1.SetUID("a") - testDeployment1.SetGeneration(1) - testDeployment2.SetUID("b") - testDeployment2.SetGeneration(1) - - replacedDeployment1 := testDeployment1.DeepCopy() - replacedDeployment1.SetUID("replaced") - - testCases := map[string]struct { - condition Condition - configureTaskContextFunc func(taskContext *TaskContext) - eventsFunc func(*cache.ResourceCacheMap, *WaitTask, *TaskContext) - waitTimeout time.Duration - expectedEvents []event.Event - expectedInventory *actuation.Inventory - }{ - "UID changed after apply means reconcile failure": { - condition: AllCurrent, - configureTaskContextFunc: func(taskContext *TaskContext) { - // mark deployment as apply succeeded - taskContext.InventoryManager().AddSuccessfulApply(testDeployment1ID, - testDeployment1.GetUID(), testDeployment1.GetGeneration()) - taskContext.InventoryManager().AddSuccessfulApply(testDeployment2ID, - testDeployment2.GetUID(), testDeployment2.GetGeneration()) - }, - eventsFunc: func(resourceCache *cache.ResourceCacheMap, task *WaitTask, taskContext *TaskContext) { - // any status update after apply success should trigger failure if the UID changed - resourceCache.Put(testDeployment1ID, cache.ResourceStatus{ - Resource: replacedDeployment1, - Status: status.CurrentStatus, - }) - task.StatusUpdate(taskContext, testDeployment1ID) - - resourceCache.Put(testDeployment2ID, cache.ResourceStatus{ - Resource: testDeployment2, - Status: status.CurrentStatus, - }) - task.StatusUpdate(taskContext, testDeployment2ID) - }, - waitTimeout: 2 * time.Second, - expectedEvents: []event.Event{ - // deployment1 pending - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: taskName, - Identifier: testDeployment1ID, - Status: event.ReconcilePending, - }, - }, - // deployment2 pending - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: taskName, - Identifier: testDeployment2ID, - Status: event.ReconcilePending, - }, - }, - // deployment1 is failed - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: taskName, - Identifier: testDeployment1ID, - Status: event.ReconcileFailed, - }, - }, - // deployment2 current - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: taskName, - Identifier: testDeployment2ID, - Status: event.ReconcileSuccessful, - }, - }, - }, - expectedInventory: &actuation.Inventory{ - Status: actuation.InventoryStatus{ - Objects: []actuation.ObjectStatus{ - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata(testDeployment1ID), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationSucceeded, - // UID change causes failure after apply - Reconcile: actuation.ReconcileFailed, - // Recorded UID should be from the applied object, not the new replacement - UID: testDeployment1.GetUID(), - Generation: testDeployment1.GetGeneration(), - }, - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata(testDeployment2ID), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationSucceeded, - Reconcile: actuation.ReconcileSucceeded, - UID: testDeployment2.GetUID(), - Generation: testDeployment2.GetGeneration(), - }, - }, - }, - }, - }, - "UID changed after delete means reconcile success": { - condition: AllNotFound, - configureTaskContextFunc: func(taskContext *TaskContext) { - // mark deployment as apply succeeded - taskContext.InventoryManager().AddSuccessfulDelete(testDeployment1ID, - testDeployment1.GetUID()) - taskContext.InventoryManager().AddSuccessfulDelete(testDeployment2ID, - testDeployment2.GetUID()) - }, - eventsFunc: func(resourceCache *cache.ResourceCacheMap, task *WaitTask, taskContext *TaskContext) { - // any status update after delete should trigger success if the UID changed - resourceCache.Put(testDeployment1ID, cache.ResourceStatus{ - Resource: replacedDeployment1, - Status: status.InProgressStatus, - }) - task.StatusUpdate(taskContext, testDeployment1ID) - - resourceCache.Put(testDeployment2ID, cache.ResourceStatus{ - Resource: testDeployment2, - Status: status.NotFoundStatus, - }) - task.StatusUpdate(taskContext, testDeployment2ID) - }, - waitTimeout: 2 * time.Second, - expectedEvents: []event.Event{ - // deployment1 pending - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: taskName, - Identifier: testDeployment1ID, - Status: event.ReconcilePending, - }, - }, - // deployment2 pending - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: taskName, - Identifier: testDeployment2ID, - Status: event.ReconcilePending, - }, - }, - // deployment1 is replaced - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: taskName, - Identifier: testDeployment1ID, - Status: event.ReconcileSuccessful, - }, - }, - // deployment2 not found - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: taskName, - Identifier: testDeployment2ID, - Status: event.ReconcileSuccessful, - }, - }, - }, - expectedInventory: &actuation.Inventory{ - Status: actuation.InventoryStatus{ - Objects: []actuation.ObjectStatus{ - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata(testDeployment1ID), - Strategy: actuation.ActuationStrategyDelete, - Actuation: actuation.ActuationSucceeded, - // UID change causes success after delete - Reconcile: actuation.ReconcileSucceeded, - // Recorded UID should be from the deleted object, not the new replacement - UID: testDeployment1.GetUID(), - // Deleted generation is unknown - }, - { - ObjectReference: inventory.ObjectReferenceFromObjMetadata(testDeployment2ID), - Strategy: actuation.ActuationStrategyDelete, - Actuation: actuation.ActuationSucceeded, - Reconcile: actuation.ReconcileSucceeded, - UID: testDeployment2.GetUID(), - // Deleted generation is unknown - }, - }, - }, - }, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - ids := object.ObjMetadataSet{ - testDeployment1ID, - testDeployment2ID, - } - task := NewWaitTask(taskName, ids, tc.condition, - tc.waitTimeout, testutil.NewFakeRESTMapper()) - - eventChannel := make(chan event.Event) - resourceCache := cache.NewResourceCacheMap() - taskContext := NewTaskContext(eventChannel, resourceCache) - defer close(eventChannel) - - tc.configureTaskContextFunc(taskContext) - - // run task async, to let the test collect events - go func() { - // start the task - task.Start(taskContext) - - tc.eventsFunc(resourceCache, task, taskContext) - }() - - // wait for task result - timer := time.NewTimer(5 * time.Second) - receivedEvents := []event.Event{} - loop: - for { - select { - case e := <-taskContext.EventChannel(): - receivedEvents = append(receivedEvents, e) - case res := <-taskContext.TaskChannel(): - timer.Stop() - assert.NoError(t, res.Err) - break loop - case <-timer.C: - t.Fatalf("timed out waiting for TaskResult") - } - } - - testutil.AssertEqual(t, tc.expectedEvents, receivedEvents, - "Actual events (%d) do not match expected events (%d)", - len(receivedEvents), len(tc.expectedEvents)) - - testutil.AssertEqual(t, tc.expectedInventory, taskContext.InventoryManager().Inventory()) - }) - } -} diff --git a/pkg/common/common.go b/pkg/common/common.go deleted file mode 100644 index db18af0d..00000000 --- a/pkg/common/common.go +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package common - -import ( - "fmt" - - "k8s.io/apimachinery/pkg/util/rand" - cmdutil "k8s.io/kubectl/pkg/cmd/util" -) - -const ( - // InventoryLabel is the label stored on the ConfigMap - // inventory object. The value of the label is a unique - // identifier (by default a UUID), representing the set of - // objects applied at the same time as the inventory object. - // This inventory object is used for pruning and deletion. - InventoryLabel = "cli-utils.sigs.k8s.io/inventory-id" - // InventoryHash defines an annotation which stores the hash of - // the set of objects applied at the same time as the inventory - // object. This annotation is set on the inventory object at the - // time of the apply. The hash is computed from the sorted strings - // of the applied object's metadata (ObjMetadata). The hash is - // used as a suffix of the inventory object name. Example: - // inventory-1e5824fb - InventoryHash = "cli-utils.sigs.k8s.io/inventory-hash" - // Resource lifecycle annotation key for "on-remove" operations. - OnRemoveAnnotation = "cli-utils.sigs.k8s.io/on-remove" - // Resource lifecycle annotation value to prevent deletion. - OnRemoveKeep = "keep" - // Maximum random number, non-inclusive, eight digits. - maxRandInt = 100000000 - // DefaultFieldManager is default owner of applied fields in - // server-side apply. - DefaultFieldManager = "kubectl" - - // LifecycleDeletionAnnotation is the lifecycle annotation key for deletion operation. - LifecycleDeleteAnnotation = "client.lifecycle.config.k8s.io/deletion" - - // PreventDeletion is the value used with LifecycleDeletionAnnotation - // to prevent deleting a resource. - PreventDeletion = "detach" -) - -// RandomStr returns an eight-digit (with leading zeros) string of a -// random number. -func RandomStr() string { - randomInt := rand.Intn(maxRandInt) // nolint:gosec - return fmt.Sprintf("%08d", randomInt) -} - -// NoDeletion checks the passed in annotation key and value and returns -// true if that matches with the prevent deletion annotation. -func NoDeletion(key, value string) bool { - m := map[string]string{ - LifecycleDeleteAnnotation: PreventDeletion, - OnRemoveAnnotation: OnRemoveKeep, - } - if val, found := m[key]; found { - return val == value - } - return false -} - -var Strategies = []DryRunStrategy{DryRunClient, DryRunServer} - -//go:generate stringer -type=DryRunStrategy -type DryRunStrategy int - -const ( - // DryRunNone indicates the client will make all mutating calls - DryRunNone DryRunStrategy = iota - - // DryRunClient, or client-side dry-run, indicates the client will prevent - // making mutating calls such as CREATE, PATCH, and DELETE - DryRunClient - - // DryRunServer, or server-side dry-run, indicates the client will send - // mutating calls to the APIServer with the dry-run parameter to prevent - // persisting changes. - // - // Note that clients sending server-side dry-run calls should verify that - // the APIServer and the resource supports server-side dry-run, and otherwise - // clients should fail early. - // - // If a client sends a server-side dry-run call to an APIServer that doesn't - // support server-side dry-run, then the APIServer will persist changes inadvertently. - DryRunServer -) - -// ClientDryRun returns true if input drs is DryRunClient -// -//nolint:stylecheck // Prevent lint errors on receiver names caused by string generation above -func (drs DryRunStrategy) ClientDryRun() bool { - return drs == DryRunClient -} - -// ServerDryRun returns true if input drs is DryRunServer -func (drs DryRunStrategy) ServerDryRun() bool { - return drs == DryRunServer -} - -// ClientOrServerDryRun returns true if input drs is either client or server dry run -func (drs DryRunStrategy) ClientOrServerDryRun() bool { - return drs == DryRunClient || drs == DryRunServer -} - -// Strategy returns the -func (drs DryRunStrategy) Strategy() cmdutil.DryRunStrategy { - switch drs { - case DryRunClient: - return cmdutil.DryRunClient - case DryRunServer: - return cmdutil.DryRunServer - default: - return cmdutil.DryRunNone - } -} - -// ServerSideOptions encapsulates the fields to implement server-side apply. -type ServerSideOptions struct { - // ServerSideApply means the merge patch is calculated on the API server instead of the client. - ServerSideApply bool - - // ForceConflicts overwrites the fields when applying if the field manager differs. - ForceConflicts bool - - // FieldManager identifies the client "owner" of the applied fields (e.g. kubectl) - FieldManager string -} diff --git a/pkg/common/dryrunstrategy_string.go b/pkg/common/dryrunstrategy_string.go deleted file mode 100644 index a6e3823f..00000000 --- a/pkg/common/dryrunstrategy_string.go +++ /dev/null @@ -1,25 +0,0 @@ -// Code generated by "stringer -type=DryRunStrategy"; DO NOT EDIT. - -package common - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[DryRunNone-0] - _ = x[DryRunClient-1] - _ = x[DryRunServer-2] -} - -const _DryRunStrategy_name = "DryRunNoneDryRunClientDryRunServer" - -var _DryRunStrategy_index = [...]uint8{0, 10, 22, 34} - -func (i DryRunStrategy) String() string { - if i < 0 || i >= DryRunStrategy(len(_DryRunStrategy_index)-1) { - return "DryRunStrategy(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _DryRunStrategy_name[_DryRunStrategy_index[i]:_DryRunStrategy_index[i+1]] -} diff --git a/pkg/common/path.go b/pkg/common/path.go deleted file mode 100644 index b6b3ed5e..00000000 --- a/pkg/common/path.go +++ /dev/null @@ -1,168 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package common - -import ( - "fmt" - "io" - "os" - "path/filepath" - - "k8s.io/cli-runtime/pkg/genericclioptions" - "k8s.io/klog/v2" - "sigs.k8s.io/kustomize/kyaml/kio" - "sigs.k8s.io/kustomize/kyaml/kio/kioutil" - "sigs.k8s.io/kustomize/kyaml/yaml" -) - -const ( - stdinDash = "-" - tmpDirPrefix = "diff-cmd-config" - fileRegexp = "config-*.yaml" -) - -func processPaths(paths []string) genericclioptions.FileNameFlags { - // No arguments means we are reading from StdIn - fileNameFlags := genericclioptions.FileNameFlags{} - if len(paths) == 0 { - fileNames := []string{stdinDash} - fileNameFlags.Filenames = &fileNames - return fileNameFlags - } - - // Must be a single directory here; set recursive flag. - t := true - fileNameFlags.Filenames = &paths - fileNameFlags.Recursive = &t - return fileNameFlags -} - -// DemandOneDirectoryOrStdin processes "paths" to ensure the -// single argument in the array is a directory. Returns FileNameFlags -// populated with the directory (recursive flag set), or -// the StdIn dash. An empty array gets treated as StdIn -// (adding dash to the array). Returns an error if more than -// one element in the array or the filepath is not a directory. -func DemandOneDirectory(paths []string) (genericclioptions.FileNameFlags, error) { - result := genericclioptions.FileNameFlags{} - if len(paths) == 1 { - dirPath := paths[0] - if !IsDir(dirPath) { - return result, fmt.Errorf("argument '%s' is not but must be a directory", dirPath) - } - } - if len(paths) > 1 { - return result, fmt.Errorf( - "specify exactly one directory path argument; rejecting %v", paths) - } - result = processPaths(paths) - return result, nil -} - -func IsDir(dir string) bool { - if f, err := os.Stat(dir); err == nil { - if f.Mode().IsDir() { - return true - } - } - return false -} - -// ExpandPackageDir expands the one package directory entry in the flags to all -// the config file paths recursively. Excludes the inventory object (since -// this object is specially processed). Used for the diff command, so it will -// not always show a diff of the inventory object. Must be called AFTER -// DemandOneDirectory. -func ExpandPackageDir(f genericclioptions.FileNameFlags) (genericclioptions.FileNameFlags, error) { - if len(*f.Filenames) != 1 { - return f, fmt.Errorf("expand package directory should pass one package directory. "+ - "Passed the following paths: %v", f.Filenames) - } - _, configFilepaths, err := ExpandDir((*f.Filenames)[0]) - if err != nil { - return f, err - } - f.Filenames = &configFilepaths - return f, nil -} - -// FilterInputFile copies the resource config on stdin into a file -// at the tmpDir, filtering the inventory object. It is the -// responsibility of the caller to clean up the tmpDir. Returns -// an error if one occurs. -func FilterInputFile(in io.Reader, tmpDir string) error { - // Copy the config from "in" into a local temp file. - dir, err := os.MkdirTemp("", tmpDirPrefix) - if err != nil { - return err - } - tmpFile, err := os.CreateTemp(dir, fileRegexp) - if err != nil { - return err - } - defer os.RemoveAll(dir) - klog.V(6).Infof("Temp File: %s", tmpFile.Name()) - if _, err := io.Copy(tmpFile, in); err != nil { - return err - } - // Read the config stored locally, parsing into RNodes - r := kio.LocalPackageReader{PackagePath: dir} - nodes, err := r.Read() - if err != nil { - return err - } - klog.V(6).Infof("Num read configs: %d", len(nodes)) - // Filter RNodes to remove the inventory object. - filteredNodes := []*yaml.RNode{} - for _, node := range nodes { - meta, err := node.GetMeta() - if err != nil { - continue - } - // If object has inventory label, skip it. - labels := meta.Labels - if _, exists := labels[InventoryLabel]; !exists { - filteredNodes = append(filteredNodes, node) - } - } - // Write the remaining configs into a file in the tmpDir - w := kio.LocalPackageWriter{ - PackagePath: tmpDir, - KeepReaderAnnotations: false, - } - klog.V(6).Infof("Writing %d configs", len(filteredNodes)) - return w.Write(filteredNodes) -} - -// ExpandDir takes a single package directory as a parameter, and returns -// the inventory template filepath and an array of config file paths. If no -// inventory template file, then the first return value is an empty string. -// Returns an error if one occurred while processing the paths. -func ExpandDir(dir string) (string, []string, error) { - filepaths := []string{} - r := kio.LocalPackageReader{PackagePath: dir} - nodes, err := r.Read() - if err != nil { - return "", filepaths, err - } - var invFilepath string - for _, node := range nodes { - meta, err := node.GetMeta() - if err != nil { - continue - } - path := meta.Annotations[kioutil.PathAnnotation] - path = filepath.Join(dir, path) - // If object has inventory label, skip it. - labels := meta.Labels - if _, exists := labels[InventoryLabel]; exists { - if invFilepath == "" { - invFilepath = path - } - continue - } - filepaths = append(filepaths, path) - } - return invFilepath, filepaths, nil -} diff --git a/pkg/common/path_test.go b/pkg/common/path_test.go deleted file mode 100644 index c12040a3..00000000 --- a/pkg/common/path_test.go +++ /dev/null @@ -1,380 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package common - -import ( - "bytes" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/fluxcd/cli-utils/pkg/testutil" - "github.com/stretchr/testify/assert" - "k8s.io/cli-runtime/pkg/genericclioptions" -) - -const ( - packageDir = "test-pkg-dir" - subFolder = "sub-folder" - inventoryFilename = "inventory.yaml" - secondInventoryFilename = "inventory-2.yaml" - podAFilename = "pod-a.yaml" - podBFilename = "pod-b.yaml" - configSeparator = "---" -) - -var ( - inventoryFilePath = filepath.Join(packageDir, inventoryFilename) - secondInventoryFilePath = filepath.Join(packageDir, subFolder, secondInventoryFilename) - podAFilePath = filepath.Join(packageDir, podAFilename) - podBFilePath = filepath.Join(packageDir, podBFilename) -) - -func setupTestFilesystem(t *testing.T) testutil.TestFilesystem { - // Create the test filesystem, and add package config files - // to it. - t.Log("Creating test filesystem") - tf := testutil.Setup(t, packageDir) - t.Logf("Adding File: %s", inventoryFilePath) - tf.WriteFile(t, inventoryFilePath, inventoryConfigMap) - t.Logf("Adding File: %s", secondInventoryFilePath) - tf.WriteFile(t, secondInventoryFilePath, secondInventoryConfigMap) - t.Logf("Adding File: %s", podAFilePath) - tf.WriteFile(t, podAFilePath, podA) - t.Logf("Adding File: %s", podBFilePath) - tf.WriteFile(t, podBFilePath, podB) - return tf -} - -var inventoryConfigMap = []byte(` -apiVersion: v1 -kind: ConfigMap -metadata: - namespace: test-namespace - name: inventory - labels: - cli-utils.sigs.k8s.io/inventory-id: test-inventory -`) - -var secondInventoryConfigMap = []byte(` -apiVersion: v1 -kind: ConfigMap -metadata: - namespace: test-namespace - name: inventory-2 - labels: - cli-utils.sigs.k8s.io/inventory-id: test-inventory -`) - -var podA = []byte(` -apiVersion: v1 -kind: Pod -metadata: - name: pod-a - namespace: test-namespace - labels: - name: test-pod-label -spec: - containers: - - name: kubernetes-pause - image: registry.k8s.io/pause:2.0 -`) - -var podB = []byte(` -apiVersion: v1 -kind: Pod -metadata: - name: pod-b - namespace: test-namespace - labels: - name: test-pod-label -spec: - containers: - - name: kubernetes-pause - image: registry.k8s.io/pause:2.0 -`) - -func buildMultiResourceConfig(configs ...[]byte) []byte { - r := []byte{} - for i, config := range configs { - if i > 0 { - r = append(r, []byte(configSeparator)...) - } - r = append(r, config...) - } - return r -} - -func TestProcessPaths(t *testing.T) { - tf := setupTestFilesystem(t) - defer tf.Clean() - - trueVal := true - testCases := map[string]struct { - paths []string - expectedFileNameFlags genericclioptions.FileNameFlags - errFromDemandOneDirectory string - }{ - "empty slice means reading from StdIn": { - paths: []string{}, - expectedFileNameFlags: genericclioptions.FileNameFlags{ - Filenames: &[]string{"-"}, - }, - }, - "single file in slice is error; must be directory": { - paths: []string{podAFilePath}, - expectedFileNameFlags: genericclioptions.FileNameFlags{ - Filenames: nil, - Recursive: nil, - }, - errFromDemandOneDirectory: "argument 'test-pkg-dir/pod-a.yaml' is not but must be a directory", - }, - "single dir in slice": { - paths: []string{tf.GetRootDir()}, - expectedFileNameFlags: genericclioptions.FileNameFlags{ - Filenames: &[]string{tf.GetRootDir()}, - Recursive: &trueVal, - }, - }, - "multiple arguments is an error": { - paths: []string{podAFilePath, podBFilePath}, - expectedFileNameFlags: genericclioptions.FileNameFlags{ - Filenames: nil, - Recursive: nil, - }, - errFromDemandOneDirectory: "specify exactly one directory path argument; rejecting [test-pkg-dir/pod-a.yaml test-pkg-dir/pod-b.yaml]", - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - fileNameFlags, err := DemandOneDirectory(tc.paths) - assert.Equal(t, tc.expectedFileNameFlags, fileNameFlags) - if err != nil && err.Error() != tc.errFromDemandOneDirectory { - assert.Equal(t, err.Error(), tc.errFromDemandOneDirectory) - } - }) - } -} - -func TestFilterInputFile(t *testing.T) { - tf := testutil.Setup(t) - defer tf.Clean() - - testCases := map[string]struct { - configObjects [][]byte - expectedObjects [][]byte - }{ - "Empty config objects writes empty file": { - configObjects: [][]byte{}, - expectedObjects: [][]byte{}, - }, - "Only inventory obj writes empty file": { - configObjects: [][]byte{inventoryConfigMap}, - expectedObjects: [][]byte{}, - }, - "Only pods writes both pods": { - configObjects: [][]byte{podA, podB}, - expectedObjects: [][]byte{podA, podB}, - }, - "Basic case of inventory obj and two pods": { - configObjects: [][]byte{inventoryConfigMap, podA, podB}, - expectedObjects: [][]byte{podA, podB}, - }, - "Basic case of inventory obj and two pods in different order": { - configObjects: [][]byte{podB, inventoryConfigMap, podA}, - expectedObjects: [][]byte{podB, podA}, - }, - } - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - // Build a single file of multiple resource configs, and - // call the tested function FilterInputFile. This writes - // the passed file to the test filesystem, filtering - // the inventory object if it exists in the passed file. - in := buildMultiResourceConfig(tc.configObjects...) - err := FilterInputFile(bytes.NewReader(in), tf.GetRootDir()) - if err != nil { - t.Fatalf("Unexpected error in FilterInputFile: %s", err) - } - // Retrieve the files from the test filesystem. - actualFiles, err := os.ReadDir(tf.GetRootDir()) - if err != nil { - t.Fatalf("Error reading test filesystem directory: %s", err) - } - // Since we remove the generated file for each test, there should - // not be more than one file in the test filesystem. - if len(actualFiles) > 1 { - t.Fatalf("Wrong number of files (%d) in dir: %s", len(actualFiles), tf.GetRootDir()) - } - // If there is a generated file, then read it into actualStr. - actualStr := "" - if len(actualFiles) != 0 { - actualFilename := actualFiles[0].Name() - defer os.Remove(actualFilename) - actual, err := os.ReadFile(actualFilename) - if err != nil { - t.Fatalf("Error reading created file (%s): %s", actualFilename, err) - } - actualStr = strings.TrimSpace(string(actual)) - } - // Build the expected string from the expectedObjects. This expected - // string should not have the inventory object config in it. - expected := buildMultiResourceConfig(tc.expectedObjects...) - expectedStr := strings.TrimSpace(string(expected)) - if expectedStr != actualStr { - t.Errorf("Expected file contents (%s) not equal to actual file contents (%s)", - expectedStr, actualStr) - } - }) - } -} - -func TestExpandDir(t *testing.T) { - tf := setupTestFilesystem(t) - defer tf.Clean() - - testCases := map[string]struct { - packageDirPath string - expandedInventory string - expandedPaths []string - isError bool - }{ - "empty path is error": { - packageDirPath: "", - isError: true, - }, - "path that is not dir is error": { - packageDirPath: "fakedir1", - isError: true, - }, - "root package dir excludes inventory object": { - packageDirPath: tf.GetRootDir(), - expandedInventory: "inventory.yaml", - expandedPaths: []string{ - "pod-a.yaml", - "pod-b.yaml", - }, - isError: false, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - actualInventory, actualPaths, err := ExpandDir(tc.packageDirPath) - if tc.isError { - if err == nil { - t.Fatalf("expected error but received none") - } - return - } - if err != nil { - t.Fatalf("received unexpected error %#v", err) - return - } - actualFilename := filepath.Base(actualInventory) - if tc.expandedInventory != actualFilename { - t.Errorf("expected inventory template filepath (%s), got (%s)", tc.expandedInventory, actualFilename) - } - if len(tc.expandedPaths) != len(actualPaths) { - t.Errorf("expected (%d) resource filepaths, got (%d)", len(tc.expandedPaths), len(actualPaths)) - } - for _, expectedPath := range tc.expandedPaths { - found := false - for _, actualPath := range actualPaths { - actualFilename := filepath.Base(actualPath) - if expectedPath == actualFilename { - found = true - break - } - } - if !found { - t.Errorf("expected filename (%s) not found", expectedPath) - } - } - }) - } -} - -func TestExpandDirErrors(t *testing.T) { - tf := setupTestFilesystem(t) - defer tf.Clean() - - testCases := map[string]struct { - packageDirPath []string - expandedPaths []string - isError bool - }{ - "empty path is error": { - packageDirPath: []string{}, - isError: true, - }, - "more than one path is error": { - packageDirPath: []string{"fakedir1", "fakedir2"}, - isError: true, - }, - "path that is not dir is error": { - packageDirPath: []string{"fakedir1"}, - isError: true, - }, - "root package dir excludes inventory object": { - packageDirPath: []string{tf.GetRootDir()}, - expandedPaths: []string{ - filepath.Join(packageDir, "pod-a.yaml"), - filepath.Join(packageDir, "pod-b.yaml"), - }, - isError: false, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - trueVal := true - filenameFlags := genericclioptions.FileNameFlags{ - Filenames: &tc.packageDirPath, - Recursive: &trueVal, - } - actualFlags, err := ExpandPackageDir(filenameFlags) - if tc.isError && err == nil { - t.Fatalf("expected error but received none") - } - if !tc.isError { - if err != nil { - t.Fatalf("unexpected error received: %v", err) - } - actualPaths := *actualFlags.Filenames - if len(tc.expandedPaths) != len(actualPaths) { - t.Errorf("expected config filepaths (%s), got (%s)", - tc.expandedPaths, actualPaths) - } - for _, expected := range tc.expandedPaths { - if !filepathExists(expected, actualPaths) { - t.Errorf("expected config filepath (%s) in actual filepaths (%s)", - expected, actualPaths) - } - } - // Check the inventory object is not in the filename flags. - for _, actualPath := range actualPaths { - if strings.Contains(actualPath, "inventory.yaml") { - t.Errorf("inventory object should be excluded") - } - } - } - }) - } -} - -// filepathExists returns true if the passed "filepath" is a substring -// of any of the passed full "filepaths"; false otherwise. For example: -// if filepath = "test/a.yaml", and filepaths includes "/tmp/test/a.yaml", -// this function returns true. -func filepathExists(filepath string, filepaths []string) bool { - for _, fp := range filepaths { - if strings.Contains(fp, filepath) { - return true - } - } - return false -} diff --git a/pkg/config/initoptions.go b/pkg/config/initoptions.go deleted file mode 100644 index 2fb5e456..00000000 --- a/pkg/config/initoptions.go +++ /dev/null @@ -1,250 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package config - -import ( - "fmt" - "os" - "path/filepath" - "regexp" - "strings" - "time" - - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/inventory/configmap" - "github.com/google/uuid" - "k8s.io/cli-runtime/pkg/genericclioptions" - "k8s.io/klog/v2" - cmdutil "k8s.io/kubectl/pkg/cmd/util" - "sigs.k8s.io/kustomize/kyaml/kio" - "sigs.k8s.io/kustomize/kyaml/kio/filters" - "sigs.k8s.io/kustomize/kyaml/openapi" -) - -const ( - manifestFilename = "inventory-template.yaml" -) - -// InitOptions contains the fields necessary to generate a -// inventory object template ConfigMap. -type InitOptions struct { - factory cmdutil.Factory - - ioStreams genericclioptions.IOStreams - // Template string; must be a valid k8s resource. - Template string - // Package directory argument; must be valid directory. - Dir string - // Namespace for inventory object; can not be empty. - Namespace string - // Inventory object label value; must be a valid k8s label value. - InventoryID string -} - -func NewInitOptions(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *InitOptions { - return &InitOptions{ - factory: f, - ioStreams: ioStreams, - Template: configmap.ConfigMapTemplate, - } -} - -// Complete fills in the InitOptions fields. -// TODO(seans3): Look into changing this kubectl-inspired way of organizing -// the InitOptions (e.g. Complete and Run methods). -func (i *InitOptions) Complete(args []string) error { - if len(args) != 1 { - return fmt.Errorf("need one 'directory' arg; have %d", len(args)) - } - dir, err := NormalizeDir(args[0]) - if err != nil { - return err - } - i.Dir = dir - klog.V(4).Infof("init directory: %s", i.Dir) - - ns, err := FindNamespace(i.factory.ToRawKubeConfigLoader(), i.Dir) - if err != nil { - return err - } - i.Namespace = ns - - // Set the default inventory label if one does not exist. - if len(i.InventoryID) == 0 { - inventoryID, err := i.defaultInventoryID() - if err != nil { - return err - } - i.InventoryID = inventoryID - } - if !validateInventoryID(i.InventoryID) { - return fmt.Errorf("invalid group name: %s", i.InventoryID) - } - // Output the calculated namespace used for inventory object. - fmt.Fprintf(i.ioStreams.Out, "namespace: %s is used for inventory object\n", i.Namespace) - return nil -} - -type namespaceLoader interface { - Namespace() (string, bool, error) -} - -// FindNamespace looks up the namespace that should be used for the -// inventory template of the package. If the namespace is specified with -// the --namespace flag, it will be used no matter what. If not, this -// will look at all the resource, and if all belong in the same namespace, -// it will return that namespace. Otherwise, it will return the namespace -// set in the context. -func FindNamespace(loader namespaceLoader, dir string) (string, error) { - namespace, enforceNamespace, err := loader.Namespace() - if err != nil { - return "", err - } - if enforceNamespace { - klog.V(6).Infof("enforcing namespace: %s", namespace) - return namespace, nil - } - - ns, allInSameNs, err := allInSameNamespace(dir) - if err != nil { - return "", err - } - if allInSameNs { - klog.V(6).Infof("all in same namespace: %s", ns) - return ns, nil - } - klog.V(6).Infof("returning namespace: %s", namespace) - return namespace, nil -} - -// NormalizeDir returns full absolute directory path of the -// passed directory or an error. This function cleans up paths -// such as current directory (.), relative directories (..), or -// multiple separators. -func NormalizeDir(dirPath string) (string, error) { - if !common.IsDir(dirPath) { - return "", fmt.Errorf("invalid directory argument: %s", dirPath) - } - return filepath.Abs(dirPath) -} - -// allInSameNamespace goes through all resources in the package and -// checks the namespace for all of them. If they all have the namespace -// set and they all have the same value, this will return that namespace -// and the second return value will be true. Otherwise, it will not return -// a namespace and the second return value will be false. -func allInSameNamespace(packageDir string) (string, bool, error) { - r := kio.LocalPackageReader{PackagePath: packageDir} - nodes, err := r.Read() - if err != nil { - return "", false, err - } - - // Filter out any resources with the LocalConfig annotation - nodes, err = (&filters.IsLocalConfig{}).Filter(nodes) - if err != nil { - return "", false, err - } - - var ns string - for _, node := range nodes { - rm, err := node.GetMeta() - if err != nil { - return "", false, err - } - // Skip found cluster-scoped resources. If not found, just assume namespaced. - namespaced, found := openapi.IsNamespaceScoped(rm.TypeMeta) - if found && !namespaced { - klog.V(6).Infof("cluster-scoped resource %s--skip namespace calc", rm.TypeMeta) - continue - } - if rm.Namespace == "" { - klog.V(6).Infof("one resource missing namespace (%s): return empty namespace", rm.Name) - return "", false, nil - } - if ns == "" { - ns = rm.Namespace - } else if rm.Namespace != ns { - klog.V(6).Infof("two namespaces not same: %s versus %s", rm.Namespace, ns) - return "", false, nil - } - } - if ns != "" { - klog.V(6).Infof("returning empty namespace") - return ns, true, nil - } - return "", false, nil -} - -// defaultInventoryID returns a UUID string as a default unique -// identifier for a inventory object label. -func (i *InitOptions) defaultInventoryID() (string, error) { - u, err := uuid.NewRandom() - if err != nil { - return "", err - } - return u.String(), nil -} - -// Must begin and end with an alphanumeric character ([a-z0-9A-Z]) -// with dashes (-), underscores (_), dots (.), and alphanumerics -// between. -const inventoryIDRegexp = `^[a-zA-Z0-9][a-zA-Z0-9\-\_\.]+[a-zA-Z0-9]$` - -// validateInventoryID returns true of the passed group name is a -// valid label value; false otherwise. The valid label values -// are [a-z0-9A-Z] "-", "_", and "." The inventoryID must not -// be empty, but it can not be more than 63 characters. -func validateInventoryID(inventoryID string) bool { - if len(inventoryID) == 0 || len(inventoryID) > 63 { - return false - } - re := regexp.MustCompile(inventoryIDRegexp) - return re.MatchString(inventoryID) -} - -// fileExists returns true if a file at path already exists; -// false otherwise. -func fileExists(path string) bool { - f, err := os.Stat(path) - if os.IsNotExist(err) { - return false - } - return !f.IsDir() -} - -// fillInValues returns a string of the inventory object template -// ConfigMap with values filled in (eg. namespace, inventoryID). -// TODO(seans3): Look into text/template package. -func (i *InitOptions) fillInValues() string { - now := time.Now() - nowStr := now.Format("2006-01-02 15:04:05 MST") - randomSuffix := common.RandomStr() - manifestStr := i.Template - klog.V(4).Infof("namespace/inventory-id: %s/%s", i.Namespace, i.InventoryID) - manifestStr = strings.ReplaceAll(manifestStr, "", nowStr) - manifestStr = strings.ReplaceAll(manifestStr, "", i.Namespace) - manifestStr = strings.ReplaceAll(manifestStr, "", randomSuffix) - manifestStr = strings.ReplaceAll(manifestStr, "", i.InventoryID) - return manifestStr -} - -func (i *InitOptions) Run() error { - manifestFilePath := filepath.Join(i.Dir, manifestFilename) - if fileExists(manifestFilePath) { - return fmt.Errorf("inventory object template file already exists: %s", manifestFilePath) - } - klog.V(4).Infof("creating manifest filename: %s", manifestFilePath) - f, err := os.Create(manifestFilePath) - if err != nil { - return fmt.Errorf("unable to create inventory object template file: %s", err) - } - defer f.Close() - _, err = f.WriteString(i.fillInValues()) - if err != nil { - return fmt.Errorf("unable to write inventory object template file: %s", manifestFilePath) - } - fmt.Fprintf(i.ioStreams.Out, "Initialized: %s\n", manifestFilePath) - return nil -} diff --git a/pkg/config/initoptions_test.go b/pkg/config/initoptions_test.go deleted file mode 100644 index 20076774..00000000 --- a/pkg/config/initoptions_test.go +++ /dev/null @@ -1,373 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package config - -import ( - "fmt" - "os" - "path/filepath" - "regexp" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "k8s.io/cli-runtime/pkg/genericclioptions" - cmdtesting "k8s.io/kubectl/pkg/cmd/testing" -) - -// writeFile writes a file under the test directory -func writeFile(t *testing.T, path string, value []byte) { - err := os.WriteFile(path, value, 0600) - if !assert.NoError(t, err) { - assert.FailNow(t, err.Error()) - } -} - -var readFileA = []byte(` -apiVersion: v1 -kind: Pod -metadata: - name: objA - namespace: namespaceA -`) - -var readFileB = []byte(` -apiVersion: v1 -kind: Pod -metadata: - name: objB - namespace: namespaceB -`) - -var readFileC = []byte(` -apiVersion: v1 -kind: Pod -metadata: - name: objC -`) - -var readFileD = []byte(` -apiVersion: v1 -kind: Pod -metadata: - name: objD - namespace: namespaceD - annotations: - config.kubernetes.io/local-config: "true" -`) - -var readFileE = []byte(` -apiVersion: v1 -kind: Pod -metadata: - name: objE - namespace: namespaceA -`) - -var readFileF = []byte(` -apiVersion: v1 -kind: Namespace -metadata: - name: namespaceA -`) - -func TestComplete(t *testing.T) { - tests := map[string]struct { - args []string - files map[string][]byte - isError bool - expectedErrMessage string - expectedNamespace string - }{ - "Empty args returns error": { - args: []string{}, - isError: true, - expectedErrMessage: "need one 'directory' arg; have 0", - }, - "More than one argument should fail": { - args: []string{"foo", "bar"}, - isError: true, - expectedErrMessage: "need one 'directory' arg; have 2", - }, - "Non-directory arg should fail": { - args: []string{"foo"}, - isError: true, - expectedErrMessage: "invalid directory argument: foo", - }, - "More than one namespace should fail": { - args: []string{}, - files: map[string][]byte{ - "a_test.yaml": readFileA, - "b_test.yaml": readFileB, - }, - isError: true, - expectedErrMessage: "resources belong to different namespaces", - }, - "If at least one resource doesn't have namespace, it should use the default": { - args: []string{}, - files: map[string][]byte{ - "b_test.yaml": readFileB, - "c_test.yaml": readFileC, - }, - isError: false, - expectedNamespace: "foo", - }, - "No resources without namespace should use the default namespace": { - args: []string{}, - files: map[string][]byte{ - "c_test.yaml": readFileC, - }, - isError: false, - expectedNamespace: "foo", - }, - "Resources with the LocalConfig annotation should be ignored": { - args: []string{}, - files: map[string][]byte{ - "b_test.yaml": readFileB, - "d_test.yaml": readFileD, - }, - isError: false, - expectedNamespace: "foo", - }, - "If all resources have the LocalConfig annotation use the default namespace": { - args: []string{}, - files: map[string][]byte{ - "d_test.yaml": readFileD, - }, - isError: false, - expectedNamespace: "foo", - }, - "Cluster-scoped resources are ignored in namespace calculation": { - args: []string{}, - files: map[string][]byte{ - "a_test.yaml": readFileA, - "e_test.yaml": readFileE, - "f_test.yaml": readFileF, - }, - isError: false, - expectedNamespace: "foo", - }, - } - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - var err error - dir, err := os.MkdirTemp("", "test-dir") - if !assert.NoError(t, err) { - assert.FailNow(t, err.Error()) - } - defer os.RemoveAll(dir) - - for fileName, fileContent := range tc.files { - writeFile(t, filepath.Join(dir, fileName), fileContent) - } - if len(tc.files) > 0 { - tc.args = append(tc.args, dir) - } - - tf := cmdtesting.NewTestFactory().WithNamespace("foo") - defer tf.Cleanup() - ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() - io := NewInitOptions(tf, ioStreams) - err = io.Complete(tc.args) - - if err != nil { - if !tc.isError { - t.Errorf("Expected error, but did not receive one") - return - } - assert.Contains(t, err.Error(), tc.expectedErrMessage) - return - } - assert.Contains(t, out.String(), tc.expectedNamespace) - }) - } -} - -func TestFindNamespace(t *testing.T) { - testCases := map[string]struct { - namespace string - enforceNamespace bool - files map[string][]byte - expectedNamespace string - }{ - "fallback to default": { - namespace: "foo", - enforceNamespace: false, - files: map[string][]byte{ - "a_test.yaml": readFileA, - "b_test.yaml": readFileB, - }, - expectedNamespace: "foo", - }, - "enforce namespace": { - namespace: "bar", - enforceNamespace: true, - files: map[string][]byte{ - "a_test.yaml": readFileA, - }, - expectedNamespace: "bar", - }, - "use namespace from resource if all the same": { - namespace: "bar", - enforceNamespace: false, - files: map[string][]byte{ - "a_test.yaml": readFileA, - }, - expectedNamespace: "namespaceA", - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - var err error - dir, err := os.MkdirTemp("", "test-dir") - if !assert.NoError(t, err) { - assert.FailNow(t, err.Error()) - } - defer os.RemoveAll(dir) - - for fileName, fileContent := range tc.files { - writeFile(t, filepath.Join(dir, fileName), fileContent) - } - - fakeLoader := &fakeNamespaceLoader{ - namespace: tc.namespace, - enforceNamespace: tc.enforceNamespace, - } - - namespace, err := FindNamespace(fakeLoader, dir) - assert.NoError(t, err) - assert.Equal(t, tc.expectedNamespace, namespace) - }) - } -} - -type fakeNamespaceLoader struct { - namespace string - enforceNamespace bool -} - -func (f *fakeNamespaceLoader) Namespace() (string, bool, error) { - return f.namespace, f.enforceNamespace, nil -} - -func TestDefaultInventoryID(t *testing.T) { - tf := cmdtesting.NewTestFactory().WithNamespace("foo") - defer tf.Cleanup() - ioStreams, _, _, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled - io := NewInitOptions(tf, ioStreams) - actual, err := io.defaultInventoryID() - if err != nil { - t.Errorf("Unxpected error during UUID generation: %v", err) - } - // Example UUID: dd647113-a354-48fa-9b93-cc1b7a85aadb - var uuidRegexp = `^[a-z0-9]{8}\-[a-z0-9]{4}\-[a-z0-9]{4}\-[a-z0-9]{4}\-[a-z0-9]{12}$` - re := regexp.MustCompile(uuidRegexp) - if !re.MatchString(actual) { - t.Errorf("Expected UUID; got (%s)", actual) - } -} - -func TestValidateInventoryID(t *testing.T) { - tests := map[string]struct { - inventoryID string - isValid bool - }{ - "Empty InventoryID fails": { - inventoryID: "", - isValid: false, - }, - "InventoryID greater than sixty-three chars fails": { - inventoryID: "88888888888888888888888888888888888888888888888888888888888888888", - isValid: false, - }, - "Non-allowed characters fails": { - inventoryID: "&foo", - isValid: false, - }, - "Initial dot fails": { - inventoryID: ".foo", - isValid: false, - }, - "Initial dash fails": { - inventoryID: "-foo", - isValid: false, - }, - "Initial underscore fails": { - inventoryID: "_foo", - isValid: false, - }, - "Trailing dot fails": { - inventoryID: "foo.", - isValid: false, - }, - "Trailing dash fails": { - inventoryID: "foo-", - isValid: false, - }, - "Trailing underscore fails": { - inventoryID: "foo_", - isValid: false, - }, - "Initial digit succeeds": { - inventoryID: "90-foo.bar_test", - isValid: true, - }, - "Allowed characters succeed": { - inventoryID: "f_oo90bar-t.est90", - isValid: true, - }, - } - - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - actualValid := validateInventoryID(tc.inventoryID) - if tc.isValid != actualValid { - t.Errorf("InventoryID: %s. Expected valid (%t), got (%t)", tc.inventoryID, tc.isValid, actualValid) - } - }) - } -} - -func TestFillInValues(t *testing.T) { - tests := map[string]struct { - namespace string - inventoryID string - }{ - "Basic namespace/inventoryID": { - namespace: "foo", - inventoryID: "bar", - }, - } - - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - tf := cmdtesting.NewTestFactory().WithNamespace("foo") - defer tf.Cleanup() - ioStreams, _, _, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled - io := NewInitOptions(tf, ioStreams) - io.Namespace = tc.namespace - io.InventoryID = tc.inventoryID - actual := io.fillInValues() - expectedLabel := fmt.Sprintf("cli-utils.sigs.k8s.io/inventory-id: %s", tc.inventoryID) - if !strings.Contains(actual, expectedLabel) { - t.Errorf("\nExpected label (%s) not found in inventory object: %s\n", expectedLabel, actual) - } - expectedNamespace := fmt.Sprintf("namespace: %s", tc.namespace) - if !strings.Contains(actual, expectedNamespace) { - t.Errorf("\nExpected namespace (%s) not found in inventory object: %s\n", expectedNamespace, actual) - } - matched, err := regexp.MatchString(`name: inventory-\d{8}\n`, actual) - if err != nil { - t.Errorf("unexpected error parsing inventory name: %s", err) - } - if !matched { - t.Errorf("expected inventory name (e.g. inventory-12345678), got (%s)", actual) - } - if !strings.Contains(actual, "kind: ConfigMap") { - t.Errorf("\nExpected `kind: ConfigMap` not found in inventory object: %s\n", actual) - } - }) - } -} diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go deleted file mode 100644 index aa9259f7..00000000 --- a/pkg/errors/errors.go +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package errors - -import ( - "bytes" - "fmt" - "io" - "os" - "reflect" - "strings" - "text/template" - - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/pkg/manifestreader" - cmdutil "k8s.io/kubectl/pkg/cmd/util" -) - -const ( - DefaultErrorExitCode = 1 -) - -var errorMsgForType map[reflect.Type]string -var statusCodeForType map[reflect.Type]int - -//nolint:gochecknoinits -func init() { - errorMsgForType = make(map[reflect.Type]string) - errorMsgForType[reflect.TypeOf(inventory.NoInventoryObjError{})] = ` -Package uninitialized. Please run "{{.cmdNameBase}} init" command. - -The package needs to be initialized to generate the template -which will store state for resource sets. This state is -necessary to perform functionality such as deleting an entire -package or automatically deleting omitted resources (pruning). -` - - errorMsgForType[reflect.TypeOf(inventory.MultipleInventoryObjError{})] = ` -Package has multiple inventory object templates. - -The package should have one and only one inventory object template. -` - - errorMsgForType[reflect.TypeOf(manifestreader.UnknownTypesError{})] = ` -Unknown type(s) encountered. Every type must either be already installed in the cluster or the CRD must be among the applied manifests. - -{{- range .err.GroupKinds}} -{{ printf "%s" . }} -{{- end}} -` - - statusCodeForType = make(map[reflect.Type]int) -} - -// CheckErr looks up the appropriate error message and exit status for known -// errors. It will print the information to the provided io.Writer. If we -// don't know the error, it delegates to the error handling in cmdutil. -func CheckErr(w io.Writer, err error, cmdNameBase string) { - errText, found := textForError(err, cmdNameBase) - if found { - exitStatus := findErrExitCode(err) - if len(errText) > 0 { - if !strings.HasSuffix(errText, "\n") { - errText += "\n" - } - fmt.Fprint(w, errText) - } - os.Exit(exitStatus) - } - - cmdutil.CheckErr(err) -} - -// textForError looks up the error message based on the type of the error. -func textForError(baseErr error, cmdNameBase string) (string, bool) { - errType, found := findErrType(baseErr) - if !found { - return "", false - } - tmplText, found := errorMsgForType[errType] - if !found { - return "", false - } - - tmpl, err := template.New("errMsg").Parse(tmplText) - if err != nil { - // Just return false here instead of the error. It will just - // mean a less informative error message and we rather show the - // original error. - return "", false - } - var b bytes.Buffer - err = tmpl.Execute(&b, map[string]interface{}{ - "cmdNameBase": cmdNameBase, - "err": baseErr, - }) - if err != nil { - return "", false - } - return strings.TrimSpace(b.String()), true -} - -// findErrType finds the type of the error. It returns the real type in the -// event the error is actually a pointer to a type. -func findErrType(err error) (reflect.Type, bool) { - switch reflect.ValueOf(err).Kind() { - case reflect.Ptr: - // If the value of the interface is a pointer, we use the type - // of the real value. - return reflect.ValueOf(err).Elem().Type(), true - case reflect.Struct: - return reflect.TypeOf(err), true - default: - return nil, false - } -} - -// findErrExitCode looks up if there is a defined error code for the provided -// error type. -func findErrExitCode(err error) int { - errType, found := findErrType(err) - if !found { - return DefaultErrorExitCode - } - if exitStatus, found := statusCodeForType[errType]; found { - return exitStatus - } - return DefaultErrorExitCode -} diff --git a/pkg/errors/errors_test.go b/pkg/errors/errors_test.go deleted file mode 100644 index cab2c827..00000000 --- a/pkg/errors/errors_test.go +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package errors - -import ( - "fmt" - "strings" - "testing" - - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/stretchr/testify/assert" -) - -func TestTextForError(t *testing.T) { - testCases := map[string]struct { - err error - cmdNameBase string - expectFound bool - expectedErrText string - }{ - "kapply command base name": { - err: &inventory.NoInventoryObjError{}, - cmdNameBase: "kapply", - expectFound: true, - expectedErrText: "Please run \"kapply init\" command.", - }, - "different command base name": { - err: &inventory.NoInventoryObjError{}, - cmdNameBase: "mycommand", - expectFound: true, - expectedErrText: "Please run \"mycommand init\" command.", - }, - "known error without directives in the template": { - err: &inventory.MultipleInventoryObjError{}, - cmdNameBase: "kapply", - expectFound: true, - expectedErrText: "Package has multiple inventory object templates.", - }, - "unknown error": { - err: fmt.Errorf("this is a test"), - cmdNameBase: "kapply", - expectFound: false, - }, - "unknown error type": { - err: sliceError{}, - cmdNameBase: "kapply", - expectFound: false, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - errText, found := textForError(tc.err, tc.cmdNameBase) - - if !tc.expectFound { - assert.False(t, found) - return - } - - assert.True(t, found) - assert.Contains(t, errText, strings.TrimSpace(tc.expectedErrText)) - }) - } -} - -type sliceError []string - -func (s sliceError) Error() string { - return "this is a test" -} diff --git a/pkg/inventory/configmap/cm-template.go b/pkg/inventory/configmap/cm-template.go deleted file mode 100644 index a48359bd..00000000 --- a/pkg/inventory/configmap/cm-template.go +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package configmap - -// Template for ConfigMap inventory object. The following fields -// must be filled in for this to be valid: -// -// : The time this is auto-generated -// : The namespace to place this inventory object -// : The random suffix added to the end of the name -// : The label value to retrieve this inventory object -const ConfigMapTemplate = `# NOTE: auto-generated. Some fields should NOT be modified. -# Date: -# -# Contains the "inventory object" template ConfigMap. -# When this object is applied, it is handled specially, -# storing the metadata of all the other objects applied. -# This object and its stored inventory is subsequently -# used to calculate the set of objects to automatically -# delete (prune), when an object is omitted from further -# applies. When applied, this "inventory object" is also -# used to identify the entire set of objects to delete. -# -# NOTE: The name of this inventory template file -# does NOT have any impact on group-related functionality -# such as deletion or pruning. -# -apiVersion: v1 -kind: ConfigMap -metadata: - # DANGER: Do not change the inventory object namespace. - # Changing the namespace will cause a loss of continuity - # with previously applied grouped objects. Set deletion - # and pruning functionality will be impaired. - namespace: - # NOTE: The name of the inventory object does NOT have - # any impact on group-related functionality such as - # deletion or pruning. - name: inventory- - labels: - # DANGER: Do not change the value of this label. - # Changing this value will cause a loss of continuity - # with previously applied grouped objects. Set deletion - # and pruning functionality will be impaired. - cli-utils.sigs.k8s.io/inventory-id: -` diff --git a/pkg/inventory/fake-builder.go b/pkg/inventory/fake-builder.go deleted file mode 100644 index 30b4e296..00000000 --- a/pkg/inventory/fake-builder.go +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package inventory - -import ( - "bytes" - "io" - "net/http" - "regexp" - - "github.com/fluxcd/cli-utils/pkg/object" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/apimachinery/pkg/api/meta/testrestmapper" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/cli-runtime/pkg/resource" - "k8s.io/client-go/rest/fake" - "k8s.io/client-go/restmapper" - cmdtesting "k8s.io/kubectl/pkg/cmd/testing" - "k8s.io/kubectl/pkg/scheme" -) - -var ( - codec = scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) - cmPathRegex = regexp.MustCompile(`^/namespaces/([^/]+)/configmaps$`) -) - -// FakeBuilder encapsulates a resource Builder which will hard-code the return -// of an inventory object with the encoded past invObjs. -type FakeBuilder struct { - invObjs object.ObjMetadataSet -} - -// SetInventoryObjs sets the objects which will be encoded in -// an inventory object to be returned when queried for the cluster -// inventory object. -func (fb *FakeBuilder) SetInventoryObjs(objs object.ObjMetadataSet) { - fb.invObjs = objs -} - -// Returns the fake resource Builder with the fake client, test restmapper, -// and the fake category expander. -func (fb *FakeBuilder) GetBuilder() func() *resource.Builder { - return func() *resource.Builder { - return resource.NewFakeBuilder( - fakeClient(fb.invObjs), - func() (meta.RESTMapper, error) { - return testrestmapper.TestOnlyStaticRESTMapper(scheme.Scheme), nil - }, - func() (restmapper.CategoryExpander, error) { - return resource.FakeCategoryExpander, nil - }) - } -} - -// fakeClient hard codes the return of an inventory object that encodes the passed -// objects into the inventory object when a GET of configmaps is called. -func fakeClient(objs object.ObjMetadataSet) resource.FakeClientFunc { - return func(version schema.GroupVersion) (resource.RESTClient, error) { - return &fake.RESTClient{ - NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, - Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { - if req.Method == "POST" && cmPathRegex.Match([]byte(req.URL.Path)) { - b, err := io.ReadAll(req.Body) - if err != nil { - return nil, err - } - cm := corev1.ConfigMap{} - err = runtime.DecodeInto(codec, b, &cm) - if err != nil { - return nil, err - } - bodyRC := io.NopCloser(bytes.NewReader(b)) - return &http.Response{StatusCode: http.StatusCreated, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil - } - if req.Method == "GET" && cmPathRegex.Match([]byte(req.URL.Path)) { - cmList := corev1.ConfigMapList{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "v1", - Kind: "List", - }, - Items: []corev1.ConfigMap{}, - } - var cm = corev1.ConfigMap{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "v1", - Kind: "ConfigMap", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "inventory", - Namespace: "test-namespace", - }, - Data: objs.ToStringMap(), - } - cmList.Items = append(cmList.Items, cm) - bodyRC := io.NopCloser(bytes.NewReader(toJSONBytes(&cmList))) - return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil - } - return nil, nil - }), - }, nil - } -} - -func toJSONBytes(obj runtime.Object) []byte { - objBytes, _ := runtime.Encode(unstructured.NewJSONFallbackEncoder(codec), obj) - return objBytes -} diff --git a/pkg/inventory/fake-inventory-client.go b/pkg/inventory/fake-inventory-client.go deleted file mode 100644 index 6c483d45..00000000 --- a/pkg/inventory/fake-inventory-client.go +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package inventory - -import ( - "context" - - "github.com/fluxcd/cli-utils/pkg/apis/actuation" - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/object" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - cmdutil "k8s.io/kubectl/pkg/cmd/util" -) - -// FakeClient is a testing implementation of the Client interface. -type FakeClient struct { - Objs object.ObjMetadataSet - Status []actuation.ObjectStatus - Err error -} - -var ( - _ Client = &FakeClient{} - _ ClientFactory = FakeClientFactory{} -) - -type FakeClientFactory object.ObjMetadataSet - -func (f FakeClientFactory) NewClient(cmdutil.Factory) (Client, error) { - return NewFakeClient(object.ObjMetadataSet(f)), nil -} - -// NewFakeClient returns a FakeClient. -func NewFakeClient(initObjs object.ObjMetadataSet) *FakeClient { - return &FakeClient{ - Objs: initObjs, - Err: nil, - } -} - -// GetClusterObjs returns currently stored set of objects. -func (fic *FakeClient) GetClusterObjs(Info) (object.ObjMetadataSet, error) { - if fic.Err != nil { - return object.ObjMetadataSet{}, fic.Err - } - return fic.Objs, nil -} - -// Merge stores the passed objects with the current stored cluster inventory -// objects. Returns the set difference of the current set of objects minus -// the passed set of objects, or an error if one is set up. -func (fic *FakeClient) Merge(_ Info, objs object.ObjMetadataSet, _ common.DryRunStrategy) (object.ObjMetadataSet, error) { - if fic.Err != nil { - return object.ObjMetadataSet{}, fic.Err - } - diffObjs := fic.Objs.Diff(objs) - fic.Objs = fic.Objs.Union(objs) - return diffObjs, nil -} - -// Replace the stored cluster inventory objs with the passed obj, or an -// error if one is set up. -func (fic *FakeClient) Replace(_ Info, objs object.ObjMetadataSet, status []actuation.ObjectStatus, - _ common.DryRunStrategy) error { - if fic.Err != nil { - return fic.Err - } - fic.Objs = objs - fic.Status = status - return nil -} - -// DeleteInventoryObj returns an error if one is forced; does nothing otherwise. -func (fic *FakeClient) DeleteInventoryObj(Info, common.DryRunStrategy) error { - if fic.Err != nil { - return fic.Err - } - return nil -} - -func (fic *FakeClient) ApplyInventoryNamespace(*unstructured.Unstructured, common.DryRunStrategy) error { - if fic.Err != nil { - return fic.Err - } - return nil -} - -// SetError forces an error on the subsequent client call if it returns an error. -func (fic *FakeClient) SetError(err error) { - fic.Err = err -} - -// ClearError clears the force error -func (fic *FakeClient) ClearError() { - fic.Err = nil -} - -func (fic *FakeClient) GetClusterInventoryInfo(Info) (*unstructured.Unstructured, error) { - return nil, nil -} - -func (fic *FakeClient) GetClusterInventoryObjs(_ Info) (object.UnstructuredSet, error) { - return object.UnstructuredSet{}, nil -} - -func (fic *FakeClient) ListClusterInventoryObjs(_ context.Context) (map[string]object.ObjMetadataSet, error) { - return map[string]object.ObjMetadataSet{}, nil -} diff --git a/pkg/inventory/idmatchstatus_string.go b/pkg/inventory/idmatchstatus_string.go deleted file mode 100644 index 01f69c94..00000000 --- a/pkg/inventory/idmatchstatus_string.go +++ /dev/null @@ -1,25 +0,0 @@ -// Code generated by "stringer -type=IDMatchStatus"; DO NOT EDIT. - -package inventory - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[Empty-0] - _ = x[Match-1] - _ = x[NoMatch-2] -} - -const _IDMatchStatus_name = "EmptyMatchNoMatch" - -var _IDMatchStatus_index = [...]uint8{0, 5, 10, 17} - -func (i IDMatchStatus) String() string { - if i < 0 || i >= IDMatchStatus(len(_IDMatchStatus_index)-1) { - return "IDMatchStatus(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _IDMatchStatus_name[_IDMatchStatus_index[i]:_IDMatchStatus_index[i+1]] -} diff --git a/pkg/inventory/inventory-client-factory.go b/pkg/inventory/inventory-client-factory.go deleted file mode 100644 index 97749bc5..00000000 --- a/pkg/inventory/inventory-client-factory.go +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package inventory - -import ( - cmdutil "k8s.io/kubectl/pkg/cmd/util" -) - -var ( - _ ClientFactory = ClusterClientFactory{} -) - -// ClientFactory is a factory that constructs new Client instances. -type ClientFactory interface { - NewClient(factory cmdutil.Factory) (Client, error) -} - -// ClusterClientFactory is a factory that creates instances of ClusterClient inventory client. -type ClusterClientFactory struct { - StatusPolicy StatusPolicy -} - -func (ccf ClusterClientFactory) NewClient(factory cmdutil.Factory) (Client, error) { - return NewClient(factory, WrapInventoryObj, InvInfoToConfigMap, ccf.StatusPolicy, ConfigMapGVK) -} diff --git a/pkg/inventory/inventory-client.go b/pkg/inventory/inventory-client.go deleted file mode 100644 index 979d092a..00000000 --- a/pkg/inventory/inventory-client.go +++ /dev/null @@ -1,501 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package inventory - -import ( - "context" - "fmt" - - "github.com/fluxcd/cli-utils/pkg/apis/actuation" - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/object" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/discovery" - "k8s.io/client-go/dynamic" - "k8s.io/klog/v2" - cmdutil "k8s.io/kubectl/pkg/cmd/util" - "k8s.io/kubectl/pkg/util" -) - -// Client expresses an interface for interacting with -// objects which store references to objects (inventory objects). -type Client interface { - // GetClusterObjs returns the set of previously applied objects as ObjMetadata, - // or an error if one occurred. This set of previously applied object references - // is stored in the inventory objects living in the cluster. - GetClusterObjs(inv Info) (object.ObjMetadataSet, error) - // Merge applies the union of the passed objects with the currently - // stored objects in the inventory object. Returns the set of - // objects which are not in the passed objects (objects to be pruned). - // Otherwise, returns an error if one happened. - Merge(inv Info, objs object.ObjMetadataSet, dryRun common.DryRunStrategy) (object.ObjMetadataSet, error) - // Replace replaces the set of objects stored in the inventory - // object with the passed set of objects, or an error if one occurs. - Replace(inv Info, objs object.ObjMetadataSet, status []actuation.ObjectStatus, dryRun common.DryRunStrategy) error - // DeleteInventoryObj deletes the passed inventory object from the APIServer. - DeleteInventoryObj(inv Info, dryRun common.DryRunStrategy) error - // ApplyInventoryNamespace applies the Namespace that the inventory object should be in. - ApplyInventoryNamespace(invNamespace *unstructured.Unstructured, dryRun common.DryRunStrategy) error - // GetClusterInventoryInfo returns the cluster inventory object. - GetClusterInventoryInfo(inv Info) (*unstructured.Unstructured, error) - // GetClusterInventoryObjs looks up the inventory objects from the cluster. - GetClusterInventoryObjs(inv Info) (object.UnstructuredSet, error) - // ListClusterInventoryObjs returns a map mapping from inventory name to a list of cluster inventory objects - ListClusterInventoryObjs(ctx context.Context) (map[string]object.ObjMetadataSet, error) -} - -// ClusterClient is a concrete implementation of the -// Client interface. -type ClusterClient struct { - dc dynamic.Interface - discoveryClient discovery.CachedDiscoveryInterface - mapper meta.RESTMapper - InventoryFactoryFunc StorageFactoryFunc - invToUnstructuredFunc ToUnstructuredFunc - statusPolicy StatusPolicy - gvk schema.GroupVersionKind -} - -var _ Client = &ClusterClient{} - -// NewClient returns a concrete implementation of the -// Client interface or an error. -func NewClient(factory cmdutil.Factory, - invFunc StorageFactoryFunc, - invToUnstructuredFunc ToUnstructuredFunc, - statusPolicy StatusPolicy, - gvk schema.GroupVersionKind, -) (*ClusterClient, error) { - dc, err := factory.DynamicClient() - if err != nil { - return nil, err - } - mapper, err := factory.ToRESTMapper() - if err != nil { - return nil, err - } - discoveryClinet, err := factory.ToDiscoveryClient() - if err != nil { - return nil, err - } - clusterClient := ClusterClient{ - dc: dc, - discoveryClient: discoveryClinet, - mapper: mapper, - InventoryFactoryFunc: invFunc, - invToUnstructuredFunc: invToUnstructuredFunc, - statusPolicy: statusPolicy, - gvk: gvk, - } - return &clusterClient, nil -} - -// Merge stores the union of the passed objects with the objects currently -// stored in the cluster inventory object. Retrieves and caches the cluster -// inventory object. Returns the set differrence of the cluster inventory -// objects and the currently applied objects. This is the set of objects -// to prune. Creates the initial cluster inventory object storing the passed -// objects if an inventory object does not exist. Returns an error if one -// occurred. -func (cic *ClusterClient) Merge(localInv Info, objs object.ObjMetadataSet, dryRun common.DryRunStrategy) (object.ObjMetadataSet, error) { - pruneIDs := object.ObjMetadataSet{} - invObj := cic.invToUnstructuredFunc(localInv) - clusterInv, err := cic.GetClusterInventoryInfo(localInv) - if err != nil { - return pruneIDs, err - } - - // Inventory does not exist on the cluster. - if clusterInv == nil { - // Wrap inventory object and store the inventory in it. - var status []actuation.ObjectStatus - if cic.statusPolicy == StatusPolicyAll { - status = getObjStatus(nil, objs) - } - inv := cic.InventoryFactoryFunc(invObj) - if err := inv.Store(objs, status); err != nil { - return nil, err - } - klog.V(4).Infof("creating initial inventory object with %d objects", len(objs)) - - if dryRun.ClientOrServerDryRun() { - klog.V(4).Infof("dry-run create inventory object: not created") - return nil, nil - } - - err = inv.Apply(cic.dc, cic.mapper, cic.statusPolicy) - return nil, err - } - - // Update existing cluster inventory with merged union of objects - clusterObjs, err := cic.GetClusterObjs(localInv) - if err != nil { - return pruneIDs, err - } - pruneIDs = clusterObjs.Diff(objs) - unionObjs := clusterObjs.Union(objs) - var status []actuation.ObjectStatus - if cic.statusPolicy == StatusPolicyAll { - status = getObjStatus(pruneIDs, unionObjs) - } - klog.V(4).Infof("num objects to prune: %d", len(pruneIDs)) - klog.V(4).Infof("num merged objects to store in inventory: %d", len(unionObjs)) - wrappedInv := cic.InventoryFactoryFunc(clusterInv) - if err = wrappedInv.Store(unionObjs, status); err != nil { - return pruneIDs, err - } - - // Update not required when all objects in inventory are the same and - // status does not need to be updated. If status is stored, always update the - // inventory to store the latest status. - if objs.Equal(clusterObjs) && cic.statusPolicy == StatusPolicyNone { - return pruneIDs, nil - } - - if dryRun.ClientOrServerDryRun() { - klog.V(4).Infof("dry-run create inventory object: not created") - return pruneIDs, nil - } - err = wrappedInv.Apply(cic.dc, cic.mapper, cic.statusPolicy) - return pruneIDs, err -} - -// Replace stores the passed objects in the cluster inventory object, or -// an error if one occurred. -func (cic *ClusterClient) Replace(localInv Info, objs object.ObjMetadataSet, status []actuation.ObjectStatus, - dryRun common.DryRunStrategy) error { - // Skip entire function for dry-run. - if dryRun.ClientOrServerDryRun() { - klog.V(4).Infoln("dry-run replace inventory object: not applied") - return nil - } - clusterInv, err := cic.GetClusterInventoryInfo(localInv) - if err != nil { - return fmt.Errorf("failed to read inventory from cluster: %w", err) - } - - clusterObjs, err := cic.GetClusterObjs(localInv) - if err != nil { - return fmt.Errorf("failed to read inventory objects from cluster: %w", err) - } - - clusterInv, wrappedInv, err := cic.replaceInventory(clusterInv, objs, status) - if err != nil { - return err - } - - // Update not required when all objects in inventory are the same and - // status does not need to be updated. If status is stored, always update the - // inventory to store the latest status. - if objs.Equal(clusterObjs) && cic.statusPolicy == StatusPolicyNone { - return nil - } - - klog.V(4).Infof("replace cluster inventory: %s/%s", clusterInv.GetNamespace(), clusterInv.GetName()) - klog.V(4).Infof("replace cluster inventory %d objects", len(objs)) - - if err := wrappedInv.ApplyWithPrune(cic.dc, cic.mapper, cic.statusPolicy, objs); err != nil { - return fmt.Errorf("failed to write updated inventory to cluster: %w", err) - } - - return nil -} - -// replaceInventory stores the passed objects into the passed inventory object. -func (cic *ClusterClient) replaceInventory(inv *unstructured.Unstructured, objs object.ObjMetadataSet, - status []actuation.ObjectStatus) (*unstructured.Unstructured, Storage, error) { - if cic.statusPolicy == StatusPolicyNone { - status = nil - } - wrappedInv := cic.InventoryFactoryFunc(inv) - if err := wrappedInv.Store(objs, status); err != nil { - return nil, nil, err - } - clusterInv, err := wrappedInv.GetObject() - if err != nil { - return nil, nil, err - } - - return clusterInv, wrappedInv, nil -} - -// DeleteInventoryObj deletes the inventory object from the cluster. -func (cic *ClusterClient) DeleteInventoryObj(localInv Info, dryRun common.DryRunStrategy) error { - if localInv == nil { - return fmt.Errorf("retrieving cluster inventory object with nil local inventory") - } - switch localInv.Strategy() { - case NameStrategy: - return cic.deleteInventoryObjByName(cic.invToUnstructuredFunc(localInv), dryRun) - case LabelStrategy: - return cic.deleteInventoryObjsByLabel(localInv, dryRun) - default: - panic(fmt.Errorf("unknown inventory strategy: %s", localInv.Strategy())) - } -} - -func (cic *ClusterClient) deleteInventoryObjsByLabel(inv Info, dryRun common.DryRunStrategy) error { - clusterInvObjs, err := cic.getClusterInventoryObjsByLabel(inv) - if err != nil { - return err - } - for _, invObj := range clusterInvObjs { - if err := cic.deleteInventoryObjByName(invObj, dryRun); err != nil { - return err - } - } - return nil -} - -// GetClusterObjs returns the objects stored in the cluster inventory object, or -// an error if one occurred. -func (cic *ClusterClient) GetClusterObjs(localInv Info) (object.ObjMetadataSet, error) { - var objs object.ObjMetadataSet - clusterInv, err := cic.GetClusterInventoryInfo(localInv) - if err != nil { - return objs, fmt.Errorf("failed to read inventory from cluster: %w", err) - } - // First time; no inventory obj yet. - if clusterInv == nil { - return objs, nil - } - wrapped := cic.InventoryFactoryFunc(clusterInv) - return wrapped.Load() -} - -// getClusterInventoryObj returns a pointer to the cluster inventory object, or -// an error if one occurred. Returns the cached cluster inventory object if it -// has been previously retrieved. Uses the ResourceBuilder to retrieve the -// inventory object in the cluster, using the namespace, group resource, and -// inventory label. Merges multiple inventory objects into one if it retrieves -// more than one (this should be very rare). -// -// TODO(seans3): Remove the special case code to merge multiple cluster inventory -// objects once we've determined that this case is no longer possible. -func (cic *ClusterClient) GetClusterInventoryInfo(inv Info) (*unstructured.Unstructured, error) { - clusterInvObjects, err := cic.GetClusterInventoryObjs(inv) - if err != nil { - return nil, fmt.Errorf("failed to read inventory objects from cluster: %w", err) - } - - var clusterInv *unstructured.Unstructured - if len(clusterInvObjects) == 1 { - clusterInv = clusterInvObjects[0] - } else if l := len(clusterInvObjects); l > 1 { - return nil, fmt.Errorf("found %d inventory objects with inventory id %s", l, inv.ID()) - } - return clusterInv, nil -} - -func (cic *ClusterClient) getClusterInventoryObjsByLabel(inv Info) (object.UnstructuredSet, error) { - localInv := cic.invToUnstructuredFunc(inv) - if localInv == nil { - return nil, fmt.Errorf("retrieving cluster inventory object with nil local inventory") - } - localObj := object.UnstructuredToObjMetadata(localInv) - mapping, err := cic.getMapping(localInv) - if err != nil { - return nil, err - } - groupResource := mapping.Resource.GroupResource().String() - namespace := localObj.Namespace - label, err := retrieveInventoryLabel(localInv) - if err != nil { - return nil, err - } - labelSelector := fmt.Sprintf("%s=%s", common.InventoryLabel, label) - klog.V(4).Infof("inventory object fetch by label (group: %q, namespace: %q, selector: %q)", groupResource, namespace, labelSelector) - - uList, err := cic.dc.Resource(mapping.Resource).Namespace(namespace).List(context.TODO(), metav1.ListOptions{ - LabelSelector: labelSelector, - }) - if err != nil { - return nil, err - } - var invList []*unstructured.Unstructured - for i := range uList.Items { - invList = append(invList, &uList.Items[i]) - } - return invList, nil -} - -func (cic *ClusterClient) getClusterInventoryObjsByName(inv Info) (object.UnstructuredSet, error) { - localInv := cic.invToUnstructuredFunc(inv) - if localInv == nil { - return nil, fmt.Errorf("retrieving cluster inventory object with nil local inventory") - } - - mapping, err := cic.getMapping(localInv) - if err != nil { - return nil, err - } - - klog.V(4).Infof("inventory object fetch by name (namespace: %q, name: %q)", inv.Namespace(), inv.Name()) - clusterInv, err := cic.dc.Resource(mapping.Resource).Namespace(inv.Namespace()). - Get(context.TODO(), inv.Name(), metav1.GetOptions{}) - if err != nil && !apierrors.IsNotFound(err) { - return nil, err - } - if apierrors.IsNotFound(err) { - return object.UnstructuredSet{}, nil - } - return object.UnstructuredSet{clusterInv}, nil -} - -func (cic *ClusterClient) GetClusterInventoryObjs(inv Info) (object.UnstructuredSet, error) { - if inv == nil { - return nil, fmt.Errorf("inventoryInfo must be specified") - } - - var clusterInvObjects object.UnstructuredSet - var err error - switch inv.Strategy() { - case NameStrategy: - clusterInvObjects, err = cic.getClusterInventoryObjsByName(inv) - case LabelStrategy: - clusterInvObjects, err = cic.getClusterInventoryObjsByLabel(inv) - default: - panic(fmt.Errorf("unknown inventory strategy: %s", inv.Strategy())) - } - return clusterInvObjects, err -} - -func (cic *ClusterClient) ListClusterInventoryObjs(ctx context.Context) (map[string]object.ObjMetadataSet, error) { - // Define the mapping - mapping, err := cic.mapper.RESTMapping(cic.gvk.GroupKind(), cic.gvk.Version) - if err != nil { - return nil, err - } - - // retrieve the list from the cluster - clusterInvs, err := cic.dc.Resource(mapping.Resource).List(ctx, metav1.ListOptions{}) - if err != nil && !apierrors.IsNotFound(err) { - return nil, err - } - if apierrors.IsNotFound(err) { - return map[string]object.ObjMetadataSet{}, nil - } - - identifiers := make(map[string]object.ObjMetadataSet) - - for i, inv := range clusterInvs.Items { - invName := inv.GetName() - identifiers[invName] = object.ObjMetadataSet{} - wrappedInvObjSlice, err := cic.InventoryFactoryFunc(&clusterInvs.Items[i]).Load() - if err != nil { - return nil, err - } - identifiers[invName] = append(identifiers[invName], wrappedInvObjSlice...) - } - - return identifiers, nil -} - -// createInventoryObj creates the passed inventory object on the APIServer. -func (cic *ClusterClient) createInventoryObj(obj *unstructured.Unstructured, dryRun common.DryRunStrategy) (*unstructured.Unstructured, error) { - if dryRun.ClientOrServerDryRun() { - klog.V(4).Infof("dry-run create inventory object: not created") - return obj.DeepCopy(), nil - } - if obj == nil { - return nil, fmt.Errorf("attempting create a nil inventory object") - } - // Default inventory name gets random suffix. Fixes problem where legacy - // inventory templates within same namespace will collide on name. - err := fixLegacyInventoryName(obj) - if err != nil { - return nil, err - } - - mapping, err := cic.getMapping(obj) - if err != nil { - return nil, err - } - - klog.V(4).Infof("creating inventory object: %s/%s", obj.GetNamespace(), obj.GetName()) - return cic.dc.Resource(mapping.Resource).Namespace(obj.GetNamespace()). - Create(context.TODO(), obj, metav1.CreateOptions{}) -} - -// deleteInventoryObjByName deletes the passed inventory object from the APIServer, or -// an error if one occurs. -func (cic *ClusterClient) deleteInventoryObjByName(obj *unstructured.Unstructured, dryRun common.DryRunStrategy) error { - if dryRun.ClientOrServerDryRun() { - klog.V(4).Infof("dry-run delete inventory object: not deleted") - return nil - } - if obj == nil { - return fmt.Errorf("attempting delete a nil inventory object") - } - - mapping, err := cic.getMapping(obj) - if err != nil { - return err - } - - klog.V(4).Infof("deleting inventory object: %s/%s", obj.GetNamespace(), obj.GetName()) - return cic.dc.Resource(mapping.Resource).Namespace(obj.GetNamespace()). - Delete(context.TODO(), obj.GetName(), metav1.DeleteOptions{}) -} - -// ApplyInventoryNamespace creates the passed namespace if it does not already -// exist, or returns an error if one happened. NOTE: No error if already exists. -func (cic *ClusterClient) ApplyInventoryNamespace(obj *unstructured.Unstructured, dryRun common.DryRunStrategy) error { - if dryRun.ClientOrServerDryRun() { - klog.V(4).Infof("dry-run apply inventory namespace (%s): not applied", obj.GetName()) - return nil - } - - invNamespace := obj.DeepCopy() - klog.V(4).Infof("applying inventory namespace: %s", obj.GetName()) - object.StripKyamlAnnotations(invNamespace) - if err := util.CreateApplyAnnotation(invNamespace, unstructured.UnstructuredJSONScheme); err != nil { - return err - } - - mapping, err := cic.getMapping(obj) - if err != nil { - return err - } - - _, err = cic.dc.Resource(mapping.Resource).Create(context.TODO(), invNamespace, metav1.CreateOptions{}) - if apierrors.IsAlreadyExists(err) { - return nil - } - return err -} - -// getMapping returns the RESTMapping for the provided resource. -func (cic *ClusterClient) getMapping(obj *unstructured.Unstructured) (*meta.RESTMapping, error) { - return cic.mapper.RESTMapping(obj.GroupVersionKind().GroupKind(), obj.GroupVersionKind().Version) -} - -// getObjStatus returns the list of object status -// at the beginning of an apply process. -func getObjStatus(pruneIDs, unionIDs []object.ObjMetadata) []actuation.ObjectStatus { - status := []actuation.ObjectStatus{} - for _, obj := range unionIDs { - status = append(status, - actuation.ObjectStatus{ - ObjectReference: ObjectReferenceFromObjMetadata(obj), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }) - } - for _, obj := range pruneIDs { - status = append(status, - actuation.ObjectStatus{ - ObjectReference: ObjectReferenceFromObjMetadata(obj), - Strategy: actuation.ActuationStrategyDelete, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }) - } - return status -} diff --git a/pkg/inventory/inventory-client_test.go b/pkg/inventory/inventory-client_test.go deleted file mode 100644 index d3b53f15..00000000 --- a/pkg/inventory/inventory-client_test.go +++ /dev/null @@ -1,598 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package inventory - -import ( - "fmt" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/cli-runtime/pkg/resource" - clienttesting "k8s.io/client-go/testing" - cmdtesting "k8s.io/kubectl/pkg/cmd/testing" - - "github.com/fluxcd/cli-utils/pkg/apis/actuation" - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/object" -) - -func podStatus(info *resource.Info) actuation.ObjectStatus { - return actuation.ObjectStatus{ - ObjectReference: ObjectReferenceFromObjMetadata(ignoreErrInfoToObjMeta(info)), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationSucceeded, - Reconcile: actuation.ReconcileSucceeded, - } -} - -func podData(name string) map[string]string { - return map[string]string{ - fmt.Sprintf("test-inventory-namespace_%s__Pod", name): "{\"actuation\":\"Succeeded\",\"reconcile\":\"Succeeded\",\"strategy\":\"Apply\"}", - } -} - -func podDataNoStatus(name string) map[string]string { - return map[string]string{ - fmt.Sprintf("test-inventory-namespace_%s__Pod", name): "", - } -} - -func TestGetClusterInventoryInfo(t *testing.T) { - tests := map[string]struct { - statusPolicy StatusPolicy - inv Info - localObjs object.ObjMetadataSet - objStatus []actuation.ObjectStatus - isError bool - }{ - "Nil local inventory object is an error": { - inv: nil, - localObjs: object.ObjMetadataSet{}, - isError: true, - }, - "Empty local inventory object": { - inv: localInv, - localObjs: object.ObjMetadataSet{}, - isError: false, - }, - "Local inventory with a single object": { - inv: localInv, - localObjs: object.ObjMetadataSet{ - ignoreErrInfoToObjMeta(pod2Info), - }, - objStatus: []actuation.ObjectStatus{podStatus(pod2Info)}, - isError: false, - }, - "Local inventory with multiple objects": { - inv: localInv, - localObjs: object.ObjMetadataSet{ - ignoreErrInfoToObjMeta(pod1Info), - ignoreErrInfoToObjMeta(pod2Info), - ignoreErrInfoToObjMeta(pod3Info)}, - objStatus: []actuation.ObjectStatus{ - podStatus(pod1Info), - podStatus(pod2Info), - podStatus(pod3Info), - }, - isError: false, - }, - } - - tf := cmdtesting.NewTestFactory().WithNamespace(testNamespace) - defer tf.Cleanup() - - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - invClient, err := NewClient(tf, - WrapInventoryObj, InvInfoToConfigMap, tc.statusPolicy, ConfigMapGVK) - require.NoError(t, err) - - var inv *unstructured.Unstructured - if tc.inv != nil { - inv = storeObjsInInventory(tc.inv, tc.localObjs, tc.objStatus) - } - clusterInv, err := invClient.GetClusterInventoryInfo(WrapInventoryInfoObj(inv)) - if tc.isError { - if err == nil { - t.Fatalf("expected error but received none") - } - return - } - if !tc.isError && err != nil { - t.Fatalf("unexpected error received: %s", err) - } - if clusterInv != nil { - wrapped := WrapInventoryObj(clusterInv) - clusterObjs, err := wrapped.Load() - if err != nil { - t.Fatalf("unexpected error received: %s", err) - } - if !tc.localObjs.Equal(clusterObjs) { - t.Fatalf("expected cluster objs (%v), got (%v)", tc.localObjs, clusterObjs) - } - } - }) - } -} - -func TestMerge(t *testing.T) { - tests := map[string]struct { - statusPolicy StatusPolicy - localInv Info - localObjs object.ObjMetadataSet - clusterObjs object.ObjMetadataSet - pruneObjs object.ObjMetadataSet - isError bool - }{ - "Nil local inventory object is error": { - localInv: nil, - localObjs: object.ObjMetadataSet{}, - clusterObjs: object.ObjMetadataSet{}, - pruneObjs: object.ObjMetadataSet{}, - isError: true, - statusPolicy: StatusPolicyAll, - }, - "Cluster and local inventories empty: no prune objects; no change": { - localInv: copyInventory(), - localObjs: object.ObjMetadataSet{}, - clusterObjs: object.ObjMetadataSet{}, - pruneObjs: object.ObjMetadataSet{}, - isError: false, - statusPolicy: StatusPolicyAll, - }, - "Cluster and local inventories same: no prune objects; no change": { - localInv: copyInventory(), - localObjs: object.ObjMetadataSet{ - ignoreErrInfoToObjMeta(pod1Info), - }, - clusterObjs: object.ObjMetadataSet{ - ignoreErrInfoToObjMeta(pod1Info), - }, - pruneObjs: object.ObjMetadataSet{}, - isError: false, - statusPolicy: StatusPolicyAll, - }, - "Cluster two obj, local one: prune obj": { - localInv: copyInventory(), - localObjs: object.ObjMetadataSet{ - ignoreErrInfoToObjMeta(pod1Info), - }, - clusterObjs: object.ObjMetadataSet{ - ignoreErrInfoToObjMeta(pod1Info), - ignoreErrInfoToObjMeta(pod3Info), - }, - pruneObjs: object.ObjMetadataSet{ - ignoreErrInfoToObjMeta(pod3Info), - }, - statusPolicy: StatusPolicyAll, - isError: false, - }, - "Cluster multiple objs, local multiple different objs: prune objs": { - localInv: copyInventory(), - localObjs: object.ObjMetadataSet{ - ignoreErrInfoToObjMeta(pod2Info), - }, - clusterObjs: object.ObjMetadataSet{ - ignoreErrInfoToObjMeta(pod1Info), - ignoreErrInfoToObjMeta(pod2Info), - ignoreErrInfoToObjMeta(pod3Info)}, - pruneObjs: object.ObjMetadataSet{ - ignoreErrInfoToObjMeta(pod1Info), - ignoreErrInfoToObjMeta(pod3Info), - }, - statusPolicy: StatusPolicyAll, - isError: false, - }, - } - - for name, tc := range tests { - for i := range common.Strategies { - drs := common.Strategies[i] - t.Run(name, func(t *testing.T) { - tf := cmdtesting.NewTestFactory().WithNamespace(testNamespace) - defer tf.Cleanup() - - tf.FakeDynamicClient.PrependReactor("list", "configmaps", toReactionFunc(tc.clusterObjs)) - // Create the local inventory object storing "tc.localObjs" - invClient, err := NewClient(tf, - WrapInventoryObj, InvInfoToConfigMap, tc.statusPolicy, ConfigMapGVK) - require.NoError(t, err) - - // Call "Merge" to create the union of clusterObjs and localObjs. - pruneObjs, err := invClient.Merge(tc.localInv, tc.localObjs, drs) - if tc.isError { - if err == nil { - t.Fatalf("expected error but received none") - } - return - } - if !tc.isError && err != nil { - t.Fatalf("unexpected error: %s", err) - } - if !tc.pruneObjs.Equal(pruneObjs) { - t.Errorf("expected (%v) prune objs; got (%v)", tc.pruneObjs, pruneObjs) - } - }) - } - } -} - -func TestCreateInventory(t *testing.T) { - tests := map[string]struct { - statusPolicy StatusPolicy - inv Info - localObjs object.ObjMetadataSet - error string - objStatus []actuation.ObjectStatus - }{ - "Nil local inventory object is an error": { - inv: nil, - localObjs: object.ObjMetadataSet{}, - error: "attempting create a nil inventory object", - }, - "Empty local inventory object": { - inv: localInv, - localObjs: object.ObjMetadataSet{}, - }, - "Local inventory with a single object": { - inv: localInv, - localObjs: object.ObjMetadataSet{ - ignoreErrInfoToObjMeta(pod2Info), - }, - objStatus: []actuation.ObjectStatus{podStatus(pod2Info)}, - }, - "Local inventory with multiple objects": { - inv: localInv, - localObjs: object.ObjMetadataSet{ - ignoreErrInfoToObjMeta(pod1Info), - ignoreErrInfoToObjMeta(pod2Info), - ignoreErrInfoToObjMeta(pod3Info)}, - objStatus: []actuation.ObjectStatus{ - podStatus(pod1Info), - podStatus(pod2Info), - podStatus(pod3Info), - }, - }, - } - - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - tf := cmdtesting.NewTestFactory().WithNamespace(testNamespace) - defer tf.Cleanup() - - var storedInventory map[string]string - tf.FakeDynamicClient.PrependReactor("create", "configmaps", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { - obj := *action.(clienttesting.CreateAction).GetObject().(*unstructured.Unstructured) - storedInventory, _, _ = unstructured.NestedStringMap(obj.Object, "data") - return true, nil, nil - }) - - invClient, err := NewClient(tf, - WrapInventoryObj, InvInfoToConfigMap, tc.statusPolicy, ConfigMapGVK) - require.NoError(t, err) - inv := invClient.invToUnstructuredFunc(tc.inv) - if inv != nil { - inv = storeObjsInInventory(tc.inv, tc.localObjs, tc.objStatus) - } - _, err = invClient.createInventoryObj(inv, common.DryRunNone) - if tc.error != "" { - assert.EqualError(t, err, tc.error) - } else { - assert.NoError(t, err) - } - - expectedInventory := tc.localObjs.ToStringMap() - // handle empty inventories special to avoid problems with empty vs nil maps - if len(expectedInventory) != 0 || len(storedInventory) != 0 { - for key := range expectedInventory { - if _, found := storedInventory[key]; !found { - t.Errorf("%s not found in the stored inventory", key) - } - } - } - }) - } -} - -func TestReplace(t *testing.T) { - tests := map[string]struct { - statusPolicy StatusPolicy - localObjs object.ObjMetadataSet - clusterObjs object.ObjMetadataSet - objStatus []actuation.ObjectStatus - data map[string]string - }{ - "Cluster and local inventories empty": { - localObjs: object.ObjMetadataSet{}, - clusterObjs: object.ObjMetadataSet{}, - data: map[string]string{}, - }, - "Cluster and local inventories same": { - localObjs: object.ObjMetadataSet{ - ignoreErrInfoToObjMeta(pod1Info), - }, - clusterObjs: object.ObjMetadataSet{ - ignoreErrInfoToObjMeta(pod1Info), - }, - objStatus: []actuation.ObjectStatus{podStatus(pod1Info)}, - data: podData("pod-1"), - statusPolicy: StatusPolicyAll, - }, - "Cluster two obj, local one": { - localObjs: object.ObjMetadataSet{ - ignoreErrInfoToObjMeta(pod1Info), - }, - clusterObjs: object.ObjMetadataSet{ - ignoreErrInfoToObjMeta(pod1Info), - ignoreErrInfoToObjMeta(pod3Info), - }, - objStatus: []actuation.ObjectStatus{podStatus(pod1Info), podStatus(pod3Info)}, - data: podData("pod-1"), - statusPolicy: StatusPolicyAll, - }, - "Cluster multiple objs, local multiple different objs": { - localObjs: object.ObjMetadataSet{ - ignoreErrInfoToObjMeta(pod2Info), - }, - clusterObjs: object.ObjMetadataSet{ - ignoreErrInfoToObjMeta(pod1Info), - ignoreErrInfoToObjMeta(pod2Info), - ignoreErrInfoToObjMeta(pod3Info)}, - objStatus: []actuation.ObjectStatus{podStatus(pod2Info), podStatus(pod1Info), podStatus(pod3Info)}, - data: podData("pod-2"), - statusPolicy: StatusPolicyAll, - }, - "Cluster multiple objs, local multiple different objs with StatusPolicyNone": { - localObjs: object.ObjMetadataSet{ - ignoreErrInfoToObjMeta(pod2Info), - }, - clusterObjs: object.ObjMetadataSet{ - ignoreErrInfoToObjMeta(pod1Info), - ignoreErrInfoToObjMeta(pod2Info), - ignoreErrInfoToObjMeta(pod3Info)}, - objStatus: []actuation.ObjectStatus{podStatus(pod2Info), podStatus(pod1Info), podStatus(pod3Info)}, - data: podDataNoStatus("pod-2"), - statusPolicy: StatusPolicyNone, - }, - } - - tf := cmdtesting.NewTestFactory().WithNamespace(testNamespace) - defer tf.Cleanup() - - // Client and server dry-run do not throw errors. - invClient, err := NewClient(tf, - WrapInventoryObj, InvInfoToConfigMap, StatusPolicyAll, ConfigMapGVK) - require.NoError(t, err) - err = invClient.Replace(copyInventory(), object.ObjMetadataSet{}, nil, common.DryRunClient) - if err != nil { - t.Fatalf("unexpected error received: %s", err) - } - err = invClient.Replace(copyInventory(), object.ObjMetadataSet{}, nil, common.DryRunServer) - if err != nil { - t.Fatalf("unexpected error received: %s", err) - } - - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - // Create inventory client, and store the cluster objs in the inventory object. - invClient, err := NewClient(tf, - WrapInventoryObj, InvInfoToConfigMap, tc.statusPolicy, ConfigMapGVK) - require.NoError(t, err) - wrappedInv := invClient.InventoryFactoryFunc(inventoryObj) - if err := wrappedInv.Store(tc.clusterObjs, tc.objStatus); err != nil { - t.Fatalf("unexpected error storing inventory objects: %s", err) - } - inv, err := wrappedInv.GetObject() - if err != nil { - t.Fatalf("unexpected error storing inventory objects: %s", err) - } - // Call replaceInventory with the new set of "localObjs" - inv, _, err = invClient.replaceInventory(inv, tc.localObjs, tc.objStatus) - if err != nil { - t.Fatalf("unexpected error received: %s", err) - } - wrappedInv = invClient.InventoryFactoryFunc(inv) - // Validate that the stored objects are now the "localObjs". - actualObjs, err := wrappedInv.Load() - if err != nil { - t.Fatalf("unexpected error received: %s", err) - } - if !tc.localObjs.Equal(actualObjs) { - t.Errorf("expected objects (%s), got (%s)", tc.localObjs, actualObjs) - } - data, _, err := unstructured.NestedStringMap(inv.Object, "data") - if err != nil { - t.Fatalf("unexpected error received: %s", err) - } - if diff := cmp.Diff(data, tc.data); diff != "" { - t.Fatal(diff) - } - }) - } -} - -func TestGetClusterObjs(t *testing.T) { - tests := map[string]struct { - statusPolicy StatusPolicy - localInv Info - clusterObjs object.ObjMetadataSet - isError bool - }{ - "Nil cluster inventory is error": { - localInv: nil, - clusterObjs: object.ObjMetadataSet{}, - isError: true, - }, - "No cluster objs": { - localInv: copyInventory(), - clusterObjs: object.ObjMetadataSet{}, - isError: false, - }, - "Single cluster obj": { - localInv: copyInventory(), - clusterObjs: object.ObjMetadataSet{ignoreErrInfoToObjMeta(pod1Info)}, - isError: false, - }, - "Multiple cluster objs": { - localInv: copyInventory(), - clusterObjs: object.ObjMetadataSet{ignoreErrInfoToObjMeta(pod1Info), ignoreErrInfoToObjMeta(pod3Info)}, - isError: false, - }, - } - - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - tf := cmdtesting.NewTestFactory().WithNamespace(testNamespace) - defer tf.Cleanup() - tf.FakeDynamicClient.PrependReactor("list", "configmaps", toReactionFunc(tc.clusterObjs)) - - invClient, err := NewClient(tf, - WrapInventoryObj, InvInfoToConfigMap, tc.statusPolicy, ConfigMapGVK) - require.NoError(t, err) - clusterObjs, err := invClient.GetClusterObjs(tc.localInv) - if tc.isError { - if err == nil { - t.Fatalf("expected error but received none") - } - return - } - if !tc.isError && err != nil { - t.Fatalf("unexpected error received: %s", err) - } - if !tc.clusterObjs.Equal(clusterObjs) { - t.Errorf("expected (%v) cluster inventory objs; got (%v)", tc.clusterObjs, clusterObjs) - } - }) - } -} - -func TestDeleteInventoryObj(t *testing.T) { - tests := map[string]struct { - statusPolicy StatusPolicy - inv Info - localObjs object.ObjMetadataSet - objStatus []actuation.ObjectStatus - }{ - "Nil local inventory object is an error": { - inv: nil, - localObjs: object.ObjMetadataSet{}, - }, - "Empty local inventory object": { - inv: localInv, - localObjs: object.ObjMetadataSet{}, - }, - "Local inventory with a single object": { - inv: localInv, - localObjs: object.ObjMetadataSet{ - ignoreErrInfoToObjMeta(pod2Info), - }, - objStatus: []actuation.ObjectStatus{podStatus(pod2Info)}, - }, - "Local inventory with multiple objects": { - inv: localInv, - localObjs: object.ObjMetadataSet{ - ignoreErrInfoToObjMeta(pod1Info), - ignoreErrInfoToObjMeta(pod2Info), - ignoreErrInfoToObjMeta(pod3Info)}, - objStatus: []actuation.ObjectStatus{ - podStatus(pod1Info), - podStatus(pod2Info), - podStatus(pod3Info), - }, - }, - } - - for name, tc := range tests { - for i := range common.Strategies { - drs := common.Strategies[i] - t.Run(name, func(t *testing.T) { - tf := cmdtesting.NewTestFactory().WithNamespace(testNamespace) - defer tf.Cleanup() - - invClient, err := NewClient(tf, - WrapInventoryObj, InvInfoToConfigMap, tc.statusPolicy, ConfigMapGVK) - require.NoError(t, err) - inv := invClient.invToUnstructuredFunc(tc.inv) - if inv != nil { - inv = storeObjsInInventory(tc.inv, tc.localObjs, tc.objStatus) - } - err = invClient.deleteInventoryObjByName(inv, drs) - if err != nil { - t.Fatalf("unexpected error received: %s", err) - } - }) - } - } -} - -func TestApplyInventoryNamespace(t *testing.T) { - testCases := map[string]struct { - statusPolicy StatusPolicy - namespace *unstructured.Unstructured - dryRunStrategy common.DryRunStrategy - reactorError error - }{ - "inventory namespace doesn't exist": { - namespace: inventoryNamespace, - dryRunStrategy: common.DryRunNone, - reactorError: nil, - }, - "inventory namespace already exist": { - namespace: inventoryNamespace, - dryRunStrategy: common.DryRunNone, - reactorError: errors.NewAlreadyExists(schema.GroupResource{ - Group: "", - Resource: "namespaces", - }, testNamespace), - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - tf := cmdtesting.NewTestFactory().WithNamespace(testNamespace) - defer tf.Cleanup() - - tf.FakeDynamicClient.PrependReactor("create", "namespaces", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { - return true, nil, tc.reactorError - }) - - invClient, err := NewClient(tf, - WrapInventoryObj, InvInfoToConfigMap, tc.statusPolicy, ConfigMapGVK) - require.NoError(t, err) - err = invClient.ApplyInventoryNamespace(tc.namespace, tc.dryRunStrategy) - assert.NoError(t, err) - }) - } -} - -func ignoreErrInfoToObjMeta(info *resource.Info) object.ObjMetadata { - objMeta, _ := object.InfoToObjMeta(info) - return objMeta -} - -func toReactionFunc(objs object.ObjMetadataSet) clienttesting.ReactionFunc { - return func(action clienttesting.Action) (bool, runtime.Object, error) { - u := copyInventoryInfo() - err := unstructured.SetNestedStringMap(u.Object, objs.ToStringMap(), "data") - if err != nil { - return true, nil, err - } - list := &unstructured.UnstructuredList{} - list.Items = []unstructured.Unstructured{*u} - return true, list, err - } -} - -func storeObjsInInventory(info Info, objs object.ObjMetadataSet, status []actuation.ObjectStatus) *unstructured.Unstructured { - wrapped := WrapInventoryObj(InvInfoToConfigMap(info)) - _ = wrapped.Store(objs, status) - inv, _ := wrapped.GetObject() - return inv -} diff --git a/pkg/inventory/inventory-info.go b/pkg/inventory/inventory-info.go deleted file mode 100644 index 085becc3..00000000 --- a/pkg/inventory/inventory-info.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package inventory - -type Strategy string - -const ( - NameStrategy Strategy = "name" - LabelStrategy Strategy = "label" -) - -// Info provides the minimal information for the applier -// to create, look up and update an inventory. -// The inventory object can be any type, the Provider in the applier -// needs to know how to create, look up and update it based -// on the Info. -type Info interface { - // Namespace of the inventory object. - // It should be the value of the field .metadata.namespace. - Namespace() string - - // Name of the inventory object. - // It should be the value of the field .metadata.name. - Name() string - - // ID of the inventory object. It is optional. - // The Provider contained in the applier should know - // if the Id is necessary and how to use it for pruning objects. - ID() string - - Strategy() Strategy -} diff --git a/pkg/inventory/inventory_error.go b/pkg/inventory/inventory_error.go deleted file mode 100644 index cceed08b..00000000 --- a/pkg/inventory/inventory_error.go +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright 2019 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 -// -// Errors when applying inventory object templates. - -package inventory - -import ( - "fmt" - - "github.com/fluxcd/cli-utils/pkg/apis/actuation" - "github.com/fluxcd/cli-utils/pkg/object" -) - -const noInventoryErrorStr = `Package uninitialized. Please run "init" command. - -The package needs to be initialized to generate the template -which will store state for resource sets. This state is -necessary to perform functionality such as deleting an entire -package or automatically deleting omitted resources (pruning). -` - -const multipleInventoryErrorStr = `Package has multiple inventory object templates. - -The package should have one and only one inventory object template. -` - -type NoInventoryObjError struct{} - -func (e *NoInventoryObjError) Error() string { - return noInventoryErrorStr -} - -// Is returns true if the specified error is equal to this error. -// Use errors.Is(error) to recursively check if an error wraps this error. -func (e *NoInventoryObjError) Is(err error) bool { - if err == nil { - return false - } - _, ok := err.(*NoInventoryObjError) - return ok -} - -type MultipleInventoryObjError struct { - InventoryObjectTemplates object.UnstructuredSet -} - -func (e *MultipleInventoryObjError) Error() string { - return multipleInventoryErrorStr -} - -// Is returns true if the specified error is equal to this error. -// Use errors.Is(error) to recursively check if an error wraps this error. -func (e *MultipleInventoryObjError) Is(err error) bool { - if err == nil { - return false - } - tErr, ok := err.(*MultipleInventoryObjError) - if !ok { - return false - } - return e.InventoryObjectTemplates.Equal(tErr.InventoryObjectTemplates) -} - -type PolicyPreventedActuationError struct { - Strategy actuation.ActuationStrategy - Policy Policy - Status IDMatchStatus -} - -func (e *PolicyPreventedActuationError) Error() string { - return fmt.Sprintf("inventory policy prevented actuation (strategy: %s, status: %s, policy: %s)", - e.Strategy, e.Status, e.Policy) -} - -// Is returns true if the specified error is equal to this error. -// Use errors.Is(error) to recursively check if an error wraps this error. -func (e *PolicyPreventedActuationError) Is(err error) bool { - if err == nil { - return false - } - tErr, ok := err.(*PolicyPreventedActuationError) - if !ok { - return false - } - return e.Strategy == tErr.Strategy && - e.Policy == tErr.Policy && - e.Status == tErr.Status -} diff --git a/pkg/inventory/inventory_test.go b/pkg/inventory/inventory_test.go deleted file mode 100644 index 5369bce5..00000000 --- a/pkg/inventory/inventory_test.go +++ /dev/null @@ -1,432 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package inventory - -import ( - "regexp" - "testing" - - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/object" - "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/cli-runtime/pkg/resource" -) - -var testNamespace = "test-inventory-namespace" -var inventoryObjName = "test-inventory-obj" -var pod1Name = "pod-1" -var pod2Name = "pod-2" -var pod3Name = "pod-3" - -var testInventoryLabel = "test-app-label" - -var inventoryObj = &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "v1", - "kind": "ConfigMap", - "metadata": map[string]interface{}{ - "name": inventoryObjName, - "namespace": testNamespace, - "labels": map[string]interface{}{ - common.InventoryLabel: testInventoryLabel, - }, - }, - }, -} - -var legacyInvObj = &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "v1", - "kind": "ConfigMap", - "metadata": map[string]interface{}{ - "name": legacyInvName, - "namespace": testNamespace, - "labels": map[string]interface{}{ - common.InventoryLabel: testInventoryLabel, - }, - }, - }, -} - -var localInv = WrapInventoryInfoObj(inventoryObj) - -var invInfo = &resource.Info{ - Namespace: testNamespace, - Name: inventoryObjName, - Mapping: &meta.RESTMapping{ - Scope: meta.RESTScopeNamespace, - }, - Object: inventoryObj, -} - -var pod1 = &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "v1", - "kind": "Pod", - "metadata": map[string]interface{}{ - "name": pod1Name, - "namespace": testNamespace, - "uid": "uid1", - }, - }, -} - -var pod1Info = &resource.Info{ - Namespace: testNamespace, - Name: pod1Name, - Mapping: &meta.RESTMapping{ - Scope: meta.RESTScopeNamespace, - }, - Object: pod1, -} - -var pod2 = &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "v1", - "kind": "Pod", - "metadata": map[string]interface{}{ - "name": pod2Name, - "namespace": testNamespace, - "uid": "uid2", - }, - }, -} - -var pod2Info = &resource.Info{ - Namespace: testNamespace, - Name: pod2Name, - Mapping: &meta.RESTMapping{ - Scope: meta.RESTScopeNamespace, - }, - Object: pod2, -} - -var pod3 = &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "v1", - "kind": "Pod", - "metadata": map[string]interface{}{ - "name": pod3Name, - "namespace": testNamespace, - "uid": "uid3", - }, - }, -} - -var pod3Info = &resource.Info{ - Namespace: testNamespace, - Name: pod3Name, - Mapping: &meta.RESTMapping{ - Scope: meta.RESTScopeNamespace, - }, - Object: pod3, -} - -var inventoryNamespace = &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "v1", - "kind": "Namespace", - "metadata": map[string]interface{}{ - "name": testNamespace, - }, - }, -} - -func TestFindInventoryObj(t *testing.T) { - tests := map[string]struct { - infos []*unstructured.Unstructured - exists bool - name string - }{ - "No inventory object is false": { - infos: []*unstructured.Unstructured{}, - exists: false, - name: "", - }, - "Nil inventory object is false": { - infos: []*unstructured.Unstructured{nil}, - exists: false, - name: "", - }, - "Only inventory object is true": { - infos: []*unstructured.Unstructured{copyInventoryInfo()}, - exists: true, - name: inventoryObjName, - }, - "Missing inventory object is false": { - infos: []*unstructured.Unstructured{pod1}, - exists: false, - name: "", - }, - "Multiple non-inventory objects is false": { - infos: []*unstructured.Unstructured{pod1, pod2, pod3}, - exists: false, - name: "", - }, - "Inventory object with multiple others is true": { - infos: []*unstructured.Unstructured{pod1, pod2, copyInventoryInfo(), pod3}, - exists: true, - name: inventoryObjName, - }, - } - - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - inventoryObj := FindInventoryObj(tc.infos) - if tc.exists && inventoryObj == nil { - t.Errorf("Should have found inventory object") - } - if !tc.exists && inventoryObj != nil { - t.Errorf("Inventory object found, but it does not exist: %#v", inventoryObj) - } - if tc.exists && inventoryObj != nil && tc.name != inventoryObj.GetName() { - t.Errorf("Inventory object name does not match: %s/%s", tc.name, inventoryObj.GetName()) - } - }) - } -} - -func TestIsInventoryObject(t *testing.T) { - tests := []struct { - invInfo *resource.Info - isInventory bool - }{ - { - invInfo: invInfo, - isInventory: true, - }, - { - invInfo: pod2Info, - isInventory: false, - }, - } - - for _, test := range tests { - inventory := IsInventoryObject(test.invInfo.Object.(*unstructured.Unstructured)) - if test.isInventory && !inventory { - t.Errorf("Inventory object not identified: %#v", test.invInfo) - } - if !test.isInventory && inventory { - t.Errorf("Non-inventory object identifed as inventory obj: %#v", test.invInfo) - } - } -} - -func TestRetrieveInventoryLabel(t *testing.T) { - tests := []struct { - inventoryInfo *resource.Info - inventoryLabel string - isError bool - }{ - // Pod is not a inventory object. - { - inventoryInfo: pod2Info, - inventoryLabel: "", - isError: true, - }, - { - inventoryInfo: invInfo, - inventoryLabel: testInventoryLabel, - isError: false, - }, - } - - for _, test := range tests { - actual, err := retrieveInventoryLabel(object.InfoToUnstructured(test.inventoryInfo)) - if test.isError && err == nil { - t.Errorf("Did not receive expected error.\n") - } - if !test.isError { - if err != nil { - t.Fatalf("Received unexpected error: %s\n", err) - } - if test.inventoryLabel != actual { - t.Errorf("Expected inventory label (%s), got (%s)\n", test.inventoryLabel, actual) - } - } - } -} - -func TestSplitUnstructureds(t *testing.T) { - tests := map[string]struct { - allObjs []*unstructured.Unstructured - expectedInv *unstructured.Unstructured - expectedObjs []*unstructured.Unstructured - isError bool - }{ - "No objects returns error": { - allObjs: []*unstructured.Unstructured{}, - expectedInv: nil, - expectedObjs: []*unstructured.Unstructured{}, - isError: true, - }, - "Only inventory object returns inv and no objects": { - allObjs: []*unstructured.Unstructured{inventoryObj}, - expectedInv: inventoryObj, - expectedObjs: []*unstructured.Unstructured{}, - isError: false, - }, - "Inventory object with single object returns inventory and object": { - allObjs: []*unstructured.Unstructured{inventoryObj, pod1}, - expectedInv: inventoryObj, - expectedObjs: []*unstructured.Unstructured{pod1}, - isError: false, - }, - "Multiple non-inventory objects returns error": { - allObjs: []*unstructured.Unstructured{pod1, pod2, pod3}, - expectedInv: nil, - expectedObjs: []*unstructured.Unstructured{pod1, pod2, pod3}, - isError: true, - }, - "Inventory object with multiple others splits correctly": { - allObjs: []*unstructured.Unstructured{pod1, pod2, inventoryObj, pod3}, - expectedInv: inventoryObj, - expectedObjs: []*unstructured.Unstructured{pod1, pod2, pod3}, - isError: false, - }, - "Multiple inventory objects returns error": { - allObjs: []*unstructured.Unstructured{pod1, legacyInvObj, inventoryObj, pod3}, - expectedInv: nil, - expectedObjs: []*unstructured.Unstructured{}, - isError: true, - }, - } - - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - actualInv, actualObjs, err := SplitUnstructureds(tc.allObjs) - if !tc.isError && err != nil { - t.Fatalf("unexpected error received: %s", err) - } - if tc.isError { - if err == nil { - t.Fatalf("expected error not received") - } - return - } - if tc.expectedInv != actualInv { - t.Errorf("expected inventory object (%v), got (%v)", tc.expectedInv, actualInv) - } - if len(tc.expectedObjs) != len(actualObjs) { - t.Errorf("expected %d objects; got %d", len(tc.expectedObjs), len(actualObjs)) - } - }) - } -} - -func TestAddSuffixToName(t *testing.T) { - tests := []struct { - obj *unstructured.Unstructured - suffix string - expected string - isError bool - }{ - // Nil info should return error. - { - obj: nil, - suffix: "", - expected: "", - isError: true, - }, - // Empty suffix should return error. - { - obj: copyInventoryInfo(), - suffix: "", - expected: "", - isError: true, - }, - // Empty suffix should return error. - { - obj: copyInventoryInfo(), - suffix: " \t", - expected: "", - isError: true, - }, - { - obj: copyInventoryInfo(), - suffix: "hashsuffix", - expected: inventoryObjName + "-hashsuffix", - isError: false, - }, - } - - for _, test := range tests { - err := addSuffixToName(test.obj, test.suffix) - if test.isError { - if err == nil { - t.Errorf("Should have produced an error, but returned none.") - } - } - if !test.isError { - if err != nil { - t.Fatalf("Received error when expecting none (%s)\n", err) - } - actualName := test.obj.GetName() - if test.expected != actualName { - t.Errorf("Expected name (%s), got (%s)\n", test.expected, actualName) - } - } - } -} - -func TestLegacyInventoryName(t *testing.T) { - tests := map[string]struct { - obj *unstructured.Unstructured - invName string // Expected inventory name (if not modified) - isModified bool // Should inventory name be changed - isError bool // Should an error be thrown - }{ - "Legacy inventory name gets random suffix": { - obj: legacyInvObj, - invName: legacyInvName, - isModified: true, - isError: false, - }, - "Non-legacy inventory name does not get modified": { - obj: inventoryObj, - invName: inventoryObjName, - isModified: false, - isError: false, - }, - } - - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - err := fixLegacyInventoryName(tc.obj) - if tc.isError { - if err == nil { - t.Fatalf("Should have produced an error, but returned none.") - } - return - } - if !tc.isError && err != nil { - t.Fatalf("Received error when expecting none (%s)\n", err) - } - actualName := tc.obj.GetName() - if !tc.isModified { - if tc.invName != tc.obj.GetName() { - t.Fatalf("expected non-modified name (%s), got (%s)", tc.invName, tc.obj.GetName()) - } - return - } - matched, err := regexp.MatchString(`inventory-\d{8}`, actualName) - if err != nil { - t.Errorf("unexpected error parsing inventory name: %s", err) - } - if !matched { - t.Errorf("expected inventory name with random suffix, got (%s)", actualName) - } - }) - } -} - -func copyInventoryInfo() *unstructured.Unstructured { - return inventoryObj.DeepCopy() -} - -func copyInventory() Info { - u := inventoryObj.DeepCopy() - return WrapInventoryInfoObj(u) -} diff --git a/pkg/inventory/inventorycm.go b/pkg/inventory/inventorycm.go deleted file mode 100644 index cd8a72a5..00000000 --- a/pkg/inventory/inventorycm.go +++ /dev/null @@ -1,226 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 -// -// Introduces the ConfigMap struct which implements -// the Inventory interface. The ConfigMap wraps a -// ConfigMap resource which stores the set of inventory -// (object metadata). - -package inventory - -import ( - "context" - "encoding/json" - "fmt" - - "github.com/fluxcd/cli-utils/pkg/apis/actuation" - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/object" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/dynamic" - "k8s.io/klog/v2" -) - -var ConfigMapGVK = schema.GroupVersionKind{ - Group: "", - Kind: "ConfigMap", - Version: "v1", -} - -// WrapInventoryObj takes a passed ConfigMap (as a resource.Info), -// wraps it with the ConfigMap and upcasts the wrapper as -// an the Inventory interface. -func WrapInventoryObj(inv *unstructured.Unstructured) Storage { - return &ConfigMap{inv: inv} -} - -// WrapInventoryInfoObj takes a passed ConfigMap (as a resource.Info), -// wraps it with the ConfigMap and upcasts the wrapper as -// an the Info interface. -func WrapInventoryInfoObj(inv *unstructured.Unstructured) Info { - return &ConfigMap{inv: inv} -} - -func InvInfoToConfigMap(inv Info) *unstructured.Unstructured { - icm, ok := inv.(*ConfigMap) - if ok { - return icm.inv - } - return nil -} - -// ConfigMap wraps a ConfigMap resource and implements -// the Inventory interface. This wrapper loads and stores the -// object metadata (inventory) to and from the wrapped ConfigMap. -type ConfigMap struct { - inv *unstructured.Unstructured - objMetas object.ObjMetadataSet - objStatus []actuation.ObjectStatus -} - -var _ Info = &ConfigMap{} -var _ Storage = &ConfigMap{} - -func (icm *ConfigMap) Name() string { - return icm.inv.GetName() -} - -func (icm *ConfigMap) Namespace() string { - return icm.inv.GetNamespace() -} - -func (icm *ConfigMap) ID() string { - // Empty string if not set. - return icm.inv.GetLabels()[common.InventoryLabel] -} - -func (icm *ConfigMap) Strategy() Strategy { - return LabelStrategy -} - -func (icm *ConfigMap) UnstructuredInventory() *unstructured.Unstructured { - return icm.inv -} - -// Load is an Inventory interface function returning the set of -// object metadata from the wrapped ConfigMap, or an error. -func (icm *ConfigMap) Load() (object.ObjMetadataSet, error) { - objs := object.ObjMetadataSet{} - objMap, exists, err := unstructured.NestedStringMap(icm.inv.Object, "data") - if err != nil { - err := fmt.Errorf("error retrieving object metadata from inventory object") - return objs, err - } - if exists { - for objStr := range objMap { - obj, err := object.ParseObjMetadata(objStr) - if err != nil { - return objs, err - } - objs = append(objs, obj) - } - } - return objs, nil -} - -// Store is an Inventory interface function implemented to store -// the object metadata in the wrapped ConfigMap. Actual storing -// happens in "GetObject". -func (icm *ConfigMap) Store(objMetas object.ObjMetadataSet, status []actuation.ObjectStatus) error { - icm.objMetas = objMetas - icm.objStatus = status - return nil -} - -// GetObject returns the wrapped object (ConfigMap) as a resource.Info -// or an error if one occurs. -func (icm *ConfigMap) GetObject() (*unstructured.Unstructured, error) { - // Create the objMap of all the resources, and compute the hash. - objMap := buildObjMap(icm.objMetas, icm.objStatus) - // Create the inventory object by copying the template. - invCopy := icm.inv.DeepCopy() - // Adds the inventory map to the ConfigMap "data" section. - err := unstructured.SetNestedStringMap(invCopy.UnstructuredContent(), - objMap, "data") - if err != nil { - return nil, err - } - return invCopy, nil -} - -// Apply is an Storage interface function implemented to apply the inventory -// object. StatusPolicy is not needed since ConfigMaps do not have a status subresource. -func (icm *ConfigMap) Apply(dc dynamic.Interface, mapper meta.RESTMapper, _ StatusPolicy) error { - invInfo, namespacedClient, err := icm.getNamespacedClient(dc, mapper) - if err != nil { - return err - } - - // Get cluster object, if exsists. - clusterObj, err := namespacedClient.Get(context.TODO(), invInfo.GetName(), metav1.GetOptions{}) - if err != nil && !apierrors.IsNotFound(err) { - return err - } - - // Create cluster inventory object, if it does not exist on cluster. - if clusterObj == nil { - klog.V(4).Infof("creating inventory object: %s/%s", invInfo.GetNamespace(), invInfo.GetName()) - _, err = namespacedClient.Create(context.TODO(), invInfo, metav1.CreateOptions{}) - return err - } - - // Update the cluster inventory object instead. - klog.V(4).Infof("updating inventory object: %s/%s", invInfo.GetNamespace(), invInfo.GetName()) - _, err = namespacedClient.Update(context.TODO(), invInfo, metav1.UpdateOptions{}) - return err -} - -// ApplyWithPrune is a Storage interface function implemented to apply the inventory object with a list of objects -// to be pruned. StatusPolicy is not needed since ConfigMaps do not have a status subresource. -func (icm *ConfigMap) ApplyWithPrune(dc dynamic.Interface, mapper meta.RESTMapper, _ StatusPolicy, _ object.ObjMetadataSet) error { - invInfo, namespacedClient, err := icm.getNamespacedClient(dc, mapper) - if err != nil { - return err - } - - // Update the cluster inventory object. - klog.V(4).Infof("updating inventory object: %s/%s", invInfo.GetNamespace(), invInfo.GetName()) - _, err = namespacedClient.Update(context.TODO(), invInfo, metav1.UpdateOptions{}) - return err -} - -// getNamespacedClient is a helper function for Apply and ApplyWithPrune that creates a namespaced client for interacting with the live -// cluster, as well as returning the ConfigMap object as a wrapped resource.Info object. -func (icm *ConfigMap) getNamespacedClient(dc dynamic.Interface, mapper meta.RESTMapper) (*unstructured.Unstructured, dynamic.ResourceInterface, error) { - invInfo, err := icm.GetObject() - if err != nil { - return nil, nil, err - } - if invInfo == nil { - return nil, nil, fmt.Errorf("attempting to create a nil inventory object") - } - - mapping, err := mapper.RESTMapping(invInfo.GroupVersionKind().GroupKind(), invInfo.GroupVersionKind().Version) - if err != nil { - return nil, nil, err - } - - // Create client to interact with cluster. - namespacedClient := dc.Resource(mapping.Resource).Namespace(invInfo.GetNamespace()) - - return invInfo, namespacedClient, nil -} - -func buildObjMap(objMetas object.ObjMetadataSet, objStatus []actuation.ObjectStatus) map[string]string { - objMap := map[string]string{} - objStatusMap := map[object.ObjMetadata]actuation.ObjectStatus{} - for _, status := range objStatus { - objStatusMap[ObjMetadataFromObjectReference(status.ObjectReference)] = status - } - for _, objMetadata := range objMetas { - if status, found := objStatusMap[objMetadata]; found { - objMap[objMetadata.String()] = stringFrom(status) - } else { - // It's possible that the passed in status doesn't any object status - objMap[objMetadata.String()] = "" - } - } - return objMap -} - -func stringFrom(status actuation.ObjectStatus) string { - tmp := map[string]string{ - "strategy": status.Strategy.String(), - "actuation": status.Actuation.String(), - "reconcile": status.Reconcile.String(), - } - data, err := json.Marshal(tmp) - if err != nil || string(data) == "{}" { - return "" - } - return string(data) -} diff --git a/pkg/inventory/inventorycm_test.go b/pkg/inventory/inventorycm_test.go deleted file mode 100644 index ff8f9eef..00000000 --- a/pkg/inventory/inventorycm_test.go +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package inventory - -import ( - "testing" - - "github.com/google/go-cmp/cmp" - - "github.com/fluxcd/cli-utils/pkg/apis/actuation" - "github.com/fluxcd/cli-utils/pkg/object" -) - -func TestBuildObjMap(t *testing.T) { - obj1 := actuation.ObjectReference{ - Group: "group1", - Kind: "Kind", - Namespace: "ns", - Name: "na", - } - obj2 := actuation.ObjectReference{ - Group: "group2", - Kind: "Kind", - Namespace: "ns", - Name: "na", - } - - tests := map[string]struct { - objSet object.ObjMetadataSet - objStatus []actuation.ObjectStatus - expected map[string]string - hasError bool - }{ - "objMetadata matches the status": { - objSet: object.ObjMetadataSet{ObjMetadataFromObjectReference(obj1), ObjMetadataFromObjectReference(obj2)}, - objStatus: []actuation.ObjectStatus{ - { - ObjectReference: obj1, - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationSucceeded, - Reconcile: actuation.ReconcilePending, - }, - { - ObjectReference: obj2, - Strategy: actuation.ActuationStrategyDelete, - Actuation: actuation.ActuationSkipped, - Reconcile: actuation.ReconcileSucceeded, - }, - }, - expected: map[string]string{ - "ns_na_group1_Kind": `{"actuation":"Succeeded","reconcile":"Pending","strategy":"Apply"}`, - "ns_na_group2_Kind": `{"actuation":"Skipped","reconcile":"Succeeded","strategy":"Delete"}`, - }, - }, - "empty object status list": { - objSet: object.ObjMetadataSet{ObjMetadataFromObjectReference(obj1), ObjMetadataFromObjectReference(obj2)}, - hasError: false, - expected: map[string]string{ - "ns_na_group1_Kind": "", - "ns_na_group2_Kind": "", - }, - }, - } - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - actual := buildObjMap(tc.objSet, tc.objStatus) - if diff := cmp.Diff(actual, tc.expected); diff != "" { - t.Error(diff) - } - }) - } -} diff --git a/pkg/inventory/manager.go b/pkg/inventory/manager.go deleted file mode 100644 index 289eedef..00000000 --- a/pkg/inventory/manager.go +++ /dev/null @@ -1,447 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package inventory - -import ( - "fmt" - - "github.com/fluxcd/cli-utils/pkg/apis/actuation" - "github.com/fluxcd/cli-utils/pkg/object" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/sets" -) - -// Manager wraps an Inventory with convenience methods that use ObjMetadata. -type Manager struct { - inventory *actuation.Inventory -} - -// NewManager returns a new manager instance. -func NewManager() *Manager { - return &Manager{ - inventory: &actuation.Inventory{}, - } -} - -// Inventory returns the in-memory version of the managed inventory. -func (tc *Manager) Inventory() *actuation.Inventory { - return tc.inventory -} - -// ObjectStatus retrieves the status of an object with the specified ID. -// The returned status is a pointer and can be updated in-place for efficiency. -func (tc *Manager) ObjectStatus(id object.ObjMetadata) (*actuation.ObjectStatus, bool) { - ref := ObjectReferenceFromObjMetadata(id) - for i, objStatus := range tc.inventory.Status.Objects { - if objStatus.ObjectReference == ref { - return &(tc.inventory.Status.Objects[i]), true - } - } - return nil, false -} - -// ObjectsWithActuationStatus retrieves the set of objects with the -// specified actuation strategy and status. -func (tc *Manager) ObjectsWithActuationStatus(strategy actuation.ActuationStrategy, status actuation.ActuationStatus) object.ObjMetadataSet { - var ids object.ObjMetadataSet - for _, objStatus := range tc.inventory.Status.Objects { - if objStatus.Strategy == strategy && objStatus.Actuation == status { - ids = append(ids, ObjMetadataFromObjectReference(objStatus.ObjectReference)) - } - } - return ids -} - -// ObjectsWithActuationStatus retrieves the set of objects with the -// specified reconcile status, regardless of actuation strategy. -func (tc *Manager) ObjectsWithReconcileStatus(status actuation.ReconcileStatus) object.ObjMetadataSet { - var ids object.ObjMetadataSet - for _, objStatus := range tc.inventory.Status.Objects { - if objStatus.Reconcile == status { - ids = append(ids, ObjMetadataFromObjectReference(objStatus.ObjectReference)) - } - } - return ids -} - -// SetObjectStatus updates or adds an ObjectStatus record to the inventory. -func (tc *Manager) SetObjectStatus(newObjStatus actuation.ObjectStatus) { - for i, oldObjStatus := range tc.inventory.Status.Objects { - if oldObjStatus.ObjectReference == newObjStatus.ObjectReference { - tc.inventory.Status.Objects[i] = newObjStatus - return - } - } - tc.inventory.Status.Objects = append(tc.inventory.Status.Objects, newObjStatus) -} - -// IsSuccessfulApply returns true if the object apply was successful -func (tc *Manager) IsSuccessfulApply(id object.ObjMetadata) bool { - objStatus, found := tc.ObjectStatus(id) - if !found { - return false - } - return objStatus.Strategy == actuation.ActuationStrategyApply && - objStatus.Actuation == actuation.ActuationSucceeded -} - -// AddSuccessfulApply updates the context with information about the -// resource identified by the provided id. Currently, we keep information -// about the generation of the resource after the apply operation completed. -func (tc *Manager) AddSuccessfulApply(id object.ObjMetadata, uid types.UID, gen int64) { - tc.SetObjectStatus(actuation.ObjectStatus{ - ObjectReference: ObjectReferenceFromObjMetadata(id), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationSucceeded, - Reconcile: actuation.ReconcilePending, - UID: uid, - Generation: gen, - }) -} - -// SuccessfulApplies returns all the objects (as ObjMetadata) that -// were added as applied resources to the Manager. -func (tc *Manager) SuccessfulApplies() object.ObjMetadataSet { - return tc.ObjectsWithActuationStatus(actuation.ActuationStrategyApply, - actuation.ActuationSucceeded) -} - -// AppliedResourceUID looks up the UID of a successfully applied resource -func (tc *Manager) AppliedResourceUID(id object.ObjMetadata) (types.UID, bool) { - objStatus, found := tc.ObjectStatus(id) - return objStatus.UID, found && - objStatus.Strategy == actuation.ActuationStrategyApply && - objStatus.Actuation == actuation.ActuationSucceeded -} - -// AppliedResourceUIDs returns a set with the UIDs of all the -// successfully applied resources. -func (tc *Manager) AppliedResourceUIDs() sets.String { // nolint:staticcheck - uids := sets.NewString() - for _, objStatus := range tc.inventory.Status.Objects { - if objStatus.Strategy == actuation.ActuationStrategyApply && - objStatus.Actuation == actuation.ActuationSucceeded { - if objStatus.UID != "" { - uids.Insert(string(objStatus.UID)) - } - } - } - return uids -} - -// AppliedGeneration looks up the generation of the given resource -// after it was applied. -func (tc *Manager) AppliedGeneration(id object.ObjMetadata) (int64, bool) { - objStatus, found := tc.ObjectStatus(id) - if !found { - return 0, false - } - return objStatus.Generation, true -} - -// IsSuccessfulDelete returns true if the object delete was successful -func (tc *Manager) IsSuccessfulDelete(id object.ObjMetadata) bool { - objStatus, found := tc.ObjectStatus(id) - if !found { - return false - } - return objStatus.Strategy == actuation.ActuationStrategyDelete && - objStatus.Actuation == actuation.ActuationSucceeded -} - -// AddSuccessfulDelete updates the context with information about the -// resource identified by the provided id. Currently, we only track the uid, -// because the DELETE call doesn't always return the generation, namely if the -// object was scheduled to be deleted asynchronously, which might cause further -// updates by finalizers. The UID will change if the object is re-created. -func (tc *Manager) AddSuccessfulDelete(id object.ObjMetadata, uid types.UID) { - tc.SetObjectStatus(actuation.ObjectStatus{ - ObjectReference: ObjectReferenceFromObjMetadata(id), - Strategy: actuation.ActuationStrategyDelete, - Actuation: actuation.ActuationSucceeded, - Reconcile: actuation.ReconcilePending, - UID: uid, - }) -} - -// SuccessfulDeletes returns all the objects (as ObjMetadata) that -// were successfully deleted. -func (tc *Manager) SuccessfulDeletes() object.ObjMetadataSet { - return tc.ObjectsWithActuationStatus(actuation.ActuationStrategyDelete, - actuation.ActuationSucceeded) -} - -// IsFailedApply returns true if the object failed to apply -func (tc *Manager) IsFailedApply(id object.ObjMetadata) bool { - objStatus, found := tc.ObjectStatus(id) - if !found { - return false - } - return objStatus.Strategy == actuation.ActuationStrategyApply && - objStatus.Actuation == actuation.ActuationFailed -} - -// AddFailedApply registers that the object failed to apply -func (tc *Manager) AddFailedApply(id object.ObjMetadata) { - tc.SetObjectStatus(actuation.ObjectStatus{ - ObjectReference: ObjectReferenceFromObjMetadata(id), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationFailed, - Reconcile: actuation.ReconcilePending, - }) -} - -// FailedApplies returns all the objects that failed to apply -func (tc *Manager) FailedApplies() object.ObjMetadataSet { - return tc.ObjectsWithActuationStatus(actuation.ActuationStrategyApply, actuation.ActuationFailed) -} - -// IsFailedDelete returns true if the object failed to delete -func (tc *Manager) IsFailedDelete(id object.ObjMetadata) bool { - objStatus, found := tc.ObjectStatus(id) - if !found { - return false - } - return objStatus.Strategy == actuation.ActuationStrategyDelete && - objStatus.Actuation == actuation.ActuationFailed -} - -// AddFailedDelete registers that the object failed to delete -func (tc *Manager) AddFailedDelete(id object.ObjMetadata) { - tc.SetObjectStatus(actuation.ObjectStatus{ - ObjectReference: ObjectReferenceFromObjMetadata(id), - Strategy: actuation.ActuationStrategyDelete, - Actuation: actuation.ActuationFailed, - Reconcile: actuation.ReconcilePending, - }) -} - -// FailedDeletes returns all the objects that failed to delete -func (tc *Manager) FailedDeletes() object.ObjMetadataSet { - return tc.ObjectsWithActuationStatus(actuation.ActuationStrategyDelete, - actuation.ActuationFailed) -} - -// IsSkippedApply returns true if the object apply was skipped -func (tc *Manager) IsSkippedApply(id object.ObjMetadata) bool { - objStatus, found := tc.ObjectStatus(id) - if !found { - return false - } - return objStatus.Strategy == actuation.ActuationStrategyApply && - objStatus.Actuation == actuation.ActuationSkipped -} - -// AddSkippedApply registers that the object apply was skipped -func (tc *Manager) AddSkippedApply(id object.ObjMetadata) { - tc.SetObjectStatus(actuation.ObjectStatus{ - ObjectReference: ObjectReferenceFromObjMetadata(id), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationSkipped, - Reconcile: actuation.ReconcilePending, - }) -} - -// SkippedApplies returns all the objects where apply was skipped -func (tc *Manager) SkippedApplies() object.ObjMetadataSet { - return tc.ObjectsWithActuationStatus(actuation.ActuationStrategyApply, actuation.ActuationSkipped) -} - -// IsSkippedDelete returns true if the object delete was skipped -func (tc *Manager) IsSkippedDelete(id object.ObjMetadata) bool { - objStatus, found := tc.ObjectStatus(id) - if !found { - return false - } - return objStatus.Strategy == actuation.ActuationStrategyDelete && - objStatus.Actuation == actuation.ActuationSkipped -} - -// AddSkippedDelete registers that the object delete was skipped -func (tc *Manager) AddSkippedDelete(id object.ObjMetadata) { - tc.SetObjectStatus(actuation.ObjectStatus{ - ObjectReference: ObjectReferenceFromObjMetadata(id), - Strategy: actuation.ActuationStrategyDelete, - Actuation: actuation.ActuationSkipped, - Reconcile: actuation.ReconcilePending, - }) -} - -// SkippedDeletes returns all the objects where deletion was skipped -func (tc *Manager) SkippedDeletes() object.ObjMetadataSet { - return tc.ObjectsWithActuationStatus(actuation.ActuationStrategyDelete, - actuation.ActuationSkipped) -} - -// IsSuccessfulReconcile returns true if the object is reconciled -func (tc *Manager) IsSuccessfulReconcile(id object.ObjMetadata) bool { - objStatus, found := tc.ObjectStatus(id) - if !found { - return false - } - return objStatus.Reconcile == actuation.ReconcileSucceeded -} - -// SetSuccessfulReconcile registers that the object is reconciled -func (tc *Manager) SetSuccessfulReconcile(id object.ObjMetadata) error { - objStatus, found := tc.ObjectStatus(id) - if !found { - return fmt.Errorf("object not in inventory: %q", id) - } - objStatus.Reconcile = actuation.ReconcileSucceeded - return nil -} - -// SuccessfulReconciles returns all the reconciled objects -func (tc *Manager) SuccessfulReconciles() object.ObjMetadataSet { - return tc.ObjectsWithReconcileStatus(actuation.ReconcileSucceeded) -} - -// IsFailedReconcile returns true if the object failed to reconcile -func (tc *Manager) IsFailedReconcile(id object.ObjMetadata) bool { - objStatus, found := tc.ObjectStatus(id) - if !found { - return false - } - return objStatus.Reconcile == actuation.ReconcileFailed -} - -// SetFailedReconcile registers that the object failed to reconcile -func (tc *Manager) SetFailedReconcile(id object.ObjMetadata) error { - objStatus, found := tc.ObjectStatus(id) - if !found { - return fmt.Errorf("object not in inventory: %q", id) - } - objStatus.Reconcile = actuation.ReconcileFailed - return nil -} - -// FailedReconciles returns all the objects that failed to reconcile -func (tc *Manager) FailedReconciles() object.ObjMetadataSet { - return tc.ObjectsWithReconcileStatus(actuation.ReconcileFailed) -} - -// IsSkippedReconcile returns true if the object reconcile was skipped -func (tc *Manager) IsSkippedReconcile(id object.ObjMetadata) bool { - objStatus, found := tc.ObjectStatus(id) - if !found { - return false - } - return objStatus.Reconcile == actuation.ReconcileSkipped -} - -// SetSkippedReconcile registers that the object reconcile was skipped -func (tc *Manager) SetSkippedReconcile(id object.ObjMetadata) error { - objStatus, found := tc.ObjectStatus(id) - if !found { - return fmt.Errorf("object not in inventory: %q", id) - } - objStatus.Reconcile = actuation.ReconcileSkipped - return nil -} - -// SkippedReconciles returns all the objects where reconcile was skipped -func (tc *Manager) SkippedReconciles() object.ObjMetadataSet { - return tc.ObjectsWithReconcileStatus(actuation.ReconcileSkipped) -} - -// IsTimeoutReconcile returns true if the object reconcile was skipped -func (tc *Manager) IsTimeoutReconcile(id object.ObjMetadata) bool { - objStatus, found := tc.ObjectStatus(id) - if !found { - return false - } - return objStatus.Reconcile == actuation.ReconcileTimeout -} - -// SetTimeoutReconcile registers that the object reconcile was skipped -func (tc *Manager) SetTimeoutReconcile(id object.ObjMetadata) error { - objStatus, found := tc.ObjectStatus(id) - if !found { - return fmt.Errorf("object not in inventory: %q", id) - } - objStatus.Reconcile = actuation.ReconcileTimeout - return nil -} - -// TimeoutReconciles returns all the objects where reconcile was skipped -func (tc *Manager) TimeoutReconciles() object.ObjMetadataSet { - return tc.ObjectsWithReconcileStatus(actuation.ReconcileTimeout) -} - -// IsPendingReconcile returns true if the object reconcile is pending -func (tc *Manager) IsPendingReconcile(id object.ObjMetadata) bool { - objStatus, found := tc.ObjectStatus(id) - if !found { - return false - } - return objStatus.Reconcile == actuation.ReconcilePending -} - -// SetPendingReconcile registers that the object reconcile is pending -func (tc *Manager) SetPendingReconcile(id object.ObjMetadata) error { - objStatus, found := tc.ObjectStatus(id) - if !found { - return fmt.Errorf("object not in inventory: %q", id) - } - objStatus.Reconcile = actuation.ReconcilePending - return nil -} - -// PendingReconciles returns all the objects where reconcile is pending -func (tc *Manager) PendingReconciles() object.ObjMetadataSet { - return tc.ObjectsWithReconcileStatus(actuation.ReconcilePending) -} - -// IsPendingApply returns true if the object pending apply -func (tc *Manager) IsPendingApply(id object.ObjMetadata) bool { - objStatus, found := tc.ObjectStatus(id) - if !found { - return false - } - return objStatus.Strategy == actuation.ActuationStrategyApply && - objStatus.Actuation == actuation.ActuationPending -} - -// AddPendingApply registers that the object is pending apply -func (tc *Manager) AddPendingApply(id object.ObjMetadata) { - tc.SetObjectStatus(actuation.ObjectStatus{ - ObjectReference: ObjectReferenceFromObjMetadata(id), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }) -} - -// PendingApplies returns all the objects that are pending apply -func (tc *Manager) PendingApplies() object.ObjMetadataSet { - return tc.ObjectsWithActuationStatus(actuation.ActuationStrategyApply, - actuation.ActuationPending) -} - -// IsPendingDelete returns true if the object pending delete -func (tc *Manager) IsPendingDelete(id object.ObjMetadata) bool { - objStatus, found := tc.ObjectStatus(id) - if !found { - return false - } - return objStatus.Strategy == actuation.ActuationStrategyDelete && - objStatus.Actuation == actuation.ActuationPending -} - -// AddPendingDelete registers that the object is pending delete -func (tc *Manager) AddPendingDelete(id object.ObjMetadata) { - tc.SetObjectStatus(actuation.ObjectStatus{ - ObjectReference: ObjectReferenceFromObjMetadata(id), - Strategy: actuation.ActuationStrategyDelete, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - }) -} - -// PendingDeletes returns all the objects that are pending delete -func (tc *Manager) PendingDeletes() object.ObjMetadataSet { - return tc.ObjectsWithActuationStatus(actuation.ActuationStrategyDelete, - actuation.ActuationPending) -} diff --git a/pkg/inventory/manager_test.go b/pkg/inventory/manager_test.go deleted file mode 100644 index cfe9adaa..00000000 --- a/pkg/inventory/manager_test.go +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package inventory - -import ( - "testing" - - "github.com/fluxcd/cli-utils/pkg/apis/actuation" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/stretchr/testify/require" - "k8s.io/apimachinery/pkg/runtime/schema" -) - -func TestObjectStatusGetSet(t *testing.T) { - manager := NewManager() - - id := object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "group", - Kind: "kind", - }, - Name: "name", - Namespace: "namespace", - } - - // Test get before set - outStatus, found := manager.ObjectStatus(id) - require.False(t, found) - require.Nil(t, outStatus) - - // Test get after set - inStatus1 := actuation.ObjectStatus{ - ObjectReference: ObjectReferenceFromObjMetadata(id), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationPending, - Reconcile: actuation.ReconcilePending, - } - manager.SetObjectStatus(inStatus1) - outStatus, found = manager.ObjectStatus(id) - require.True(t, found) - require.Equal(t, &inStatus1, outStatus) - - // Test get after re-set - inStatus2 := actuation.ObjectStatus{ - ObjectReference: ObjectReferenceFromObjMetadata(id), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationSucceeded, - Reconcile: actuation.ReconcilePending, - } - manager.SetObjectStatus(inStatus2) - outStatus, found = manager.ObjectStatus(id) - require.True(t, found) - require.Equal(t, &inStatus2, outStatus) - - // Test get after set via returned pointer - outStatus.Reconcile = actuation.ReconcileFailed - outStatus, found = manager.ObjectStatus(id) - require.True(t, found) - expStatus := actuation.ObjectStatus{ - ObjectReference: ObjectReferenceFromObjMetadata(id), - Strategy: actuation.ActuationStrategyApply, - Actuation: actuation.ActuationSucceeded, - Reconcile: actuation.ReconcileFailed, - } - require.Equal(t, &expStatus, outStatus) -} diff --git a/pkg/inventory/policy.go b/pkg/inventory/policy.go deleted file mode 100644 index 485b648d..00000000 --- a/pkg/inventory/policy.go +++ /dev/null @@ -1,151 +0,0 @@ -// Copyright 2019 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package inventory - -import ( - "fmt" - - "github.com/fluxcd/cli-utils/pkg/apis/actuation" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -// Policy defines if an inventory object can take over -// objects that belong to another inventory object or don't -// belong to any inventory object. -// This is done by determining if the apply/prune operation -// can go through for a resource based on the comparison -// the inventory-id value in the package and the owning-inventory -// annotation in the live object. -// -//go:generate stringer -type=Policy -linecomment -type Policy int - -const ( - // PolicyMustMatch: This policy enforces that the resources being applied can not - // have any overlap with objects in other inventories or objects that already exist - // in the cluster but don't belong to an inventory. - // - // The apply operation can go through when - // - A new resources in the package doesn't exist in the cluster - // - An existing resource in the package doesn't exist in the cluster - // - An existing resource exist in the cluster. The owning-inventory annotation in the live object - // matches with that in the package. - // - // The prune operation can go through when - // - The owning-inventory annotation in the live object match with that - // in the package. - PolicyMustMatch Policy = iota // MustMatch - - // PolicyAdoptIfNoInventory: This policy enforces that resources being applied - // can not have any overlap with objects in other inventories, but are - // permitted to take ownership of objects that don't belong to any inventories. - // - // The apply operation can go through when - // - New resource in the package doesn't exist in the cluster - // - If a new resource exist in the cluster, its owning-inventory annotation is empty - // - Existing resource in the package doesn't exist in the cluster - // - If existing resource exist in the cluster, its owning-inventory annotation in the live object - // is empty - // - An existing resource exist in the cluster. The owning-inventory annotation in the live object - // matches with that in the package. - // - // The prune operation can go through when - // - The owning-inventory annotation in the live object match with that - // in the package. - // - The live object doesn't have the owning-inventory annotation. - PolicyAdoptIfNoInventory // AdoptIfNoInventory - - // PolicyAdoptAll: This policy will let the current inventory take ownership of any objects. - // - // The apply operation can go through for any resource in the package even if the - // live object has an unmatched owning-inventory annotation. - // - // The prune operation can go through when - // - The owning-inventory annotation in the live object match or doesn't match with that - // in the package. - // - The live object doesn't have the owning-inventory annotation. - PolicyAdoptAll // AdoptAll -) - -// OwningInventoryKey is the annotation key indicating the inventory owning an object. -const OwningInventoryKey = "config.k8s.io/owning-inventory" - -// IDMatchStatus represents the result of comparing the -// id from current inventory info and the inventory-id from a live object. -// -//go:generate stringer -type=IDMatchStatus -type IDMatchStatus int - -const ( - Empty IDMatchStatus = iota - Match - NoMatch -) - -func IDMatch(inv Info, obj *unstructured.Unstructured) IDMatchStatus { - annotations := obj.GetAnnotations() - value, found := annotations[OwningInventoryKey] - if !found { - return Empty - } - if value == inv.ID() { - return Match - } - return NoMatch -} - -func CanApply(inv Info, obj *unstructured.Unstructured, policy Policy) (bool, error) { - matchStatus := IDMatch(inv, obj) - switch matchStatus { - case Empty: - if policy != PolicyMustMatch { - return true, nil - } - case Match: - return true, nil - case NoMatch: - if policy == PolicyAdoptAll { - return true, nil - } - default: - return false, fmt.Errorf("invalid inventory policy: %v", policy) - } - return false, &PolicyPreventedActuationError{ - Strategy: actuation.ActuationStrategyApply, - Policy: policy, - Status: matchStatus, - } -} - -func CanPrune(inv Info, obj *unstructured.Unstructured, policy Policy) (bool, error) { - matchStatus := IDMatch(inv, obj) - switch matchStatus { - case Empty: - if policy == PolicyAdoptIfNoInventory || policy == PolicyAdoptAll { - return true, nil - } - case Match: - return true, nil - case NoMatch: - if policy == PolicyAdoptAll { - return true, nil - } - default: - return false, fmt.Errorf("invalid inventory policy: %v", policy) - } - return false, &PolicyPreventedActuationError{ - Strategy: actuation.ActuationStrategyDelete, - Policy: policy, - Status: matchStatus, - } -} - -func AddInventoryIDAnnotation(obj *unstructured.Unstructured, inv Info) { - annotations := obj.GetAnnotations() - if annotations == nil { - annotations = make(map[string]string) - } - annotations[OwningInventoryKey] = inv.ID() - obj.SetAnnotations(annotations) -} diff --git a/pkg/inventory/policy_string.go b/pkg/inventory/policy_string.go deleted file mode 100644 index 64371657..00000000 --- a/pkg/inventory/policy_string.go +++ /dev/null @@ -1,25 +0,0 @@ -// Code generated by "stringer -type=Policy -linecomment"; DO NOT EDIT. - -package inventory - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[PolicyMustMatch-0] - _ = x[PolicyAdoptIfNoInventory-1] - _ = x[PolicyAdoptAll-2] -} - -const _Policy_name = "MustMatchAdoptIfNoInventoryAdoptAll" - -var _Policy_index = [...]uint8{0, 9, 27, 35} - -func (i Policy) String() string { - if i < 0 || i >= Policy(len(_Policy_index)-1) { - return "Policy(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _Policy_name[_Policy_index[i]:_Policy_index[i+1]] -} diff --git a/pkg/inventory/policy_test.go b/pkg/inventory/policy_test.go deleted file mode 100644 index 555191c8..00000000 --- a/pkg/inventory/policy_test.go +++ /dev/null @@ -1,280 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package inventory - -import ( - "testing" - - "github.com/fluxcd/cli-utils/pkg/apis/actuation" - "github.com/fluxcd/cli-utils/pkg/testutil" - "github.com/stretchr/testify/assert" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -type fakeInventoryInfo struct { - id string -} - -func (i *fakeInventoryInfo) Name() string { - return "" -} - -func (i *fakeInventoryInfo) Namespace() string { - return "" -} - -func (i *fakeInventoryInfo) ID() string { - return i.id -} - -func (i *fakeInventoryInfo) Strategy() Strategy { - return NameStrategy -} - -func testObjectWithAnnotation(key, val string) *unstructured.Unstructured { - obj := &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "v1", - "kind": "Deployment", - "metadata": map[string]interface{}{ - "name": "foo", - "namespace": "ns", - }, - }, - } - if key != "" { - obj.SetAnnotations(map[string]string{ - key: val, - }) - } - return obj -} - -func TestInventoryIDMatch(t *testing.T) { - testcases := []struct { - name string - obj *unstructured.Unstructured - inv Info - expected IDMatchStatus - }{ - { - name: "empty", - obj: testObjectWithAnnotation("", ""), - inv: &fakeInventoryInfo{id: "random-id"}, - expected: Empty, - }, - { - name: "matched", - obj: testObjectWithAnnotation(OwningInventoryKey, "matched"), - inv: &fakeInventoryInfo{id: "matched"}, - expected: Match, - }, - { - name: "unmatched", - obj: testObjectWithAnnotation(OwningInventoryKey, "unmatched"), - inv: &fakeInventoryInfo{id: "random-id"}, - expected: NoMatch, - }, - } - for _, tc := range testcases { - actual := IDMatch(tc.inv, tc.obj) - if actual != tc.expected { - t.Errorf("expected %v, but got %v", tc.expected, actual) - } - } -} - -func TestCanApply(t *testing.T) { - testcases := []struct { - name string - obj *unstructured.Unstructured - inv Info - policy Policy - canApply bool - expectedError error - }{ - { - name: "empty with AdoptIfNoInventory", - obj: testObjectWithAnnotation("", ""), - inv: &fakeInventoryInfo{id: "random-id"}, - policy: PolicyAdoptIfNoInventory, - canApply: true, - }, - { - name: "empty with AdoptAll", - obj: testObjectWithAnnotation("", ""), - inv: &fakeInventoryInfo{id: "random-id"}, - policy: PolicyAdoptAll, - canApply: true, - }, - { - name: "empty with InventoryPolicyMustMatch", - obj: testObjectWithAnnotation("", ""), - inv: &fakeInventoryInfo{id: "random-id"}, - policy: PolicyMustMatch, - canApply: false, - expectedError: &PolicyPreventedActuationError{ - Strategy: actuation.ActuationStrategyApply, - Policy: PolicyMustMatch, - Status: Empty, - }, - }, - { - name: "matched with InventoryPolicyMustMatch", - obj: testObjectWithAnnotation(OwningInventoryKey, "matched"), - inv: &fakeInventoryInfo{id: "matched"}, - policy: PolicyMustMatch, - canApply: true, - }, - { - name: "matched with AdoptIfNoInventory", - obj: testObjectWithAnnotation(OwningInventoryKey, "matched"), - inv: &fakeInventoryInfo{id: "matched"}, - policy: PolicyAdoptIfNoInventory, - canApply: true, - }, - { - name: "matched with AloptAll", - obj: testObjectWithAnnotation(OwningInventoryKey, "matched"), - inv: &fakeInventoryInfo{id: "matched"}, - policy: PolicyAdoptAll, - canApply: true, - }, - { - name: "unmatched with InventoryPolicyMustMatch", - obj: testObjectWithAnnotation(OwningInventoryKey, "unmatched"), - inv: &fakeInventoryInfo{id: "random-id"}, - policy: PolicyMustMatch, - canApply: false, - expectedError: &PolicyPreventedActuationError{ - Strategy: actuation.ActuationStrategyApply, - Policy: PolicyMustMatch, - Status: NoMatch, - }, - }, - { - name: "unmatched with AdoptIfNoInventory", - obj: testObjectWithAnnotation(OwningInventoryKey, "unmatched"), - inv: &fakeInventoryInfo{id: "random-id"}, - policy: PolicyAdoptIfNoInventory, - canApply: false, - expectedError: &PolicyPreventedActuationError{ - Strategy: actuation.ActuationStrategyApply, - Policy: PolicyAdoptIfNoInventory, - Status: NoMatch, - }, - }, - { - name: "unmatched with AdoptAll", - obj: testObjectWithAnnotation(OwningInventoryKey, "unmatched"), - inv: &fakeInventoryInfo{id: "random-id"}, - policy: PolicyAdoptAll, - canApply: true, - }, - } - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - ok, err := CanApply(tc.inv, tc.obj, tc.policy) - assert.Equal(t, tc.canApply, ok) - testutil.AssertEqual(t, tc.expectedError, err) - }) - } -} - -func TestCanPrune(t *testing.T) { - testcases := []struct { - name string - obj *unstructured.Unstructured - inv Info - policy Policy - canPrune bool - expectedError error - }{ - { - name: "empty with AdoptIfNoInventory", - obj: testObjectWithAnnotation("", ""), - inv: &fakeInventoryInfo{id: "random-id"}, - policy: PolicyAdoptIfNoInventory, - canPrune: true, - }, - { - name: "empty with AdoptAll", - obj: testObjectWithAnnotation("", ""), - inv: &fakeInventoryInfo{id: "random-id"}, - policy: PolicyAdoptAll, - canPrune: true, - }, - { - name: "empty with PolicyMustMatch", - obj: testObjectWithAnnotation("", ""), - inv: &fakeInventoryInfo{id: "random-id"}, - policy: PolicyMustMatch, - canPrune: false, - expectedError: &PolicyPreventedActuationError{ - Strategy: actuation.ActuationStrategyDelete, - Policy: PolicyMustMatch, - Status: Empty, - }, - }, - { - name: "matched with PolicyMustMatch", - obj: testObjectWithAnnotation(OwningInventoryKey, "matched"), - inv: &fakeInventoryInfo{id: "matched"}, - policy: PolicyMustMatch, - canPrune: true, - }, - { - name: "matched with AdoptIfNoInventory", - obj: testObjectWithAnnotation(OwningInventoryKey, "matched"), - inv: &fakeInventoryInfo{id: "matched"}, - policy: PolicyAdoptIfNoInventory, - canPrune: true, - }, - { - name: "matched with AloptAll", - obj: testObjectWithAnnotation(OwningInventoryKey, "matched"), - inv: &fakeInventoryInfo{id: "matched"}, - policy: PolicyAdoptAll, - canPrune: true, - }, - { - name: "unmatched with PolicyMustMatch", - obj: testObjectWithAnnotation(OwningInventoryKey, "unmatched"), - inv: &fakeInventoryInfo{id: "random-id"}, - policy: PolicyMustMatch, - canPrune: false, - expectedError: &PolicyPreventedActuationError{ - Strategy: actuation.ActuationStrategyDelete, - Policy: PolicyMustMatch, - Status: NoMatch, - }, - }, - { - name: "unmatched with AdoptIfNoInventory", - obj: testObjectWithAnnotation(OwningInventoryKey, "unmatched"), - inv: &fakeInventoryInfo{id: "random-id"}, - policy: PolicyAdoptIfNoInventory, - canPrune: false, - expectedError: &PolicyPreventedActuationError{ - Strategy: actuation.ActuationStrategyDelete, - Policy: PolicyAdoptIfNoInventory, - Status: NoMatch, - }, - }, - { - name: "unmatched with AdoptAll", - obj: testObjectWithAnnotation(OwningInventoryKey, "unmatched"), - inv: &fakeInventoryInfo{id: "random-id"}, - policy: PolicyAdoptAll, - canPrune: true, - }, - } - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - ok, err := CanPrune(tc.inv, tc.obj, tc.policy) - assert.Equal(t, tc.canPrune, ok) - testutil.AssertEqual(t, tc.expectedError, err) - }) - } -} diff --git a/pkg/inventory/status-policy.go b/pkg/inventory/status-policy.go deleted file mode 100644 index 95be3f2b..00000000 --- a/pkg/inventory/status-policy.go +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2022 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package inventory - -// StatusPolicy specifies whether the inventory client should apply status to -// the inventory object. The status contains the actuation and reconcile stauts -// of each object in the inventory. -// -//go:generate stringer -type=StatusPolicy -linecomment -type StatusPolicy int - -const ( - // StatusPolicyNone disables inventory status updates. - StatusPolicyNone StatusPolicy = iota // None - - // StatusPolicyAll fully enables inventory status updates. - StatusPolicyAll // All -) diff --git a/pkg/inventory/statuspolicy_string.go b/pkg/inventory/statuspolicy_string.go deleted file mode 100644 index 6acd1d6f..00000000 --- a/pkg/inventory/statuspolicy_string.go +++ /dev/null @@ -1,24 +0,0 @@ -// Code generated by "stringer -type=StatusPolicy -linecomment"; DO NOT EDIT. - -package inventory - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[StatusPolicyNone-0] - _ = x[StatusPolicyAll-1] -} - -const _StatusPolicy_name = "NoneAll" - -var _StatusPolicy_index = [...]uint8{0, 4, 7} - -func (i StatusPolicy) String() string { - if i < 0 || i >= StatusPolicy(len(_StatusPolicy_index)-1) { - return "StatusPolicy(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _StatusPolicy_name[_StatusPolicy_index[i]:_StatusPolicy_index[i+1]] -} diff --git a/pkg/inventory/storage.go b/pkg/inventory/storage.go deleted file mode 100644 index 2c510e5b..00000000 --- a/pkg/inventory/storage.go +++ /dev/null @@ -1,174 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 -// -// This file contains code for a "inventory" object which -// stores object metadata to keep track of sets of -// resources. This "inventory" object must be a ConfigMap -// and it stores the object metadata in the data field -// of the ConfigMap. By storing metadata from all applied -// objects, we can correctly prune and teardown sets -// of resources. - -package inventory - -import ( - "fmt" - "strings" - - "github.com/fluxcd/cli-utils/pkg/apis/actuation" - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/object" - "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/client-go/dynamic" - "k8s.io/klog/v2" -) - -// The default inventory name stored in the inventory template. -const legacyInvName = "inventory" - -// Storage describes methods necessary for an object which -// can persist the object metadata for pruning and other group -// operations. -type Storage interface { - // Load retrieves the set of object metadata from the inventory object - Load() (object.ObjMetadataSet, error) - // Store the set of object metadata in the inventory object. This will - // replace the metadata, spec and status. - Store(objs object.ObjMetadataSet, status []actuation.ObjectStatus) error - // GetObject returns the object that stores the inventory - GetObject() (*unstructured.Unstructured, error) - // Apply applies the inventory object. This utility function is used - // in InventoryClient.Merge and merges the metadata, spec and status. - Apply(dynamic.Interface, meta.RESTMapper, StatusPolicy) error - // ApplyWithPrune applies the inventory object with a set of pruneIDs of - // objects to be pruned (object.ObjMetadataSet). This function is used in - // InventoryClient.Replace. pruneIDs are required for enabling custom logic - // handling of multiple ResourceGroup inventories. - ApplyWithPrune(dynamic.Interface, meta.RESTMapper, StatusPolicy, object.ObjMetadataSet) error -} - -// StorageFactoryFunc creates the object which implements the Inventory -// interface from the passed info object. -type StorageFactoryFunc func(*unstructured.Unstructured) Storage - -// ToUnstructuredFunc returns the unstructured object for the -// given Info. -type ToUnstructuredFunc func(Info) *unstructured.Unstructured - -// FindInventoryObj returns the "Inventory" object (ConfigMap with -// inventory label) if it exists, or nil if it does not exist. -func FindInventoryObj(objs object.UnstructuredSet) *unstructured.Unstructured { - for _, obj := range objs { - if IsInventoryObject(obj) { - return obj - } - } - return nil -} - -// IsInventoryObject returns true if the passed object has the -// inventory label. -func IsInventoryObject(obj *unstructured.Unstructured) bool { - if obj == nil { - return false - } - inventoryLabel, err := retrieveInventoryLabel(obj) - if err == nil && len(inventoryLabel) > 0 { - return true - } - return false -} - -// retrieveInventoryLabel returns the string value of the InventoryLabel -// for the passed inventory object. Returns error if the passed object is nil or -// is not a inventory object. -func retrieveInventoryLabel(obj *unstructured.Unstructured) (string, error) { - inventoryLabel, exists := obj.GetLabels()[common.InventoryLabel] - if !exists { - return "", fmt.Errorf("inventory label does not exist for inventory object: %s", common.InventoryLabel) - } - return inventoryLabel, nil -} - -// ValidateNoInventory takes a set of unstructured.Unstructured objects and -// validates that no inventory object is in the input slice. -func ValidateNoInventory(objs object.UnstructuredSet) error { - invs := make(object.UnstructuredSet, 0) - for _, obj := range objs { - if IsInventoryObject(obj) { - invs = append(invs, obj) - } - } - if len(invs) == 0 { - return nil - } - return &MultipleInventoryObjError{ - InventoryObjectTemplates: invs, - } -} - -// splitUnstructureds takes a set of unstructured.Unstructured objects and -// splits it into one set that contains the inventory object templates and -// another one that contains the remaining resources. Returns an error if there -// there is no inventory object or more than one inventory objects. -func SplitUnstructureds(objs object.UnstructuredSet) (*unstructured.Unstructured, object.UnstructuredSet, error) { - invs := make(object.UnstructuredSet, 0) - resources := make(object.UnstructuredSet, 0) - for _, obj := range objs { - if IsInventoryObject(obj) { - invs = append(invs, obj) - } else { - resources = append(resources, obj) - } - } - var inv *unstructured.Unstructured - var err error - switch len(invs) { - case 0: - err = &NoInventoryObjError{} - case 1: - inv = invs[0] - default: - err = &MultipleInventoryObjError{InventoryObjectTemplates: invs} - } - return inv, resources, err -} - -// addSuffixToName adds the passed suffix (usually a hash) as a suffix -// to the name of the passed object stored in the Info struct. Returns -// an error if name stored in the object differs from the name in -// the Info struct. -func addSuffixToName(obj *unstructured.Unstructured, suffix string) error { - suffix = strings.TrimSpace(suffix) - if len(suffix) == 0 { - return fmt.Errorf("passed empty suffix") - } - - name := obj.GetName() - if name != obj.GetName() { - return fmt.Errorf("inventory object (%s) and resource.Info (%s) have different names", name, obj.GetName()) - } - // Error if name already has suffix. - suffix = "-" + suffix - if strings.HasSuffix(name, suffix) { - return fmt.Errorf("name already has suffix: %s", name) - } - name += suffix - obj.SetName(name) - - return nil -} - -// fixLegacyInventoryName modifies the inventory name if it is -// the legacy default name (i.e. inventory) by adding a random suffix. -// This fixes a problem where inventory object names collide if -// they are created in the same namespace. -func fixLegacyInventoryName(obj *unstructured.Unstructured) error { - if obj.GetName() == legacyInvName { - klog.V(4).Infof("renaming legacy inventory name") - randomSuffix := common.RandomStr() - return addSuffixToName(obj, randomSuffix) - } - return nil -} diff --git a/pkg/inventory/type-conv.go b/pkg/inventory/type-conv.go deleted file mode 100644 index 8509840a..00000000 --- a/pkg/inventory/type-conv.go +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package inventory - -import ( - "github.com/fluxcd/cli-utils/pkg/apis/actuation" - "github.com/fluxcd/cli-utils/pkg/object" - "k8s.io/apimachinery/pkg/runtime/schema" -) - -// ObjectReferenceFromObjMetadata converts an ObjMetadata to a ObjectReference -func ObjectReferenceFromObjMetadata(id object.ObjMetadata) actuation.ObjectReference { - return actuation.ObjectReference{ - Group: id.GroupKind.Group, - Kind: id.GroupKind.Kind, - Name: id.Name, - Namespace: id.Namespace, - } -} - -// ObjMetadataFromObjectReference converts an ObjectReference to a ObjMetadata -func ObjMetadataFromObjectReference(ref actuation.ObjectReference) object.ObjMetadata { - return object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: ref.Group, - Kind: ref.Kind, - }, - Name: ref.Name, - Namespace: ref.Namespace, - } -} diff --git a/pkg/jsonpath/jsonpath.go b/pkg/jsonpath/jsonpath.go deleted file mode 100644 index 728a5f09..00000000 --- a/pkg/jsonpath/jsonpath.go +++ /dev/null @@ -1,188 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package jsonpath - -import ( - "encoding/json" - "fmt" - - // Using gopkg.in/yaml.v3 instead of sigs.k8s.io/yaml on purpose. - // yaml.v3 correctly parses ints: - // https://github.com/kubernetes-sigs/yaml/issues/45 - // yaml.v3 Node is also used as input to yqlib. - "gopkg.in/yaml.v3" - "k8s.io/klog/v2" - - "github.com/spyzhov/ajson" -) - -// Get evaluates the JSONPath expression to extract values from the input map. -// Returns the node values that were found (zero or more), or an error. -// For details about the JSONPath expression language, see: -// https://goessner.net/articles/JsonPath/ -func Get(obj map[string]interface{}, expression string) ([]interface{}, error) { - // format input object as json for input into jsonpath library - jsonBytes, err := json.Marshal(obj) - if err != nil { - return nil, fmt.Errorf("failed to marshal input to json: %w", err) - } - - klog.V(7).Infof("jsonpath.Get input as json:\n%s", jsonBytes) - - // parse json into an ajson node - root, err := ajson.Unmarshal(jsonBytes) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal input json: %w", err) - } - - // find nodes that match the expression - nodes, err := root.JSONPath(expression) - if err != nil { - return nil, fmt.Errorf("failed to evaluate jsonpath expression (%s): %w", expression, err) - } - - result := make([]interface{}, len(nodes)) - - // get value of all matching nodes - for i, node := range nodes { - // format node value as json - jsonBytes, err = ajson.Marshal(node) - if err != nil { - return nil, fmt.Errorf("failed to marshal jsonpath result to json: %w", err) - } - - klog.V(7).Infof("jsonpath.Get output as json:\n%s", jsonBytes) - - // parse json back into a Go primitive - var value interface{} - err = yaml.Unmarshal(jsonBytes, &value) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal jsonpath result: %w", err) - } - result[i] = value - } - - return result, nil -} - -// Set evaluates the JSONPath expression to set a value in the input map. -// Returns the number of matching nodes that were updated, or an error. -// For details about the JSONPath expression language, see: -// https://goessner.net/articles/JsonPath/ -func Set(obj map[string]interface{}, expression string, value interface{}) (int, error) { - // format input object as json for input into jsonpath library - jsonBytes, err := json.Marshal(obj) - if err != nil { - return 0, fmt.Errorf("failed to marshal input to json: %w", err) - } - - klog.V(7).Infof("jsonpath.Set input as json:\n%s", jsonBytes) - - // parse json into an ajson node - root, err := ajson.Unmarshal(jsonBytes) - if err != nil { - return 0, fmt.Errorf("failed to unmarshal input json: %w", err) - } - - // retrieve nodes that match the expression - nodes, err := root.JSONPath(expression) - if err != nil { - return 0, fmt.Errorf("failed to evaluate jsonpath expression (%s): %w", expression, err) - } - if len(nodes) == 0 { - // zero nodes found, none updated - return 0, nil - } - - // set value of all matching nodes - for _, node := range nodes { - switch typedValue := value.(type) { - case bool: - err = node.SetBool(typedValue) - case string: - err = node.SetString(typedValue) - case int: - err = node.SetNumeric(float64(typedValue)) - case float64: - err = node.SetNumeric(typedValue) - case []interface{}: - var arrayValue []*ajson.Node - arrayValue, err = toArrayOfNodes(typedValue) - if err != nil { - break - } - err = node.SetArray(arrayValue) - case map[string]interface{}: - var mapValue map[string]*ajson.Node - mapValue, err = toMapOfNodes(typedValue) - if err != nil { - break - } - err = node.SetObject(mapValue) - default: - if value == nil { - err = node.SetNull() - } else { - err = fmt.Errorf("unsupported value type: %T", value) - } - } - if err != nil { - return 0, err - } - } - - // format into an ajson node - jsonBytes, err = ajson.Marshal(root) - if err != nil { - return 0, fmt.Errorf("failed to marshal jsonpath result to json: %w", err) - } - - klog.V(7).Infof("jsonpath.Set output as json:\n%s", jsonBytes) - - // parse json back into the input map - err = yaml.Unmarshal(jsonBytes, &obj) - if err != nil { - return 0, fmt.Errorf("failed to unmarshal jsonpath result: %w", err) - } - - return len(nodes), nil -} - -func toArrayOfNodes(obj []interface{}) ([]*ajson.Node, error) { - out := make([]*ajson.Node, len(obj)) - for index, value := range obj { - // format input object as json for input into jsonpath library - jsonBytes, err := json.Marshal(value) - if err != nil { - return nil, fmt.Errorf("failed to marshal array element to json: %w", err) - } - - // parse json into an ajson node - node, err := ajson.Unmarshal(jsonBytes) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal array element: %w", err) - } - out[index] = node - } - return out, nil -} - -func toMapOfNodes(obj map[string]interface{}) (map[string]*ajson.Node, error) { - out := make(map[string]*ajson.Node, len(obj)) - for key, value := range obj { - // format input object as json for input into jsonpath library - jsonBytes, err := json.Marshal(value) - if err != nil { - return nil, fmt.Errorf("failed to marshal map value to json: %w", err) - } - - // parse json into an ajson node - node, err := ajson.Unmarshal(jsonBytes) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal map value: %w", err) - } - out[key] = node - } - return out, nil -} diff --git a/pkg/jsonpath/jsonpath_test.go b/pkg/jsonpath/jsonpath_test.go deleted file mode 100644 index 303c9fd2..00000000 --- a/pkg/jsonpath/jsonpath_test.go +++ /dev/null @@ -1,256 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package jsonpath - -import ( - "testing" - - ktestutil "github.com/fluxcd/cli-utils/pkg/kstatus/polling/testutil" - "github.com/stretchr/testify/require" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "sigs.k8s.io/yaml" -) - -var o1y = ` -apiVersion: v1 -kind: Pod -metadata: - name: pod-name - namespace: pod-namespace -list: -- 1 -- b -- false -map: - a: - - "1" - - "2" - - "3" - b: null - c: - - x - - ?: null - - z -entries: -- name: a - value: x -- name: b - value: "y" -- name: c - value: z -` - -var o2y = ` -apiVersion: v1 -kind: Pod -metadata: - name: pod-name - namespace: pod-namespace -list: -- null -- null -- null -map: - a: - - null - - null - - null - b: null - c: - - null - - ?: null - - null -entries: -- name: a - value: x -- name: b - value: "y" -- name: c - value: z -` - -func TestGet(t *testing.T) { - o1 := ktestutil.YamlToUnstructured(t, o1y) - - testCases := map[string]struct { - obj *unstructured.Unstructured - path string - values []interface{} - errMsg string - }{ - "missing": { - obj: o1, - path: "$.nope", - values: []interface{}{}, - }, - "invalid jsonpath": { - obj: o1, - path: "$.nope[", - values: nil, - errMsg: "failed to evaluate jsonpath expression ($.nope[): unexpected end of file", - }, - "string": { - obj: o1, - path: "$.kind", - values: []interface{}{"Pod"}, - }, - "string in map": { - obj: o1, - path: "$.metadata.name", - values: []interface{}{"pod-name"}, - }, - "int in array": { - obj: o1, - path: "$.list[0]", - values: []interface{}{1}, - }, - "string in array": { - obj: o1, - path: "$.list[1]", - values: []interface{}{"b"}, - }, - "bool in array": { - obj: o1, - path: "$.list[2]", - values: []interface{}{false}, - }, - "string in array in map": { - obj: o1, - path: "$.map.c[2]", - values: []interface{}{"z"}, - }, - "nil in map in array in map": { - obj: o1, - path: "$.map.c[1][\"?\"]", - values: []interface{}{nil}, - }, - "array in map": { - obj: o1, - path: "$.map.a", - values: []interface{}{[]interface{}{"1", "2", "3"}}, - }, - "array values": { - obj: o1, - path: "$.map.a.*", - values: []interface{}{"1", "2", "3"}, - }, - "field selector": { - obj: o1, - path: `$.entries[?(@.name=="b")].value`, - values: []interface{}{"y"}, - }, - "multi-field selector": { - obj: o1, - path: `$.entries[?(@.name=="a" || @.name=="c")].value`, - values: []interface{}{"x", "z"}, - }, - } - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - testCtx := []interface{}{"path: %s\nobject:\n%s", tc.path, toYaml(t, tc.obj.Object)} - values, err := Get(tc.obj.Object, tc.path) - if tc.errMsg != "" { - require.EqualError(t, err, tc.errMsg, testCtx...) - } else { - require.NoError(t, err, testCtx...) - } - require.Equal(t, tc.values, values, testCtx...) - }) - } -} - -func TestSet(t *testing.T) { - testCases := map[string]struct { - obj *unstructured.Unstructured - path string - value interface{} - found int - err error - }{ - "string": { - obj: ktestutil.YamlToUnstructured(t, o2y), - path: "$.kind", - value: "Pod", - found: 1, - }, - "string in map": { - obj: ktestutil.YamlToUnstructured(t, o2y), - path: "$.metadata.name", - value: "pod-name", - found: 1, - }, - "int in array": { - obj: ktestutil.YamlToUnstructured(t, o2y), - path: "$.list[0]", - value: 1, - found: 1, - }, - "string in array": { - obj: ktestutil.YamlToUnstructured(t, o2y), - path: "$.list[1]", - value: "b", - found: 1, - }, - "bool in array": { - obj: ktestutil.YamlToUnstructured(t, o2y), - path: "$.list[2]", - value: false, - found: 1, - }, - "string in array in map": { - obj: ktestutil.YamlToUnstructured(t, o2y), - path: "$.map.c[2]", - value: "z", - found: 1, - }, - "nil in map in array in map": { - obj: ktestutil.YamlToUnstructured(t, o2y), - path: "$.map.c[1][\"?\"]", - value: nil, - found: 1, - }, - "array in map": { - obj: ktestutil.YamlToUnstructured(t, o2y), - path: "$.map.a", - value: []interface{}{"1", "2", "3"}, - found: 1, - }, - "field selector": { - obj: ktestutil.YamlToUnstructured(t, o2y), - path: `$.entries[?(@.name=="b")].value`, - value: "k", - found: 1, - }, - "multi-field selector": { - obj: ktestutil.YamlToUnstructured(t, o2y), - path: `$.entries[?(@.name=="a" || @.name=="c")].value`, - value: "k", - found: 2, - }, - } - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - found, err := Set(tc.obj.Object, tc.path, tc.value) - testCtx := []interface{}{"path: %s\nobject (mutated):\n%s", tc.path, toYaml(t, tc.obj.Object)} - require.Equal(t, tc.err, err, testCtx...) - require.Equal(t, tc.found, found, testCtx...) - - values, err := Get(tc.obj.Object, tc.path) - require.NoError(t, err, testCtx...) - for i, value := range values { - testCtx := []interface{}{"path: %s\nindex: %d\nobject (mutated):\n%s", tc.path, i, toYaml(t, tc.obj.Object)} - require.IsType(t, tc.value, value, testCtx...) - require.Equal(t, tc.value, value, testCtx...) - } - }) - } -} - -func toYaml(t *testing.T, in interface{}) string { - yamlBytes, err := yaml.Marshal(in) - require.NoError(t, err) - return string(yamlBytes) -} diff --git a/pkg/jsonpath/main_test.go b/pkg/jsonpath/main_test.go deleted file mode 100644 index a39516f7..00000000 --- a/pkg/jsonpath/main_test.go +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package jsonpath - -import ( - "os" - "testing" - - "k8s.io/klog/v2" -) - -// TestMain executes the tests for this package, with logging enabled. -// go test github.com/fluxcd/cli-utils/pkg/jsonpath -v -args -v=5 -func TestMain(m *testing.M) { - klog.InitFlags(nil) - os.Exit(m.Run()) -} diff --git a/pkg/kstatus/.golangci.yml b/pkg/kstatus/.golangci.yml deleted file mode 100644 index 6bbfd890..00000000 --- a/pkg/kstatus/.golangci.yml +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright 2019 The Kubernetes Authors. -# SPDX-License-Identifier: Apache-2.0 - -run: - deadline: 30m - -linters: - # please, do not use `enable-all`: it's deprecated and will be removed soon. - # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint - disable-all: true - enable: - - bodyclose - - deadcode - # - depguard - - dogsled - - dupl - - errcheck - # - funlen - - gochecknoinits - - goconst - - gocritic - - gocyclo - - gofmt - - goimports - - golint - - gosec - - gosimple - - govet - - ineffassign - - interfacer - - lll - - misspell - - nakedret - - scopelint - - staticcheck - - structcheck - - stylecheck - - typecheck - - unconvert - - unparam - - unused - - varcheck - - whitespace - - -linters-settings: - dupl: - threshold: 400 - lll: - line-length: 170 - gocyclo: - min-complexity: 30 - golint: - min-confidence: 0.85 diff --git a/pkg/manifestreader/common.go b/pkg/manifestreader/common.go deleted file mode 100644 index ba4ee433..00000000 --- a/pkg/manifestreader/common.go +++ /dev/null @@ -1,162 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package manifestreader - -import ( - "encoding/json" - "errors" - "fmt" - "strings" - - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/pkg/object" - "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "sigs.k8s.io/kustomize/kyaml/kio/filters" - "sigs.k8s.io/kustomize/kyaml/kio/kioutil" - "sigs.k8s.io/kustomize/kyaml/yaml" -) - -// UnknownTypesError captures information about unknown types encountered. -type UnknownTypesError struct { - GroupVersionKinds []schema.GroupVersionKind -} - -func (e *UnknownTypesError) Error() string { - var gvks []string - for _, gvk := range e.GroupVersionKinds { - gvks = append(gvks, fmt.Sprintf("%s/%s/%s", - gvk.Group, gvk.Version, gvk.Kind)) - } - return fmt.Sprintf("unknown resource types: %s", strings.Join(gvks, ",")) -} - -// NamespaceMismatchError is returned if all resources must be in a specific -// namespace, and resources are found using other namespaces. -type NamespaceMismatchError struct { - RequiredNamespace string - Namespace string -} - -func (e *NamespaceMismatchError) Error() string { - return fmt.Sprintf("found namespace %q, but all resources must be in namespace %q", - e.Namespace, e.RequiredNamespace) -} - -// SetNamespaces verifies that every namespaced resource has the namespace -// set, and if one does not, it will set the namespace to the provided -// defaultNamespace. -// This implementation will check each resource (that doesn't already have -// the namespace set) on whether it is namespace or cluster scoped. It does -// this by first checking the RESTMapper, and it there is not match there, -// it will look for CRDs in the provided Unstructureds. -func SetNamespaces(mapper meta.RESTMapper, objs []*unstructured.Unstructured, - defaultNamespace string, enforceNamespace bool) error { - var crdObjs []*unstructured.Unstructured - - // find any crds in the set of resources. - for _, obj := range objs { - if object.IsCRD(obj) { - crdObjs = append(crdObjs, obj) - } - } - - var unknownGVKs []schema.GroupVersionKind - for _, obj := range objs { - // Exclude any inventory objects here since we don't want to change - // their namespace. - if inventory.IsInventoryObject(obj) { - continue - } - - // Look up the scope of the resource so we know if the resource - // should have a namespace set or not. - scope, err := object.LookupResourceScope(obj, crdObjs, mapper) - if err != nil { - var unknownTypeError *object.UnknownTypeError - if errors.As(err, &unknownTypeError) { - // If no scope was found, just add the resource type to the list - // of unknown types. - unknownGVKs = append(unknownGVKs, unknownTypeError.GroupVersionKind) - continue - } - // If something went wrong when looking up the scope, just - // give up. - return err - } - - switch scope { - case meta.RESTScopeNamespace: - if obj.GetNamespace() == "" { - obj.SetNamespace(defaultNamespace) - } else { - ns := obj.GetNamespace() - if enforceNamespace && ns != defaultNamespace { - return &NamespaceMismatchError{ - Namespace: ns, - RequiredNamespace: defaultNamespace, - } - } - } - case meta.RESTScopeRoot: - if ns := obj.GetNamespace(); ns != "" { - return fmt.Errorf("resource is cluster-scoped but has a non-empty namespace %q", ns) - } - default: - return fmt.Errorf("unknown RESTScope %q", scope.Name()) - } - } - if len(unknownGVKs) > 0 { - return &UnknownTypesError{ - GroupVersionKinds: unknownGVKs, - } - } - return nil -} - -// FilterLocalConfig returns a new slice of Unstructured where all resources -// with the LocalConfig annotation is filtered out. -func FilterLocalConfig(objs []*unstructured.Unstructured) []*unstructured.Unstructured { - var filteredObjs []*unstructured.Unstructured - for _, obj := range objs { - // Ignoring the value of the LocalConfigAnnotation here. This is to be - // consistent with the behavior in the kyaml library: - // https://github.com/kubernetes-sigs/kustomize/blob/30b58e90a39485bc5724b2278651c5d26b815cb2/kyaml/kio/filters/local.go#L29 - if _, found := obj.GetAnnotations()[filters.LocalConfigAnnotation]; !found { - filteredObjs = append(filteredObjs, obj) - } - } - return filteredObjs -} - -// RemoveAnnotations removes the specified kioutil annotations from the resource. -func RemoveAnnotations(n *yaml.RNode, annotations ...kioutil.AnnotationKey) error { - for _, a := range annotations { - err := n.PipeE(yaml.ClearAnnotation(a)) - if err != nil { - return err - } - } - return nil -} - -// KyamlNodeToUnstructured take a resource represented as a kyaml RNode and -// turns it into an Unstructured object. -func KyamlNodeToUnstructured(n *yaml.RNode) (*unstructured.Unstructured, error) { - b, err := n.MarshalJSON() - if err != nil { - return nil, err - } - - var m map[string]interface{} - err = json.Unmarshal(b, &m) - if err != nil { - return nil, err - } - - return &unstructured.Unstructured{ - Object: m, - }, nil -} diff --git a/pkg/manifestreader/common_test.go b/pkg/manifestreader/common_test.go deleted file mode 100644 index 8d455dca..00000000 --- a/pkg/manifestreader/common_test.go +++ /dev/null @@ -1,408 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package manifestreader - -import ( - "fmt" - "testing" - - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - cmdtesting "k8s.io/kubectl/pkg/cmd/testing" - "sigs.k8s.io/kustomize/kyaml/kio/filters" - "sigs.k8s.io/kustomize/kyaml/kio/kioutil" - "sigs.k8s.io/kustomize/kyaml/yaml" -) - -func TestSetNamespaces(t *testing.T) { - testCases := map[string]struct { - objs []*unstructured.Unstructured - defaultNamespace string - enforceNamespace bool - - expectedNamespaces []string - expectedErr error - }{ - "resources already have namespace": { - objs: []*unstructured.Unstructured{ - toUnstructured(schema.GroupVersionKind{ - Group: "apps", - Version: "v1", - Kind: "Deployment", - }, "default"), - toUnstructured(schema.GroupVersionKind{ - Group: "policy", - Version: "v1beta1", - Kind: "PodDisruptionBudget", - }, "default"), - }, - defaultNamespace: "foo", - enforceNamespace: false, - expectedNamespaces: []string{ - "default", - "default", - }, - }, - "resources without namespace and mapping in RESTMapper": { - objs: []*unstructured.Unstructured{ - toUnstructured(schema.GroupVersionKind{ - Group: "apps", - Version: "v1", - Kind: "Deployment", - }, ""), - }, - defaultNamespace: "foo", - enforceNamespace: false, - expectedNamespaces: []string{"foo"}, - }, - "resource with namespace that does match enforced ns": { - objs: []*unstructured.Unstructured{ - toUnstructured(schema.GroupVersionKind{ - Group: "apps", - Version: "v1", - Kind: "Deployment", - }, "bar"), - }, - defaultNamespace: "bar", - enforceNamespace: true, - expectedNamespaces: []string{"bar"}, - }, - "resource with namespace that doesn't match enforced ns": { - objs: []*unstructured.Unstructured{ - toUnstructured(schema.GroupVersionKind{ - Group: "apps", - Version: "v1", - Kind: "Deployment", - }, "foo"), - }, - defaultNamespace: "bar", - enforceNamespace: true, - expectedErr: &NamespaceMismatchError{ - RequiredNamespace: "bar", - Namespace: "foo", - }, - }, - "cluster-scoped CR with CRD": { - objs: []*unstructured.Unstructured{ - toUnstructured(schema.GroupVersionKind{ - Group: "custom.io", - Version: "v1", - Kind: "Custom", - }, ""), - toCRDUnstructured(schema.GroupVersionKind{ - Group: "apiextensions.k8s.io", - Version: "v1", - Kind: "CustomResourceDefinition", - }, schema.GroupVersionKind{ - Group: "custom.io", - Version: "v1", - Kind: "Custom", - }, "Cluster"), - }, - defaultNamespace: "bar", - enforceNamespace: true, - expectedNamespaces: []string{"", ""}, - }, - "namespace-scoped CR with CRD": { - objs: []*unstructured.Unstructured{ - toCRDUnstructured(schema.GroupVersionKind{ - Group: "apiextensions.k8s.io", - Version: "v1", - Kind: "CustomResourceDefinition", - }, schema.GroupVersionKind{ - Group: "custom.io", - Version: "v1", - Kind: "Custom", - }, "Namespaced"), - toUnstructured(schema.GroupVersionKind{ - Group: "custom.io", - Version: "v1", - Kind: "Custom", - }, ""), - }, - defaultNamespace: "bar", - enforceNamespace: true, - expectedNamespaces: []string{"", "bar"}, - }, - "unknown types in CRs": { - objs: []*unstructured.Unstructured{ - toUnstructured(schema.GroupVersionKind{ - Group: "custom.io", - Version: "v1", - Kind: "Custom", - }, ""), - toUnstructured(schema.GroupVersionKind{ - Group: "custom.io", - Version: "v1", - Kind: "AnotherCustom", - }, ""), - }, - expectedErr: &UnknownTypesError{ - GroupVersionKinds: []schema.GroupVersionKind{ - { - Group: "custom.io", - Version: "v1", - Kind: "Custom", - }, - { - Group: "custom.io", - Version: "v1", - Kind: "AnotherCustom", - }, - }, - }, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - tf := cmdtesting.NewTestFactory().WithNamespace("namespace") - defer tf.Cleanup() - - mapper, err := tf.ToRESTMapper() - require.NoError(t, err) - crdGV := schema.GroupVersion{Group: "apiextensions.k8s.io", Version: "v1"} - crdMapper := meta.NewDefaultRESTMapper([]schema.GroupVersion{crdGV}) - crdMapper.AddSpecific(crdGV.WithKind("CustomResourceDefinition"), - crdGV.WithResource("customresourcedefinitions"), - crdGV.WithResource("customresourcedefinition"), meta.RESTScopeRoot) - mapper = meta.MultiRESTMapper([]meta.RESTMapper{mapper, crdMapper}) - - err = SetNamespaces(mapper, tc.objs, tc.defaultNamespace, tc.enforceNamespace) - - if tc.expectedErr != nil { - require.Error(t, err) - assert.Equal(t, tc.expectedErr, err) - return - } - - require.NoError(t, err) - - for i, obj := range tc.objs { - assert.Equal(t, tc.expectedNamespaces[i], obj.GetNamespace()) - } - }) - } -} - -var ( - depID = object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - }, - Namespace: "default", - Name: "foo", - } - - clusterRoleID = object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "rbac.authorization.k8s.io", - Kind: "ClusterRole", - }, - Name: "bar", - } -) - -func TestFilterLocalConfigs(t *testing.T) { - testCases := map[string]struct { - input []*unstructured.Unstructured - expected []string - }{ - "don't filter if no annotation": { - input: []*unstructured.Unstructured{ - objMetaToUnstructured(depID), - objMetaToUnstructured(clusterRoleID), - }, - expected: []string{ - depID.Name, - clusterRoleID.Name, - }, - }, - "filter all if all have annotation": { - input: []*unstructured.Unstructured{ - addAnnotation(t, objMetaToUnstructured(depID), filters.LocalConfigAnnotation, "true"), - addAnnotation(t, objMetaToUnstructured(clusterRoleID), filters.LocalConfigAnnotation, "false"), - }, - expected: []string{}, - }, - "filter even if resource have other annotations": { - input: []*unstructured.Unstructured{ - addAnnotation(t, - addAnnotation( - t, objMetaToUnstructured(depID), - filters.LocalConfigAnnotation, "true"), - "my-annotation", "foo"), - }, - expected: []string{}, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - res := FilterLocalConfig(tc.input) - - var names []string - for _, obj := range res { - names = append(names, obj.GetName()) - } - - // Avoid test failures due to nil slice and empty slice - // not being equal. - if len(tc.expected) == 0 && len(names) == 0 { - return - } - assert.Equal(t, tc.expected, names) - }) - } -} - -func TestRemoveAnnotations(t *testing.T) { - testCases := map[string]struct { - node *yaml.RNode - removeAnnotations []kioutil.AnnotationKey - expectedAnnotations []kioutil.AnnotationKey - }{ - "filter both kioutil annotations": { - node: yaml.MustParse(` -apiVersion: apps/v1 -kind: Deployment -metadata: - name: foo - annotations: - config.kubernetes.io/path: deployment.yaml - config.kubernetes.io/index: 0 -`), - removeAnnotations: []kioutil.AnnotationKey{ - kioutil.PathAnnotation, - kioutil.IndexAnnotation, - }, - }, - "filter only a subset of the annotations": { - node: yaml.MustParse(` -apiVersion: apps/v1 -kind: Deployment -metadata: - name: foo - annotations: - internal.config.kubernetes.io/path: deployment.yaml - internal.config.kubernetes.io/index: 0 -`), - removeAnnotations: []kioutil.AnnotationKey{ - kioutil.IndexAnnotation, - }, - expectedAnnotations: []kioutil.AnnotationKey{ - kioutil.PathAnnotation, - }, - }, - "filter none of the annotations": { - node: yaml.MustParse(` -apiVersion: apps/v1 -kind: Deployment -metadata: - name: foo - annotations: - internal.config.kubernetes.io/path: deployment.yaml - internal.config.kubernetes.io/index: 0 -`), - removeAnnotations: []kioutil.AnnotationKey{}, - expectedAnnotations: []kioutil.AnnotationKey{ - kioutil.PathAnnotation, - kioutil.IndexAnnotation, - }, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - node := tc.node - err := RemoveAnnotations(node, tc.removeAnnotations...) - if !assert.NoError(t, err) { - t.FailNow() - } - - for _, anno := range tc.removeAnnotations { - n, err := node.Pipe(yaml.GetAnnotation(anno)) - if !assert.NoError(t, err) { - t.FailNow() - } - assert.Nil(t, n) - } - for _, anno := range tc.expectedAnnotations { - n, err := node.Pipe(yaml.GetAnnotation(anno)) - if !assert.NoError(t, err) { - t.FailNow() - } - assert.NotNil(t, n) - } - }) - } -} - -func toUnstructured(gvk schema.GroupVersionKind, namespace string) *unstructured.Unstructured { - return &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": gvk.GroupVersion().String(), - "kind": gvk.Kind, - "metadata": map[string]interface{}{ - "namespace": namespace, - }, - }, - } -} - -func toCRDUnstructured(crdGvk schema.GroupVersionKind, crGvk schema.GroupVersionKind, - scope string) *unstructured.Unstructured { - return &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": crdGvk.GroupVersion().String(), - "kind": crdGvk.Kind, - "spec": map[string]interface{}{ - "group": crGvk.Group, - "names": map[string]interface{}{ - "kind": crGvk.Kind, - }, - "scope": scope, - "versions": []interface{}{ - map[string]interface{}{ - "name": crGvk.Version, - }, - }, - }, - }, - } -} - -func objMetaToUnstructured(id object.ObjMetadata) *unstructured.Unstructured { - return &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": fmt.Sprintf("%s/v1", id.GroupKind.Group), - "kind": id.GroupKind.Kind, - "metadata": map[string]interface{}{ - "namespace": id.Namespace, - "name": id.Name, - }, - }, - } -} - -func addAnnotation(t *testing.T, u *unstructured.Unstructured, name, val string) *unstructured.Unstructured { - annos, found, err := unstructured.NestedStringMap(u.Object, "metadata", "annotations") - if err != nil { - t.Fatal(err) - } - if !found { - annos = make(map[string]string) - } - annos[name] = val - err = unstructured.SetNestedStringMap(u.Object, annos, "metadata", "annotations") - if err != nil { - t.Fatal(err) - } - return u -} diff --git a/pkg/manifestreader/fake-loader.go b/pkg/manifestreader/fake-loader.go deleted file mode 100644 index f980928f..00000000 --- a/pkg/manifestreader/fake-loader.go +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package manifestreader - -import ( - "io" - - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/pkg/object" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/kubectl/pkg/cmd/util" -) - -type FakeLoader struct { - Factory util.Factory - InvClient *inventory.FakeClient -} - -var _ ManifestLoader = &FakeLoader{} - -func NewFakeLoader(f util.Factory, objs object.ObjMetadataSet) *FakeLoader { - return &FakeLoader{ - Factory: f, - InvClient: inventory.NewFakeClient(objs), - } -} - -func (f *FakeLoader) ManifestReader(reader io.Reader, _ string) (ManifestReader, error) { - mapper, err := f.Factory.ToRESTMapper() - if err != nil { - return nil, err - } - - readerOptions := ReaderOptions{ - Mapper: mapper, - Namespace: metav1.NamespaceDefault, - } - return &StreamManifestReader{ - ReaderName: "stdin", - Reader: reader, - ReaderOptions: readerOptions, - }, nil -} - -func (f *FakeLoader) InventoryInfo(objs []*unstructured.Unstructured) (inventory.Info, []*unstructured.Unstructured, error) { - inv, objs, err := inventory.SplitUnstructureds(objs) - return inventory.WrapInventoryInfoObj(inv), objs, err -} diff --git a/pkg/manifestreader/manifestloader.go b/pkg/manifestreader/manifestloader.go deleted file mode 100644 index e89cd3b6..00000000 --- a/pkg/manifestreader/manifestloader.go +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package manifestreader - -import ( - "io" - - "k8s.io/kubectl/pkg/cmd/util" -) - -// ManifestLoader is an interface for reading -// and parsing the resources -type ManifestLoader interface { - ManifestReader(reader io.Reader, path string) (ManifestReader, error) -} - -// manifestLoader implements the ManifestLoader interface -type manifestLoader struct { - factory util.Factory -} - -// NewManifestLoader returns an instance of manifestLoader. -func NewManifestLoader(f util.Factory) ManifestLoader { - return &manifestLoader{ - factory: f, - } -} - -func (f *manifestLoader) ManifestReader(reader io.Reader, path string) (ManifestReader, error) { - // Fetch the namespace from the configloader. The source of this - // either the namespace flag or the context. If the namespace is provided - // with the flag, enforceNamespace will be true. In this case, it is - // an error if any of the resources in the package has a different - // namespace set. - namespace, enforceNamespace, err := f.factory.ToRawKubeConfigLoader().Namespace() - if err != nil { - return nil, err - } - - mapper, err := f.factory.ToRESTMapper() - if err != nil { - return nil, err - } - - readerOptions := ReaderOptions{ - Mapper: mapper, - Namespace: namespace, - EnforceNamespace: enforceNamespace, - } - - return mReader(path, reader, readerOptions), nil -} - -// mReader returns the ManifestReader based in the input args -func mReader(path string, reader io.Reader, readerOptions ReaderOptions) ManifestReader { - var mReader ManifestReader - // Read from stdin if "-" is specified, similar to kubectl - if path == "-" { - mReader = &StreamManifestReader{ - ReaderName: "stdin", - Reader: reader, - ReaderOptions: readerOptions, - } - } else { - mReader = &PathManifestReader{ - Path: path, - ReaderOptions: readerOptions, - } - } - return mReader -} diff --git a/pkg/manifestreader/manifestloader_test.go b/pkg/manifestreader/manifestloader_test.go deleted file mode 100644 index 4128431e..00000000 --- a/pkg/manifestreader/manifestloader_test.go +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package manifestreader - -import ( - "os" - "path/filepath" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - cmdtesting "k8s.io/kubectl/pkg/cmd/testing" -) - -func TestMReader_Read(t *testing.T) { - testCases := map[string]struct { - manifests map[string]string - namespace string - enforceNamespace bool - validate bool - path string - - infosCount int - namespaces []string - }{ - "path mReader: namespace should be set if not already present": { - namespace: "foo", - enforceNamespace: true, - path: "${reader-test-dir}", - infosCount: 1, - namespaces: []string{"foo"}, - }, - "stream mReader: namespace should be set if not already present": { - namespace: "foo", - enforceNamespace: true, - path: "-", - infosCount: 1, - namespaces: []string{"foo"}, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - tf := cmdtesting.NewTestFactory().WithNamespace("test-ns") - defer tf.Cleanup() - - mapper, err := tf.ToRESTMapper() - if !assert.NoError(t, err) { - t.FailNow() - } - - dir, err := os.MkdirTemp("", "reader-test") - assert.NoError(t, err) - p := filepath.Join(dir, "dep.yaml") - err = os.WriteFile(p, []byte(depManifest), 0600) - assert.NoError(t, err) - stringReader := strings.NewReader(depManifest) - - if tc.path == "${reader-test-dir}" { - tc.path = dir - } - - objs, err := mReader(tc.path, stringReader, ReaderOptions{ - Mapper: mapper, - Namespace: tc.namespace, - EnforceNamespace: tc.enforceNamespace, - Validate: tc.validate, - }).Read() - - assert.NoError(t, err) - assert.Equal(t, len(objs), tc.infosCount) - - for i, obj := range objs { - assert.Equal(t, tc.namespaces[i], obj.GetNamespace()) - } - }) - } -} diff --git a/pkg/manifestreader/manifestreader.go b/pkg/manifestreader/manifestreader.go deleted file mode 100644 index 171cc334..00000000 --- a/pkg/manifestreader/manifestreader.go +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package manifestreader - -import ( - "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -// ManifestReader defines the interface for reading a set -// of manifests into info objects. -type ManifestReader interface { - Read() ([]*unstructured.Unstructured, error) -} - -// ReaderOptions defines the shared inputs for the different -// implementations of the ManifestReader interface. -type ReaderOptions struct { - Mapper meta.RESTMapper - Validate bool - Namespace string - EnforceNamespace bool -} diff --git a/pkg/manifestreader/path.go b/pkg/manifestreader/path.go deleted file mode 100644 index b0f3cce0..00000000 --- a/pkg/manifestreader/path.go +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package manifestreader - -import ( - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "sigs.k8s.io/kustomize/kyaml/kio" - "sigs.k8s.io/kustomize/kyaml/kio/kioutil" -) - -// PathManifestReader implements ManifestReader interface. -var _ ManifestReader = &PathManifestReader{} - -// PathManifestReader reads manifests from the provided path -// and returns them as Info objects. The returned Infos will not have -// client or mapping set. -type PathManifestReader struct { - Path string - - ReaderOptions -} - -// Read reads the manifests and returns them as Info objects. -func (p *PathManifestReader) Read() ([]*unstructured.Unstructured, error) { - var objs []*unstructured.Unstructured - nodes, err := (&kio.LocalPackageReader{ - PackagePath: p.Path, - }).Read() - if err != nil { - return objs, err - } - - for _, n := range nodes { - err = RemoveAnnotations(n, kioutil.IndexAnnotation) - if err != nil { - return objs, err - } - u, err := KyamlNodeToUnstructured(n) - if err != nil { - return objs, err - } - objs = append(objs, u) - } - - objs = FilterLocalConfig(objs) - - err = SetNamespaces(p.Mapper, objs, p.Namespace, p.EnforceNamespace) - return objs, err -} diff --git a/pkg/manifestreader/path_test.go b/pkg/manifestreader/path_test.go deleted file mode 100644 index 110ca8f6..00000000 --- a/pkg/manifestreader/path_test.go +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package manifestreader - -import ( - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - cmdtesting "k8s.io/kubectl/pkg/cmd/testing" - "sigs.k8s.io/kustomize/kyaml/kio/kioutil" -) - -func TestPathManifestReader_Read(t *testing.T) { - testCases := map[string]struct { - manifests map[string]string - namespace string - enforceNamespace bool - validate bool - - infosCount int - namespaces []string - }{ - "namespace should be set if not already present": { - manifests: map[string]string{ - "dep.yaml": depManifest, - }, - namespace: "foo", - enforceNamespace: true, - - infosCount: 1, - namespaces: []string{"foo"}, - }, - "multiple manifests": { - manifests: map[string]string{ - "dep.yaml": depManifest, - "cm.yaml": cmManifest, - }, - namespace: "default", - enforceNamespace: true, - - infosCount: 2, - namespaces: []string{"default", "default"}, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - tf := cmdtesting.NewTestFactory().WithNamespace("test-ns") - defer tf.Cleanup() - - mapper, err := tf.ToRESTMapper() - if !assert.NoError(t, err) { - t.FailNow() - } - - dir, err := os.MkdirTemp("", "path-reader-test") - assert.NoError(t, err) - for filename, content := range tc.manifests { - p := filepath.Join(dir, filename) - err := os.WriteFile(p, []byte(content), 0600) - assert.NoError(t, err) - } - - objs, err := (&PathManifestReader{ - Path: dir, - ReaderOptions: ReaderOptions{ - Mapper: mapper, - Namespace: tc.namespace, - EnforceNamespace: tc.enforceNamespace, - Validate: tc.validate, - }, - }).Read() - - assert.NoError(t, err) - assert.Equal(t, len(objs), tc.infosCount) - - for i, obj := range objs { - assert.Equal(t, tc.namespaces[i], obj.GetNamespace()) - _, ok := obj.GetAnnotations()[kioutil.PathAnnotation] - assert.True(t, ok) - } - }) - } -} diff --git a/pkg/manifestreader/stream.go b/pkg/manifestreader/stream.go deleted file mode 100644 index d786b724..00000000 --- a/pkg/manifestreader/stream.go +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package manifestreader - -import ( - "io" - - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "sigs.k8s.io/kustomize/kyaml/kio" - "sigs.k8s.io/kustomize/kyaml/kio/kioutil" -) - -// StreamManifestReader implements ManifestReader interface. -var _ ManifestReader = &StreamManifestReader{} - -// StreamManifestReader reads manifest from the provided io.Reader -// and returns them as Info objects. The returned Infos will not have -// client or mapping set. -type StreamManifestReader struct { - ReaderName string - Reader io.Reader - - ReaderOptions -} - -// Read reads the manifests and returns them as Info objects. -func (r *StreamManifestReader) Read() ([]*unstructured.Unstructured, error) { - var objs []*unstructured.Unstructured - nodes, err := (&kio.ByteReader{ - Reader: r.Reader, - }).Read() - if err != nil { - return objs, err - } - - for _, n := range nodes { - err = RemoveAnnotations(n, kioutil.IndexAnnotation) - if err != nil { - return objs, err - } - u, err := KyamlNodeToUnstructured(n) - if err != nil { - return objs, err - } - objs = append(objs, u) - } - - objs = FilterLocalConfig(objs) - - err = SetNamespaces(r.Mapper, objs, r.Namespace, r.EnforceNamespace) - return objs, err -} diff --git a/pkg/manifestreader/stream_test.go b/pkg/manifestreader/stream_test.go deleted file mode 100644 index 41c1d29a..00000000 --- a/pkg/manifestreader/stream_test.go +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package manifestreader - -import ( - "strings" - "testing" - - "github.com/stretchr/testify/assert" - cmdtesting "k8s.io/kubectl/pkg/cmd/testing" -) - -func TestStreamManifestReader_Read(t *testing.T) { - testCases := map[string]struct { - manifests string - namespace string - enforceNamespace bool - validate bool - - infosCount int - namespaces []string - }{ - "namespace should be set if not already present": { - manifests: depManifest, - namespace: "foo", - enforceNamespace: true, - - infosCount: 1, - namespaces: []string{"foo"}, - }, - "multiple resources": { - manifests: depManifest + "\n---\n" + cmManifest, - namespace: "bar", - enforceNamespace: false, - - infosCount: 2, - namespaces: []string{"bar", "bar"}, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - tf := cmdtesting.NewTestFactory().WithNamespace("test-ns") - defer tf.Cleanup() - - mapper, err := tf.ToRESTMapper() - if !assert.NoError(t, err) { - t.FailNow() - } - - stringReader := strings.NewReader(tc.manifests) - - objs, err := (&StreamManifestReader{ - ReaderName: "testReader", - Reader: stringReader, - ReaderOptions: ReaderOptions{ - Mapper: mapper, - Namespace: tc.namespace, - EnforceNamespace: tc.enforceNamespace, - Validate: tc.validate, - }, - }).Read() - - assert.NoError(t, err) - assert.Equal(t, len(objs), tc.infosCount) - - for i, obj := range objs { - assert.Equal(t, tc.namespaces[i], obj.GetNamespace()) - } - }) - } -} diff --git a/pkg/manifestreader/testdata.go b/pkg/manifestreader/testdata.go deleted file mode 100644 index b68ab600..00000000 --- a/pkg/manifestreader/testdata.go +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package manifestreader - -var ( - depManifest = ` -kind: Deployment -apiVersion: apps/v1 -metadata: - name: dep -spec: - replicas: 1 -` - - cmManifest = ` -kind: ConfigMap -apiVersion: v1 -metadata: - name: cm -data: - foo: bar -` -) diff --git a/pkg/multierror/multierror.go b/pkg/multierror/multierror.go deleted file mode 100644 index 016fed69..00000000 --- a/pkg/multierror/multierror.go +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright 2022 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package multierror - -import ( - "fmt" - "strings" -) - -const Prefix = "- " -const Indent = " " - -type Interface interface { - Errors() []error -} - -// New returns a new MultiError wrapping the specified error list. -func New(causes ...error) *MultiError { - return &MultiError{ - Causes: causes, - } -} - -// MultiError wraps multiple errors and formats them for multi-line output. -type MultiError struct { - Causes []error -} - -func (mve *MultiError) Errors() []error { - return mve.Causes -} - -func (mve *MultiError) Error() string { - if len(mve.Causes) == 1 { - return mve.Causes[0].Error() - } - var b strings.Builder - _, _ = fmt.Fprintf(&b, "%d errors:\n", len(mve.Causes)) - for _, err := range mve.Causes { - _, _ = fmt.Fprintf(&b, "%s\n", formatError(err)) - } - return b.String() -} - -func formatError(err error) string { - lines := strings.Split(err.Error(), "\n") - return Prefix + strings.Join(lines, fmt.Sprintf("\n%s", Indent)) -} - -// Wrap merges zero or more errors and/or MultiErrors into one error. -// MultiErrors are recursively unwrapped to reduce depth. -// If only one error is received, that error is returned without a wrapper. -func Wrap(errs ...error) error { - if len(errs) == 0 { - return nil - } - errs = Unwrap(errs...) - var err error - switch { - case len(errs) == 0: - err = nil - case len(errs) == 1: - err = errs[0] - case len(errs) > 1: - err = &MultiError{ - Causes: errs, - } - } - return err -} - -// Unwrap flattens zero or more errors and/or MultiErrors into a list of errors. -// MultiErrors are recursively unwrapped to reduce depth. -func Unwrap(errs ...error) []error { - if len(errs) == 0 { - return nil - } - var errors []error - for _, err := range errs { - if mve, ok := err.(Interface); ok { - // Recursively unwrap MultiErrors - for _, cause := range mve.Errors() { - errors = append(errors, Unwrap(cause)...) - } - } else { - errors = append(errors, err) - } - } - return errors -} diff --git a/pkg/object/dependson/annotation.go b/pkg/object/dependson/annotation.go deleted file mode 100644 index 0fc8d878..00000000 --- a/pkg/object/dependson/annotation.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 -// - -package dependson - -import ( - "errors" - "fmt" - - "github.com/fluxcd/cli-utils/pkg/object" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/klog/v2" -) - -const ( - Annotation = "config.kubernetes.io/depends-on" -) - -// HasAnnotation returns true if the config.kubernetes.io/depends-on annotation -// is present, false if not. -func HasAnnotation(u *unstructured.Unstructured) bool { - if u == nil { - return false - } - _, found := u.GetAnnotations()[Annotation] - return found -} - -// ReadAnnotation reads the depends-on annotation and parses the the set of -// object references. -func ReadAnnotation(u *unstructured.Unstructured) (DependencySet, error) { - depSet := DependencySet{} - if u == nil { - return depSet, nil - } - depSetStr, found := u.GetAnnotations()[Annotation] - if !found { - return depSet, nil - } - klog.V(5).Infof("depends-on annotation found for %s/%s: %q", - u.GetNamespace(), u.GetName(), depSetStr) - - depSet, err := ParseDependencySet(depSetStr) - if err != nil { - return depSet, object.InvalidAnnotationError{ - Annotation: Annotation, - Cause: err, - } - } - return depSet, nil -} - -// WriteAnnotation updates the supplied unstructured object to add the -// depends-on annotation. The value is a string of objmetas delimited by commas. -// Each objmeta is formatted as "${group}/${kind}/${name}" if cluster-scoped or -// "${group}/namespaces/${namespace}/${kind}/${name}" if namespace-scoped. -func WriteAnnotation(obj *unstructured.Unstructured, depSet DependencySet) error { - if obj == nil { - return errors.New("object is nil") - } - if depSet.Equal(DependencySet{}) { - return errors.New("dependency set is empty") - } - - depSetStr, err := FormatDependencySet(depSet) - if err != nil { - return fmt.Errorf("failed to format depends-on annotation: %w", err) - } - - a := obj.GetAnnotations() - if a == nil { - a = map[string]string{} - } - a[Annotation] = depSetStr - obj.SetAnnotations(a) - return nil -} diff --git a/pkg/object/dependson/annotation_test.go b/pkg/object/dependson/annotation_test.go deleted file mode 100644 index c71057c9..00000000 --- a/pkg/object/dependson/annotation_test.go +++ /dev/null @@ -1,200 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 -// - -package dependson - -import ( - "testing" - - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -var u1 = &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "v1", - "kind": "ConfigMap", - "metadata": map[string]interface{}{ - "name": "unused", - "namespace": "unused", - "annotations": map[string]interface{}{ - Annotation: "test-group/test-kind/cluster-obj", - }, - }, - }, -} - -var u2 = &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "v1", - "kind": "ConfigMap", - "metadata": map[string]interface{}{ - "name": "unused", - "namespace": "unused", - "annotations": map[string]interface{}{ - Annotation: "test-group/namespaces/test-namespace/test-kind/namespaced-obj", - }, - }, - }, -} - -var multipleAnnotations = &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "v1", - "kind": "ConfigMap", - "metadata": map[string]interface{}{ - "name": "unused", - "namespace": "unused", - "annotations": map[string]interface{}{ - Annotation: "test-group/namespaces/test-namespace/test-kind/namespaced-obj," + - "test-group/test-kind/cluster-obj", - }, - }, - }, -} - -var noAnnotations = &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "v1", - "kind": "ConfigMap", - "metadata": map[string]interface{}{ - "name": "unused", - "namespace": "unused", - }, - }, -} - -var badAnnotation = &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "v1", - "kind": "ConfigMap", - "metadata": map[string]interface{}{ - "name": "unused", - "namespace": "unused", - "annotations": map[string]interface{}{ - Annotation: "test-group:namespaces:test-namespace:test-kind:namespaced-obj", - }, - }, - }, -} - -func TestReadAnnotation(t *testing.T) { - testCases := map[string]struct { - obj *unstructured.Unstructured - expected DependencySet - isError bool - }{ - "nil object is not found": { - obj: nil, - expected: DependencySet{}, - }, - "Object with no annotations returns not found": { - obj: noAnnotations, - expected: DependencySet{}, - }, - "Unparseable depends on annotation returns not found": { - obj: badAnnotation, - expected: DependencySet{}, - isError: true, - }, - "Cluster-scoped object depends on annotation": { - obj: u1, - expected: DependencySet{clusterScopedObj}, - }, - "Namespaced object depends on annotation": { - obj: u2, - expected: DependencySet{namespacedObj}, - }, - "Multiple objects specified in annotation": { - obj: multipleAnnotations, - expected: DependencySet{namespacedObj, clusterScopedObj}, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - actual, err := ReadAnnotation(tc.obj) - if tc.isError { - if err == nil { - t.Fatalf("expected error not received") - } - } else { - if err != nil { - t.Fatalf("unexpected error received: %s", err) - } - if !actual.Equal(tc.expected) { - t.Errorf("expected (%s), got (%s)", tc.expected, actual) - } - } - }) - } -} - -// getDependsOnAnnotation wraps the depends-on annotation with a pointer. -// Returns nil if the annotation is missing. -func getDependsOnAnnotation(obj *unstructured.Unstructured) *string { - value, found := obj.GetAnnotations()[Annotation] - if !found { - return nil - } - return &value -} - -func TestWriteAnnotation(t *testing.T) { - testCases := map[string]struct { - obj *unstructured.Unstructured - dependson DependencySet - expected *string - isError bool - }{ - "nil object": { - obj: nil, - dependson: DependencySet{}, - expected: nil, - isError: true, - }, - "empty mutation": { - obj: &unstructured.Unstructured{}, - dependson: DependencySet{}, - expected: nil, - isError: true, - }, - "Namespace-scoped object": { - obj: &unstructured.Unstructured{}, - dependson: DependencySet{namespacedObj}, - expected: getDependsOnAnnotation(u2), - }, - "Cluster-scoped object": { - obj: &unstructured.Unstructured{}, - dependson: DependencySet{clusterScopedObj}, - expected: getDependsOnAnnotation(u1), - }, - "Multiple objects": { - obj: &unstructured.Unstructured{}, - dependson: DependencySet{namespacedObj, clusterScopedObj}, - expected: getDependsOnAnnotation(multipleAnnotations), - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - err := WriteAnnotation(tc.obj, tc.dependson) - if tc.isError { - if err == nil { - t.Fatalf("expected error not received") - } - } else { - if err != nil { - t.Fatalf("unexpected error received: %s", err) - } - received := getDependsOnAnnotation(tc.obj) - if received != tc.expected && (received == nil || tc.expected == nil) { - t.Errorf("\nexpected:\t%#v\nreceived:\t%#v", tc.expected, received) - } - if *received != *tc.expected { - t.Errorf("\nexpected:\t%#v\nreceived:\t%#v", *tc.expected, *received) - } - } - }) - } -} diff --git a/pkg/object/dependson/strings.go b/pkg/object/dependson/strings.go deleted file mode 100644 index 7408e482..00000000 --- a/pkg/object/dependson/strings.go +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 -// - -package dependson - -import ( - "fmt" - "strings" - - "github.com/fluxcd/cli-utils/pkg/object" - "k8s.io/apimachinery/pkg/runtime/schema" -) - -const ( - // Number of fields for a cluster-scoped depends-on object value. Example: - // rbac.authorization.k8s.io/ClusterRole/my-cluster-role-name - numFieldsClusterScoped = 3 - // Number of fields for a namespace-scoped depends-on object value. Example: - // apps/namespaces/my-namespace/Deployment/my-deployment-name - numFieldsNamespacedScoped = 5 - // Used to separate multiple depends-on objects. - annotationSeparator = "," - // Used to separate the fields for a depends-on object value. - fieldSeparator = "/" - namespacesField = "namespaces" -) - -// FormatDependencySet formats the passed dependency set as a string. -// -// Object references are separated by ','. -// -// Returns the formatted DependencySet or an error if unable to format. -func FormatDependencySet(depSet DependencySet) (string, error) { - var dependsOnStr string - for i, depObj := range depSet { - if i > 0 { - dependsOnStr += annotationSeparator - } - objStr, err := FormatObjMetadata(depObj) - if err != nil { - return "", fmt.Errorf("failed to format object metadata (index: %d): %w", i, err) - } - dependsOnStr += objStr - } - return dependsOnStr, nil -} - -// ParseDependencySet parses the passed string as a set of object -// references. -// -// Object references are separated by ','. -// -// Returns the parsed DependencySet or an error if unable to parse. -func ParseDependencySet(depsStr string) (DependencySet, error) { - objs := DependencySet{} - for i, objStr := range strings.Split(depsStr, annotationSeparator) { - obj, err := ParseObjMetadata(objStr) - if err != nil { - return objs, fmt.Errorf("failed to parse object reference (index: %d): %w", i, err) - } - objs = append(objs, obj) - } - return objs, nil -} - -// FormatObjMetadata formats the passed object metadata as a string. -// -// Object references can have either three fields (cluster-scoped object) or -// five fields (namespace-scoped object). -// -// Fields are separated by '/'. -// -// Examples: -// -// Cluster-Scoped: // (3 fields) -// Namespaced: /namespaces/// (5 fields) -// -// Group and namespace may be empty, but name and kind may not. -// -// Returns the formatted ObjMetadata string or an error if unable to format. -func FormatObjMetadata(obj object.ObjMetadata) (string, error) { - gk := obj.GroupKind - // group and namespace are allowed to be empty, but name and kind are not - if gk.Kind == "" { - return "", fmt.Errorf("invalid object metadata: kind is empty") - } - if obj.Name == "" { - return "", fmt.Errorf("invalid object metadata: name is empty") - } - if obj.Namespace != "" { - return fmt.Sprintf("%s/namespaces/%s/%s/%s", gk.Group, obj.Namespace, gk.Kind, obj.Name), nil - } - return fmt.Sprintf("%s/%s/%s", gk.Group, gk.Kind, obj.Name), nil -} - -// ParseObjMetadata parses the passed string as a object metadata. -// -// Object references can have either three fields (cluster-scoped object) or -// five fields (namespace-scoped object). -// -// Fields are separated by '/'. -// -// Examples: -// -// Cluster-Scoped: // (3 fields) -// Namespaced: /namespaces/// (5 fields) -// -// Group and namespace may be empty, but name and kind may not. -// -// Returns the parsed ObjMetadata or an error if unable to parse. -func ParseObjMetadata(objStr string) (object.ObjMetadata, error) { - var obj object.ObjMetadata - var group, kind, namespace, name string - objStr = strings.TrimSpace(objStr) - fields := strings.Split(objStr, fieldSeparator) - - if len(fields) != numFieldsClusterScoped && len(fields) != numFieldsNamespacedScoped { - return obj, fmt.Errorf("expected %d or %d fields, found %d: %q", - numFieldsClusterScoped, numFieldsNamespacedScoped, len(fields), objStr) - } - - group = fields[0] - if len(fields) == 3 { - kind = fields[1] - name = fields[2] - } else { - if fields[1] != namespacesField { - return obj, fmt.Errorf("missing %q field: %q", namespacesField, objStr) - } - namespace = fields[2] - kind = fields[3] - name = fields[4] - } - - id := object.ObjMetadata{ - Namespace: namespace, - Name: name, - GroupKind: schema.GroupKind{ - Group: group, - Kind: kind, - }, - } - return id, nil -} diff --git a/pkg/object/dependson/strings_test.go b/pkg/object/dependson/strings_test.go deleted file mode 100644 index b9a6f8f9..00000000 --- a/pkg/object/dependson/strings_test.go +++ /dev/null @@ -1,277 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 -// - -package dependson - -import ( - "testing" - - "github.com/fluxcd/cli-utils/pkg/object" - "k8s.io/apimachinery/pkg/runtime/schema" -) - -var ( - clusterScopedObj = object.ObjMetadata{ - Name: "cluster-obj", - GroupKind: schema.GroupKind{ - Group: "test-group", - Kind: "test-kind", - }, - } - namespacedObj = object.ObjMetadata{ - Namespace: "test-namespace", - Name: "namespaced-obj", - GroupKind: schema.GroupKind{ - Group: "test-group", - Kind: "test-kind", - }, - } -) - -func TestParseDependencySet(t *testing.T) { - testCases := map[string]struct { - annotation string - expected DependencySet - isError bool - }{ - "empty annotation is error": { - annotation: "", - expected: DependencySet{}, - isError: true, - }, - "wrong number of namespace-scoped fields in annotation is error": { - annotation: "test-group/test-namespace/test-kind/namespaced-obj", - expected: DependencySet{}, - isError: true, - }, - "wrong number of cluster-scoped fields in annotation is error": { - annotation: "test-group/namespaces/test-kind/cluster-obj", - expected: DependencySet{}, - isError: true, - }, - "cluster-scoped object annotation": { - annotation: "test-group/test-kind/cluster-obj", - expected: DependencySet{clusterScopedObj}, - isError: false, - }, - "namespaced object annotation": { - annotation: "test-group/namespaces/test-namespace/test-kind/namespaced-obj", - expected: DependencySet{namespacedObj}, - isError: false, - }, - "namespaced object annotation with whitespace at ends is valid": { - annotation: " test-group/namespaces/test-namespace/test-kind/namespaced-obj\n", - expected: DependencySet{namespacedObj}, - isError: false, - }, - "multiple object annotation": { - annotation: "test-group/namespaces/test-namespace/test-kind/namespaced-obj," + - "test-group/test-kind/cluster-obj", - expected: DependencySet{clusterScopedObj, namespacedObj}, - isError: false, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - actual, err := ParseDependencySet(tc.annotation) - if err == nil && tc.isError { - t.Fatalf("expected error, but received none") - } - if err != nil && !tc.isError { - t.Errorf("unexpected error: %s", err) - } - if !actual.Equal(tc.expected) { - t.Errorf("expected (%s), got (%s)", tc.expected, actual) - } - }) - } -} - -func TestParseObjMetadata(t *testing.T) { - testCases := map[string]struct { - metaStr string - expected object.ObjMetadata - isError bool - }{ - "empty annotation is error": { - metaStr: "", - expected: object.ObjMetadata{}, - isError: true, - }, - "wrong number of namespace-scoped fields in annotation is error": { - metaStr: "test-group/test-namespace/test-kind/namespaced-obj", - expected: object.ObjMetadata{}, - isError: true, - }, - "wrong number of cluster-scoped fields in annotation is error": { - metaStr: "test-group/namespaces/test-kind/cluster-obj", - expected: object.ObjMetadata{}, - isError: true, - }, - "cluster-scoped object annotation": { - metaStr: "test-group/test-kind/cluster-obj", - expected: clusterScopedObj, - isError: false, - }, - "namespaced object annotation": { - metaStr: "test-group/namespaces/test-namespace/test-kind/namespaced-obj", - expected: namespacedObj, - isError: false, - }, - "namespaced object annotation with whitespace at ends is valid": { - metaStr: " test-group/namespaces/test-namespace/test-kind/namespaced-obj\n", - expected: namespacedObj, - isError: false, - }, - "multiple is error": { - metaStr: "test-group/namespaces/test-namespace/test-kind/namespaced-obj," + - "test-group/test-kind/cluster-obj", - expected: object.ObjMetadata{}, - isError: true, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - actual, err := ParseObjMetadata(tc.metaStr) - if err == nil && tc.isError { - t.Fatalf("expected error, but received none") - } - if err != nil && !tc.isError { - t.Errorf("unexpected error: %s", err) - } - if actual != tc.expected { - t.Errorf("expected (%s), got (%s)", tc.expected, actual) - } - }) - } -} - -func TestFormatDependencySet(t *testing.T) { - testCases := map[string]struct { - depSet DependencySet - expected string - isError bool - }{ - "empty set is not error": { - depSet: DependencySet{}, - expected: "", - }, - "missing kind is error": { - depSet: DependencySet{ - { - Name: "cluster-obj", - GroupKind: schema.GroupKind{ - Group: "test-group", - }, - }, - }, - expected: "", - isError: true, - }, - "missing name is error": { - depSet: DependencySet{ - { - GroupKind: schema.GroupKind{ - Group: "test-group", - Kind: "test-kind", - }, - }, - }, - expected: "", - isError: true, - }, - "cluster-scoped": { - depSet: DependencySet{clusterScopedObj}, - expected: "test-group/test-kind/cluster-obj", - isError: false, - }, - "namespace-scoped": { - depSet: DependencySet{namespacedObj}, - expected: "test-group/namespaces/test-namespace/test-kind/namespaced-obj", - isError: false, - }, - "multiple dependencies": { - depSet: DependencySet{clusterScopedObj, namespacedObj}, - expected: "test-group/test-kind/cluster-obj," + - "test-group/namespaces/test-namespace/test-kind/namespaced-obj", - isError: false, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - actual, err := FormatDependencySet(tc.depSet) - if err == nil && tc.isError { - t.Fatalf("expected error, but received none") - } - if err != nil && !tc.isError { - t.Errorf("unexpected error: %s", err) - } - if actual != tc.expected { - t.Errorf("expected (%s), got (%s)", tc.expected, actual) - } - }) - } -} - -func TestFormatObjMetadata(t *testing.T) { - testCases := map[string]struct { - objMeta object.ObjMetadata - expected string - isError bool - }{ - "empty is error": { - objMeta: object.ObjMetadata{}, - expected: "", - isError: true, - }, - "missing kind is error": { - objMeta: object.ObjMetadata{ - Name: "cluster-obj", - GroupKind: schema.GroupKind{ - Group: "test-group", - }, - }, - expected: "", - isError: true, - }, - "missing name is error": { - objMeta: object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "test-group", - Kind: "test-kind", - }, - }, - expected: "", - isError: true, - }, - "cluster-scoped": { - objMeta: clusterScopedObj, - expected: "test-group/test-kind/cluster-obj", - isError: false, - }, - "namespace-scoped": { - objMeta: namespacedObj, - expected: "test-group/namespaces/test-namespace/test-kind/namespaced-obj", - isError: false, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - actual, err := FormatObjMetadata(tc.objMeta) - if err == nil && tc.isError { - t.Fatalf("expected error, but received none") - } - if err != nil && !tc.isError { - t.Errorf("unexpected error: %s", err) - } - if actual != tc.expected { - t.Errorf("expected (%s), got (%s)", tc.expected, actual) - } - }) - } -} diff --git a/pkg/object/dependson/types.go b/pkg/object/dependson/types.go deleted file mode 100644 index 5fa67e86..00000000 --- a/pkg/object/dependson/types.go +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 -// - -package dependson - -import ( - "github.com/fluxcd/cli-utils/pkg/object" -) - -// DependencySet is a set of object references. -// When testing equality, order is not importent. -type DependencySet object.ObjMetadataSet - -// Equal returns true if the ObjMetadata sets are equivalent, ignoring order. -// Fulfills Equal interface from github.com/google/go-cmp -func (a DependencySet) Equal(b DependencySet) bool { - return object.ObjMetadataSet(a).Equal(object.ObjMetadataSet(b)) -} diff --git a/pkg/object/graph/depends.go b/pkg/object/graph/depends.go deleted file mode 100644 index b2363efc..00000000 --- a/pkg/object/graph/depends.go +++ /dev/null @@ -1,325 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -// This package provides a object sorting functionality -// based on the explicit "depends-on" annotation, and -// implicit object dependencies like namespaces and CRD's. -package graph - -import ( - "sort" - - "github.com/fluxcd/cli-utils/pkg/multierror" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/object/dependson" - "github.com/fluxcd/cli-utils/pkg/object/mutation" - "github.com/fluxcd/cli-utils/pkg/object/validation" - "github.com/fluxcd/cli-utils/pkg/ordering" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/klog/v2" -) - -// DependencyGraph returns a new graph, populated with the supplied objects as -// vetices and edges built from their dependencies. -func DependencyGraph(objs object.UnstructuredSet) (*Graph, error) { - g := New() - if len(objs) == 0 { - return g, nil - } - var errors []error - - // Convert to IDs (same length & order as objs) - // This is simply an optimiation to avoid repeating obj -> id conversion. - ids := object.UnstructuredSetToObjMetadataSet(objs) - - // Add objects as graph vertices - addVertices(g, ids) - // Add dependencies as graph edges - addCRDEdges(g, objs, ids) - addNamespaceEdges(g, objs, ids) - if err := addDependsOnEdges(g, objs, ids); err != nil { - errors = append(errors, err) - } - if err := addApplyTimeMutationEdges(g, objs, ids); err != nil { - errors = append(errors, err) - } - if len(errors) > 0 { - return g, multierror.Wrap(errors...) - } - return g, nil -} - -// HydrateSetList takes a list of sets of ids and a set of objects and returns -// a list of set of objects. The output set list will be the same order as the -// input set list, but with IDs converted into Objects. Any IDs that do not -// match objects in the provided object set will be skipped (filtered) in the -// output. -func HydrateSetList(idSetList []object.ObjMetadataSet, objs object.UnstructuredSet) []object.UnstructuredSet { - var objSetList []object.UnstructuredSet - - // Build a map of id -> obj. - objToUnstructured := map[object.ObjMetadata]*unstructured.Unstructured{} - for _, obj := range objs { - objToUnstructured[object.UnstructuredToObjMetadata(obj)] = obj - } - - // Map the object metadata back to the sorted sets of unstructured objects. - // Ignore any edges that aren't part of the input set (don't wait for them). - for _, objSet := range idSetList { - currentSet := object.UnstructuredSet{} - for _, id := range objSet { - var found bool - var obj *unstructured.Unstructured - if obj, found = objToUnstructured[id]; found { - currentSet = append(currentSet, obj) - } - } - if len(currentSet) > 0 { - // Sort each set in apply order - sort.Sort(ordering.SortableUnstructureds(currentSet)) - objSetList = append(objSetList, currentSet) - } - } - - return objSetList -} - -// SortObjs returns a slice of the sets of objects to apply (in order). -// Each of the objects in an apply set is applied together. The order of -// the returned applied sets is a topological ordering of the sets to apply. -// Returns an single empty apply set if there are no objects to apply. -func SortObjs(objs object.UnstructuredSet) ([]object.UnstructuredSet, error) { - var errors []error - if len(objs) == 0 { - return nil, nil - } - - g, err := DependencyGraph(objs) - if err != nil { - // collect and continue - errors = multierror.Unwrap(err) - } - - idSetList, err := g.Sort() - if err != nil { - errors = append(errors, err) - } - - objSetList := HydrateSetList(idSetList, objs) - - if len(errors) > 0 { - return objSetList, multierror.Wrap(errors...) - } - return objSetList, nil -} - -// ReverseSortObjs is the same as SortObjs but using reverse ordering. -func ReverseSortObjs(objs object.UnstructuredSet) ([]object.UnstructuredSet, error) { - // Sorted objects using normal ordering. - s, err := SortObjs(objs) - if err != nil { - return s, err - } - ReverseSetList(s) - return s, nil -} - -// ReverseSetList deep reverses of a list of object lists -func ReverseSetList(setList []object.UnstructuredSet) { - // Reverse the ordering of the object sets using swaps. - for i, j := 0, len(setList)-1; i < j; i, j = i+1, j-1 { - setList[i], setList[j] = setList[j], setList[i] - } - // Reverse the ordering of the objects in each set using swaps. - for _, set := range setList { - for i, j := 0, len(set)-1; i < j; i, j = i+1, j-1 { - set[i], set[j] = set[j], set[i] - } - } -} - -// addVertices adds all the IDs in the set as graph vertices. -func addVertices(g *Graph, ids object.ObjMetadataSet) { - for _, id := range ids { - klog.V(3).Infof("adding vertex: %s", id) - g.AddVertex(id) - } -} - -// addApplyTimeMutationEdges updates the graph with edges from objects -// with an explicit "apply-time-mutation" annotation. -// The objs and ids must match in order and length (optimization). -func addApplyTimeMutationEdges(g *Graph, objs object.UnstructuredSet, ids object.ObjMetadataSet) error { - var errors []error - for i, obj := range objs { - if !mutation.HasAnnotation(obj) { - continue - } - id := ids[i] - subs, err := mutation.ReadAnnotation(obj) - if err != nil { - klog.V(3).Infof("failed to add edges from: %s: %v", id, err) - errors = append(errors, validation.NewError(err, id)) - continue - } - seen := make(map[object.ObjMetadata]struct{}) - var objErrors []error - for _, sub := range subs { - dep := sub.SourceRef.ToObjMetadata() - // Duplicate dependencies can be safely skipped. - if _, found := seen[dep]; found { - continue - } - // Mark as seen - seen[dep] = struct{}{} - // Require dependencies to be in the same resource group. - // Waiting for external dependencies isn't implemented (yet). - if !ids.Contains(dep) { - err := object.InvalidAnnotationError{ - Annotation: mutation.Annotation, - Cause: ExternalDependencyError{ - Edge: Edge{ - From: id, - To: dep, - }, - }, - } - objErrors = append(objErrors, err) - klog.V(3).Infof("failed to add edges: %v", err) - continue - } - klog.V(3).Infof("adding edge from: %s, to: %s", id, dep) - g.AddEdge(id, dep) - } - if len(objErrors) > 0 { - errors = append(errors, - validation.NewError(multierror.Wrap(objErrors...), id)) - } - } - if len(errors) > 0 { - return multierror.Wrap(errors...) - } - return nil -} - -// addDependsOnEdges updates the graph with edges from objects -// with an explicit "depends-on" annotation. -// The objs and ids must match in order and length (optimization). -func addDependsOnEdges(g *Graph, objs object.UnstructuredSet, ids object.ObjMetadataSet) error { - var errors []error - for i, obj := range objs { - if !dependson.HasAnnotation(obj) { - continue - } - id := ids[i] - deps, err := dependson.ReadAnnotation(obj) - if err != nil { - klog.V(3).Infof("failed to add edges from: %s: %v", id, err) - errors = append(errors, validation.NewError(err, id)) - continue - } - seen := make(map[object.ObjMetadata]struct{}) - var objErrors []error - for _, dep := range deps { - // Duplicate dependencies in the same annotation are not allowed. - // Having duplicates won't break the graph, but skip it anyway. - if _, found := seen[dep]; found { - err := object.InvalidAnnotationError{ - Annotation: dependson.Annotation, - Cause: DuplicateDependencyError{ - Edge: Edge{ - From: id, - To: dep, - }, - }, - } - objErrors = append(objErrors, err) - klog.V(3).Infof("failed to add edges from: %s: %v", id, err) - continue - } - // Mark as seen - seen[dep] = struct{}{} - // Require dependencies to be in the same resource group. - // Waiting for external dependencies isn't implemented (yet). - if !ids.Contains(dep) { - err := object.InvalidAnnotationError{ - Annotation: dependson.Annotation, - Cause: ExternalDependencyError{ - Edge: Edge{ - From: id, - To: dep, - }, - }, - } - objErrors = append(objErrors, err) - klog.V(3).Infof("failed to add edges: %v", err) - continue - } - klog.V(3).Infof("adding edge from: %s, to: %s", id, dep) - g.AddEdge(id, dep) - } - if len(objErrors) > 0 { - errors = append(errors, - validation.NewError(multierror.Wrap(objErrors...), id)) - } - } - if len(errors) > 0 { - return multierror.Wrap(errors...) - } - return nil -} - -// addCRDEdges adds edges to the dependency graph from custom -// resources to their definitions to ensure the CRD's exist -// before applying the custom resources created with the definition. -// The objs and ids must match in order and length (optimization). -func addCRDEdges(g *Graph, objs object.UnstructuredSet, ids object.ObjMetadataSet) { - crds := map[string]object.ObjMetadata{} - // First create a map of all the CRD's. - for i, u := range objs { - if object.IsCRD(u) { - groupKind, found := object.GetCRDGroupKind(u) - if found { - crds[groupKind.String()] = ids[i] - } - } - } - // Iterate through all resources to see if we are applying any - // custom resources defined by previously recorded CRD's. - for i, u := range objs { - gvk := u.GroupVersionKind() - groupKind := gvk.GroupKind() - if to, found := crds[groupKind.String()]; found { - from := ids[i] - klog.V(3).Infof("adding edge from: custom resource %s, to CRD: %s", from, to) - g.AddEdge(from, to) - } - } -} - -// addNamespaceEdges adds edges to the dependency graph from namespaced -// objects to the namespace objects. Ensures the namespaces exist -// before the resources in those namespaces are applied. -// The objs and ids must match in order and length (optimization). -func addNamespaceEdges(g *Graph, objs object.UnstructuredSet, ids object.ObjMetadataSet) { - namespaces := map[string]object.ObjMetadata{} - // First create a map of all the namespaces objects live in. - for i, obj := range objs { - if object.IsKindNamespace(obj) { - namespace := obj.GetName() - namespaces[namespace] = ids[i] - } - } - // Next, if the namespace of a namespaced object is being applied, - // then create an edge from the namespaced object to its namespace. - for i, obj := range objs { - if object.IsNamespaced(obj) { - objNamespace := obj.GetNamespace() - if to, found := namespaces[objNamespace]; found { - from := ids[i] - klog.V(3).Infof("adding edge from: %s to namespace: %s", from, to) - g.AddEdge(from, to) - } - } - } -} diff --git a/pkg/object/graph/depends_test.go b/pkg/object/graph/depends_test.go deleted file mode 100644 index 81640193..00000000 --- a/pkg/object/graph/depends_test.go +++ /dev/null @@ -1,1644 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package graph - -import ( - "errors" - "testing" - - "github.com/fluxcd/cli-utils/pkg/multierror" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/object/dependson" - "github.com/fluxcd/cli-utils/pkg/object/mutation" - mutationutil "github.com/fluxcd/cli-utils/pkg/object/mutation/testutil" - "github.com/fluxcd/cli-utils/pkg/object/validation" - "github.com/fluxcd/cli-utils/pkg/testutil" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" -) - -var ( - resources = map[string]string{ - "pod": ` -kind: Pod -apiVersion: v1 -metadata: - name: test-pod - namespace: test-namespace -`, - "default-pod": ` -kind: Pod -apiVersion: v1 -metadata: - name: pod-in-default-namespace - namespace: default -`, - "deployment": ` -kind: Deployment -apiVersion: apps/v1 -metadata: - name: foo - namespace: test-namespace - uid: dep-uid - generation: 1 -spec: - replicas: 1 -`, - "secret": ` -kind: Secret -apiVersion: v1 -metadata: - name: secret - namespace: test-namespace - uid: secret-uid - generation: 1 -type: Opaque -spec: - foo: bar -`, - "namespace": ` -kind: Namespace -apiVersion: v1 -metadata: - name: test-namespace -`, - - "crd": ` -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: crontabs.stable.example.com -spec: - group: stable.example.com - versions: - - name: v1 - served: true - storage: true - scope: Namespaced - names: - plural: crontabs - singular: crontab - kind: CronTab -`, - "crontab1": ` -apiVersion: "stable.example.com/v1" -kind: CronTab -metadata: - name: cron-tab-01 - namespace: test-namespace -`, - "crontab2": ` -apiVersion: "stable.example.com/v1" -kind: CronTab -metadata: - name: cron-tab-02 - namespace: test-namespace -`, - } -) - -func TestSortObjs(t *testing.T) { - testCases := map[string]struct { - objs []*unstructured.Unstructured - expected []object.UnstructuredSet - isError bool - }{ - "no objects returns no object sets": { - objs: []*unstructured.Unstructured{}, - expected: []object.UnstructuredSet{}, - isError: false, - }, - "one object returns single object set": { - objs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["deployment"]), - }, - expected: []object.UnstructuredSet{ - { - testutil.Unstructured(t, resources["deployment"]), - }, - }, - isError: false, - }, - "two unrelated objects returns single object set with two objs": { - objs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["deployment"]), - testutil.Unstructured(t, resources["secret"]), - }, - expected: []object.UnstructuredSet{ - { - testutil.Unstructured(t, resources["deployment"]), - testutil.Unstructured(t, resources["secret"]), - }, - }, - isError: false, - }, - "one object depends on the other; two single object sets": { - objs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["deployment"], - testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), - testutil.Unstructured(t, resources["secret"]), - }, - expected: []object.UnstructuredSet{ - { - testutil.Unstructured(t, resources["secret"]), - }, - { - testutil.Unstructured(t, resources["deployment"]), - }, - }, - isError: false, - }, - "three objects depend on another; three single object sets": { - objs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["deployment"], - testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), - testutil.Unstructured(t, resources["secret"], - testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["pod"]))), - testutil.Unstructured(t, resources["pod"]), - }, - expected: []object.UnstructuredSet{ - { - testutil.Unstructured(t, resources["pod"]), - }, - { - testutil.Unstructured(t, resources["secret"]), - }, - { - testutil.Unstructured(t, resources["deployment"]), - }, - }, - isError: false, - }, - "Two objects depend on secret; two object sets": { - objs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["deployment"], - testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), - testutil.Unstructured(t, resources["pod"], - testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), - testutil.Unstructured(t, resources["secret"]), - }, - expected: []object.UnstructuredSet{ - { - testutil.Unstructured(t, resources["secret"]), - }, - { - testutil.Unstructured(t, resources["pod"]), - testutil.Unstructured(t, resources["deployment"]), - }, - }, - isError: false, - }, - "two objects applied with their namespace; two object sets": { - objs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["deployment"]), - testutil.Unstructured(t, resources["namespace"]), - testutil.Unstructured(t, resources["secret"]), - }, - expected: []object.UnstructuredSet{ - { - testutil.Unstructured(t, resources["namespace"]), - }, - { - testutil.Unstructured(t, resources["secret"]), - testutil.Unstructured(t, resources["deployment"]), - }, - }, - isError: false, - }, - "two custom resources applied with their CRD; two object sets": { - objs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["crontab1"]), - testutil.Unstructured(t, resources["crontab2"]), - testutil.Unstructured(t, resources["crd"]), - }, - expected: []object.UnstructuredSet{ - { - testutil.Unstructured(t, resources["crd"]), - }, - { - testutil.Unstructured(t, resources["crontab1"]), - testutil.Unstructured(t, resources["crontab2"]), - }, - }, - isError: false, - }, - "two custom resources wit CRD and namespace; two object sets": { - objs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["crontab1"]), - testutil.Unstructured(t, resources["crontab2"]), - testutil.Unstructured(t, resources["namespace"]), - testutil.Unstructured(t, resources["crd"]), - }, - expected: []object.UnstructuredSet{ - { - testutil.Unstructured(t, resources["crd"]), - testutil.Unstructured(t, resources["namespace"]), - }, - { - testutil.Unstructured(t, resources["crontab1"]), - testutil.Unstructured(t, resources["crontab2"]), - }, - }, - isError: false, - }, - "two objects depends on each other is cyclic dependency": { - objs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["deployment"], - testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), - testutil.Unstructured(t, resources["secret"], - testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["deployment"]))), - }, - expected: []object.UnstructuredSet{}, - isError: true, - }, - "three objects in cyclic dependency": { - objs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["deployment"], - testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), - testutil.Unstructured(t, resources["secret"], - testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["pod"]))), - testutil.Unstructured(t, resources["pod"], - testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["deployment"]))), - }, - expected: []object.UnstructuredSet{}, - isError: true, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - actual, err := SortObjs(tc.objs) - if tc.isError { - assert.NotNil(t, err, "expected error, but received none") - return - } - assert.Nil(t, err, "unexpected error received") - verifyObjSets(t, tc.expected, actual) - }) - } -} - -func TestReverseSortObjs(t *testing.T) { - testCases := map[string]struct { - objs []*unstructured.Unstructured - expected []object.UnstructuredSet - isError bool - }{ - "no objects returns no object sets": { - objs: []*unstructured.Unstructured{}, - expected: []object.UnstructuredSet{}, - isError: false, - }, - "one object returns single object set": { - objs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["deployment"]), - }, - expected: []object.UnstructuredSet{ - { - testutil.Unstructured(t, resources["deployment"]), - }, - }, - isError: false, - }, - "three objects depend on another; three single object sets in opposite order": { - objs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["deployment"], - testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), - testutil.Unstructured(t, resources["secret"], - testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["pod"]))), - testutil.Unstructured(t, resources["pod"]), - }, - expected: []object.UnstructuredSet{ - { - testutil.Unstructured(t, resources["deployment"]), - }, - { - testutil.Unstructured(t, resources["secret"]), - }, - { - testutil.Unstructured(t, resources["pod"]), - }, - }, - isError: false, - }, - "two objects applied with their namespace; two sets in opposite order": { - objs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["deployment"]), - testutil.Unstructured(t, resources["namespace"]), - testutil.Unstructured(t, resources["secret"]), - }, - expected: []object.UnstructuredSet{ - { - testutil.Unstructured(t, resources["secret"]), - testutil.Unstructured(t, resources["deployment"]), - }, - { - testutil.Unstructured(t, resources["namespace"]), - }, - }, - isError: false, - }, - "two custom resources wit CRD and namespace; two object sets in opposite order": { - objs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["crontab1"]), - testutil.Unstructured(t, resources["crontab2"]), - testutil.Unstructured(t, resources["namespace"]), - testutil.Unstructured(t, resources["crd"]), - }, - expected: []object.UnstructuredSet{ - { - testutil.Unstructured(t, resources["crontab1"]), - testutil.Unstructured(t, resources["crontab2"]), - }, - { - testutil.Unstructured(t, resources["crd"]), - testutil.Unstructured(t, resources["namespace"]), - }, - }, - isError: false, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - actual, err := ReverseSortObjs(tc.objs) - if tc.isError { - assert.NotNil(t, err, "expected error, but received none") - return - } - assert.Nil(t, err, "unexpected error received") - verifyObjSets(t, tc.expected, actual) - }) - } -} - -func TestDependencyGraph(t *testing.T) { - // Use a custom Asserter to customize the graph options - asserter := testutil.NewAsserter( - cmpopts.EquateErrors(), - graphComparer(), - ) - - testCases := map[string]struct { - objs object.UnstructuredSet - graph *Graph - expectedError error - }{ - "no objects": { - objs: object.UnstructuredSet{}, - graph: New(), - }, - "one object no dependencies": { - objs: object.UnstructuredSet{ - testutil.Unstructured(t, resources["deployment"]), - }, - graph: &Graph{ - edges: map[object.ObjMetadata]object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["deployment"]): {}, - }, - reverseEdges: map[object.ObjMetadata]object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["deployment"]): {}, - }, - }, - }, - "two unrelated objects": { - objs: object.UnstructuredSet{ - testutil.Unstructured(t, resources["deployment"]), - testutil.Unstructured(t, resources["secret"]), - }, - graph: &Graph{ - edges: map[object.ObjMetadata]object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["deployment"]): {}, - testutil.ToIdentifier(t, resources["secret"]): {}, - }, - reverseEdges: map[object.ObjMetadata]object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["deployment"]): {}, - testutil.ToIdentifier(t, resources["secret"]): {}, - }, - }, - }, - "two objects one dependency": { - objs: object.UnstructuredSet{ - testutil.Unstructured(t, resources["deployment"], - testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), - testutil.Unstructured(t, resources["secret"]), - }, - graph: &Graph{ - edges: map[object.ObjMetadata]object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["deployment"]): { - testutil.ToIdentifier(t, resources["secret"]), - }, - testutil.ToIdentifier(t, resources["secret"]): {}, - }, - reverseEdges: map[object.ObjMetadata]object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["deployment"]): {}, - testutil.ToIdentifier(t, resources["secret"]): { - testutil.ToIdentifier(t, resources["deployment"]), - }, - }, - }, - }, - "three objects two dependencies": { - objs: object.UnstructuredSet{ - testutil.Unstructured(t, resources["deployment"], - testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), - testutil.Unstructured(t, resources["secret"], - testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["pod"]))), - testutil.Unstructured(t, resources["pod"]), - }, - graph: &Graph{ - edges: map[object.ObjMetadata]object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["deployment"]): { - testutil.ToIdentifier(t, resources["secret"]), - }, - testutil.ToIdentifier(t, resources["secret"]): { - testutil.ToIdentifier(t, resources["pod"]), - }, - testutil.ToIdentifier(t, resources["pod"]): {}, - }, - reverseEdges: map[object.ObjMetadata]object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["pod"]): { - testutil.ToIdentifier(t, resources["secret"]), - }, - testutil.ToIdentifier(t, resources["secret"]): { - testutil.ToIdentifier(t, resources["deployment"]), - }, - testutil.ToIdentifier(t, resources["deployment"]): {}, - }, - }, - }, - "three objects two dependencies on the same object": { - objs: object.UnstructuredSet{ - testutil.Unstructured(t, resources["deployment"], - testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), - testutil.Unstructured(t, resources["pod"], - testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), - testutil.Unstructured(t, resources["secret"]), - }, - graph: &Graph{ - edges: map[object.ObjMetadata]object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["deployment"]): { - testutil.ToIdentifier(t, resources["secret"]), - }, - testutil.ToIdentifier(t, resources["pod"]): { - testutil.ToIdentifier(t, resources["secret"]), - }, - testutil.ToIdentifier(t, resources["secret"]): {}, - }, - reverseEdges: map[object.ObjMetadata]object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["secret"]): { - testutil.ToIdentifier(t, resources["deployment"]), - testutil.ToIdentifier(t, resources["pod"]), - }, - testutil.ToIdentifier(t, resources["pod"]): {}, - testutil.ToIdentifier(t, resources["deployment"]): {}, - }, - }, - }, - "two objects and their namespace": { - objs: object.UnstructuredSet{ - testutil.Unstructured(t, resources["deployment"]), - testutil.Unstructured(t, resources["namespace"]), - testutil.Unstructured(t, resources["secret"]), - }, - graph: &Graph{ - edges: map[object.ObjMetadata]object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["deployment"]): { - testutil.ToIdentifier(t, resources["namespace"]), - }, - testutil.ToIdentifier(t, resources["secret"]): { - testutil.ToIdentifier(t, resources["namespace"]), - }, - testutil.ToIdentifier(t, resources["namespace"]): {}, - }, - reverseEdges: map[object.ObjMetadata]object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["namespace"]): { - testutil.ToIdentifier(t, resources["secret"]), - testutil.ToIdentifier(t, resources["deployment"]), - }, - testutil.ToIdentifier(t, resources["secret"]): {}, - testutil.ToIdentifier(t, resources["deployment"]): {}, - }, - }, - }, - "two custom resources and their CRD": { - objs: object.UnstructuredSet{ - testutil.Unstructured(t, resources["crontab1"]), - testutil.Unstructured(t, resources["crontab2"]), - testutil.Unstructured(t, resources["crd"]), - }, - graph: &Graph{ - edges: map[object.ObjMetadata]object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["crontab1"]): { - testutil.ToIdentifier(t, resources["crd"]), - }, - testutil.ToIdentifier(t, resources["crontab2"]): { - testutil.ToIdentifier(t, resources["crd"]), - }, - testutil.ToIdentifier(t, resources["crd"]): {}, - }, - reverseEdges: map[object.ObjMetadata]object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["crd"]): { - testutil.ToIdentifier(t, resources["crontab1"]), - testutil.ToIdentifier(t, resources["crontab2"]), - }, - testutil.ToIdentifier(t, resources["crontab1"]): {}, - testutil.ToIdentifier(t, resources["crontab2"]): {}, - }, - }, - }, - "two custom resources with their CRD and namespace": { - objs: object.UnstructuredSet{ - testutil.Unstructured(t, resources["crontab1"]), - testutil.Unstructured(t, resources["crontab2"]), - testutil.Unstructured(t, resources["namespace"]), - testutil.Unstructured(t, resources["crd"]), - }, - graph: &Graph{ - edges: map[object.ObjMetadata]object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["crontab1"]): { - testutil.ToIdentifier(t, resources["crd"]), - testutil.ToIdentifier(t, resources["namespace"]), - }, - testutil.ToIdentifier(t, resources["crontab2"]): { - testutil.ToIdentifier(t, resources["crd"]), - testutil.ToIdentifier(t, resources["namespace"]), - }, - testutil.ToIdentifier(t, resources["crd"]): {}, - testutil.ToIdentifier(t, resources["namespace"]): {}, - }, - reverseEdges: map[object.ObjMetadata]object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["crd"]): { - testutil.ToIdentifier(t, resources["crontab1"]), - testutil.ToIdentifier(t, resources["crontab2"]), - }, - testutil.ToIdentifier(t, resources["namespace"]): { - testutil.ToIdentifier(t, resources["crontab1"]), - testutil.ToIdentifier(t, resources["crontab2"]), - }, - testutil.ToIdentifier(t, resources["crontab1"]): {}, - testutil.ToIdentifier(t, resources["crontab2"]): {}, - }, - }, - }, - "two object cyclic dependency": { - objs: object.UnstructuredSet{ - testutil.Unstructured(t, resources["deployment"], - testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), - testutil.Unstructured(t, resources["secret"], - testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["deployment"]))), - }, - graph: &Graph{ - edges: map[object.ObjMetadata]object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["deployment"]): { - testutil.ToIdentifier(t, resources["secret"]), - }, - testutil.ToIdentifier(t, resources["secret"]): { - testutil.ToIdentifier(t, resources["deployment"]), - }, - }, - reverseEdges: map[object.ObjMetadata]object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["secret"]): { - testutil.ToIdentifier(t, resources["deployment"]), - }, - testutil.ToIdentifier(t, resources["deployment"]): { - testutil.ToIdentifier(t, resources["secret"]), - }, - }, - }, - }, - "three object cyclic dependency": { - objs: object.UnstructuredSet{ - testutil.Unstructured(t, resources["deployment"], - testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), - testutil.Unstructured(t, resources["secret"], - testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["pod"]))), - testutil.Unstructured(t, resources["pod"], - testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["deployment"]))), - }, - graph: &Graph{ - edges: map[object.ObjMetadata]object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["deployment"]): { - testutil.ToIdentifier(t, resources["secret"]), - }, - testutil.ToIdentifier(t, resources["secret"]): { - testutil.ToIdentifier(t, resources["pod"]), - }, - testutil.ToIdentifier(t, resources["pod"]): { - testutil.ToIdentifier(t, resources["deployment"]), - }, - }, - reverseEdges: map[object.ObjMetadata]object.ObjMetadataSet{ - testutil.ToIdentifier(t, resources["deployment"]): { - testutil.ToIdentifier(t, resources["pod"]), - }, - testutil.ToIdentifier(t, resources["pod"]): { - testutil.ToIdentifier(t, resources["secret"]), - }, - testutil.ToIdentifier(t, resources["secret"]): { - testutil.ToIdentifier(t, resources["deployment"]), - }, - }, - }, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - g, err := DependencyGraph(tc.objs) - if tc.expectedError != nil { - require.EqualError(t, err, tc.expectedError.Error()) - return - } - assert.NoError(t, err) - asserter.Equal(t, tc.graph, g) - }) - } -} - -func TestHydrateSetList(t *testing.T) { - testCases := map[string]struct { - idSetList []object.ObjMetadataSet - objs object.UnstructuredSet - expected []object.UnstructuredSet - }{ - "no object sets": { - idSetList: []object.ObjMetadataSet{}, - expected: nil, - }, - "one object set": { - idSetList: []object.ObjMetadataSet{ - { - testutil.ToIdentifier(t, resources["deployment"]), - }, - }, - objs: object.UnstructuredSet{ - testutil.Unstructured(t, resources["deployment"]), - }, - expected: []object.UnstructuredSet{ - { - testutil.Unstructured(t, resources["deployment"]), - }, - }, - }, - "two out of three": { - idSetList: []object.ObjMetadataSet{ - { - testutil.ToIdentifier(t, resources["deployment"]), - }, - { - testutil.ToIdentifier(t, resources["secret"]), - }, - { - testutil.ToIdentifier(t, resources["pod"]), - }, - }, - objs: object.UnstructuredSet{ - testutil.Unstructured(t, resources["deployment"]), - testutil.Unstructured(t, resources["pod"]), - }, - expected: []object.UnstructuredSet{ - { - testutil.Unstructured(t, resources["deployment"]), - }, - { - testutil.Unstructured(t, resources["pod"]), - }, - }, - }, - "two uneven sets": { - idSetList: []object.ObjMetadataSet{ - { - testutil.ToIdentifier(t, resources["secret"]), - testutil.ToIdentifier(t, resources["deployment"]), - }, - { - testutil.ToIdentifier(t, resources["namespace"]), - }, - }, - objs: object.UnstructuredSet{ - testutil.Unstructured(t, resources["namespace"]), - testutil.Unstructured(t, resources["deployment"]), - testutil.Unstructured(t, resources["secret"]), - testutil.Unstructured(t, resources["pod"]), - }, - expected: []object.UnstructuredSet{ - { - testutil.Unstructured(t, resources["secret"]), - testutil.Unstructured(t, resources["deployment"]), - }, - { - testutil.Unstructured(t, resources["namespace"]), - }, - }, - }, - "one of two sets": { - idSetList: []object.ObjMetadataSet{ - { - testutil.ToIdentifier(t, resources["namespace"]), - testutil.ToIdentifier(t, resources["crd"]), - }, - { - testutil.ToIdentifier(t, resources["crontab1"]), - testutil.ToIdentifier(t, resources["crontab2"]), - }, - }, - objs: object.UnstructuredSet{ - testutil.Unstructured(t, resources["namespace"]), - testutil.Unstructured(t, resources["crd"]), - }, - expected: []object.UnstructuredSet{ - { - testutil.Unstructured(t, resources["namespace"]), - testutil.Unstructured(t, resources["crd"]), - }, - }, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - objSetList := HydrateSetList(tc.idSetList, tc.objs) - assert.Equal(t, tc.expected, objSetList) - }) - } -} - -func TestReverseSetList(t *testing.T) { - testCases := map[string]struct { - setList []object.UnstructuredSet - expected []object.UnstructuredSet - }{ - "no object sets": { - setList: []object.UnstructuredSet{}, - expected: []object.UnstructuredSet{}, - }, - "one object set": { - setList: []object.UnstructuredSet{ - { - testutil.Unstructured(t, resources["deployment"]), - }, - }, - expected: []object.UnstructuredSet{ - { - testutil.Unstructured(t, resources["deployment"]), - }, - }, - }, - "three object sets": { - setList: []object.UnstructuredSet{ - { - testutil.Unstructured(t, resources["deployment"]), - }, - { - testutil.Unstructured(t, resources["secret"]), - }, - { - testutil.Unstructured(t, resources["pod"]), - }, - }, - expected: []object.UnstructuredSet{ - { - testutil.Unstructured(t, resources["pod"]), - }, - { - testutil.Unstructured(t, resources["secret"]), - }, - { - testutil.Unstructured(t, resources["deployment"]), - }, - }, - }, - "two uneven sets": { - setList: []object.UnstructuredSet{ - { - testutil.Unstructured(t, resources["secret"]), - testutil.Unstructured(t, resources["deployment"]), - }, - { - testutil.Unstructured(t, resources["namespace"]), - }, - }, - expected: []object.UnstructuredSet{ - { - testutil.Unstructured(t, resources["namespace"]), - }, - { - testutil.Unstructured(t, resources["deployment"]), - testutil.Unstructured(t, resources["secret"]), - }, - }, - }, - "two even sets": { - setList: []object.UnstructuredSet{ - { - testutil.Unstructured(t, resources["crontab1"]), - testutil.Unstructured(t, resources["crontab2"]), - }, - { - testutil.Unstructured(t, resources["crd"]), - testutil.Unstructured(t, resources["namespace"]), - }, - }, - expected: []object.UnstructuredSet{ - { - testutil.Unstructured(t, resources["namespace"]), - testutil.Unstructured(t, resources["crd"]), - }, - { - testutil.Unstructured(t, resources["crontab2"]), - testutil.Unstructured(t, resources["crontab1"]), - }, - }, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - ReverseSetList(tc.setList) - assert.Equal(t, tc.expected, tc.setList) - }) - } -} - -func TestApplyTimeMutationEdges(t *testing.T) { - testCases := map[string]struct { - objs []*unstructured.Unstructured - expected []Edge - expectedError error - }{ - "no objects adds no graph edges": { - objs: []*unstructured.Unstructured{}, - expected: []Edge{}, - }, - "no depends-on annotations adds no graph edges": { - objs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["deployment"]), - }, - expected: []Edge{}, - }, - "no depends-on annotations, two objects, adds no graph edges": { - objs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["deployment"]), - testutil.Unstructured(t, resources["secret"]), - }, - expected: []Edge{}, - }, - "two dependent objects, adds one edge": { - objs: []*unstructured.Unstructured{ - testutil.Unstructured( - t, - resources["deployment"], - mutationutil.AddApplyTimeMutation(t, &mutation.ApplyTimeMutation{ - { - SourceRef: mutation.ResourceReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["secret"]), - ), - SourcePath: "unused", - TargetPath: "unused", - Token: "unused", - }, - }), - ), - testutil.Unstructured(t, resources["secret"]), - }, - expected: []Edge{ - { - From: testutil.ToIdentifier(t, resources["deployment"]), - To: testutil.ToIdentifier(t, resources["secret"]), - }, - }, - }, - "three dependent objects, adds two edges": { - objs: []*unstructured.Unstructured{ - testutil.Unstructured( - t, - resources["deployment"], - mutationutil.AddApplyTimeMutation(t, &mutation.ApplyTimeMutation{ - { - SourceRef: mutation.ResourceReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["secret"]), - ), - SourcePath: "unused", - TargetPath: "unused", - Token: "unused", - }, - }), - ), - testutil.Unstructured( - t, - resources["pod"], - mutationutil.AddApplyTimeMutation(t, &mutation.ApplyTimeMutation{ - { - SourceRef: mutation.ResourceReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["secret"]), - ), - SourcePath: "unused", - TargetPath: "unused", - Token: "unused", - }, - }), - ), - testutil.Unstructured(t, resources["secret"]), - }, - expected: []Edge{ - { - From: testutil.ToIdentifier(t, resources["deployment"]), - To: testutil.ToIdentifier(t, resources["secret"]), - }, - { - From: testutil.ToIdentifier(t, resources["pod"]), - To: testutil.ToIdentifier(t, resources["secret"]), - }, - }, - }, - "pod has two dependencies, adds two edges": { - objs: []*unstructured.Unstructured{ - testutil.Unstructured( - t, - resources["pod"], - mutationutil.AddApplyTimeMutation(t, &mutation.ApplyTimeMutation{ - { - SourceRef: mutation.ResourceReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["secret"]), - ), - SourcePath: "unused", - TargetPath: "unused", - Token: "unused", - }, - { - SourceRef: mutation.ResourceReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["deployment"]), - ), - SourcePath: "unused", - TargetPath: "unused", - Token: "unused", - }, - }), - ), - testutil.Unstructured(t, resources["deployment"]), - testutil.Unstructured(t, resources["secret"]), - }, - expected: []Edge{ - { - From: testutil.ToIdentifier(t, resources["pod"]), - To: testutil.ToIdentifier(t, resources["secret"]), - }, - { - From: testutil.ToIdentifier(t, resources["pod"]), - To: testutil.ToIdentifier(t, resources["deployment"]), - }, - }, - }, - "error: invalid annotation": { - objs: []*unstructured.Unstructured{ - { - Object: map[string]interface{}{ - "apiVersion": "apps/v1", - "kind": "Deployment", - "metadata": map[string]interface{}{ - "name": "foo", - "namespace": "default", - "annotations": map[string]interface{}{ - mutation.Annotation: "invalid-mutation", - }, - }, - }, - }, - }, - expected: []Edge{}, - expectedError: validation.NewError( - object.InvalidAnnotationError{ - Annotation: mutation.Annotation, - Cause: errors.New("error unmarshaling JSON: " + - "while decoding JSON: json: " + - "cannot unmarshal string into Go value of type mutation.ApplyTimeMutation"), - }, - object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - }, - Name: "foo", - Namespace: "default", - }, - ), - }, - "error: dependency not in object set": { - objs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["pod"], - mutationutil.AddApplyTimeMutation(t, &mutation.ApplyTimeMutation{ - { - SourceRef: mutation.ResourceReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["deployment"]), - ), - }, - }), - ), - }, - expected: []Edge{}, - expectedError: validation.NewError( - object.InvalidAnnotationError{ - Annotation: mutation.Annotation, - Cause: ExternalDependencyError{ - Edge: Edge{ - From: testutil.ToIdentifier(t, resources["pod"]), - To: testutil.ToIdentifier(t, resources["deployment"]), - }, - }, - }, - object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "", - Kind: "Pod", - }, - Name: "test-pod", - Namespace: "test-namespace", - }, - ), - }, - "error: two invalid objects": { - objs: []*unstructured.Unstructured{ - { - Object: map[string]interface{}{ - "apiVersion": "apps/v1", - "kind": "Deployment", - "metadata": map[string]interface{}{ - "name": "foo", - "namespace": "default", - "annotations": map[string]interface{}{ - mutation.Annotation: "invalid-mutation", - }, - }, - }, - }, - testutil.Unstructured(t, resources["pod"], - mutationutil.AddApplyTimeMutation(t, &mutation.ApplyTimeMutation{ - { - SourceRef: mutation.ResourceReferenceFromObjMetadata( - testutil.ToIdentifier(t, resources["secret"]), - ), - }, - }), - ), - }, - expected: []Edge{}, - expectedError: multierror.New( - validation.NewError( - object.InvalidAnnotationError{ - Annotation: mutation.Annotation, - Cause: errors.New("error unmarshaling JSON: " + - "while decoding JSON: json: " + - "cannot unmarshal string into Go value of type mutation.ApplyTimeMutation"), - }, - object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - }, - Name: "foo", - Namespace: "default", - }, - ), - validation.NewError( - object.InvalidAnnotationError{ - Annotation: mutation.Annotation, - Cause: ExternalDependencyError{ - Edge: Edge{ - From: testutil.ToIdentifier(t, resources["pod"]), - To: testutil.ToIdentifier(t, resources["secret"]), - }, - }, - }, - object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "", - Kind: "Pod", - }, - Name: "test-pod", - Namespace: "test-namespace", - }, - ), - ), - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - g := New() - ids := object.UnstructuredSetToObjMetadataSet(tc.objs) - err := addApplyTimeMutationEdges(g, tc.objs, ids) - if tc.expectedError != nil { - assert.EqualError(t, err, tc.expectedError.Error()) - } else { - assert.NoError(t, err) - } - actual := edgeMapToList(g.edges) - verifyEdges(t, tc.expected, actual) - }) - } -} - -func TestAddDependsOnEdges(t *testing.T) { - testCases := map[string]struct { - objs []*unstructured.Unstructured - expected []Edge - expectedError error - }{ - "no objects adds no graph edges": { - objs: []*unstructured.Unstructured{}, - expected: []Edge{}, - }, - "no depends-on annotations adds no graph edges": { - objs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["deployment"]), - }, - expected: []Edge{}, - }, - "no depends-on annotations, two objects, adds no graph edges": { - objs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["deployment"]), - testutil.Unstructured(t, resources["secret"]), - }, - expected: []Edge{}, - }, - "two dependent objects, adds one edge": { - objs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["deployment"], - testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), - testutil.Unstructured(t, resources["secret"]), - }, - expected: []Edge{ - { - From: testutil.ToIdentifier(t, resources["deployment"]), - To: testutil.ToIdentifier(t, resources["secret"]), - }, - }, - }, - "three dependent objects, adds two edges": { - objs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["deployment"], - testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), - testutil.Unstructured(t, resources["pod"], - testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), - testutil.Unstructured(t, resources["secret"]), - }, - expected: []Edge{ - { - From: testutil.ToIdentifier(t, resources["deployment"]), - To: testutil.ToIdentifier(t, resources["secret"]), - }, - { - From: testutil.ToIdentifier(t, resources["pod"]), - To: testutil.ToIdentifier(t, resources["secret"]), - }, - }, - }, - "pod has two dependencies, adds two edges": { - objs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["pod"], - testutil.AddDependsOn(t, - testutil.ToIdentifier(t, resources["secret"]), - testutil.ToIdentifier(t, resources["deployment"]), - ), - ), - testutil.Unstructured(t, resources["deployment"]), - testutil.Unstructured(t, resources["secret"]), - }, - expected: []Edge{ - { - From: testutil.ToIdentifier(t, resources["pod"]), - To: testutil.ToIdentifier(t, resources["secret"]), - }, - { - From: testutil.ToIdentifier(t, resources["pod"]), - To: testutil.ToIdentifier(t, resources["deployment"]), - }, - }, - }, - "error: invalid annotation": { - objs: []*unstructured.Unstructured{ - { - Object: map[string]interface{}{ - "apiVersion": "apps/v1", - "kind": "Deployment", - "metadata": map[string]interface{}{ - "name": "foo", - "namespace": "default", - "annotations": map[string]interface{}{ - dependson.Annotation: "invalid-obj-ref", - }, - }, - }, - }, - }, - expected: []Edge{}, - expectedError: validation.NewError( - object.InvalidAnnotationError{ - Annotation: dependson.Annotation, - Cause: errors.New("failed to parse object reference (index: 0): " + - `expected 3 or 5 fields, found 1: "invalid-obj-ref"`), - }, - object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - }, - Name: "foo", - Namespace: "default", - }, - ), - }, - "error: duplicate reference": { - objs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["pod"], - testutil.AddDependsOn(t, - testutil.ToIdentifier(t, resources["deployment"]), - testutil.ToIdentifier(t, resources["deployment"]), - ), - ), - testutil.Unstructured(t, resources["deployment"]), - }, - expected: []Edge{ - { - From: testutil.ToIdentifier(t, resources["pod"]), - To: testutil.ToIdentifier(t, resources["deployment"]), - }, - }, - expectedError: validation.NewError( - object.InvalidAnnotationError{ - Annotation: dependson.Annotation, - Cause: DuplicateDependencyError{ - Edge: Edge{ - From: testutil.ToIdentifier(t, resources["pod"]), - To: testutil.ToIdentifier(t, resources["deployment"]), - }, - }, - }, - object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "", - Kind: "Pod", - }, - Name: "test-pod", - Namespace: "test-namespace", - }, - ), - }, - "error: external dependency": { - objs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["pod"], - testutil.AddDependsOn(t, - testutil.ToIdentifier(t, resources["deployment"]), - ), - ), - }, - expected: []Edge{}, - expectedError: validation.NewError( - object.InvalidAnnotationError{ - Annotation: dependson.Annotation, - Cause: ExternalDependencyError{ - Edge: Edge{ - From: testutil.ToIdentifier(t, resources["pod"]), - To: testutil.ToIdentifier(t, resources["deployment"]), - }, - }, - }, - object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "", - Kind: "Pod", - }, - Name: "test-pod", - Namespace: "test-namespace", - }, - ), - }, - "error: two invalid objects": { - objs: []*unstructured.Unstructured{ - { - Object: map[string]interface{}{ - "apiVersion": "apps/v1", - "kind": "Deployment", - "metadata": map[string]interface{}{ - "name": "foo", - "namespace": "default", - "annotations": map[string]interface{}{ - dependson.Annotation: "invalid-obj-ref", - }, - }, - }, - }, - testutil.Unstructured(t, resources["pod"], - testutil.AddDependsOn(t, - testutil.ToIdentifier(t, resources["secret"]), - ), - ), - }, - expected: []Edge{}, - expectedError: multierror.New( - validation.NewError( - object.InvalidAnnotationError{ - Annotation: dependson.Annotation, - Cause: errors.New("failed to parse object reference (index: 0): " + - `expected 3 or 5 fields, found 1: "invalid-obj-ref"`), - }, - object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - }, - Name: "foo", - Namespace: "default", - }, - ), - validation.NewError( - object.InvalidAnnotationError{ - Annotation: dependson.Annotation, - Cause: ExternalDependencyError{ - Edge: Edge{ - From: testutil.ToIdentifier(t, resources["pod"]), - To: testutil.ToIdentifier(t, resources["secret"]), - }, - }, - }, - object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "", - Kind: "Pod", - }, - Name: "test-pod", - Namespace: "test-namespace", - }, - ), - ), - }, - "error: one object with two errors": { - objs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["pod"], - testutil.AddDependsOn(t, - testutil.ToIdentifier(t, resources["deployment"]), - testutil.ToIdentifier(t, resources["deployment"]), - ), - ), - }, - expected: []Edge{}, - expectedError: validation.NewError( - multierror.New( - object.InvalidAnnotationError{ - Annotation: dependson.Annotation, - Cause: ExternalDependencyError{ - Edge: Edge{ - From: testutil.ToIdentifier(t, resources["pod"]), - To: testutil.ToIdentifier(t, resources["deployment"]), - }, - }, - }, - object.InvalidAnnotationError{ - Annotation: dependson.Annotation, - Cause: DuplicateDependencyError{ - Edge: Edge{ - From: testutil.ToIdentifier(t, resources["pod"]), - To: testutil.ToIdentifier(t, resources["deployment"]), - }, - }, - }, - ), - object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "", - Kind: "Pod", - }, - Name: "test-pod", - Namespace: "test-namespace", - }, - ), - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - g := New() - ids := object.UnstructuredSetToObjMetadataSet(tc.objs) - err := addDependsOnEdges(g, tc.objs, ids) - if tc.expectedError != nil { - assert.EqualError(t, err, tc.expectedError.Error()) - } else { - assert.NoError(t, err) - } - actual := edgeMapToList(g.edges) - verifyEdges(t, tc.expected, actual) - }) - } -} - -func TestAddNamespaceEdges(t *testing.T) { - testCases := map[string]struct { - objs []*unstructured.Unstructured - expected []Edge - }{ - "no namespace objects adds no graph edges": { - objs: []*unstructured.Unstructured{}, - expected: []Edge{}, - }, - "single namespace adds no graph edges": { - objs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["namespace"]), - }, - expected: []Edge{}, - }, - "pod within namespace adds one edge": { - objs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["namespace"]), - testutil.Unstructured(t, resources["pod"]), - }, - expected: []Edge{ - { - From: testutil.ToIdentifier(t, resources["pod"]), - To: testutil.ToIdentifier(t, resources["namespace"]), - }, - }, - }, - "pod not in namespace does not add edge": { - objs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["namespace"]), - testutil.Unstructured(t, resources["default-pod"]), - }, - expected: []Edge{}, - }, - "pod, secret, and namespace adds two edges": { - objs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["namespace"]), - testutil.Unstructured(t, resources["secret"]), - testutil.Unstructured(t, resources["pod"]), - }, - expected: []Edge{ - { - From: testutil.ToIdentifier(t, resources["pod"]), - To: testutil.ToIdentifier(t, resources["namespace"]), - }, - { - From: testutil.ToIdentifier(t, resources["secret"]), - To: testutil.ToIdentifier(t, resources["namespace"]), - }, - }, - }, - "one pod in namespace, one not, adds only one edge": { - objs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["namespace"]), - testutil.Unstructured(t, resources["default-pod"]), - testutil.Unstructured(t, resources["pod"]), - }, - expected: []Edge{ - { - From: testutil.ToIdentifier(t, resources["pod"]), - To: testutil.ToIdentifier(t, resources["namespace"]), - }, - }, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - g := New() - ids := object.UnstructuredSetToObjMetadataSet(tc.objs) - addNamespaceEdges(g, tc.objs, ids) - actual := edgeMapToList(g.edges) - verifyEdges(t, tc.expected, actual) - }) - } -} - -func TestAddCRDEdges(t *testing.T) { - testCases := map[string]struct { - objs []*unstructured.Unstructured - expected []Edge - }{ - "no CRD objects adds no graph edges": { - objs: []*unstructured.Unstructured{}, - expected: []Edge{}, - }, - "single namespace adds no graph edges": { - objs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["crd"]), - }, - expected: []Edge{}, - }, - "two custom resources adds no graph edges": { - objs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["crontab1"]), - testutil.Unstructured(t, resources["crontab2"]), - }, - expected: []Edge{}, - }, - "two custom resources with crd adds two edges": { - objs: []*unstructured.Unstructured{ - testutil.Unstructured(t, resources["crd"]), - testutil.Unstructured(t, resources["crontab1"]), - testutil.Unstructured(t, resources["crontab2"]), - }, - expected: []Edge{ - { - From: testutil.ToIdentifier(t, resources["crontab1"]), - To: testutil.ToIdentifier(t, resources["crd"]), - }, - { - From: testutil.ToIdentifier(t, resources["crontab2"]), - To: testutil.ToIdentifier(t, resources["crd"]), - }, - }, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - g := New() - ids := object.UnstructuredSetToObjMetadataSet(tc.objs) - addCRDEdges(g, tc.objs, ids) - actual := edgeMapToList(g.edges) - verifyEdges(t, tc.expected, actual) - }) - } -} - -// verifyObjSets ensures the expected and actual slice of object sets are the same, -// and the sets are in order. -func verifyObjSets(t *testing.T, expected []object.UnstructuredSet, actual []object.UnstructuredSet) { - if len(expected) != len(actual) { - t.Fatalf("expected (%d) object sets, got (%d)", len(expected), len(actual)) - return - } - // Order matters - for i := range expected { - expectedSet := expected[i] - actualSet := actual[i] - if len(expectedSet) != len(actualSet) { - t.Fatalf("set %d: expected object size (%d), got (%d)", i, len(expectedSet), len(actualSet)) - return - } - for _, actualObj := range actualSet { - if !containsObjs(expectedSet, actualObj) { - t.Fatalf("set #%d: actual object (%v) not found in set of expected objects", i, actualObj) - return - } - } - } -} - -// containsUnstructured returns true if the passed object is within the passed -// slice of objects; false otherwise. Order is not important. -func containsObjs(objs []*unstructured.Unstructured, obj *unstructured.Unstructured) bool { - ids := object.UnstructuredSetToObjMetadataSet(objs) - id := object.UnstructuredToObjMetadata(obj) - for _, i := range ids { - if i == id { - return true - } - } - return false -} - -// verifyEdges ensures the slices of directed Edges contain the same elements. -// Order is not important. -func verifyEdges(t *testing.T, expected []Edge, actual []Edge) { - if len(expected) != len(actual) { - t.Fatalf("expected (%d) edges, got (%d)", len(expected), len(actual)) - return - } - for _, actualEdge := range actual { - if !containsEdge(expected, actualEdge) { - t.Errorf("actual Edge (%v) not found in expected Edges", actualEdge) - return - } - } -} - -// containsEdge return true if the passed Edge is in the slice of Edges; -// false otherwise. -func containsEdge(edges []Edge, edge Edge) bool { - for _, e := range edges { - if e.To == edge.To && e.From == edge.From { - return true - } - } - return false -} - -// waitTaskComparer allows comparion of WaitTasks, ignoring private fields. -func graphComparer() cmp.Option { - return cmp.Comparer(func(x, y *Graph) bool { - if x == nil { - return y == nil - } - if y == nil { - return false - } - return cmp.Equal(x.edges, y.edges) && - cmp.Equal(x.reverseEdges, y.reverseEdges) - }) -} diff --git a/pkg/object/graph/edge.go b/pkg/object/graph/edge.go deleted file mode 100644 index f956fe15..00000000 --- a/pkg/object/graph/edge.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2022 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package graph - -import ( - "sort" - - "github.com/fluxcd/cli-utils/pkg/object" -) - -// Edge encapsulates a pair of vertices describing a -// directed edge. -type Edge struct { - From object.ObjMetadata - To object.ObjMetadata -} - -// SortableEdges sorts a list of edges alphanumerically by From and then To. -type SortableEdges []Edge - -var _ sort.Interface = SortableEdges{} - -func (a SortableEdges) Len() int { return len(a) } -func (a SortableEdges) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a SortableEdges) Less(i, j int) bool { - if a[i].From != a[j].From { - return metaIsLessThan(a[i].From, a[j].From) - } - return metaIsLessThan(a[i].To, a[j].To) -} - -func metaIsLessThan(i, j object.ObjMetadata) bool { - if i.GroupKind.Group != j.GroupKind.Group { - return i.GroupKind.Group < j.GroupKind.Group - } - if i.GroupKind.Kind != j.GroupKind.Kind { - return i.GroupKind.Kind < j.GroupKind.Kind - } - if i.Namespace != j.Namespace { - return i.Namespace < j.Namespace - } - return i.Name < j.Name -} diff --git a/pkg/object/graph/edge_test.go b/pkg/object/graph/edge_test.go deleted file mode 100644 index 2cc4640b..00000000 --- a/pkg/object/graph/edge_test.go +++ /dev/null @@ -1,212 +0,0 @@ -// Copyright 2022 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package graph - -import ( - "sort" - "testing" - - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/stretchr/testify/assert" - "k8s.io/apimachinery/pkg/runtime/schema" -) - -func TestEdgeSort(t *testing.T) { - testCases := map[string]struct { - edges []Edge - expected []Edge - }{ - "one edge": { - edges: []Edge{ - { - From: object.ObjMetadata{Name: "obj1"}, - To: object.ObjMetadata{Name: "obj2"}, - }, - }, - expected: []Edge{ - { - From: object.ObjMetadata{Name: "obj1"}, - To: object.ObjMetadata{Name: "obj2"}, - }, - }, - }, - "two edges no change": { - edges: []Edge{ - { - From: object.ObjMetadata{Name: "obj1"}, - To: object.ObjMetadata{Name: "obj2"}, - }, - { - From: object.ObjMetadata{Name: "obj2"}, - To: object.ObjMetadata{Name: "obj3"}, - }, - }, - expected: []Edge{ - { - From: object.ObjMetadata{Name: "obj1"}, - To: object.ObjMetadata{Name: "obj2"}, - }, - { - From: object.ObjMetadata{Name: "obj2"}, - To: object.ObjMetadata{Name: "obj3"}, - }, - }, - }, - "two edges same from": { - edges: []Edge{ - { - From: object.ObjMetadata{Name: "obj1"}, - To: object.ObjMetadata{Name: "obj3"}, - }, - { - From: object.ObjMetadata{Name: "obj1"}, - To: object.ObjMetadata{Name: "obj2"}, - }, - }, - expected: []Edge{ - { - From: object.ObjMetadata{Name: "obj1"}, - To: object.ObjMetadata{Name: "obj2"}, - }, - { - From: object.ObjMetadata{Name: "obj1"}, - To: object.ObjMetadata{Name: "obj3"}, - }, - }, - }, - "two edges": { - edges: []Edge{ - { - From: object.ObjMetadata{Name: "obj2"}, - To: object.ObjMetadata{Name: "obj3"}, - }, - { - From: object.ObjMetadata{Name: "obj1"}, - To: object.ObjMetadata{Name: "obj2"}, - }, - }, - expected: []Edge{ - { - From: object.ObjMetadata{Name: "obj1"}, - To: object.ObjMetadata{Name: "obj2"}, - }, - { - From: object.ObjMetadata{Name: "obj2"}, - To: object.ObjMetadata{Name: "obj3"}, - }, - }, - }, - "two edges by name": { - edges: []Edge{ - { - From: object.ObjMetadata{Name: "obj2", Namespace: "ns1", GroupKind: schema.GroupKind{Kind: "Pod"}}, - To: object.ObjMetadata{Name: "obj3", Namespace: "ns1", GroupKind: schema.GroupKind{Kind: "Pod"}}, - }, - { - From: object.ObjMetadata{Name: "obj1", Namespace: "ns1", GroupKind: schema.GroupKind{Kind: "Pod"}}, - To: object.ObjMetadata{Name: "obj2", Namespace: "ns1", GroupKind: schema.GroupKind{Kind: "Pod"}}, - }, - }, - expected: []Edge{ - { - From: object.ObjMetadata{Name: "obj1", Namespace: "ns1", GroupKind: schema.GroupKind{Kind: "Pod"}}, - To: object.ObjMetadata{Name: "obj2", Namespace: "ns1", GroupKind: schema.GroupKind{Kind: "Pod"}}, - }, - { - From: object.ObjMetadata{Name: "obj2", Namespace: "ns1", GroupKind: schema.GroupKind{Kind: "Pod"}}, - To: object.ObjMetadata{Name: "obj3", Namespace: "ns1", GroupKind: schema.GroupKind{Kind: "Pod"}}, - }, - }, - }, - "three edges": { - edges: []Edge{ - { - From: object.ObjMetadata{Name: "obj3"}, - To: object.ObjMetadata{Name: "obj4"}, - }, - { - From: object.ObjMetadata{Name: "obj2"}, - To: object.ObjMetadata{Name: "obj3"}, - }, - { - From: object.ObjMetadata{Name: "obj1"}, - To: object.ObjMetadata{Name: "obj2"}, - }, - }, - expected: []Edge{ - { - From: object.ObjMetadata{Name: "obj1"}, - To: object.ObjMetadata{Name: "obj2"}, - }, - { - From: object.ObjMetadata{Name: "obj2"}, - To: object.ObjMetadata{Name: "obj3"}, - }, - { - From: object.ObjMetadata{Name: "obj3"}, - To: object.ObjMetadata{Name: "obj4"}, - }, - }, - }, - "two edges cycle": { - edges: []Edge{ - { - From: object.ObjMetadata{Name: "obj2", Namespace: "ns1", GroupKind: schema.GroupKind{Kind: "Pod"}}, - To: object.ObjMetadata{Name: "obj1", Namespace: "ns1", GroupKind: schema.GroupKind{Kind: "Pod"}}, - }, - { - From: object.ObjMetadata{Name: "obj1", Namespace: "ns1", GroupKind: schema.GroupKind{Kind: "Pod"}}, - To: object.ObjMetadata{Name: "obj2", Namespace: "ns1", GroupKind: schema.GroupKind{Kind: "Pod"}}, - }, - }, - expected: []Edge{ - { - From: object.ObjMetadata{Name: "obj1", Namespace: "ns1", GroupKind: schema.GroupKind{Kind: "Pod"}}, - To: object.ObjMetadata{Name: "obj2", Namespace: "ns1", GroupKind: schema.GroupKind{Kind: "Pod"}}, - }, - { - From: object.ObjMetadata{Name: "obj2", Namespace: "ns1", GroupKind: schema.GroupKind{Kind: "Pod"}}, - To: object.ObjMetadata{Name: "obj1", Namespace: "ns1", GroupKind: schema.GroupKind{Kind: "Pod"}}, - }, - }, - }, - "three edges cycle": { - edges: []Edge{ - { - From: object.ObjMetadata{Name: "obj3"}, - To: object.ObjMetadata{Name: "obj1"}, - }, - { - From: object.ObjMetadata{Name: "obj2"}, - To: object.ObjMetadata{Name: "obj3"}, - }, - { - From: object.ObjMetadata{Name: "obj1"}, - To: object.ObjMetadata{Name: "obj2"}, - }, - }, - expected: []Edge{ - { - From: object.ObjMetadata{Name: "obj1"}, - To: object.ObjMetadata{Name: "obj2"}, - }, - { - From: object.ObjMetadata{Name: "obj2"}, - To: object.ObjMetadata{Name: "obj3"}, - }, - { - From: object.ObjMetadata{Name: "obj3"}, - To: object.ObjMetadata{Name: "obj1"}, - }, - }, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - sort.Sort(SortableEdges(tc.edges)) - assert.Equal(t, tc.expected, tc.edges) - }) - } -} diff --git a/pkg/object/graph/error.go b/pkg/object/graph/error.go deleted file mode 100644 index 2ea00813..00000000 --- a/pkg/object/graph/error.go +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright 2022 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package graph - -import ( - "bytes" - "fmt" - - "github.com/fluxcd/cli-utils/pkg/multierror" - "github.com/fluxcd/cli-utils/pkg/object/mutation" -) - -// ExternalDependencyError represents an invalid graph edge caused by an -// object that is not in the object set. -type ExternalDependencyError struct { - Edge Edge -} - -func (ede ExternalDependencyError) Error() string { - return fmt.Sprintf("external dependency: %s -> %s", - mutation.ResourceReferenceFromObjMetadata(ede.Edge.From), - mutation.ResourceReferenceFromObjMetadata(ede.Edge.To)) -} - -// CyclicDependencyError represents a cycle in the graph, making topological -// sort impossible. -type CyclicDependencyError struct { - Edges []Edge -} - -func (cde CyclicDependencyError) Error() string { - var errorBuf bytes.Buffer - errorBuf.WriteString("cyclic dependency:") - for _, edge := range cde.Edges { - errorBuf.WriteString(fmt.Sprintf("\n%s%s -> %s", multierror.Prefix, - mutation.ResourceReferenceFromObjMetadata(edge.From), - mutation.ResourceReferenceFromObjMetadata(edge.To))) - } - return errorBuf.String() -} - -// DuplicateDependencyError represents an invalid depends-on annotation with -// duplicate references. -type DuplicateDependencyError struct { - Edge Edge -} - -func (dde DuplicateDependencyError) Error() string { - return fmt.Sprintf("duplicate dependency: %s -> %s", - mutation.ResourceReferenceFromObjMetadata(dde.Edge.From), - mutation.ResourceReferenceFromObjMetadata(dde.Edge.To)) -} diff --git a/pkg/object/graph/error_test.go b/pkg/object/graph/error_test.go deleted file mode 100644 index b672f868..00000000 --- a/pkg/object/graph/error_test.go +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright 2022 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package graph - -import ( - "testing" - - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/stretchr/testify/assert" - "k8s.io/apimachinery/pkg/runtime/schema" -) - -var ( - on1 = object.ObjMetadata{Name: "obj1", Namespace: "ns1", GroupKind: schema.GroupKind{Group: "test", Kind: "foo"}} - on2 = object.ObjMetadata{Name: "obj2", Namespace: "ns1", GroupKind: schema.GroupKind{Group: "test", Kind: "foo"}} -) - -func TestExternalDependencyErrorString(t *testing.T) { - testCases := map[string]struct { - err ExternalDependencyError - expectedString string - }{ - "cluster-scoped": { - err: ExternalDependencyError{ - Edge: Edge{ - From: o1, - To: o2, - }, - }, - expectedString: `external dependency: test/foo/obj1 -> test/foo/obj2`, - }, - "namespace-scoped": { - err: ExternalDependencyError{ - Edge: Edge{ - From: on1, - To: on2, - }, - }, - expectedString: `external dependency: test/namespaces/ns1/foo/obj1 -> test/namespaces/ns1/foo/obj2`, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - assert.Equal(t, tc.expectedString, tc.err.Error()) - }) - } -} - -func TestCyclicDependencyErrorString(t *testing.T) { - testCases := map[string]struct { - err CyclicDependencyError - expectedString string - }{ - "two object cycle": { - err: CyclicDependencyError{ - Edges: []Edge{ - { - From: o1, - To: o2, - }, - { - From: o2, - To: o1, - }, - }, - }, - expectedString: `cyclic dependency: -- test/foo/obj1 -> test/foo/obj2 -- test/foo/obj2 -> test/foo/obj1`, - }, - "three object cycle": { - err: CyclicDependencyError{ - Edges: []Edge{ - { - From: o1, - To: o2, - }, - { - From: o2, - To: o3, - }, - { - From: o3, - To: o1, - }, - }, - }, - expectedString: `cyclic dependency: -- test/foo/obj1 -> test/foo/obj2 -- test/foo/obj2 -> test/foo/obj3 -- test/foo/obj3 -> test/foo/obj1`, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - assert.Equal(t, tc.expectedString, tc.err.Error()) - }) - } -} - -func TestDuplicateDependencyErrorString(t *testing.T) { - testCases := map[string]struct { - err DuplicateDependencyError - expectedString string - }{ - "cluster-scoped": { - err: DuplicateDependencyError{ - Edge: Edge{ - From: o1, - To: o2, - }, - }, - expectedString: `duplicate dependency: test/foo/obj1 -> test/foo/obj2`, - }, - "namespace-scoped": { - err: DuplicateDependencyError{ - Edge: Edge{ - From: on1, - To: on2, - }, - }, - expectedString: `duplicate dependency: test/namespaces/ns1/foo/obj1 -> test/namespaces/ns1/foo/obj2`, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - assert.Equal(t, tc.expectedString, tc.err.Error()) - }) - } -} diff --git a/pkg/object/graph/graph.go b/pkg/object/graph/graph.go deleted file mode 100644 index 4bf47a3f..00000000 --- a/pkg/object/graph/graph.go +++ /dev/null @@ -1,183 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -// This package provides a graph data struture -// and graph functionality using ObjMetadata as -// vertices in the graph. -package graph - -import ( - "sort" - - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/object/validation" - "github.com/fluxcd/cli-utils/pkg/ordering" -) - -// Graph is contains a directed set of edges, implemented as -// an adjacency list (map key is "from" vertex, slice are "to" -// vertices). -type Graph struct { - // map "from" vertex -> list of "to" vertices - edges map[object.ObjMetadata]object.ObjMetadataSet - // map "to" vertex -> list of "from" vertices - reverseEdges map[object.ObjMetadata]object.ObjMetadataSet -} - -// New returns a pointer to an empty Graph data structure. -func New() *Graph { - g := &Graph{} - g.edges = make(map[object.ObjMetadata]object.ObjMetadataSet) - g.reverseEdges = make(map[object.ObjMetadata]object.ObjMetadataSet) - return g -} - -// AddVertex adds an ObjMetadata vertex to the graph, with -// an initial empty set of edges from added vertex. -func (g *Graph) AddVertex(v object.ObjMetadata) { - if _, exists := g.edges[v]; !exists { - g.edges[v] = object.ObjMetadataSet{} - } - if _, exists := g.reverseEdges[v]; !exists { - g.reverseEdges[v] = object.ObjMetadataSet{} - } -} - -// edgeMapKeys returns a sorted set of unique vertices in the graph. -func edgeMapKeys(edgeMap map[object.ObjMetadata]object.ObjMetadataSet) object.ObjMetadataSet { - keys := make(object.ObjMetadataSet, len(edgeMap)) - i := 0 - for k := range edgeMap { - keys[i] = k - i++ - } - sort.Sort(ordering.SortableMetas(keys)) - return keys -} - -// AddEdge adds a edge from one ObjMetadata vertex to another. The -// direction of the edge is "from" -> "to". -func (g *Graph) AddEdge(from object.ObjMetadata, to object.ObjMetadata) { - // Add "from" vertex if it doesn't already exist. - if _, exists := g.edges[from]; !exists { - g.edges[from] = object.ObjMetadataSet{} - } - if _, exists := g.reverseEdges[from]; !exists { - g.reverseEdges[from] = object.ObjMetadataSet{} - } - // Add "to" vertex if it doesn't already exist. - if _, exists := g.edges[to]; !exists { - g.edges[to] = object.ObjMetadataSet{} - } - if _, exists := g.reverseEdges[to]; !exists { - g.reverseEdges[to] = object.ObjMetadataSet{} - } - // Add edge "from" -> "to" if it doesn't already exist - // into the adjacency list. - if !g.isAdjacent(from, to) { - g.edges[from] = append(g.edges[from], to) - g.reverseEdges[to] = append(g.reverseEdges[to], from) - } -} - -// edgeMapToList returns a sorted slice of directed graph edges (vertex pairs). -func edgeMapToList(edgeMap map[object.ObjMetadata]object.ObjMetadataSet) []Edge { - edges := []Edge{} - for from, toList := range edgeMap { - for _, to := range toList { - edge := Edge{From: from, To: to} - edges = append(edges, edge) - } - } - sort.Sort(SortableEdges(edges)) - return edges -} - -// isAdjacent returns true if an edge "from" vertex -> "to" vertex exists; -// false otherwise. -func (g *Graph) isAdjacent(from object.ObjMetadata, to object.ObjMetadata) bool { - // If "from" vertex does not exist, it is impossible edge exists; return false. - if _, exists := g.edges[from]; !exists { - return false - } - // Iterate through adjacency list to see if "to" vertex is adjacent. - for _, vertex := range g.edges[from] { - if vertex == to { - return true - } - } - return false -} - -// Size returns the number of vertices in the graph. -func (g *Graph) Size() int { - return len(g.edges) -} - -// removeVertex removes the passed vertex as well as any edges into the vertex. -func removeVertex(edges map[object.ObjMetadata]object.ObjMetadataSet, r object.ObjMetadata) { - // First, remove the object from all adjacency lists. - for v, adj := range edges { - edges[v] = adj.Remove(r) - } - // Finally, remove the vertex - delete(edges, r) -} - -// Dependencies returns the objects that this object depends on. -func (g *Graph) Dependencies(from object.ObjMetadata) object.ObjMetadataSet { - edgesFrom, exists := g.edges[from] - if !exists { - return nil - } - c := make(object.ObjMetadataSet, len(edgesFrom)) - copy(c, edgesFrom) - return c -} - -// Dependents returns the objects that depend on this object. -func (g *Graph) Dependents(to object.ObjMetadata) object.ObjMetadataSet { - edgesTo, exists := g.reverseEdges[to] - if !exists { - return nil - } - c := make(object.ObjMetadataSet, len(edgesTo)) - copy(c, edgesTo) - return c -} - -// Sort returns the ordered set of vertices after a topological sort. -func (g *Graph) Sort() ([]object.ObjMetadataSet, error) { - // deep copy edge map to avoid destructive sorting - edges := make(map[object.ObjMetadata]object.ObjMetadataSet, len(g.edges)) - for vertex, deps := range g.edges { - c := make(object.ObjMetadataSet, len(deps)) - copy(c, deps) - edges[vertex] = c - } - - sorted := []object.ObjMetadataSet{} - for len(edges) > 0 { - // Identify all the leaf vertices. - leafVertices := object.ObjMetadataSet{} - for v, adj := range edges { - if len(adj) == 0 { - leafVertices = append(leafVertices, v) - } - } - // No leaf vertices means cycle in the directed graph, - // where remaining edges define the cycle. - if len(leafVertices) == 0 { - // Error can be ignored, so return the full set list - return sorted, validation.NewError(CyclicDependencyError{ - Edges: edgeMapToList(edges), - }, edgeMapKeys(edges)...) - } - // Remove all edges to leaf vertices. - for _, v := range leafVertices { - removeVertex(edges, v) - } - sorted = append(sorted, leafVertices) - } - return sorted, nil -} diff --git a/pkg/object/graph/graph_test.go b/pkg/object/graph/graph_test.go deleted file mode 100644 index de9b6928..00000000 --- a/pkg/object/graph/graph_test.go +++ /dev/null @@ -1,256 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -// This package provides a graph data struture -// and graph functionality. -package graph - -import ( - "testing" - - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/object/validation" - "github.com/fluxcd/cli-utils/pkg/testutil" - "github.com/stretchr/testify/assert" - "k8s.io/apimachinery/pkg/runtime/schema" -) - -var ( - o1 = object.ObjMetadata{Name: "obj1", GroupKind: schema.GroupKind{Group: "test", Kind: "foo"}} - o2 = object.ObjMetadata{Name: "obj2", GroupKind: schema.GroupKind{Group: "test", Kind: "foo"}} - o3 = object.ObjMetadata{Name: "obj3", GroupKind: schema.GroupKind{Group: "test", Kind: "foo"}} - o4 = object.ObjMetadata{Name: "obj4", GroupKind: schema.GroupKind{Group: "test", Kind: "foo"}} - o5 = object.ObjMetadata{Name: "obj5", GroupKind: schema.GroupKind{Group: "test", Kind: "foo"}} -) - -var ( - e1 = Edge{From: o1, To: o2} - e2 = Edge{From: o2, To: o3} - e3 = Edge{From: o1, To: o3} - e4 = Edge{From: o3, To: o4} - e5 = Edge{From: o2, To: o4} - e6 = Edge{From: o2, To: o1} - e7 = Edge{From: o3, To: o1} - e8 = Edge{From: o4, To: o5} -) - -func TestObjectGraphSort(t *testing.T) { - testCases := map[string]struct { - vertices object.ObjMetadataSet - edges []Edge - expected []object.ObjMetadataSet - expectedError error - }{ - "one edge": { - vertices: object.ObjMetadataSet{o1, o2}, - edges: []Edge{e1}, - expected: []object.ObjMetadataSet{{o2}, {o1}}, - }, - "two edges": { - vertices: object.ObjMetadataSet{o1, o2, o3}, - edges: []Edge{e1, e2}, - expected: []object.ObjMetadataSet{{o3}, {o2}, {o1}}, - }, - "three edges": { - vertices: object.ObjMetadataSet{o1, o2, o3}, - edges: []Edge{e1, e3, e2}, - expected: []object.ObjMetadataSet{{o3}, {o2}, {o1}}, - }, - "four edges": { - vertices: object.ObjMetadataSet{o1, o2, o3, o4}, - edges: []Edge{e1, e2, e4, e5}, - expected: []object.ObjMetadataSet{{o4}, {o3}, {o2}, {o1}}, - }, - "five edges": { - vertices: object.ObjMetadataSet{o1, o2, o3, o4}, - edges: []Edge{e5, e1, e3, e2, e4}, - expected: []object.ObjMetadataSet{{o4}, {o3}, {o2}, {o1}}, - }, - "no edges means all in the same first set": { - vertices: object.ObjMetadataSet{o1, o2, o3, o4}, - edges: []Edge{}, - expected: []object.ObjMetadataSet{{o4, o3, o2, o1}}, - }, - "multiple objects in first set": { - vertices: object.ObjMetadataSet{o1, o2, o3, o4, o5}, - edges: []Edge{e1, e2, e5, e8}, - expected: []object.ObjMetadataSet{{o5, o3}, {o4}, {o2}, {o1}}, - }, - "simple cycle in graph is an error": { - vertices: object.ObjMetadataSet{o1, o2}, - edges: []Edge{e1, e6}, - expected: []object.ObjMetadataSet{}, - expectedError: validation.NewError( - CyclicDependencyError{ - Edges: []Edge{ - { - From: o1, - To: o2, - }, - { - From: o2, - To: o1, - }, - }, - }, - o1, o2, - ), - }, - "multi-edge cycle in graph is an error": { - vertices: object.ObjMetadataSet{o1, o2, o3}, - edges: []Edge{e1, e2, e7}, - expected: []object.ObjMetadataSet{}, - expectedError: validation.NewError( - CyclicDependencyError{ - Edges: []Edge{ - { - From: o1, - To: o2, - }, - { - From: o2, - To: o3, - }, - { - From: o3, - To: o1, - }, - }, - }, - o1, o2, o3, - ), - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - g := New() - for _, vertex := range tc.vertices { - g.AddVertex(vertex) - } - for _, edge := range tc.edges { - g.AddEdge(edge.From, edge.To) - } - actual, err := g.Sort() - if tc.expectedError != nil { - assert.EqualError(t, tc.expectedError, err.Error()) - return - } - assert.NoError(t, err) - testutil.AssertEqual(t, tc.expected, actual) - - // verify sort is repeatable & non-destructive - actual, err = g.Sort() - assert.NoError(t, err) - testutil.AssertEqual(t, tc.expected, actual) - }) - } -} - -func TestGraphDependencies(t *testing.T) { - testCases := map[string]struct { - vertices object.ObjMetadataSet - edges []Edge - from object.ObjMetadata - expected object.ObjMetadataSet - }{ - "no dependencies": { - vertices: object.ObjMetadataSet{o1, o2, o3}, - edges: []Edge{ - {From: o1, To: o2}, - {From: o1, To: o3}, - {From: o2, To: o3}, - }, - from: o3, - expected: object.ObjMetadataSet{}, - }, - "one dependency": { - vertices: object.ObjMetadataSet{o1, o2, o3}, - edges: []Edge{ - {From: o1, To: o2}, - {From: o1, To: o3}, - {From: o2, To: o3}, - }, - from: o2, - expected: object.ObjMetadataSet{o3}, - }, - "two dependencies": { - vertices: object.ObjMetadataSet{o1, o2, o3}, - edges: []Edge{ - {From: o1, To: o2}, - {From: o1, To: o3}, - {From: o2, To: o3}, - }, - from: o1, - expected: object.ObjMetadataSet{o2, o3}, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - g := New() - for _, vertex := range tc.vertices { - g.AddVertex(vertex) - } - for _, edge := range tc.edges { - g.AddEdge(edge.From, edge.To) - } - - testutil.AssertEqual(t, tc.expected, g.Dependencies(tc.from)) - }) - } -} - -func TestGraphDependents(t *testing.T) { - testCases := map[string]struct { - vertices object.ObjMetadataSet - edges []Edge - to object.ObjMetadata - expected object.ObjMetadataSet - }{ - "no dependents": { - vertices: object.ObjMetadataSet{o1, o2, o3}, - edges: []Edge{ - {From: o1, To: o2}, - {From: o1, To: o3}, - {From: o2, To: o3}, - }, - to: o1, - expected: object.ObjMetadataSet{}, - }, - "one dependent": { - vertices: object.ObjMetadataSet{o1, o2, o3}, - edges: []Edge{ - {From: o1, To: o2}, - {From: o1, To: o3}, - {From: o2, To: o3}, - }, - to: o2, - expected: object.ObjMetadataSet{o1}, - }, - "two dependents": { - vertices: object.ObjMetadataSet{o1, o2, o3}, - edges: []Edge{ - {From: o1, To: o2}, - {From: o1, To: o3}, - {From: o2, To: o3}, - }, - to: o3, - expected: object.ObjMetadataSet{o1, o2}, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - g := New() - for _, vertex := range tc.vertices { - g.AddVertex(vertex) - } - for _, edge := range tc.edges { - g.AddEdge(edge.From, edge.To) - } - - testutil.AssertEqual(t, tc.expected, g.Dependents(tc.to)) - }) - } -} diff --git a/pkg/object/mutation/annotation.go b/pkg/object/mutation/annotation.go deleted file mode 100644 index 90d7ac67..00000000 --- a/pkg/object/mutation/annotation.go +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package mutation - -import ( - "errors" - "fmt" - - "github.com/fluxcd/cli-utils/pkg/object" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/klog/v2" - "sigs.k8s.io/yaml" -) - -const ( - Annotation = "config.kubernetes.io/apply-time-mutation" -) - -func HasAnnotation(u *unstructured.Unstructured) bool { - if u == nil { - return false - } - _, found := u.GetAnnotations()[Annotation] - return found -} - -// ReadAnnotation returns the slice of substitutions parsed from the -// apply-time-mutation annotation within the supplied unstructured object. -func ReadAnnotation(obj *unstructured.Unstructured) (ApplyTimeMutation, error) { - mutation := ApplyTimeMutation{} - if obj == nil { - return mutation, nil - } - mutationYaml, found := obj.GetAnnotations()[Annotation] - if !found { - return mutation, nil - } - if klog.V(5).Enabled() { - klog.Infof("object (%v) has apply-time-mutation annotation:\n%s", ResourceReferenceFromUnstructured(obj), mutationYaml) - } - - err := yaml.Unmarshal([]byte(mutationYaml), &mutation) - if err != nil { - return mutation, object.InvalidAnnotationError{ - Annotation: Annotation, - Cause: err, - } - } - return mutation, nil -} - -// WriteAnnotation updates the supplied unstructured object to add the -// apply-time-mutation annotation with a multi-line yaml value. -func WriteAnnotation(obj *unstructured.Unstructured, mutation ApplyTimeMutation) error { - if obj == nil { - return errors.New("object is nil") - } - if mutation.Equal(ApplyTimeMutation{}) { - return errors.New("mutation is empty") - } - yamlBytes, err := yaml.Marshal(mutation) - if err != nil { - return fmt.Errorf("failed to format apply-time-mutation annotation: %v", err) - } - a := obj.GetAnnotations() - if a == nil { - a = map[string]string{} - } - a[Annotation] = string(yamlBytes) - obj.SetAnnotations(a) - return nil -} diff --git a/pkg/object/mutation/annotation_test.go b/pkg/object/mutation/annotation_test.go deleted file mode 100644 index d3bd8bda..00000000 --- a/pkg/object/mutation/annotation_test.go +++ /dev/null @@ -1,340 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 -// - -package mutation - -import ( - "testing" - - ktestutil "github.com/fluxcd/cli-utils/pkg/kstatus/polling/testutil" - "github.com/stretchr/testify/require" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -var configmap1y = ` -apiVersion: v1 -kind: ConfigMap -metadata: - name: map1-name - namespace: map-namespace - annotations: - config.kubernetes.io/apply-time-mutation: | - - sourcePath: $.status.number - sourceRef: - group: resourcemanager.cnrm.cloud.google.com - kind: Project - name: example-name - namespace: example-namespace - targetPath: $.spec.member - token: ${project-number} -data: {} -` - -var m1 = ApplyTimeMutation{ - { - SourceRef: ResourceReference{ - Group: "resourcemanager.cnrm.cloud.google.com", - Kind: "Project", - Name: "example-name", - Namespace: "example-namespace", - }, - SourcePath: "$.status.number", - TargetPath: "$.spec.member", - Token: "${project-number}", - }, -} - -var configmap2y = ` -apiVersion: v1 -kind: ConfigMap -metadata: - name: map1-name - namespace: map-namespace - annotations: - config.kubernetes.io/apply-time-mutation: | - - sourcePath: .status.field - sourceRef: - kind: ConfigMap - name: example-name - targetPath: .spec.field -data: {} -` - -var m2 = ApplyTimeMutation{ - { - SourceRef: ResourceReference{ - Group: "", - Kind: "ConfigMap", - Name: "example-name", - }, - SourcePath: ".status.field", - TargetPath: ".spec.field", - }, -} - -// inline json, no spaces or linebreaks -var u1j = &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "v1", - "kind": "ConfigMap", - "metadata": map[string]interface{}{ - "name": "unused", - "namespace": "unused", - "annotations": map[string]interface{}{ - Annotation: `[` + - `{` + - `"sourceRef":{` + - `"group":"resourcemanager.cnrm.cloud.google.com",` + - `"kind":"Project",` + - `"name":"example-name",` + - `"namespace":"example-namespace"` + - `},` + - `"sourcePath": "$.status.number",` + - `"targetPath": "$.spec.member",` + - `"token": "${project-number}"` + - `},` + - `]`, - }, - }, - }, -} - -// yaml w/ multiple subs -var configmap3y = ` -apiVersion: v1 -kind: ConfigMap -metadata: - name: map1-name - namespace: map-namespace - annotations: - config.kubernetes.io/apply-time-mutation: | - - sourcePath: $.status.number - sourceRef: - group: resourcemanager.cnrm.cloud.google.com - kind: Project - name: example-name - namespace: example-namespace - targetPath: $.spec.member - token: ${project-number} - - sourcePath: .status.field - sourceRef: - kind: ConfigMap - name: example-name - targetPath: .spec.field -data: {} -` - -// json w/ multiple subs -var configmap4y = ` -apiVersion: v1 -kind: ConfigMap -metadata: - name: map1-name - namespace: map-namespace - annotations: - config.kubernetes.io/apply-time-mutation: | - [ - { - "sourceRef": { - "group": "resourcemanager.cnrm.cloud.google.com", - "kind": "Project", - "name": "example-name", - "namespace": "example-namespace" - }, - "sourcePath": "$.status.number", - "targetPath": "$.spec.member", - "token": "${project-number}" - }, - { - "sourceRef": { - "kind": "ConfigMap", - "name": "example-name" - }, - "sourcePath": ".status.field", - "targetPath": ".spec.field" - } - ] -data: {} -` - -var m3 = ApplyTimeMutation{ - { - SourceRef: ResourceReference{ - Group: "resourcemanager.cnrm.cloud.google.com", - Kind: "Project", - Name: "example-name", - Namespace: "example-namespace", - }, - SourcePath: "$.status.number", - TargetPath: "$.spec.member", - Token: "${project-number}", - }, - { - SourceRef: ResourceReference{ - Group: "", - Kind: "ConfigMap", - Name: "example-name", - }, - SourcePath: ".status.field", - TargetPath: ".spec.field", - }, -} - -var noAnnotationsYAML = ` -apiVersion: v1 -kind: ConfigMap -metadata: - name: map1-name - namespace: map-namespace -data: {} -` - -var invalidAnnotationsYAML = ` -apiVersion: v1 -kind: ConfigMap -metadata: - name: map1-name - namespace: map-namespace - annotations: - config.kubernetes.io/apply-time-mutation: this string is not a substitution -data: {} -` - -func TestReadAnnotation(t *testing.T) { - configmap1 := ktestutil.YamlToUnstructured(t, configmap1y) - configmap2 := ktestutil.YamlToUnstructured(t, configmap2y) - configmap3 := ktestutil.YamlToUnstructured(t, configmap3y) - configmap4 := ktestutil.YamlToUnstructured(t, configmap4y) - noAnnotations := ktestutil.YamlToUnstructured(t, noAnnotationsYAML) - invalidAnnotations := ktestutil.YamlToUnstructured(t, invalidAnnotationsYAML) - - testCases := map[string]struct { - obj *unstructured.Unstructured - expected ApplyTimeMutation - isError bool - }{ - "nil object is not found": { - obj: nil, - expected: ApplyTimeMutation{}, - }, - "Object with no annotations returns not found": { - obj: noAnnotations, - expected: ApplyTimeMutation{}, - }, - "Unparseable depends on annotation returns not found": { - obj: invalidAnnotations, - expected: ApplyTimeMutation{}, - isError: true, - }, - "Namespace-scoped object apply-time-mutation annotation yaml": { - obj: configmap1, - expected: m1, - }, - "Namespace-scoped object apply-time-mutation annotation json": { - obj: u1j, - expected: m1, - }, - "Cluster-scoped object apply-time-mutation annotation yaml": { - obj: configmap2, - expected: m2, - }, - "Multiple objects specified in annotation yaml": { - obj: configmap3, - expected: m3, - }, - "Multiple objects specified in annotation json": { - obj: configmap4, - expected: m3, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - actual, err := ReadAnnotation(tc.obj) - if tc.isError { - if err == nil { - t.Fatalf("expected error not received") - } - } else { - if err != nil { - t.Fatalf("unexpected error received: %s", err) - } - if !actual.Equal(tc.expected) { - t.Errorf("\nexpected:\t%#v\nreceived:\t%#v", tc.expected, actual) - } - } - }) - } -} - -func TestWriteAnnotation(t *testing.T) { - configmap1 := ktestutil.YamlToUnstructured(t, configmap1y) - configmap2 := ktestutil.YamlToUnstructured(t, configmap2y) - configmap3 := ktestutil.YamlToUnstructured(t, configmap3y) - - testCases := map[string]struct { - obj *unstructured.Unstructured - mutation ApplyTimeMutation - expected *string - isError bool - }{ - "nil object": { - obj: nil, - mutation: ApplyTimeMutation{}, - expected: nil, - isError: true, - }, - "empty mutation": { - obj: &unstructured.Unstructured{}, - mutation: ApplyTimeMutation{}, - expected: nil, - isError: true, - }, - "Namespace-scoped object": { - obj: &unstructured.Unstructured{}, - mutation: m1, - expected: getApplyTimeMutation(configmap1), - }, - "Cluster-scoped object": { - obj: &unstructured.Unstructured{}, - mutation: m2, - expected: getApplyTimeMutation(configmap2), - }, - "Multiple objects": { - obj: &unstructured.Unstructured{}, - mutation: m3, - expected: getApplyTimeMutation(configmap3), - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - err := WriteAnnotation(tc.obj, tc.mutation) - if tc.isError { - if err == nil { - t.Fatalf("expected error not received") - } - } else { - if err != nil { - t.Fatalf("unexpected error received: %s", err) - } - received := getApplyTimeMutation(tc.obj) - - if received != tc.expected && (received == nil || tc.expected == nil) { - t.Errorf("\nexpected:\t%#v\nreceived:\t%#v", tc.expected, received) - } - - require.Equal(t, *tc.expected, *received, "unexpected mutation string") - } - }) - } -} - -func getApplyTimeMutation(obj *unstructured.Unstructured) *string { - value, found := obj.GetAnnotations()[Annotation] - if !found { - return nil - } - return &value -} diff --git a/pkg/object/mutation/testutil/object.go b/pkg/object/mutation/testutil/object.go deleted file mode 100644 index 3c7c818e..00000000 --- a/pkg/object/mutation/testutil/object.go +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package testutil - -import ( - "testing" - - "github.com/fluxcd/cli-utils/pkg/object/mutation" - "github.com/fluxcd/cli-utils/pkg/testutil" - "github.com/stretchr/testify/assert" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -// AddApplyTimeMutation returns a testutil.Mutator which adds the passed objects -// as an apply-time-mutation annotation to the object which is mutated. Multiple -// objects passed in means multiple substitutions in the annotation yaml list. -func AddApplyTimeMutation(t *testing.T, mutation *mutation.ApplyTimeMutation) testutil.Mutator { - return applyTimeMutationMutator{ - t: t, - mutation: mutation, - } -} - -// applyTimeMutationMutator encapsulates fields for adding apply-time-mutation -// annotation to a test object. Implements the Mutator interface. -type applyTimeMutationMutator struct { - t *testing.T - mutation *mutation.ApplyTimeMutation -} - -// Mutate for applyTimeMutationMutator sets the apply-time-mutation annotation -// on the passed object. -func (a applyTimeMutationMutator) Mutate(u *unstructured.Unstructured) { - err := mutation.WriteAnnotation(u, *a.mutation) - if !assert.NoError(a.t, err) { - a.t.FailNow() - } -} diff --git a/pkg/object/mutation/types.go b/pkg/object/mutation/types.go deleted file mode 100644 index 2ec95f30..00000000 --- a/pkg/object/mutation/types.go +++ /dev/null @@ -1,167 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package mutation - -import ( - "fmt" - - "github.com/fluxcd/cli-utils/pkg/object" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" -) - -// ApplyTimeMutation is a list of substitutions to perform in the target -// object before applying, after waiting for the source objects to be -// reconciled. -// This most notibly allows status fields to be substituted into spec fields. -type ApplyTimeMutation []FieldSubstitution - -// Equal returns true if the substitutions are equivalent, ignoring order. -// Fulfills Equal interface from github.com/google/go-cmp -func (a ApplyTimeMutation) Equal(b ApplyTimeMutation) bool { - if len(a) != len(b) { - return false - } - - mapA := make(map[FieldSubstitution]struct{}, len(a)) - for _, sub := range a { - mapA[sub] = struct{}{} - } - mapB := make(map[FieldSubstitution]struct{}, len(b)) - for _, sub := range b { - mapB[sub] = struct{}{} - } - if len(mapA) != len(mapB) { - return false - } - for b := range mapB { - if _, exists := mapA[b]; !exists { - return false - } - } - return true -} - -// FieldSubstitution specifies a substitution that will be performed at -// apply-time. The source object field will be read and substituted into the -// target object field, replacing the token. -type FieldSubstitution struct { - // SourceRef is a reference to the object that contains the source field. - SourceRef ResourceReference `json:"sourceRef"` - - // SourcePath is a JSONPath reference to a field in the source object. - // Example: "$.status.number" - SourcePath string `json:"sourcePath"` - - // TargetPath is a JSONPath reference to a field in the target object. - // Example: "$.spec.member" - TargetPath string `json:"targetPath"` - - // Token is the substring to replace in the value of the target field. - // If empty, the target field value will be set to the source field value. - // Example: "${project-number}" - // +optional - Token string `json:"token,omitempty"` -} - -// ResourceReference is a reference to a KRM resource by name and kind. -// One of APIVersion or Group is required. -// Group is generally preferred, to avoid needing to update the version in lock -// step with the referenced resource. -// If neither is provided, the empty group is used. -type ResourceReference struct { - // Kind is a string value representing the REST resource this object represents. - // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - Kind string `json:"kind"` - - // APIVersion defines the versioned schema of this representation of an object. - // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - // +optional - APIVersion string `json:"apiVersion,omitempty"` - - // Group is accepted as a version-less alternative to APIVersion - // More info: https://kubernetes.io/docs/reference/using-api/#api-groups - // +optional - Group string `json:"group,omitempty"` - - // Name of the object. - // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - Name string `json:"name,omitempty"` - - // Namespace is optional, defaults to the namespace of the target object. - // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ - // +optional - Namespace string `json:"namespace,omitempty"` -} - -// ResourceReferenceFromUnstructured returns the object as a ResourceReference -func ResourceReferenceFromUnstructured(obj *unstructured.Unstructured) ResourceReference { - return ResourceReference{ - Name: obj.GetName(), - Namespace: obj.GetNamespace(), - Kind: obj.GetKind(), - APIVersion: obj.GetAPIVersion(), - } -} - -// ResourceReferenceFromObjMetadata returns the object as a ResourceReference -func ResourceReferenceFromObjMetadata(id object.ObjMetadata) ResourceReference { - return ResourceReference{ - Name: id.Name, - Namespace: id.Namespace, - Kind: id.GroupKind.Kind, - Group: id.GroupKind.Group, - } -} - -// GroupVersionKind satisfies the ObjectKind interface for all objects that -// embed TypeMeta. Prefers Group over APIVersion. -func (r ResourceReference) GroupVersionKind() schema.GroupVersionKind { - if r.Group != "" { - return schema.GroupVersionKind{Group: r.Group, Kind: r.Kind} - } - return schema.FromAPIVersionAndKind(r.APIVersion, r.Kind) -} - -// ToUnstructured returns the name, namespace, group, version, and kind of the -// ResourceReference, wrapped in a new Unstructured object. -// This is useful for performing operations with -// sigs.k8s.io/controller-runtime/pkg/client's unstructured Client. -func (r ResourceReference) ToUnstructured() *unstructured.Unstructured { - obj := &unstructured.Unstructured{} - obj.SetName(r.Name) - obj.SetNamespace(r.Namespace) - obj.SetGroupVersionKind(r.GroupVersionKind()) - return obj -} - -// ToUnstructured returns the name, namespace, group, and kind of the -// ResourceReference, wrapped in a new ObjMetadata object. -func (r ResourceReference) ToObjMetadata() object.ObjMetadata { - return object.ObjMetadata{ - Namespace: r.Namespace, - Name: r.Name, - GroupKind: r.GroupVersionKind().GroupKind(), - } -} - -// String returns the format GROUP[/VERSION][/namespaces/NAMESPACE]/KIND/NAME -func (r ResourceReference) String() string { - group := r.Group - if group == "" { - group = r.APIVersion - } - if r.Namespace != "" { - return fmt.Sprintf("%s/namespaces/%s/%s/%s", group, r.Namespace, r.Kind, r.Name) - } - return fmt.Sprintf("%s/%s/%s", group, r.Kind, r.Name) -} - -// Equal returns true if the ResourceReference sets are equivalent, ignoring version. -// Fulfills Equal interface from github.com/google/go-cmp -func (r ResourceReference) Equal(b ResourceReference) bool { - return r.GroupVersionKind().GroupKind() == b.GroupVersionKind().GroupKind() && - r.Name == b.Name && - r.Namespace == b.Namespace -} diff --git a/pkg/object/validation/collector.go b/pkg/object/validation/collector.go deleted file mode 100644 index 55057ff4..00000000 --- a/pkg/object/validation/collector.go +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package validation - -import ( - "errors" - - "github.com/fluxcd/cli-utils/pkg/multierror" - "github.com/fluxcd/cli-utils/pkg/object" -) - -// Collector simplifies collecting validation errors from multiple sources and -// extracting the IDs of the invalid objects. -type Collector struct { - Errors []error - InvalidIds object.ObjMetadataSet //nolint:revive -} - -// Collect unwraps MultiErrors, adds them to Errors, extracts invalid object -// IDs from validation.Error, and adds them to InvalidIds. -func (c *Collector) Collect(err error) { - errs := multierror.Unwrap(err) - c.InvalidIds = c.InvalidIds.Union(extractInvalidIDs(errs)) - c.Errors = append(c.Errors, errs...) -} - -// ToError returns the list of errors as a single error. -func (c *Collector) ToError() error { - return multierror.Wrap(c.Errors...) -} - -// FilterInvalidObjects returns a set of objects that does not contain any -// invalid objects, based on the collected InvalidIds. -func (c *Collector) FilterInvalidObjects(objs object.UnstructuredSet) object.UnstructuredSet { - var diff object.UnstructuredSet - for _, obj := range objs { - if !c.InvalidIds.Contains(object.UnstructuredToObjMetadata(obj)) { - diff = append(diff, obj) - } - } - return diff -} - -// FilterInvalidIds returns a set of object ID that does not contain any -// invalid IDs, based on the collected InvalidIds. -func (c *Collector) FilterInvalidIds(ids object.ObjMetadataSet) object.ObjMetadataSet { //nolint:revive - return ids.Diff(c.InvalidIds) -} - -// extractInvalidIDs extracts invalid object IDs from a list of possible -// validation.Error. -func extractInvalidIDs(errs []error) object.ObjMetadataSet { - var invalidIDs object.ObjMetadataSet - for _, err := range errs { - // unwrap recursively looking for a validation.Error - var vErr *Error - if errors.As(err, &vErr) { - invalidIDs = invalidIDs.Union(vErr.Identifiers()) - } - } - return invalidIDs -} diff --git a/pkg/object/validation/error.go b/pkg/object/validation/error.go deleted file mode 100644 index a561b508..00000000 --- a/pkg/object/validation/error.go +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright 2022 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package validation - -import ( - "fmt" - "strings" - - "github.com/fluxcd/cli-utils/pkg/object" -) - -func NewError(cause error, ids ...object.ObjMetadata) *Error { - return &Error{ - ids: object.ObjMetadataSet(ids), - cause: cause, - } -} - -// Error wraps an error with the object or objects it applies to. -type Error struct { - ids object.ObjMetadataSet - cause error -} - -// Identifiers returns zero or more object IDs which are invalid. -func (ve *Error) Identifiers() object.ObjMetadataSet { - return ve.ids -} - -// Unwrap returns the cause of the error. -// This may be useful when printing the cause without printing the identifiers. -func (ve *Error) Unwrap() error { - return ve.cause -} - -// Error stringifies the the error. -func (ve *Error) Error() string { - switch { - case len(ve.ids) == 0: - return fmt.Sprintf("validation error: %v", ve.cause.Error()) - case len(ve.ids) == 1: - return fmt.Sprintf("invalid object: %q: %v", ve.ids[0], ve.cause.Error()) - default: - var b strings.Builder - _, _ = fmt.Fprintf(&b, "invalid objects: [%q", ve.ids[0]) - for _, id := range ve.ids[1:] { - _, _ = fmt.Fprintf(&b, ", %q", id) - } - _, _ = fmt.Fprintf(&b, "] %v", ve.cause) - return b.String() - } -} diff --git a/pkg/object/validation/policy.go b/pkg/object/validation/policy.go deleted file mode 100644 index 34e85804..00000000 --- a/pkg/object/validation/policy.go +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2022 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package validation - -//go:generate stringer -type=Policy -type Policy int - -const ( - // ExitEarly policy errors and exits if any objects are invalid, before - // apply/delete of any objects. - ExitEarly Policy = iota - - // SkipInvalid policy skips the apply/delete of invalid objects. - SkipInvalid -) diff --git a/pkg/object/validation/policy_string.go b/pkg/object/validation/policy_string.go deleted file mode 100644 index 3ad973af..00000000 --- a/pkg/object/validation/policy_string.go +++ /dev/null @@ -1,24 +0,0 @@ -// Code generated by "stringer -type=Policy"; DO NOT EDIT. - -package validation - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[ExitEarly-0] - _ = x[SkipInvalid-1] -} - -const _Policy_name = "ExitEarlySkipInvalid" - -var _Policy_index = [...]uint8{0, 9, 20} - -func (i Policy) String() string { - if i < 0 || i >= Policy(len(_Policy_index)-1) { - return "Policy(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _Policy_name[_Policy_index[i]:_Policy_index[i+1]] -} diff --git a/pkg/object/validation/validate.go b/pkg/object/validation/validate.go deleted file mode 100644 index ae8decb4..00000000 --- a/pkg/object/validation/validate.go +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright 2022 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package validation - -import ( - "github.com/fluxcd/cli-utils/pkg/multierror" - "github.com/fluxcd/cli-utils/pkg/object" - "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/util/validation/field" -) - -// Validator contains functionality for validating a set of resources prior -// to being used by the Apply functionality. This imposes some constraint not -// always required, such as namespaced resources must have the namespace set. -type Validator struct { - Mapper meta.RESTMapper - Collector *Collector -} - -// Validate validates the provided resources. A RESTMapper will be used -// to fetch type information from the live cluster. -func (v *Validator) Validate(objs []*unstructured.Unstructured) { - crds := findCRDs(objs) - for _, obj := range objs { - var objErrors []error - if err := v.validateKind(obj); err != nil { - objErrors = append(objErrors, err) - } - if err := v.validateName(obj); err != nil { - objErrors = append(objErrors, err) - } - if err := v.validateNamespace(obj, crds); err != nil { - objErrors = append(objErrors, err) - } - if len(objErrors) > 0 { - // one error per object - v.Collector.Collect(NewError( - multierror.Wrap(objErrors...), - object.UnstructuredToObjMetadata(obj), - )) - } - } -} - -// findCRDs looks through the provided resources and returns a slice with -// the resources that are CRDs. -func findCRDs(us []*unstructured.Unstructured) []*unstructured.Unstructured { - var crds []*unstructured.Unstructured - for _, u := range us { - if object.IsCRD(u) { - crds = append(crds, u) - } - } - return crds -} - -// validateKind validates the value of the kind field of the resource. -func (v *Validator) validateKind(u *unstructured.Unstructured) error { - if u.GetKind() == "" { - return field.Required(field.NewPath("kind"), "kind is required") - } - return nil -} - -// validateName validates the value of the name field of the resource. -func (v *Validator) validateName(u *unstructured.Unstructured) error { - if u.GetName() == "" { - return field.Required(field.NewPath("metadata", "name"), "name is required") - } - return nil -} - -// validateNamespace validates the value of the namespace field of the resource. -func (v *Validator) validateNamespace(u *unstructured.Unstructured, crds []*unstructured.Unstructured) error { - // skip namespace validation if kind is missing (avoid redundant error) - if u.GetKind() == "" { - return nil - } - scope, err := object.LookupResourceScope(u, crds, v.Mapper) - if err != nil { - return err - } - - ns := u.GetNamespace() - if scope == meta.RESTScopeNamespace && ns == "" { - return field.Required(field.NewPath("metadata", "namespace"), "namespace is required") - } - if scope == meta.RESTScopeRoot && ns != "" { - return field.Invalid(field.NewPath("metadata", "namespace"), ns, "namespace must be empty") - } - return nil -} diff --git a/pkg/object/validation/validate_test.go b/pkg/object/validation/validate_test.go deleted file mode 100644 index fb7de393..00000000 --- a/pkg/object/validation/validate_test.go +++ /dev/null @@ -1,277 +0,0 @@ -// Copyright 2022 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package validation_test - -import ( - "testing" - - "github.com/fluxcd/cli-utils/pkg/multierror" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/object/validation" - "github.com/fluxcd/cli-utils/pkg/testutil" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/util/validation/field" - cmdtesting "k8s.io/kubectl/pkg/cmd/testing" -) - -func TestValidate(t *testing.T) { - testCases := map[string]struct { - resources []*unstructured.Unstructured - expectedError error - }{ - "missing kind": { - resources: []*unstructured.Unstructured{ - { - Object: map[string]interface{}{ - "apiVersion": "apps/v1", - "metadata": map[string]interface{}{ - "name": "foo", - "namespace": "default", - }, - }, - }, - }, - expectedError: validation.NewError( - &field.Error{ - Type: field.ErrorTypeRequired, - Field: "kind", - BadValue: "", - Detail: "kind is required", - }, - object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "", - }, - Name: "foo", - Namespace: "default", - }, - ), - }, - "multiple errors in one object": { - resources: []*unstructured.Unstructured{ - { - Object: map[string]interface{}{ - "apiVersion": "apps/v1", - "kind": "Deployment", - }, - }, - }, - expectedError: validation.NewError( - multierror.New( - &field.Error{ - Type: field.ErrorTypeRequired, - Field: "metadata.name", - BadValue: "", - Detail: "name is required", - }, - &field.Error{ - Type: field.ErrorTypeRequired, - Field: "metadata.namespace", - BadValue: "", - Detail: "namespace is required", - }, - ), - object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - }, - Name: "", - Namespace: "", - }, - ), - }, - "one error in multiple object": { - resources: []*unstructured.Unstructured{ - { - Object: map[string]interface{}{ - "apiVersion": "apps/v1", - "kind": "Deployment", - "metadata": map[string]interface{}{ - "namespace": "default", - }, - }, - }, - { - Object: map[string]interface{}{ - "apiVersion": "apps/v1", - "kind": "StatefulSet", - "metadata": map[string]interface{}{ - "namespace": "default", - }, - }, - }, - }, - expectedError: multierror.New( - validation.NewError( - &field.Error{ - Type: field.ErrorTypeRequired, - Field: "metadata.name", - BadValue: "", - Detail: "name is required", - }, - object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - }, - Name: "", - Namespace: "default", - }, - ), - validation.NewError( - &field.Error{ - Type: field.ErrorTypeRequired, - Field: "metadata.name", - BadValue: "", - Detail: "name is required", - }, - object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "StatefulSet", - }, - Name: "", - Namespace: "default", - }, - ), - ), - }, - "namespace must be empty (cluster-scoped)": { - resources: []*unstructured.Unstructured{ - { - Object: map[string]interface{}{ - "apiVersion": "v1", - "kind": "Namespace", - "metadata": map[string]interface{}{ - "name": "foo", - "namespace": "default", - }, - }, - }, - }, - expectedError: validation.NewError( - &field.Error{ - Type: field.ErrorTypeInvalid, - Field: "metadata.namespace", - BadValue: "default", - Detail: "namespace must be empty", - }, - object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "", - Kind: "Namespace", - }, - Name: "foo", - Namespace: "default", - }, - ), - }, - "namespace is required (namespace-scoped)": { - resources: []*unstructured.Unstructured{ - { - Object: map[string]interface{}{ - "apiVersion": "apps/v1", - "kind": "Deployment", - "metadata": map[string]interface{}{ - "name": "foo", - }, - }, - }, - }, - expectedError: validation.NewError( - &field.Error{ - Type: field.ErrorTypeRequired, - Field: "metadata.namespace", - BadValue: "", - Detail: "namespace is required", - }, - object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - }, - Name: "foo", - Namespace: "", - }, - ), - }, - "scope for CRs are found in CRDs if available": { - resources: []*unstructured.Unstructured{ - testutil.Unstructured(t, ` -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: customs.custom.io -spec: - group: custom.io - names: - kind: Custom - scope: Cluster - versions: - - name: v1 -`, - ), - testutil.Unstructured(t, ` -apiVersion: custom.io/v1 -kind: Custom -metadata: - name: foo - namespace: default -`, - ), - }, - expectedError: validation.NewError( - &field.Error{ - Type: field.ErrorTypeInvalid, - Field: "metadata.namespace", - BadValue: "default", - Detail: "namespace must be empty", - }, - object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "custom.io", - Kind: "Custom", - }, - Name: "foo", - Namespace: "default", - }, - ), - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - tf := cmdtesting.NewTestFactory().WithNamespace("test-ns") - defer tf.Cleanup() - - mapper, err := tf.ToRESTMapper() - require.NoError(t, err) - crdGV := schema.GroupVersion{Group: "apiextensions.k8s.io", Version: "v1"} - crdMapper := meta.NewDefaultRESTMapper([]schema.GroupVersion{crdGV}) - crdMapper.AddSpecific(crdGV.WithKind("CustomResourceDefinition"), - crdGV.WithResource("customresourcedefinitions"), - crdGV.WithResource("customresourcedefinition"), meta.RESTScopeRoot) - mapper = meta.MultiRESTMapper([]meta.RESTMapper{mapper, crdMapper}) - - vCollector := &validation.Collector{} - validator := &validation.Validator{ - Mapper: mapper, - Collector: vCollector, - } - validator.Validate(tc.resources) - err = vCollector.ToError() - if tc.expectedError == nil { - assert.NoError(t, err) - return - } - require.EqualError(t, err, tc.expectedError.Error()) - }) - } -} diff --git a/pkg/ordering/sort.go b/pkg/ordering/sort.go deleted file mode 100644 index 50813108..00000000 --- a/pkg/ordering/sort.go +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package ordering - -import ( - "sort" - - "github.com/fluxcd/cli-utils/pkg/object" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/cli-runtime/pkg/resource" -) - -type SortableInfos []*resource.Info - -var _ sort.Interface = SortableInfos{} - -func (a SortableInfos) Len() int { return len(a) } -func (a SortableInfos) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a SortableInfos) Less(i, j int) bool { - first, err := object.InfoToObjMeta(a[i]) - if err != nil { - return false - } - second, err := object.InfoToObjMeta(a[j]) - if err != nil { - return false - } - return less(first, second) -} - -type SortableUnstructureds []*unstructured.Unstructured - -var _ sort.Interface = SortableUnstructureds{} - -func (a SortableUnstructureds) Len() int { return len(a) } -func (a SortableUnstructureds) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a SortableUnstructureds) Less(i, j int) bool { - first := object.UnstructuredToObjMetadata(a[i]) - second := object.UnstructuredToObjMetadata(a[j]) - return less(first, second) -} - -type SortableMetas []object.ObjMetadata - -var _ sort.Interface = SortableMetas{} - -func (a SortableMetas) Len() int { return len(a) } -func (a SortableMetas) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a SortableMetas) Less(i, j int) bool { - return less(a[i], a[j]) -} - -func less(i, j object.ObjMetadata) bool { - if !Equals(i.GroupKind, j.GroupKind) { - return IsLessThan(i.GroupKind, j.GroupKind) - } - // In case of tie, compare the namespace and name combination so that the output - // order is consistent irrespective of input order - if i.Namespace != j.Namespace { - return i.Namespace < j.Namespace - } - return i.Name < j.Name -} - -var groupKind2index = computeGroupKind2index() - -func computeGroupKind2index() map[schema.GroupKind]int { - // An attempt to order things to help k8s, e.g. - // a Service should come before things that refer to it. - // Namespace should be first. - // In some cases order just specified to provide determinism. - orderFirst := []schema.GroupKind{ - {Group: "", Kind: "Namespace"}, - {Group: "", Kind: "ResourceQuota"}, - {Group: "storage.k8s.io", Kind: "StorageClass"}, - {Group: "apiextensions.k8s.io", Kind: "CustomResourceDefinition"}, - {Group: "admissionregistration.k8s.io", Kind: "MutatingWebhookConfiguration"}, - {Group: "", Kind: "ServiceAccount"}, - {Group: "extensions", Kind: "PodSecurityPolicy"}, // deprecated=1.11, removed=1.16 - {Group: "policy", Kind: "PodSecurityPolicy"}, // deprecated=1.21, removed=1.25 - {Group: "rbac.authorization.k8s.io", Kind: "Role"}, - {Group: "rbac.authorization.k8s.io", Kind: "ClusterRole"}, - {Group: "rbac.authorization.k8s.io", Kind: "RoleBinding"}, - {Group: "rbac.authorization.k8s.io", Kind: "ClusterRoleBinding"}, - {Group: "", Kind: "ConfigMap"}, - {Group: "", Kind: "Secret"}, - {Group: "", Kind: "Service"}, - {Group: "", Kind: "LimitRange"}, - {Group: "scheduling.k8s.io", Kind: "PriorityClass"}, - {Group: "extensions", Kind: "Deployment"}, // deprecated=1.8, removed=1.16 - {Group: "apps", Kind: "Deployment"}, - {Group: "apps", Kind: "StatefulSet"}, - {Group: "batch", Kind: "CronJob"}, - {Group: "policy", Kind: "PodDisruptionBudget"}, - } - orderLast := []schema.GroupKind{ - {Group: "admissionregistration.k8s.io", Kind: "ValidatingWebhookConfiguration"}, - } - kind2indexResult := make(map[schema.GroupKind]int, len(orderFirst)+len(orderLast)) - for i, n := range orderFirst { - kind2indexResult[n] = -len(orderFirst) + i - } - for i, n := range orderLast { - kind2indexResult[n] = 1 + i - } - return kind2indexResult -} - -func Equals(i, j schema.GroupKind) bool { - return i.Group == j.Group && i.Kind == j.Kind -} - -func IsLessThan(i, j schema.GroupKind) bool { - indexI := groupKind2index[i] - indexJ := groupKind2index[j] - if indexI != indexJ { - return indexI < indexJ - } - if i.Group != j.Group { - return i.Group < j.Group - } - return i.Kind < j.Kind -} diff --git a/pkg/ordering/sort_test.go b/pkg/ordering/sort_test.go deleted file mode 100644 index a89b2140..00000000 --- a/pkg/ordering/sort_test.go +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package ordering - -import ( - "sort" - "testing" - - "github.com/stretchr/testify/assert" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/cli-runtime/pkg/resource" -) - -var configMapObj = unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "v1", - "kind": "ConfigMap", - "metadata": map[string]interface{}{ - "name": "the-map", - "namespace": "testspace", - }, - }, -} - -var namespaceObj = unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "v1", - "kind": "Namespace", - "metadata": map[string]interface{}{ - "name": "testspace", - }, - }, -} - -var deploymentObj = unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "apps/v1", - "kind": "Deployment", - "metadata": map[string]interface{}{ - "name": "testdeployment", - "namespace": "testspace", - }, - }, -} - -var deploymentObj2 = unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "apps/v1", - "kind": "Deployment", - "metadata": map[string]interface{}{ - "name": "testdeployment2", - "namespace": "testspace", - }, - }, -} - -func TestResourceOrdering(t *testing.T) { - configMapInfo := resource.Info{ - Name: "the-map", - Object: &configMapObj, - } - - namespaceInfo := resource.Info{ - Name: "testspace", - Object: &namespaceObj, - } - - deploymentInfo := resource.Info{ - Name: "testdeployment", - Object: &deploymentObj, - } - - deploymentInfo2 := resource.Info{ - Name: "testdeployment2", - Object: &deploymentObj2, - } - - infos := []*resource.Info{&deploymentInfo, &configMapInfo, &namespaceInfo, &deploymentInfo2} - sort.Sort(SortableInfos(infos)) - - assert.Equal(t, infos[0].Name, "testspace") - assert.Equal(t, infos[1].Name, "the-map") - assert.Equal(t, infos[2].Name, "testdeployment") - assert.Equal(t, infos[3].Name, "testdeployment2") - - assert.Equal(t, infos[0].Object.GetObjectKind().GroupVersionKind().Kind, "Namespace") - assert.Equal(t, infos[1].Object.GetObjectKind().GroupVersionKind().Kind, "ConfigMap") - assert.Equal(t, infos[2].Object.GetObjectKind().GroupVersionKind().Kind, "Deployment") - assert.Equal(t, infos[3].Object.GetObjectKind().GroupVersionKind().Kind, "Deployment") -} - -func TestGvkLessThan(t *testing.T) { - gk1 := schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - } - - gk2 := schema.GroupKind{ - Group: "", - Kind: "Namespace", - } - - assert.False(t, IsLessThan(gk1, gk2)) -} - -func TestGvkEquals(t *testing.T) { - gk1 := schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - } - - gk2 := schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - } - - assert.True(t, Equals(gk1, gk2)) -} diff --git a/pkg/print/common/ansii.go b/pkg/print/common/ansii.go deleted file mode 100644 index 1c64dc7a..00000000 --- a/pkg/print/common/ansii.go +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package common - -const ( - // RESET is the escape sequence for unsetting any previous commands. - RESET = 0 - // ESC is the escape sequence used to send ANSI commands in the terminal. - ESC = 27 -) diff --git a/pkg/print/common/color.go b/pkg/print/common/color.go deleted file mode 100644 index 5db77d2e..00000000 --- a/pkg/print/common/color.go +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package common - -import ( - "fmt" - - "github.com/fluxcd/cli-utils/pkg/kstatus/status" -) - -// color is a type that captures the ANSI code for colors on the -// terminal. -type Color int - -var ( - RED Color = 31 - GREEN Color = 32 - YELLOW Color = 33 -) - -// SprintfWithColor formats according to the provided pattern and returns -// the result as a string with the necessary ansii escape codes for -// color -func SprintfWithColor(color Color, format string, a ...interface{}) string { - return fmt.Sprintf("%c[%dm", ESC, color) + - fmt.Sprintf(format, a...) + - fmt.Sprintf("%c[%dm", ESC, RESET) -} - -// ColorForStatus returns the appropriate Color, which represents -// the ansii escape code, for different status values. -func ColorForStatus(s status.Status) (color Color, setColor bool) { - switch s { - case status.CurrentStatus: - color = GREEN - setColor = true - case status.InProgressStatus: - color = YELLOW - setColor = true - case status.FailedStatus: - color = RED - setColor = true - } - return -} diff --git a/pkg/print/common/color_test.go b/pkg/print/common/color_test.go deleted file mode 100644 index 2158b853..00000000 --- a/pkg/print/common/color_test.go +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package common - -import ( - "testing" - - "github.com/fluxcd/cli-utils/pkg/kstatus/status" - "github.com/stretchr/testify/assert" -) - -func TestSprintfWithColor(t *testing.T) { - testCases := map[string]struct { - color Color - format string - args []interface{} - expectedResult string - }{ - "no args with color": { - color: GREEN, - format: "This is a test", - args: []interface{}{}, - expectedResult: "\x1b[32mThis is a test\x1b[0m", - }, - "with args and color": { - color: YELLOW, - format: "%s %s", - args: []interface{}{"sonic", "youth"}, - expectedResult: "\x1b[33msonic youth\x1b[0m", - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - result := SprintfWithColor(tc.color, tc.format, tc.args...) - if want, got := tc.expectedResult, result; want != got { - t.Errorf("expected %q, but got %q", want, got) - } - }) - } -} - -func TestColorForStatus(t *testing.T) { - testCases := map[string]struct { - status status.Status - expectedSetColor bool - expectedColor Color - }{ - "status with color": { - status: status.CurrentStatus, - expectedSetColor: true, - expectedColor: GREEN, - }, - "status without color": { - status: status.NotFoundStatus, - expectedSetColor: false, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - color, setColor := ColorForStatus(tc.status) - assert.Equal(t, setColor, tc.expectedSetColor) - if tc.expectedSetColor { - assert.Equal(t, color, tc.expectedColor) - } - }) - } -} diff --git a/pkg/print/common/errors.go b/pkg/print/common/errors.go deleted file mode 100644 index a3c9ac85..00000000 --- a/pkg/print/common/errors.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2022 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package common - -import ( - "fmt" - - "github.com/fluxcd/cli-utils/pkg/print/stats" -) - -// ResultErrorFromStats takes a stats object and returns either a ResultError or -// nil depending on whether the stats reports that resources failed apply/prune/delete -// or reconciliation. -func ResultErrorFromStats(s stats.Stats) error { - if s.FailedActuationSum() > 0 || s.FailedReconciliationSum() > 0 { - return &ResultError{ - Stats: s, - } - } - return nil -} - -// ResultError is returned from printers when the apply/destroy operations completed, but one or -// more resources either failed apply/prune/delete, or failed to reconcile. -type ResultError struct { - Stats stats.Stats -} - -func (a *ResultError) Error() string { - switch { - case a.Stats.FailedActuationSum() > 0 && a.Stats.FailedReconciliationSum() > 0: - return fmt.Sprintf("%d resources failed, %d resources failed to reconcile before timeout", - a.Stats.FailedActuationSum(), a.Stats.FailedReconciliationSum()) - case a.Stats.FailedActuationSum() > 0: - return fmt.Sprintf("%d resources failed", a.Stats.FailedActuationSum()) - case a.Stats.FailedReconciliationSum() > 0: - return fmt.Sprintf("%d resources failed to reconcile before timeout", - a.Stats.FailedReconciliationSum()) - default: - // Should not happen as this error is only used when at least one resource - // either failed to apply/prune/delete or reconcile. - return "unknown error" - } -} diff --git a/pkg/print/list/base.go b/pkg/print/list/base.go deleted file mode 100644 index 09bdb1c2..00000000 --- a/pkg/print/list/base.go +++ /dev/null @@ -1,137 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package list - -import ( - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/object" - printcommon "github.com/fluxcd/cli-utils/pkg/print/common" - "github.com/fluxcd/cli-utils/pkg/print/stats" -) - -type Formatter interface { - FormatValidationEvent(ve event.ValidationEvent) error - FormatApplyEvent(ae event.ApplyEvent) error - FormatStatusEvent(se event.StatusEvent) error - FormatPruneEvent(pe event.PruneEvent) error - FormatDeleteEvent(de event.DeleteEvent) error - FormatWaitEvent(we event.WaitEvent) error - FormatErrorEvent(ee event.ErrorEvent) error - FormatActionGroupEvent( - age event.ActionGroupEvent, - ags []event.ActionGroup, - s stats.Stats, - c Collector, - ) error - FormatSummary(s stats.Stats) error -} - -type FormatterFactory func(previewStrategy common.DryRunStrategy) Formatter - -type BaseListPrinter struct { - FormatterFactory FormatterFactory -} - -type Collector interface { - LatestStatus() map[object.ObjMetadata]event.StatusEvent -} - -type StatusCollector struct { - latestStatus map[object.ObjMetadata]event.StatusEvent -} - -func (sc *StatusCollector) updateStatus(id object.ObjMetadata, se event.StatusEvent) { - sc.latestStatus[id] = se -} - -func (sc *StatusCollector) LatestStatus() map[object.ObjMetadata]event.StatusEvent { - return sc.latestStatus -} - -// Print outputs the events from the provided channel in a simple -// format on StdOut. As we support other printer implementations -// this should probably be an interface. -// This function will block until the channel is closed. -// -//nolint:gocyclo -func (b *BaseListPrinter) Print(ch <-chan event.Event, previewStrategy common.DryRunStrategy, printStatus bool) error { - var actionGroups []event.ActionGroup - var statsCollector stats.Stats - statusCollector := &StatusCollector{ - latestStatus: make(map[object.ObjMetadata]event.StatusEvent), - } - formatter := b.FormatterFactory(previewStrategy) - for e := range ch { - statsCollector.Handle(e) - switch e.Type { - case event.InitType: - actionGroups = e.InitEvent.ActionGroups - case event.ErrorType: - _ = formatter.FormatErrorEvent(e.ErrorEvent) - return e.ErrorEvent.Err - case event.ValidationType: - if err := formatter.FormatValidationEvent(e.ValidationEvent); err != nil { - return err - } - case event.ApplyType: - if err := formatter.FormatApplyEvent(e.ApplyEvent); err != nil { - return err - } - case event.StatusType: - statusCollector.updateStatus(e.StatusEvent.Identifier, e.StatusEvent) - if printStatus { - if err := formatter.FormatStatusEvent(e.StatusEvent); err != nil { - return err - } - } - case event.PruneType: - if err := formatter.FormatPruneEvent(e.PruneEvent); err != nil { - return err - } - case event.DeleteType: - if err := formatter.FormatDeleteEvent(e.DeleteEvent); err != nil { - return err - } - case event.WaitType: - if err := formatter.FormatWaitEvent(e.WaitEvent); err != nil { - return err - } - case event.ActionGroupType: - if err := formatter.FormatActionGroupEvent( - e.ActionGroupEvent, - actionGroups, - statsCollector, - statusCollector, - ); err != nil { - return err - } - } - } - - if err := formatter.FormatSummary(statsCollector); err != nil { - return err - } - return printcommon.ResultErrorFromStats(statsCollector) -} - -// IsLastActionGroup returns true if the passed ActionGroupEvent is the -// last of its type in the slice of ActionGroup; false otherwise. For example, -// this function will determine if an ApplyAction is the last ApplyAction in -// the initialized task queue. This functionality is current used to determine -// when to print stats. -func IsLastActionGroup(age event.ActionGroupEvent, ags []event.ActionGroup) bool { - var found bool - var action event.ResourceAction - for _, ag := range ags { - if found && (action == ag.Action) { - return false - } - if age.GroupName == ag.Name { - found = true - action = age.Action - } - } - return true -} diff --git a/pkg/print/list/base_test.go b/pkg/print/list/base_test.go deleted file mode 100644 index 9e56a8f4..00000000 --- a/pkg/print/list/base_test.go +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2022 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package list - -import ( - "testing" - - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/print/stats" - "github.com/fluxcd/cli-utils/pkg/printers/printer" - printertesting "github.com/fluxcd/cli-utils/pkg/printers/testutil" -) - -func TestPrint(t *testing.T) { - printertesting.PrintResultErrorTest(t, func() printer.Printer { - return &BaseListPrinter{ - FormatterFactory: func(previewStrategy common.DryRunStrategy) Formatter { - return newCountingFormatter() - }, - } - }) -} - -func newCountingFormatter() *countingFormatter { - return &countingFormatter{} -} - -type countingFormatter struct { - validationEvent []event.ValidationEvent - applyEvents []event.ApplyEvent - statusEvents []event.StatusEvent - pruneEvents []event.PruneEvent - deleteEvents []event.DeleteEvent - waitEvents []event.WaitEvent - errorEvent event.ErrorEvent - actionGroupEvent []event.ActionGroupEvent -} - -func (c *countingFormatter) FormatValidationEvent(e event.ValidationEvent) error { - c.validationEvent = append(c.validationEvent, e) - return nil -} - -func (c *countingFormatter) FormatApplyEvent(e event.ApplyEvent) error { - c.applyEvents = append(c.applyEvents, e) - return nil -} - -func (c *countingFormatter) FormatStatusEvent(e event.StatusEvent) error { - c.statusEvents = append(c.statusEvents, e) - return nil -} - -func (c *countingFormatter) FormatPruneEvent(e event.PruneEvent) error { - c.pruneEvents = append(c.pruneEvents, e) - return nil -} - -func (c *countingFormatter) FormatDeleteEvent(e event.DeleteEvent) error { - c.deleteEvents = append(c.deleteEvents, e) - return nil -} - -func (c *countingFormatter) FormatWaitEvent(e event.WaitEvent) error { - c.waitEvents = append(c.waitEvents, e) - return nil -} - -func (c *countingFormatter) FormatErrorEvent(e event.ErrorEvent) error { - c.errorEvent = e - return nil -} - -func (c *countingFormatter) FormatActionGroupEvent( - e event.ActionGroupEvent, - _ []event.ActionGroup, - _ stats.Stats, - _ Collector, -) error { - c.actionGroupEvent = append(c.actionGroupEvent, e) - return nil -} - -func (c *countingFormatter) FormatSummary(s stats.Stats) error { - return nil -} diff --git a/pkg/print/stats/stats.go b/pkg/print/stats/stats.go deleted file mode 100644 index 93c9ca73..00000000 --- a/pkg/print/stats/stats.go +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright 2022 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package stats - -import ( - "fmt" - - "github.com/fluxcd/cli-utils/pkg/apply/event" -) - -// Stats captures the summarized numbers from apply/prune/delete and -// reconciliation of resources. Each item in a stats list represents the stats -// from all the events in a single action group. -type Stats struct { - ApplyStats ApplyStats - PruneStats PruneStats - DeleteStats DeleteStats - WaitStats WaitStats -} - -// FailedActuationSum returns the number of resources that failed actuation. -func (s *Stats) FailedActuationSum() int { - return s.ApplyStats.Failed + s.PruneStats.Failed + s.DeleteStats.Failed -} - -// FailedReconciliationSum returns the number of resources that failed reconciliation. -func (s *Stats) FailedReconciliationSum() int { - return s.WaitStats.Failed + s.WaitStats.Timeout -} - -// Handle updates the stats based on an event. -func (s *Stats) Handle(e event.Event) { - switch e.Type { - case event.ApplyType: - s.ApplyStats.Inc(e.ApplyEvent.Status) - case event.PruneType: - s.PruneStats.Inc(e.PruneEvent.Status) - case event.DeleteType: - s.DeleteStats.Inc(e.DeleteEvent.Status) - case event.WaitType: - s.WaitStats.Inc(e.WaitEvent.Status) - } -} - -type ApplyStats struct { - Successful int - Skipped int - Failed int -} - -func (a *ApplyStats) Inc(op event.ApplyEventStatus) { - switch op { - case event.ApplySuccessful: - a.Successful++ - case event.ApplySkipped: - a.Skipped++ - case event.ApplyFailed: - a.Failed++ - default: - panic(fmt.Errorf("invalid apply status %s", op.String())) - } -} - -func (a *ApplyStats) IncFailed() { - a.Failed++ -} - -func (a *ApplyStats) Sum() int { - return a.Successful + a.Skipped + a.Failed -} - -type PruneStats struct { - Successful int - Skipped int - Failed int -} - -func (p *PruneStats) Inc(op event.PruneEventStatus) { - switch op { - case event.PruneSuccessful: - p.Successful++ - case event.PruneSkipped: - p.Skipped++ - case event.PruneFailed: - p.Failed++ - default: - panic(fmt.Errorf("invalid prune status %s", op.String())) - } -} - -func (p *PruneStats) IncFailed() { - p.Failed++ -} - -func (p *PruneStats) Sum() int { - return p.Successful + p.Skipped + p.Failed -} - -type DeleteStats struct { - Successful int - Skipped int - Failed int -} - -func (d *DeleteStats) Inc(op event.DeleteEventStatus) { - switch op { - case event.DeleteSuccessful: - d.Successful++ - case event.DeleteSkipped: - d.Skipped++ - case event.DeleteFailed: - d.Failed++ - default: - panic(fmt.Errorf("invalid delete status %s", op.String())) - } -} - -func (d *DeleteStats) IncFailed() { - d.Failed++ -} - -func (d *DeleteStats) Sum() int { - return d.Successful + d.Skipped + d.Failed -} - -type WaitStats struct { - Successful int - Timeout int - Failed int - Skipped int -} - -func (w *WaitStats) Inc(status event.WaitEventStatus) { - switch status { - case event.ReconcilePending: - // ignore - should be replaced by one of the others before the WaitTask exits - case event.ReconcileSuccessful: - w.Successful++ - case event.ReconcileSkipped: - w.Skipped++ - case event.ReconcileTimeout: - w.Timeout++ - case event.ReconcileFailed: - w.Failed++ - default: - panic(fmt.Errorf("invalid wait status %s", status.String())) - } -} - -func (w *WaitStats) Sum() int { - return w.Successful + w.Skipped + w.Failed + w.Timeout -} diff --git a/pkg/print/table/base.go b/pkg/print/table/base.go deleted file mode 100644 index e256c0ff..00000000 --- a/pkg/print/table/base.go +++ /dev/null @@ -1,154 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package table - -import ( - "fmt" - "io" - "strings" - "unicode/utf8" - - "k8s.io/cli-runtime/pkg/genericclioptions" - - pe "github.com/fluxcd/cli-utils/pkg/kstatus/polling/event" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/print/common" -) - -// ColumnDefinition defines the columns that should be printed. -type ColumnDefinition interface { - Name() string - Header() string - Width() int - PrintResource(w io.Writer, width int, r Resource) (int, error) -} - -// ResourceStates defines the interface that must be implemented -// by the object that provides information about the resources -// that should be printed. -type ResourceStates interface { - Resources() []Resource - Error() error -} - -// Resource defines the interface that each of the Resource -// objects must implement. -type Resource interface { - Identifier() object.ObjMetadata - ResourceStatus() *pe.ResourceStatus - SubResources() []Resource -} - -// BaseTablePrinter provides functionality for printing information -// about a set of resources into a table format. -// The printer will print to the Out stream defined in IOStreams, -// and will print into the format defined by the Column definitions. -type BaseTablePrinter struct { - IOStreams genericclioptions.IOStreams - Columns []ColumnDefinition -} - -// PrintTable prints the resources defined in ResourceStates. It will -// print subresources if they exist. -// moveUpCount defines how many lines the printer should move up -// before starting printing. The return value is how many lines -// were printed. -func (t *BaseTablePrinter) PrintTable(rs ResourceStates, - moveUpCount int) int { - for i := 0; i < moveUpCount; i++ { - t.moveUp() - t.eraseCurrentLine() - } - - linePrintCount := 0 - for i, column := range t.Columns { - format := fmt.Sprintf("%%-%ds", column.Width()) - t.printOrDie(format, column.Header()) - if i == len(t.Columns)-1 { - t.printOrDie("\n") - linePrintCount++ - } else { - t.printOrDie(" ") - } - } - - for _, resource := range rs.Resources() { - for i, column := range t.Columns { - written, err := column.PrintResource(t.IOStreams.Out, column.Width(), resource) - if err != nil { - panic(err) - } - remainingSpace := column.Width() - written - t.printOrDie("%s", strings.Repeat(" ", remainingSpace)) - if i == len(t.Columns)-1 { - t.printOrDie("\n") - linePrintCount++ - } else { - t.printOrDie(" ") - } - } - - linePrintCount += t.printSubTable(resource.SubResources(), "") - } - - return linePrintCount -} - -// printSubTable prints out any subresources that belong to the -// top-level resources. This function takes care of printing the correct tree -// structure and indentation. -func (t *BaseTablePrinter) printSubTable(resources []Resource, - prefix string) int { - linePrintCount := 0 - for j, resource := range resources { - for i, column := range t.Columns { - availableWidth := column.Width() - if column.Name() == "resource" { - if j < len(resources)-1 { - t.printOrDie("%s", prefix+`├─ `) - } else { - t.printOrDie("%s", prefix+`└─ `) - } - availableWidth -= utf8.RuneCountInString(prefix) + 3 - } - written, err := column.PrintResource(t.IOStreams.Out, - availableWidth, resource) - if err != nil { - panic(err) - } - remainingSpace := availableWidth - written - t.printOrDie("%s", strings.Repeat(" ", remainingSpace)) - if i == len(t.Columns)-1 { - t.printOrDie("\n") - linePrintCount++ - } else { - t.printOrDie(" ") - } - } - - var prefix string - if j < len(resources)-1 { - prefix = `│ ` - } else { - prefix = " " - } - linePrintCount += t.printSubTable(resource.SubResources(), prefix) - } - return linePrintCount -} - -func (t *BaseTablePrinter) printOrDie(format string, a ...interface{}) { - _, err := fmt.Fprintf(t.IOStreams.Out, format, a...) - if err != nil { - panic(err) - } -} - -func (t *BaseTablePrinter) moveUp() { - t.printOrDie("%c[%dA", common.ESC, 1) -} - -func (t *BaseTablePrinter) eraseCurrentLine() { - t.printOrDie("%c[2K\r", common.ESC) -} diff --git a/pkg/print/table/base_test.go b/pkg/print/table/base_test.go deleted file mode 100644 index 0d6a3594..00000000 --- a/pkg/print/table/base_test.go +++ /dev/null @@ -1,188 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package table - -import ( - "fmt" - "io" - "strings" - "testing" - - pe "github.com/fluxcd/cli-utils/pkg/kstatus/polling/event" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/stretchr/testify/assert" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/cli-runtime/pkg/genericclioptions" -) - -var ( - endColumnDef = ColumnDef{ - ColumnName: "end", - ColumnHeader: "END", - ColumnWidth: 3, - PrintResourceFunc: func(w io.Writer, width int, r Resource) (i int, - err error) { - return fmt.Fprint(w, "end") - }, - } -) - -func TestBaseTablePrinter_PrintTable(t *testing.T) { - testCases := map[string]struct { - columnDefinitions []ColumnDefinition - resources []Resource - expectedOutput string - }{ - "no resources": { - columnDefinitions: []ColumnDefinition{ - MustColumn("resource"), - endColumnDef, - }, - resources: []Resource{}, - expectedOutput: ` -RESOURCE END -`, - }, - "with resource": { - columnDefinitions: []ColumnDefinition{ - MustColumn("resource"), - endColumnDef, - }, - resources: []Resource{ - &fakeResource{ - resourceStatus: &pe.ResourceStatus{ - Identifier: object.ObjMetadata{ - Namespace: "default", - Name: "Foo", - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - }, - }, - }, - }, - }, - expectedOutput: ` -RESOURCE END -Deployment/Foo end -`, - }, - "sub resources": { - columnDefinitions: []ColumnDefinition{ - MustColumn("resource"), - endColumnDef, - }, - resources: []Resource{ - &fakeResource{ - resourceStatus: &pe.ResourceStatus{ - Identifier: object.ObjMetadata{ - Namespace: "default", - Name: "Foo", - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - }, - }, - GeneratedResources: []*pe.ResourceStatus{ - { - Identifier: object.ObjMetadata{ - Namespace: "default", - Name: "Bar", - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "ReplicaSet", - }, - }, - }, - }, - }, - }, - }, - expectedOutput: ` -RESOURCE END -Deployment/Foo end -└─ ReplicaSet/Bar end -`, - }, - "trim long content": { - columnDefinitions: []ColumnDefinition{ - MustColumn("resource"), - endColumnDef, - }, - resources: []Resource{ - &fakeResource{ - resourceStatus: &pe.ResourceStatus{ - Identifier: object.ObjMetadata{ - Namespace: "default", - Name: "VeryLongNameThatShouldBeTrimmed", - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - }, - }, - }, - }, - }, - expectedOutput: ` -RESOURCE END -Deployment/VeryLongNameThatShouldBeTrimm end -`, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - ioStreams, _, outBuffer, _ := genericclioptions.NewTestIOStreams() - - printer := &BaseTablePrinter{ - IOStreams: ioStreams, - Columns: tc.columnDefinitions, - } - - resourceStates := &fakeResourceStates{ - resources: tc.resources, - } - - printer.PrintTable(resourceStates, 0) - - assert.Equal(t, - strings.TrimSpace(tc.expectedOutput), - strings.TrimSpace(outBuffer.String())) - }) - } -} - -type fakeResourceStates struct { - resources []Resource -} - -func (r *fakeResourceStates) Resources() []Resource { - return r.resources -} - -func (r *fakeResourceStates) Error() error { - return nil -} - -type fakeResource struct { - resourceStatus *pe.ResourceStatus -} - -func (r *fakeResource) Identifier() object.ObjMetadata { - return r.resourceStatus.Identifier -} - -func (r *fakeResource) ResourceStatus() *pe.ResourceStatus { - return r.resourceStatus -} - -func (r *fakeResource) SubResources() []Resource { - var resources []Resource - for _, res := range r.resourceStatus.GeneratedResources { - resources = append(resources, &fakeResource{ - resourceStatus: res, - }) - } - return resources -} diff --git a/pkg/print/table/columndefs.go b/pkg/print/table/columndefs.go deleted file mode 100644 index f4a1a2a5..00000000 --- a/pkg/print/table/columndefs.go +++ /dev/null @@ -1,244 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package table - -import ( - "fmt" - "io" - "time" - - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/utils/integer" - - "github.com/fluxcd/cli-utils/pkg/print/common" -) - -// ColumnDef is an implementation of the ColumnDefinition interface. -// It can be used to define simple columns that doesn't need additional -// knowledge about the actual type of the provided Resource besides the -// information available through the interface. -type ColumnDef struct { - ColumnName string - ColumnHeader string - ColumnWidth int - PrintResourceFunc func(w io.Writer, width int, r Resource) (int, error) -} - -// Name returns the name of the column. -func (c ColumnDef) Name() string { - return c.ColumnName -} - -// Header returns the header that should be printed for -// the column. -func (c ColumnDef) Header() string { - return c.ColumnHeader -} - -// Width returns the width of the column. -func (c ColumnDef) Width() int { - return c.ColumnWidth -} - -// PrintResource is called by the BaseTablePrinter to output the -// content of a particular column. This implementation just delegates -// to the provided PrintResourceFunc. -func (c ColumnDef) PrintResource(w io.Writer, width int, r Resource) (int, error) { - return c.PrintResourceFunc(w, width, r) -} - -// MustColumn returns the pre-defined column definition with the -// provided name. If the name doesn't exist, it will panic. -func MustColumn(name string) ColumnDef { - c, found := columnDefinitions[name] - if !found { - panic(fmt.Errorf("unknown column name %q", name)) - } - return c -} - -var ( - columnDefinitions = map[string]ColumnDef{ - // namespace defines a column that output the namespace of the - // resource, or nothing in the case of clusterscoped resources. - "namespace": { - ColumnName: "namespace", - ColumnHeader: "NAMESPACE", - ColumnWidth: 10, - PrintResourceFunc: func(w io.Writer, width int, r Resource) (int, - error) { - namespace := r.Identifier().Namespace - if len(namespace) > width { - namespace = namespace[:width] - } - _, err := fmt.Fprint(w, namespace) - return len(namespace), err - }, - }, - // resource defines a column that outputs the kind and name of a - // resource. - "resource": { - ColumnName: "resource", - ColumnHeader: "RESOURCE", - ColumnWidth: 40, - PrintResourceFunc: func(w io.Writer, width int, r Resource) (int, - error) { - text := fmt.Sprintf("%s/%s", r.Identifier().GroupKind.Kind, - r.Identifier().Name) - if len(text) > width { - text = text[:width] - } - _, err := fmt.Fprint(w, text) - return len(text), err - }, - }, - // status defines a column that outputs the status of a resource. It - // will use ansii escape codes to color the output. - "status": { - ColumnName: "status", - ColumnHeader: "STATUS", - ColumnWidth: 10, - PrintResourceFunc: func(w io.Writer, width int, r Resource) (int, - error) { - rs := r.ResourceStatus() - if rs == nil { - return 0, nil - } - s := rs.Status.String() - if len(s) > width { - s = s[:width] - } - color, setColor := common.ColorForStatus(rs.Status) - var outputStatus string - if setColor { - outputStatus = common.SprintfWithColor(color, "%s", s) - } else { - outputStatus = s - } - _, err := fmt.Fprint(w, outputStatus) - return len(s), err - }, - }, - // conditions defines a column that outputs the conditions for - // a resource. The output will be in colors. - "conditions": { - ColumnName: "conditions", - ColumnHeader: "CONDITIONS", - ColumnWidth: 40, - PrintResourceFunc: func(w io.Writer, width int, r Resource) (int, - error) { - rs := r.ResourceStatus() - if rs == nil { - return 0, nil - } - u := rs.Resource - if u == nil { - return fmt.Fprintf(w, "-") - } - - conditions, found, err := unstructured.NestedSlice(u.Object, - "status", "conditions") - if !found || err != nil || len(conditions) == 0 { - return fmt.Fprintf(w, "") - } - - realLength := 0 - for i, cond := range conditions { - condition := cond.(map[string]interface{}) - conditionType := condition["type"].(string) - conditionStatus := condition["status"].(string) - var color common.Color - switch conditionStatus { - case "True": - color = common.GREEN - case "False": - color = common.RED - default: - color = common.YELLOW - } - remainingWidth := width - realLength - if len(conditionType) > remainingWidth { - conditionType = conditionType[:remainingWidth] - } - _, err := fmt.Fprint(w, common.SprintfWithColor(color, "%s", conditionType)) - if err != nil { - return realLength, err - } - realLength += len(conditionType) - if i < len(conditions)-1 && width-realLength > 2 { - _, err = fmt.Fprintf(w, ",") - if err != nil { - return realLength, err - } - realLength++ - } - } - return realLength, nil - }, - }, - // age defines a column that outputs the age of a resource computed - // by looking at the creationTimestamp field. - "age": { - ColumnName: "age", - ColumnHeader: "AGE", - ColumnWidth: 6, - PrintResourceFunc: func(w io.Writer, width int, r Resource) (i int, err error) { - rs := r.ResourceStatus() - if rs == nil { - return 0, nil - } - u := rs.Resource - if u == nil { - return fmt.Fprint(w, "-") - } - - timestamp, found, err := unstructured.NestedString(u.Object, - "metadata", "creationTimestamp") - if !found || err != nil || timestamp == "" { - return fmt.Fprint(w, "-") - } - parsedTime, err := time.Parse(time.RFC3339, timestamp) - if err != nil { - return fmt.Fprint(w, "-") - } - age := time.Since(parsedTime) - switch { - case age.Seconds() <= 90: - return fmt.Fprintf(w, "%ds", - integer.RoundToInt32(age.Round(time.Second).Seconds())) - case age.Minutes() <= 90: - return fmt.Fprintf(w, "%dm", - integer.RoundToInt32(age.Round(time.Minute).Minutes())) - default: - return fmt.Fprintf(w, "%dh", - integer.RoundToInt32(age.Round(time.Hour).Hours())) - } - }, - }, - // message defines a column that outputs the message from a - // ResourceStatus, or if there is a non-nil error, output the text - // from the error instead. - "message": { - ColumnName: "message", - ColumnHeader: "MESSAGE", - ColumnWidth: 40, - PrintResourceFunc: func(w io.Writer, width int, r Resource) (i int, err error) { - rs := r.ResourceStatus() - if rs == nil { - return 0, nil - } - var message string - if rs.Error != nil { - message = rs.Error.Error() - } else { - message = rs.Message - } - if len(message) > width { - message = message[:width] - } - return fmt.Fprint(w, message) - }, - }, - } -) diff --git a/pkg/print/table/columndefs_test.go b/pkg/print/table/columndefs_test.go deleted file mode 100644 index 268beb15..00000000 --- a/pkg/print/table/columndefs_test.go +++ /dev/null @@ -1,239 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package table - -import ( - "bytes" - "fmt" - "testing" - "time" - - pe "github.com/fluxcd/cli-utils/pkg/kstatus/polling/event" - "github.com/fluxcd/cli-utils/pkg/kstatus/status" - "github.com/fluxcd/cli-utils/pkg/object" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" -) - -func TestColumnDefs(t *testing.T) { - testCases := map[string]struct { - columnName string - resource Resource - columnWidth int - expectedOutput string - }{ - "namespace": { - columnName: "namespace", - resource: &fakeResource{ - resourceStatus: &pe.ResourceStatus{ - Identifier: object.ObjMetadata{ - Namespace: "Foo", - }, - }, - }, - columnWidth: 10, - expectedOutput: "Foo", - }, - "namespace trimmed": { - columnName: "namespace", - resource: &fakeResource{ - resourceStatus: &pe.ResourceStatus{ - Identifier: object.ObjMetadata{ - Namespace: "ICanHearTheHeartBeatingAsOne", - }, - }, - }, - columnWidth: 10, - expectedOutput: "ICanHearTh", - }, - "resource": { - columnName: "resource", - resource: &fakeResource{ - resourceStatus: &pe.ResourceStatus{ - Identifier: object.ObjMetadata{ - Name: "YoLaTengo", - GroupKind: schema.GroupKind{ - Kind: "RoleBinding", - }, - }, - }, - }, - columnWidth: 40, - expectedOutput: "RoleBinding/YoLaTengo", - }, - "resource trimmed": { - columnName: "resource", - resource: &fakeResource{ - resourceStatus: &pe.ResourceStatus{ - Identifier: object.ObjMetadata{ - Name: "SlantedAndEnchanted", - GroupKind: schema.GroupKind{ - Kind: "Pavement", - }, - }, - }, - }, - columnWidth: 25, - expectedOutput: "Pavement/SlantedAndEnchan", - }, - "status with color": { - columnName: "status", - resource: &fakeResource{ - resourceStatus: &pe.ResourceStatus{ - Status: status.CurrentStatus, - }, - }, - columnWidth: 10, - expectedOutput: "\x1b[32mCurrent\x1b[0m", - }, - "status trimmed": { - columnName: "status", - resource: &fakeResource{ - resourceStatus: &pe.ResourceStatus{ - Status: status.NotFoundStatus, - }, - }, - columnWidth: 5, - expectedOutput: "NotFo", - }, - "conditions with color": { - columnName: "conditions", - resource: &fakeResource{ - resourceStatus: &pe.ResourceStatus{ - Resource: mustResourceWithConditions([]condition{ - { - Type: "Ready", - Status: v1.ConditionUnknown, - }, - }), - }, - }, - columnWidth: 10, - expectedOutput: "\x1b[33mReady\x1b[0m", - }, - "conditions trimmed": { - columnName: "conditions", - resource: &fakeResource{ - resourceStatus: &pe.ResourceStatus{ - Resource: mustResourceWithConditions([]condition{ - { - Type: "Ready", - Status: v1.ConditionTrue, - }, - { - Type: "Reconciling", - Status: v1.ConditionFalse, - }, - }), - }, - }, - columnWidth: 10, - expectedOutput: "\x1b[32mReady\x1b[0m,\x1b[31mReco\x1b[0m", - }, - "age not found": { - columnName: "age", - resource: &fakeResource{ - resourceStatus: &pe.ResourceStatus{ - Resource: &unstructured.Unstructured{}, - }, - }, - columnWidth: 10, - expectedOutput: "-", - }, - "age": { - columnName: "age", - resource: &fakeResource{ - resourceStatus: &pe.ResourceStatus{ - Resource: mustResourceWithCreationTimestamp(45 * time.Minute), - }, - }, - columnWidth: 10, - expectedOutput: "45m", - }, - "message without error": { - columnName: "message", - resource: &fakeResource{ - resourceStatus: &pe.ResourceStatus{ - Message: "this is a test", - }, - }, - columnWidth: 30, - expectedOutput: "this is a test", - }, - "message from error": { - columnName: "message", - resource: &fakeResource{ - resourceStatus: &pe.ResourceStatus{ - Message: "this is a test", - Error: fmt.Errorf("something went wrong somewhere"), - }, - }, - columnWidth: 50, - expectedOutput: "something went wrong somewhere", - }, - "message trimmed": { - columnName: "message", - resource: &fakeResource{ - resourceStatus: &pe.ResourceStatus{ - Message: "this is a test", - }, - }, - columnWidth: 6, - expectedOutput: "this i", - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - columnDef := MustColumn(tc.columnName) - - var buf bytes.Buffer - _, err := columnDef.PrintResource(&buf, tc.columnWidth, tc.resource) - if err != nil { - t.Error(err) - } - - if want, got := tc.expectedOutput, buf.String(); want != got { - t.Errorf("expected %q, but got %q", want, got) - } - }) - } -} - -type condition struct { - Type string - Status v1.ConditionStatus -} - -func mustResourceWithConditions(conditions []condition) *unstructured.Unstructured { - u := &unstructured.Unstructured{ - Object: make(map[string]interface{}), - } - var conditionsSlice []interface{} - for _, c := range conditions { - cond := make(map[string]interface{}) - cond["type"] = c.Type - cond["status"] = string(c.Status) - conditionsSlice = append(conditionsSlice, cond) - } - err := unstructured.SetNestedSlice(u.Object, conditionsSlice, - "status", "conditions") - if err != nil { - panic(err) - } - return u -} - -func mustResourceWithCreationTimestamp(age time.Duration) *unstructured.Unstructured { - u := &unstructured.Unstructured{ - Object: make(map[string]interface{}), - } - creationTime := time.Now().Add(-age) - u.SetCreationTimestamp(metav1.Time{ - Time: creationTime, - }) - return u -} diff --git a/pkg/printers/events/formatter.go b/pkg/printers/events/formatter.go deleted file mode 100644 index 5ccd03a1..00000000 --- a/pkg/printers/events/formatter.go +++ /dev/null @@ -1,177 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package events - -import ( - "fmt" - "strings" - - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/object/validation" - "github.com/fluxcd/cli-utils/pkg/print/list" - "github.com/fluxcd/cli-utils/pkg/print/stats" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/cli-runtime/pkg/genericclioptions" -) - -func NewFormatter(ioStreams genericclioptions.IOStreams, - _ common.DryRunStrategy) list.Formatter { - return &formatter{ - ioStreams: ioStreams, - } -} - -type formatter struct { - ioStreams genericclioptions.IOStreams -} - -func (ef *formatter) FormatValidationEvent(ve event.ValidationEvent) error { - // unwrap validation errors - err := ve.Error - if vErr, ok := err.(*validation.Error); ok { - err = vErr.Unwrap() - } - - switch { - case len(ve.Identifiers) == 0: - // no objects, invalid event - return fmt.Errorf("invalid validation event: no identifiers: %w", err) - case len(ve.Identifiers) == 1: - // only 1 object, unwrap for similarity with status event - id := ve.Identifiers[0] - ef.print("Invalid object (%s): %v", - resourceIDToString(id.GroupKind, id.Name), err.Error()) - default: - // more than 1 object, wrap list in brackets - var sb strings.Builder - id := ve.Identifiers[0] - _, _ = fmt.Fprintf(&sb, "Invalid objects (%s", resourceIDToString(id.GroupKind, id.Name)) - for _, id := range ve.Identifiers[1:] { - _, _ = fmt.Fprintf(&sb, ", %s", resourceIDToString(id.GroupKind, id.Name)) - } - _, _ = fmt.Fprintf(&sb, "): %v", err) - ef.print(sb.String()) - } - return nil -} - -func (ef *formatter) FormatApplyEvent(e event.ApplyEvent) error { - gk := e.Identifier.GroupKind - name := e.Identifier.Name - if e.Error != nil { - ef.print("%s apply %s: %s", resourceIDToString(gk, name), - strings.ToLower(e.Status.String()), e.Error.Error()) - } else { - ef.print("%s apply %s", resourceIDToString(gk, name), - strings.ToLower(e.Status.String())) - } - return nil -} - -func (ef *formatter) FormatStatusEvent(se event.StatusEvent) error { - id := se.Identifier - ef.printResourceStatus(id, se) - return nil -} - -func (ef *formatter) FormatPruneEvent(e event.PruneEvent) error { - gk := e.Identifier.GroupKind - name := e.Identifier.Name - if e.Error != nil { - ef.print("%s prune %s: %s", resourceIDToString(gk, name), - strings.ToLower(e.Status.String()), e.Error.Error()) - } else { - ef.print("%s prune %s", resourceIDToString(gk, name), - strings.ToLower(e.Status.String())) - } - return nil -} - -func (ef *formatter) FormatDeleteEvent(e event.DeleteEvent) error { - gk := e.Identifier.GroupKind - name := e.Identifier.Name - if e.Error != nil { - ef.print("%s delete %s: %s", resourceIDToString(gk, name), - strings.ToLower(e.Status.String()), e.Error.Error()) - } else { - ef.print("%s delete %s", resourceIDToString(gk, name), - strings.ToLower(e.Status.String())) - } - return nil -} - -func (ef *formatter) FormatWaitEvent(e event.WaitEvent) error { - gk := e.Identifier.GroupKind - name := e.Identifier.Name - ef.print("%s reconcile %s", resourceIDToString(gk, name), - strings.ToLower(e.Status.String())) - return nil -} - -func (ef *formatter) FormatErrorEvent(_ event.ErrorEvent) error { - return nil -} - -func (ef *formatter) FormatActionGroupEvent( - age event.ActionGroupEvent, - ags []event.ActionGroup, - s stats.Stats, - _ list.Collector, -) error { - switch age.Action { - case event.ApplyAction: - ef.print("apply phase %s", strings.ToLower(age.Status.String())) - case event.PruneAction: - ef.print("prune phase %s", strings.ToLower(age.Status.String())) - case event.DeleteAction: - ef.print("delete phase %s", strings.ToLower(age.Status.String())) - case event.WaitAction: - ef.print("reconcile phase %s", strings.ToLower(age.Status.String())) - case event.InventoryAction: - ef.print("inventory update %s", strings.ToLower(age.Status.String())) - default: - return fmt.Errorf("invalid action group action: %+v", age) - } - return nil -} - -func (ef *formatter) FormatSummary(s stats.Stats) error { - if s.ApplyStats != (stats.ApplyStats{}) { - as := s.ApplyStats - ef.print("apply result: %d attempted, %d successful, %d skipped, %d failed", - as.Sum(), as.Successful, as.Skipped, as.Failed) - } - if s.PruneStats != (stats.PruneStats{}) { - ps := s.PruneStats - ef.print("prune result: %d attempted, %d successful, %d skipped, %d failed", - ps.Sum(), ps.Successful, ps.Skipped, ps.Failed) - } - if s.DeleteStats != (stats.DeleteStats{}) { - ds := s.DeleteStats - ef.print("delete result: %d attempted, %d successful, %d skipped, %d failed", - ds.Sum(), ds.Successful, ds.Skipped, ds.Failed) - } - if s.WaitStats != (stats.WaitStats{}) { - ws := s.WaitStats - ef.print("reconcile result: %d attempted, %d successful, %d skipped, %d failed, %d timed out", - ws.Sum(), ws.Successful, ws.Skipped, ws.Failed, ws.Timeout) - } - return nil -} - -func (ef *formatter) printResourceStatus(id object.ObjMetadata, se event.StatusEvent) { - ef.print("%s is %s: %s", resourceIDToString(id.GroupKind, id.Name), - se.PollResourceInfo.Status.String(), se.PollResourceInfo.Message) -} - -func (ef *formatter) print(format string, a ...interface{}) { - _, _ = fmt.Fprintf(ef.ioStreams.Out, format+"\n", a...) -} - -// resourceIDToString returns the string representation of a GroupKind and a resource name. -func resourceIDToString(gk schema.GroupKind, name string) string { - return fmt.Sprintf("%s/%s", strings.ToLower(gk.String()), name) -} diff --git a/pkg/printers/events/formatter_test.go b/pkg/printers/events/formatter_test.go deleted file mode 100644 index edc1f230..00000000 --- a/pkg/printers/events/formatter_test.go +++ /dev/null @@ -1,532 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package events - -import ( - "errors" - "fmt" - "strings" - "testing" - - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/common" - pollevent "github.com/fluxcd/cli-utils/pkg/kstatus/polling/event" - "github.com/fluxcd/cli-utils/pkg/kstatus/status" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/object/graph" - "github.com/fluxcd/cli-utils/pkg/object/validation" - "github.com/fluxcd/cli-utils/pkg/print/list" - "github.com/stretchr/testify/assert" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/util/validation/field" - "k8s.io/cli-runtime/pkg/genericclioptions" -) - -func TestFormatter_FormatApplyEvent(t *testing.T) { - testCases := map[string]struct { - previewStrategy common.DryRunStrategy - event event.ApplyEvent - statusCollector list.Collector - expected string - }{ - "resource created without no dryrun": { - previewStrategy: common.DryRunNone, - event: event.ApplyEvent{ - Status: event.ApplySuccessful, - Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"), - }, - expected: "deployment.apps/my-dep apply successful", - }, - "resource updated with client dryrun": { - previewStrategy: common.DryRunClient, - event: event.ApplyEvent{ - Status: event.ApplySuccessful, - Identifier: createIdentifier("apps", "Deployment", "", "my-dep"), - }, - expected: "deployment.apps/my-dep apply successful", - }, - "resource updated with server dryrun": { - previewStrategy: common.DryRunServer, - event: event.ApplyEvent{ - Status: event.ApplySuccessful, - Identifier: createIdentifier("batch", "CronJob", "foo", "my-cron"), - }, - expected: "cronjob.batch/my-cron apply successful", - }, - "apply event with error should display the error": { - previewStrategy: common.DryRunServer, - event: event.ApplyEvent{ - Status: event.ApplyFailed, - Identifier: createIdentifier("apps", "Deployment", "", "my-dep"), - Error: fmt.Errorf("this is a test error"), - }, - expected: "deployment.apps/my-dep apply failed: this is a test error", - }, - "apply event with skip error should display the error": { - previewStrategy: common.DryRunServer, - event: event.ApplyEvent{ - Status: event.ApplySkipped, - Identifier: createIdentifier("apps", "Deployment", "", "my-dep"), - Error: fmt.Errorf("this is a test error"), - }, - expected: "deployment.apps/my-dep apply skipped: this is a test error", - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled - formatter := NewFormatter(ioStreams, tc.previewStrategy) - err := formatter.FormatApplyEvent(tc.event) - assert.NoError(t, err) - - assert.Equal(t, tc.expected, strings.TrimSpace(out.String())) - }) - } -} - -func TestFormatter_FormatStatusEvent(t *testing.T) { - testCases := map[string]struct { - previewStrategy common.DryRunStrategy - event event.StatusEvent - statusCollector list.Collector - expected string - }{ - "resource update with Current status": { - previewStrategy: common.DryRunNone, - event: event.StatusEvent{ - Identifier: object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - }, - Namespace: "foo", - Name: "bar", - }, - PollResourceInfo: &pollevent.ResourceStatus{ - Identifier: object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - }, - Namespace: "foo", - Name: "bar", - }, - Status: status.CurrentStatus, - Message: "Resource is Current", - }, - }, - expected: "deployment.apps/bar is Current: Resource is Current", - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled - formatter := NewFormatter(ioStreams, tc.previewStrategy) - err := formatter.FormatStatusEvent(tc.event) - assert.NoError(t, err) - - assert.Equal(t, tc.expected, strings.TrimSpace(out.String())) - }) - } -} - -func TestFormatter_FormatPruneEvent(t *testing.T) { - testCases := map[string]struct { - previewStrategy common.DryRunStrategy - event event.PruneEvent - expected string - }{ - "resource pruned without no dryrun": { - previewStrategy: common.DryRunNone, - event: event.PruneEvent{ - Status: event.PruneSuccessful, - Object: createObject("apps", "Deployment", "", "my-dep"), - Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"), - }, - expected: "deployment.apps/my-dep prune successful", - }, - "resource skipped with client dryrun": { - previewStrategy: common.DryRunClient, - event: event.PruneEvent{ - Status: event.PruneSkipped, - Object: createObject("apps", "Deployment", "", "my-dep"), - Identifier: createIdentifier("apps", "Deployment", "", "my-dep"), - }, - expected: "deployment.apps/my-dep prune skipped", - }, - "resource with prune error": { - previewStrategy: common.DryRunNone, - event: event.PruneEvent{ - Status: event.PruneFailed, - Object: createObject("apps", "Deployment", "", "my-dep"), - Identifier: createIdentifier("apps", "Deployment", "", "my-dep"), - Error: fmt.Errorf("this is a test"), - }, - expected: "deployment.apps/my-dep prune failed: this is a test", - }, - - "resource with prune skip error": { - previewStrategy: common.DryRunNone, - event: event.PruneEvent{ - Status: event.PruneSkipped, - Object: createObject("batch", "CronJob", "foo", "my-cron"), - Identifier: createIdentifier("batch", "CronJob", "foo", "my-cron"), - Error: fmt.Errorf("this is a test"), - }, - expected: "cronjob.batch/my-cron prune skipped: this is a test", - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled - formatter := NewFormatter(ioStreams, tc.previewStrategy) - err := formatter.FormatPruneEvent(tc.event) - assert.NoError(t, err) - - assert.Equal(t, tc.expected, strings.TrimSpace(out.String())) - }) - } -} - -func TestFormatter_FormatDeleteEvent(t *testing.T) { - testCases := map[string]struct { - previewStrategy common.DryRunStrategy - event event.DeleteEvent - statusCollector list.Collector - expected string - }{ - "resource deleted without no dryrun": { - previewStrategy: common.DryRunNone, - event: event.DeleteEvent{ - Status: event.DeleteSuccessful, - Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"), - Object: createObject("apps", "Deployment", "default", "my-dep"), - }, - expected: "deployment.apps/my-dep delete successful", - }, - "resource skipped with client dryrun": { - previewStrategy: common.DryRunClient, - event: event.DeleteEvent{ - Status: event.DeleteSkipped, - Identifier: createIdentifier("apps", "Deployment", "", "my-dep"), - Object: createObject("apps", "Deployment", "", "my-dep"), - }, - expected: "deployment.apps/my-dep delete skipped", - }, - "resource with delete error": { - previewStrategy: common.DryRunServer, - event: event.DeleteEvent{ - Status: event.DeleteFailed, - Object: createObject("apps", "Deployment", "", "my-dep"), - Identifier: createIdentifier("apps", "Deployment", "", "my-dep"), - Error: fmt.Errorf("this is a test"), - }, - expected: "deployment.apps/my-dep delete failed: this is a test", - }, - "resource with delete skip error": { - previewStrategy: common.DryRunServer, - event: event.DeleteEvent{ - Status: event.DeleteSkipped, - Object: createObject("batch", "CronJob", "foo", "my-cron"), - Identifier: createIdentifier("batch", "CronJob", "foo", "my-cron"), - Error: fmt.Errorf("this is a test"), - }, - expected: "cronjob.batch/my-cron delete skipped: this is a test", - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled - formatter := NewFormatter(ioStreams, tc.previewStrategy) - err := formatter.FormatDeleteEvent(tc.event) - assert.NoError(t, err) - - assert.Equal(t, tc.expected, strings.TrimSpace(out.String())) - }) - } -} - -func TestFormatter_FormatWaitEvent(t *testing.T) { - testCases := map[string]struct { - previewStrategy common.DryRunStrategy - event event.WaitEvent - statusCollector list.Collector - expected string - }{ - "resource reconciled": { - previewStrategy: common.DryRunNone, - event: event.WaitEvent{ - GroupName: "wait-1", - Status: event.ReconcileSuccessful, - Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"), - }, - expected: "deployment.apps/my-dep reconcile successful", - }, - "resource reconciled (client-side dry-run)": { - previewStrategy: common.DryRunClient, - event: event.WaitEvent{ - GroupName: "wait-1", - Status: event.ReconcileSuccessful, - Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"), - }, - expected: "deployment.apps/my-dep reconcile successful", - }, - "resource reconciled (server-side dry-run)": { - previewStrategy: common.DryRunServer, - event: event.WaitEvent{ - GroupName: "wait-1", - Status: event.ReconcileSuccessful, - Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"), - }, - expected: "deployment.apps/my-dep reconcile successful", - }, - "resource reconcile timeout": { - previewStrategy: common.DryRunNone, - event: event.WaitEvent{ - GroupName: "wait-1", - Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"), - Status: event.ReconcileTimeout, - }, - expected: "deployment.apps/my-dep reconcile timeout", - }, - "resource reconcile timeout (client-side dry-run)": { - previewStrategy: common.DryRunClient, - event: event.WaitEvent{ - GroupName: "wait-1", - Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"), - Status: event.ReconcileTimeout, - }, - expected: "deployment.apps/my-dep reconcile timeout", - }, - "resource reconcile timeout (server-side dry-run)": { - previewStrategy: common.DryRunServer, - event: event.WaitEvent{ - GroupName: "wait-1", - Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"), - Status: event.ReconcileTimeout, - }, - expected: "deployment.apps/my-dep reconcile timeout", - }, - "resource reconcile skipped": { - previewStrategy: common.DryRunNone, - event: event.WaitEvent{ - GroupName: "wait-1", - Status: event.ReconcileSkipped, - Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"), - }, - expected: "deployment.apps/my-dep reconcile skipped", - }, - "resource reconcile skipped (client-side dry-run)": { - previewStrategy: common.DryRunClient, - event: event.WaitEvent{ - GroupName: "wait-1", - Status: event.ReconcileSkipped, - Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"), - }, - expected: "deployment.apps/my-dep reconcile skipped", - }, - "resource reconcile skipped (server-side dry-run)": { - previewStrategy: common.DryRunServer, - event: event.WaitEvent{ - GroupName: "wait-1", - Status: event.ReconcileSkipped, - Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"), - }, - expected: "deployment.apps/my-dep reconcile skipped", - }, - "resource reconcile failed": { - previewStrategy: common.DryRunNone, - event: event.WaitEvent{ - GroupName: "wait-1", - Status: event.ReconcileFailed, - Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"), - }, - expected: "deployment.apps/my-dep reconcile failed", - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled - formatter := NewFormatter(ioStreams, tc.previewStrategy) - err := formatter.FormatWaitEvent(tc.event) - assert.NoError(t, err) - - assert.Equal(t, tc.expected, strings.TrimSpace(out.String())) - }) - } -} - -func TestFormatter_FormatValidationEvent(t *testing.T) { - testCases := map[string]struct { - previewStrategy common.DryRunStrategy - event event.ValidationEvent - statusCollector list.Collector - expected string - expectedError error - }{ - "zero objects, return error": { - previewStrategy: common.DryRunNone, - event: event.ValidationEvent{ - Identifiers: object.ObjMetadataSet{}, - Error: errors.New("unexpected"), - }, - expectedError: errors.New("invalid validation event: no identifiers: unexpected"), - }, - "one object, missing namespace": { - previewStrategy: common.DryRunNone, - event: event.ValidationEvent{ - Identifiers: object.ObjMetadataSet{ - { - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - }, - Namespace: "foo", - Name: "bar", - }, - }, - Error: validation.NewError( - field.Required(field.NewPath("metadata", "namespace"), "namespace is required"), - object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - }, - Namespace: "foo", - Name: "bar", - }, - ), - }, - expected: "Invalid object (deployment.apps/bar): metadata.namespace: Required value: namespace is required", - }, - "two objects, cyclic dependency": { - previewStrategy: common.DryRunNone, - event: event.ValidationEvent{ - Identifiers: object.ObjMetadataSet{ - { - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - }, - Namespace: "default", - Name: "bar", - }, - { - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - }, - Namespace: "default", - Name: "foo", - }, - }, - Error: validation.NewError( - graph.CyclicDependencyError{ - Edges: []graph.Edge{ - { - From: object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - }, - Namespace: "default", - Name: "bar", - }, - To: object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - }, - Namespace: "default", - Name: "foo", - }, - }, - { - From: object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - }, - Namespace: "default", - Name: "foo", - }, - To: object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - }, - Namespace: "default", - Name: "bar", - }, - }, - }, - }, - object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - }, - Namespace: "default", - Name: "bar", - }, - object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - }, - Namespace: "default", - Name: "foo", - }, - ), - }, - expected: `Invalid objects (deployment.apps/bar, deployment.apps/foo): cyclic dependency: -- apps/namespaces/default/Deployment/bar -> apps/namespaces/default/Deployment/foo -- apps/namespaces/default/Deployment/foo -> apps/namespaces/default/Deployment/bar`, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled - formatter := NewFormatter(ioStreams, tc.previewStrategy) - err := formatter.FormatValidationEvent(tc.event) - if tc.expectedError != nil { - assert.EqualError(t, err, tc.expectedError.Error()) - } else { - assert.NoError(t, err) - } - assert.Equal(t, tc.expected, strings.TrimSpace(out.String())) - }) - } -} - -func createObject(group, kind, namespace, name string) *unstructured.Unstructured { - return &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": fmt.Sprintf("%s/v1", group), - "kind": kind, - "metadata": map[string]interface{}{ - "name": name, - "namespace": namespace, - }, - }, - } -} - -func createIdentifier(group, kind, namespace, name string) object.ObjMetadata { - return object.ObjMetadata{ - Namespace: namespace, - Name: name, - GroupKind: schema.GroupKind{ - Group: group, - Kind: kind, - }, - } -} diff --git a/pkg/printers/events/printer.go b/pkg/printers/events/printer.go deleted file mode 100644 index f97fd24c..00000000 --- a/pkg/printers/events/printer.go +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package events - -import ( - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/print/list" - "github.com/fluxcd/cli-utils/pkg/printers/printer" - "k8s.io/cli-runtime/pkg/genericclioptions" -) - -func NewPrinter(ioStreams genericclioptions.IOStreams) printer.Printer { - return &list.BaseListPrinter{ - FormatterFactory: func(previewStrategy common.DryRunStrategy) list.Formatter { - return NewFormatter(ioStreams, previewStrategy) - }, - } -} diff --git a/pkg/printers/events/printer_test.go b/pkg/printers/events/printer_test.go deleted file mode 100644 index d70b30df..00000000 --- a/pkg/printers/events/printer_test.go +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package events - -import ( - "testing" - - "github.com/fluxcd/cli-utils/pkg/printers/printer" - printertesting "github.com/fluxcd/cli-utils/pkg/printers/testutil" - "k8s.io/cli-runtime/pkg/genericclioptions" -) - -func TestPrint(t *testing.T) { - printertesting.PrintResultErrorTest(t, func() printer.Printer { - ioStreams, _, _, _ := genericclioptions.NewTestIOStreams() - return NewPrinter(ioStreams) - }) -} diff --git a/pkg/printers/json/doc.go b/pkg/printers/json/doc.go deleted file mode 100644 index 1086d84a..00000000 --- a/pkg/printers/json/doc.go +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -// Package json provides a printer that outputs the eventstream in json -// format. Each event is printed as a json object, so the output will -// appear as a stream of json objects, each representing a single event. -// -// Every event will contain the following properties: -// - timestamp: RFC3339-formatted timestamp describing when the event happened. -// - type: Describes the type of the operation which the event is related to. -// Type values include: -// - validation - ValidationEvent -// - error - ErrorEvent -// - group - ActionGroupEvent -// - apply - ApplyEvent -// - prune - PruneEvent -// - delete - DeleteEvent -// - wait - WaitEvent -// - status - StatusEvent -// - summary - aggregate stats collected by the printer -// -// Validation events correspond to zero or more objects. For these events, the -// objects field includes a list of object identifiers. These generally fire -// first before most other events. -// -// Validation events have the following fields: -// * objects (array of objects) - a list of object identifiers -// - group (string, optional) - The object's API group. -// - kind (string) - The object's kind. -// - name (string) - The object's name. -// - namespace (string, optional) - The object's namespace. -// -// * timestamp (string) - ISO-8601 format -// * type (string) - "validation" -// * error (string) - a fatal error message specific to these objects -// -// Error events corespond to a fatal error received outside of a specific task -// or operation. -// -// Error events have the following fields: -// * timestamp (string) - ISO-8601 format -// * type (string) - "error" -// * error (string) - a fatal error message -// -// Group events correspond to a group of events of the same type: apply, prune, -// delete, or wait. -// -// Group events have the following fields: -// * action (string) - One of: "Apply", "Prune", "Delete", or "Wait". -// * status (string) - One of: "Started" or "Finished" -// * timestamp (string) - ISO-8601 format -// * type (string) - "group" -// -// Operation events (apply, prune, delete, and wait) corespond to an operation -// performed on a single object. For these events, the -// group, kind, name, and namespace fields identify the object. -// -// Operation events have the following fields: -// - group (string, optional) - The object's API group. -// - kind (string) - The object's kind. -// - name (string) - The object's name. -// - namespace (string, optional) - The object's namespace. -// - status (string) - One of: "Pending", "Successful", "Skipped", "Failed", or -// "Timeout". -// - timestamp (string) - ISO-8601 format -// - type (string) - "apply", "prune", "delete", or "wait" -// - error (string, optional) - A non-fatal error message specific to this object -// -// Status types are asynchronous events that correspond to status updates for -// a specific object. -// -// Status events have the following fields: -// - group (string, optional) - The object's API group. -// - kind (string) - The object's kind. -// - name (string) - The object's name. -// - namespace (string, optional) - The object's namespace. -// - status (string) - One of: "InProgress", "Failed", "Current", "Terminating", -// "NotFound", or "Unknown". -// - message (string) - Human readable description of the status. -// - timestamp (string) - ISO-8601 format -// - type (string) - "status" -// -// Summary types are a meta-event sent by the printer to summarize some stats -// that have been collected from other events. For these events, the action -// field corresponds to the event type being summarized: Apply, Prune, Delete, -// and Wait. -// -// Summary events have the following fields: -// * action (string) - One of: "Apply", "Prune", "Delete", or "Wait". -// * count (number) - Total number of objects attempted for this action -// * successful (number) - Number of objects for which the action was successful. -// * skipped (number) - Number of objects for which the action was skipped. -// * failed (number) - Number of objects for which the action failed. -// * timeout (number, optional) - Number of objects for which the action timed out. -// * timestamp (string) - ISO-8601 format -// * type (string) - "summary" -package json diff --git a/pkg/printers/json/formatter.go b/pkg/printers/json/formatter.go deleted file mode 100644 index e8adb1ba..00000000 --- a/pkg/printers/json/formatter.go +++ /dev/null @@ -1,236 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package json - -import ( - "encoding/json" - "fmt" - "time" - - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/object/validation" - "github.com/fluxcd/cli-utils/pkg/print/list" - "github.com/fluxcd/cli-utils/pkg/print/stats" - "k8s.io/cli-runtime/pkg/genericclioptions" -) - -func NewFormatter(ioStreams genericclioptions.IOStreams, - _ common.DryRunStrategy) list.Formatter { - return &formatter{ - ioStreams: ioStreams, - now: time.Now, - } -} - -type formatter struct { - ioStreams genericclioptions.IOStreams - now func() time.Time -} - -func (jf *formatter) FormatValidationEvent(ve event.ValidationEvent) error { - // unwrap validation errors - err := ve.Error - if vErr, ok := err.(*validation.Error); ok { - err = vErr.Unwrap() - } - if len(ve.Identifiers) == 0 { - // no objects, invalid event - return fmt.Errorf("invalid validation event: no identifiers: %w", err) - } - objects := make([]interface{}, len(ve.Identifiers)) - for i, id := range ve.Identifiers { - objects[i] = jf.baseResourceEvent(id) - } - return jf.printEvent("validation", map[string]interface{}{ - "objects": objects, - "error": err.Error(), - }) -} - -func (jf *formatter) FormatApplyEvent(e event.ApplyEvent) error { - eventInfo := jf.baseResourceEvent(e.Identifier) - if e.Error != nil { - eventInfo["error"] = e.Error.Error() - } - eventInfo["status"] = e.Status.String() - return jf.printEvent("apply", eventInfo) -} - -func (jf *formatter) FormatStatusEvent(se event.StatusEvent) error { - return jf.printResourceStatus(se) -} - -func (jf *formatter) printResourceStatus(se event.StatusEvent) error { - eventInfo := jf.baseResourceEvent(se.Identifier) - eventInfo["status"] = se.PollResourceInfo.Status.String() - eventInfo["message"] = se.PollResourceInfo.Message - return jf.printEvent("status", eventInfo) -} - -func (jf *formatter) FormatPruneEvent(e event.PruneEvent) error { - eventInfo := jf.baseResourceEvent(e.Identifier) - if e.Error != nil { - eventInfo["error"] = e.Error.Error() - } - eventInfo["status"] = e.Status.String() - return jf.printEvent("prune", eventInfo) -} - -func (jf *formatter) FormatDeleteEvent(e event.DeleteEvent) error { - eventInfo := jf.baseResourceEvent(e.Identifier) - if e.Error != nil { - eventInfo["error"] = e.Error.Error() - } - eventInfo["status"] = e.Status.String() - return jf.printEvent("delete", eventInfo) -} - -func (jf *formatter) FormatWaitEvent(e event.WaitEvent) error { - eventInfo := jf.baseResourceEvent(e.Identifier) - eventInfo["status"] = e.Status.String() - return jf.printEvent("wait", eventInfo) -} - -func (jf *formatter) FormatErrorEvent(e event.ErrorEvent) error { - return jf.printEvent("error", map[string]interface{}{ - "error": e.Err.Error(), - }) -} - -func (jf *formatter) FormatActionGroupEvent( - age event.ActionGroupEvent, - ags []event.ActionGroup, - s stats.Stats, - _ list.Collector, -) error { - content := map[string]interface{}{ - "action": age.Action.String(), - "status": age.Status.String(), - } - - switch age.Action { - case event.ApplyAction: - if age.Status == event.Finished { - as := s.ApplyStats - content["count"] = as.Sum() - content["successful"] = as.Successful - content["skipped"] = as.Skipped - content["failed"] = as.Failed - } - case event.PruneAction: - if age.Status == event.Finished { - ps := s.PruneStats - content["count"] = ps.Sum() - content["successful"] = ps.Successful - content["skipped"] = ps.Skipped - content["failed"] = ps.Failed - } - case event.DeleteAction: - if age.Status == event.Finished { - ds := s.DeleteStats - content["count"] = ds.Sum() - content["successful"] = ds.Successful - content["skipped"] = ds.Skipped - content["failed"] = ds.Failed - } - case event.WaitAction: - if age.Status == event.Finished { - ws := s.WaitStats - content["count"] = ws.Sum() - content["successful"] = ws.Successful - content["skipped"] = ws.Skipped - content["failed"] = ws.Failed - content["timeout"] = ws.Timeout - } - case event.InventoryAction: - // no extra content - default: - return fmt.Errorf("invalid action group action: %+v", age) - } - - return jf.printEvent("group", content) -} - -func (jf *formatter) FormatSummary(s stats.Stats) error { - if s.ApplyStats != (stats.ApplyStats{}) { - as := s.ApplyStats - err := jf.printEvent("summary", map[string]interface{}{ - "action": event.ApplyAction.String(), - "count": as.Sum(), - "successful": as.Successful, - "skipped": as.Skipped, - "failed": as.Failed, - }) - if err != nil { - return err - } - } - if s.PruneStats != (stats.PruneStats{}) { - ps := s.PruneStats - err := jf.printEvent("summary", map[string]interface{}{ - "action": event.PruneAction.String(), - "count": ps.Sum(), - "successful": ps.Successful, - "skipped": ps.Skipped, - "failed": ps.Failed, - }) - if err != nil { - return err - } - } - if s.DeleteStats != (stats.DeleteStats{}) { - ds := s.DeleteStats - err := jf.printEvent("summary", map[string]interface{}{ - "action": event.DeleteAction.String(), - "count": ds.Sum(), - "successful": ds.Successful, - "skipped": ds.Skipped, - "failed": ds.Failed, - }) - if err != nil { - return err - } - } - if s.WaitStats != (stats.WaitStats{}) { - ws := s.WaitStats - err := jf.printEvent("summary", map[string]interface{}{ - "action": event.WaitAction.String(), - "count": ws.Sum(), - "successful": ws.Successful, - "skipped": ws.Skipped, - "failed": ws.Failed, - "timeout": ws.Timeout, - }) - if err != nil { - return err - } - } - return nil -} - -func (jf *formatter) baseResourceEvent(identifier object.ObjMetadata) map[string]interface{} { - return map[string]interface{}{ - "group": identifier.GroupKind.Group, - "kind": identifier.GroupKind.Kind, - "namespace": identifier.Namespace, - "name": identifier.Name, - } -} - -func (jf *formatter) printEvent(t string, content map[string]interface{}) error { - m := make(map[string]interface{}) - m["timestamp"] = jf.now().UTC().Format(time.RFC3339) - m["type"] = t - for key, val := range content { - m[key] = val - } - b, err := json.Marshal(m) - if err != nil { - return err - } - _, err = fmt.Fprint(jf.ioStreams.Out, string(b)+"\n") - return err -} diff --git a/pkg/printers/json/formatter_test.go b/pkg/printers/json/formatter_test.go deleted file mode 100644 index 3804dad1..00000000 --- a/pkg/printers/json/formatter_test.go +++ /dev/null @@ -1,934 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package json - -import ( - "encoding/json" - "errors" - "strings" - "testing" - "time" - - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/common" - pollevent "github.com/fluxcd/cli-utils/pkg/kstatus/polling/event" - "github.com/fluxcd/cli-utils/pkg/kstatus/status" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/object/graph" - "github.com/fluxcd/cli-utils/pkg/object/validation" - "github.com/fluxcd/cli-utils/pkg/print/list" - "github.com/fluxcd/cli-utils/pkg/print/stats" - "github.com/fluxcd/cli-utils/pkg/testutil" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/util/validation/field" - "k8s.io/cli-runtime/pkg/genericclioptions" -) - -func TestFormatter_FormatApplyEvent(t *testing.T) { - testCases := map[string]struct { - previewStrategy common.DryRunStrategy - event event.ApplyEvent - expected []map[string]interface{} - }{ - "resource created without dryrun": { - previewStrategy: common.DryRunNone, - event: event.ApplyEvent{ - Status: event.ApplySuccessful, - Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"), - }, - expected: []map[string]interface{}{ - { - "group": "apps", - "kind": "Deployment", - "name": "my-dep", - "namespace": "default", - "status": "Successful", - "timestamp": "", - "type": "apply", - }, - }, - }, - "resource updated with client dryrun": { - previewStrategy: common.DryRunClient, - event: event.ApplyEvent{ - Status: event.ApplySuccessful, - Identifier: createIdentifier("apps", "Deployment", "", "my-dep"), - }, - expected: []map[string]interface{}{ - { - "group": "apps", - "kind": "Deployment", - "name": "my-dep", - "namespace": "", - "status": "Successful", - "timestamp": "", - "type": "apply", - }, - }, - }, - "resource updated with server dryrun": { - previewStrategy: common.DryRunServer, - event: event.ApplyEvent{ - Status: event.ApplySuccessful, - Identifier: createIdentifier("batch", "CronJob", "foo", "my-cron"), - }, - expected: []map[string]interface{}{ - { - "group": "batch", - "kind": "CronJob", - "name": "my-cron", - "namespace": "foo", - "status": "Successful", - "timestamp": "", - "type": "apply", - }, - }, - }, - "resource apply failed": { - previewStrategy: common.DryRunNone, - event: event.ApplyEvent{ - Status: event.ApplyFailed, - Identifier: createIdentifier("apps", "Deployment", "", "my-dep"), - Error: errors.New("example error"), - }, - expected: []map[string]interface{}{ - { - "group": "apps", - "kind": "Deployment", - "name": "my-dep", - "namespace": "", - "status": "Failed", - "timestamp": "", - "type": "apply", - "error": "example error", - }, - }, - }, - "resource apply skip error": { - previewStrategy: common.DryRunNone, - event: event.ApplyEvent{ - Status: event.ApplySkipped, - Identifier: createIdentifier("apps", "Deployment", "", "my-dep"), - Error: errors.New("example error"), - }, - expected: []map[string]interface{}{ - { - "group": "apps", - "kind": "Deployment", - "name": "my-dep", - "namespace": "", - "status": "Skipped", - "timestamp": "", - "type": "apply", - "error": "example error", - }, - }, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled - formatter := NewFormatter(ioStreams, tc.previewStrategy) - err := formatter.FormatApplyEvent(tc.event) - assert.NoError(t, err) - - objects := strings.Split(strings.TrimSpace(out.String()), "\n") - - if !assert.Equal(t, len(tc.expected), len(objects)) { - t.FailNow() - } - for i := range tc.expected { - assertOutput(t, tc.expected[i], objects[i]) - } - }) - } -} - -func TestFormatter_FormatStatusEvent(t *testing.T) { - testCases := map[string]struct { - previewStrategy common.DryRunStrategy - event event.StatusEvent - expected map[string]interface{} - }{ - "resource update with Current status": { - previewStrategy: common.DryRunNone, - event: event.StatusEvent{ - Identifier: object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - }, - Namespace: "foo", - Name: "bar", - }, - PollResourceInfo: &pollevent.ResourceStatus{ - Identifier: object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - }, - Namespace: "foo", - Name: "bar", - }, - Status: status.CurrentStatus, - Message: "Resource is Current", - }, - }, - expected: map[string]interface{}{ - "group": "apps", - "kind": "Deployment", - "message": "Resource is Current", - "name": "bar", - "namespace": "foo", - "status": "Current", - "timestamp": "", - "type": "status", - }, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled - formatter := NewFormatter(ioStreams, tc.previewStrategy) - err := formatter.FormatStatusEvent(tc.event) - assert.NoError(t, err) - - assertOutput(t, tc.expected, out.String()) - }) - } -} - -func TestFormatter_FormatPruneEvent(t *testing.T) { - testCases := map[string]struct { - previewStrategy common.DryRunStrategy - event event.PruneEvent - expected map[string]interface{} - }{ - "resource pruned without dryrun": { - previewStrategy: common.DryRunNone, - event: event.PruneEvent{ - Status: event.PruneSuccessful, - Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"), - }, - expected: map[string]interface{}{ - "group": "apps", - "kind": "Deployment", - "name": "my-dep", - "namespace": "default", - "status": "Successful", - "timestamp": "", - "type": "prune", - }, - }, - "resource skipped with client dryrun": { - previewStrategy: common.DryRunClient, - event: event.PruneEvent{ - Status: event.PruneSkipped, - Identifier: createIdentifier("apps", "Deployment", "", "my-dep"), - }, - expected: map[string]interface{}{ - "group": "apps", - "kind": "Deployment", - "name": "my-dep", - "namespace": "", - "status": "Skipped", - "timestamp": "", - "type": "prune", - }, - }, - "resource prune failed": { - previewStrategy: common.DryRunNone, - event: event.PruneEvent{ - Status: event.PruneFailed, - Identifier: createIdentifier("apps", "Deployment", "", "my-dep"), - Error: errors.New("example error"), - }, - expected: map[string]interface{}{ - "group": "apps", - "kind": "Deployment", - "name": "my-dep", - "namespace": "", - "status": "Failed", - "timestamp": "", - "type": "prune", - "error": "example error", - }, - }, - "resource prune skip error": { - previewStrategy: common.DryRunNone, - event: event.PruneEvent{ - Status: event.PruneSkipped, - Identifier: createIdentifier("apps", "Deployment", "", "my-dep"), - Error: errors.New("example error"), - }, - expected: map[string]interface{}{ - "group": "apps", - "kind": "Deployment", - "name": "my-dep", - "namespace": "", - "status": "Skipped", - "timestamp": "", - "type": "prune", - "error": "example error", - }, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled - formatter := NewFormatter(ioStreams, tc.previewStrategy) - err := formatter.FormatPruneEvent(tc.event) - assert.NoError(t, err) - - assertOutput(t, tc.expected, out.String()) - }) - } -} - -func TestFormatter_FormatDeleteEvent(t *testing.T) { - testCases := map[string]struct { - previewStrategy common.DryRunStrategy - event event.DeleteEvent - statusCollector list.Collector - expected map[string]interface{} - }{ - "resource deleted without no dryrun": { - previewStrategy: common.DryRunNone, - event: event.DeleteEvent{ - Status: event.DeleteSuccessful, - Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"), - }, - expected: map[string]interface{}{ - "group": "apps", - "kind": "Deployment", - "name": "my-dep", - "namespace": "default", - "status": "Successful", - "timestamp": "", - "type": "delete", - }, - }, - "resource skipped with client dryrun": { - previewStrategy: common.DryRunClient, - event: event.DeleteEvent{ - Status: event.DeleteSkipped, - Identifier: createIdentifier("apps", "Deployment", "", "my-dep"), - }, - expected: map[string]interface{}{ - "group": "apps", - "kind": "Deployment", - "name": "my-dep", - "namespace": "", - "status": "Skipped", - "timestamp": "", - "type": "delete", - }, - }, - "resource delete failed": { - previewStrategy: common.DryRunNone, - event: event.DeleteEvent{ - Status: event.DeleteFailed, - Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"), - Error: errors.New("example error"), - }, - expected: map[string]interface{}{ - "group": "apps", - "kind": "Deployment", - "name": "my-dep", - "namespace": "default", - "status": "Failed", - "timestamp": "", - "type": "delete", - "error": "example error", - }, - }, - "resource delete skip error": { - previewStrategy: common.DryRunNone, - event: event.DeleteEvent{ - Status: event.DeleteSkipped, - Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"), - Error: errors.New("example error"), - }, - expected: map[string]interface{}{ - "group": "apps", - "kind": "Deployment", - "name": "my-dep", - "namespace": "default", - "status": "Skipped", - "timestamp": "", - "type": "delete", - "error": "example error", - }, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled - formatter := NewFormatter(ioStreams, tc.previewStrategy) - err := formatter.FormatDeleteEvent(tc.event) - assert.NoError(t, err) - - assertOutput(t, tc.expected, out.String()) - }) - } -} - -func TestFormatter_FormatWaitEvent(t *testing.T) { - testCases := map[string]struct { - previewStrategy common.DryRunStrategy - event event.WaitEvent - statusCollector list.Collector - expected map[string]interface{} - }{ - "resource reconciled": { - previewStrategy: common.DryRunNone, - event: event.WaitEvent{ - GroupName: "wait-1", - Status: event.ReconcileSuccessful, - Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"), - }, - expected: map[string]interface{}{ - "group": "apps", - "kind": "Deployment", - "name": "my-dep", - "namespace": "default", - "status": "Successful", - "timestamp": "", - "type": "wait", - }, - }, - "resource reconciled (client-side dry-run)": { - previewStrategy: common.DryRunClient, - event: event.WaitEvent{ - GroupName: "wait-1", - Status: event.ReconcileSuccessful, - Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"), - }, - expected: map[string]interface{}{ - "group": "apps", - "kind": "Deployment", - "name": "my-dep", - "namespace": "default", - "status": "Successful", - "timestamp": "", - "type": "wait", - }, - }, - "resource reconciled (server-side dry-run)": { - previewStrategy: common.DryRunServer, - event: event.WaitEvent{ - GroupName: "wait-1", - Status: event.ReconcileSuccessful, - Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"), - }, - expected: map[string]interface{}{ - "group": "apps", - "kind": "Deployment", - "name": "my-dep", - "namespace": "default", - "status": "Successful", - "timestamp": "", - "type": "wait", - }, - }, - "resource reconcile pending": { - previewStrategy: common.DryRunServer, - event: event.WaitEvent{ - GroupName: "wait-1", - Status: event.ReconcilePending, - Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"), - }, - expected: map[string]interface{}{ - "group": "apps", - "kind": "Deployment", - "name": "my-dep", - "namespace": "default", - "status": "Pending", - "timestamp": "", - "type": "wait", - }, - }, - "resource reconcile skipped": { - previewStrategy: common.DryRunServer, - event: event.WaitEvent{ - GroupName: "wait-1", - Status: event.ReconcileSkipped, - Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"), - }, - expected: map[string]interface{}{ - "group": "apps", - "kind": "Deployment", - "name": "my-dep", - "namespace": "default", - "status": "Skipped", - "timestamp": "", - "type": "wait", - }, - }, - "resource reconcile timeout": { - previewStrategy: common.DryRunServer, - event: event.WaitEvent{ - GroupName: "wait-1", - Status: event.ReconcileTimeout, - Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"), - }, - expected: map[string]interface{}{ - "group": "apps", - "kind": "Deployment", - "name": "my-dep", - "namespace": "default", - "status": "Timeout", - "timestamp": "", - "type": "wait", - }, - }, - "resource reconcile failed": { - previewStrategy: common.DryRunNone, - event: event.WaitEvent{ - GroupName: "wait-1", - Status: event.ReconcileFailed, - Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"), - }, - expected: map[string]interface{}{ - "group": "apps", - "kind": "Deployment", - "name": "my-dep", - "namespace": "default", - "status": "Failed", - "timestamp": "", - "type": "wait", - }, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled - formatter := NewFormatter(ioStreams, tc.previewStrategy) - err := formatter.FormatWaitEvent(tc.event) - assert.NoError(t, err) - - assertOutput(t, tc.expected, out.String()) - }) - } -} - -func TestFormatter_FormatActionGroupEvent(t *testing.T) { - testCases := map[string]struct { - previewStrategy common.DryRunStrategy - event event.ActionGroupEvent - actionGroups []event.ActionGroup - statsCollector stats.Stats - statusCollector list.Collector - expected map[string]interface{} - }{ - "not the last apply action group finished": { - previewStrategy: common.DryRunNone, - event: event.ActionGroupEvent{ - GroupName: "age-1", - Action: event.ApplyAction, - Status: event.Finished, - }, - actionGroups: []event.ActionGroup{ - { - Name: "age-1", - Action: event.ApplyAction, - }, - { - Name: "age-2", - Action: event.ApplyAction, - }, - }, - statsCollector: stats.Stats{ - ApplyStats: stats.ApplyStats{}, - }, - expected: map[string]interface{}{ - "action": "Apply", - "count": 0, - "failed": 0, - "skipped": 0, - "status": "Finished", - "successful": 0, - "timestamp": "2022-03-24T01:35:04Z", - "type": "group", - }, - }, - "the last apply action group finished": { - previewStrategy: common.DryRunNone, - event: event.ActionGroupEvent{ - GroupName: "age-2", - Action: event.ApplyAction, - Status: event.Finished, - }, - actionGroups: []event.ActionGroup{ - { - Name: "age-1", - Action: event.ApplyAction, - }, - { - Name: "age-2", - Action: event.ApplyAction, - }, - }, - statsCollector: stats.Stats{ - ApplyStats: stats.ApplyStats{ - Successful: 42, - }, - }, - expected: map[string]interface{}{ - "action": "Apply", - "count": 42, - "failed": 0, - "skipped": 0, - "status": "Finished", - "successful": 42, - "timestamp": "2022-03-24T01:35:04Z", - "type": "group", - }, - }, - "last prune action group started": { - previewStrategy: common.DryRunNone, - event: event.ActionGroupEvent{ - GroupName: "age-2", - Action: event.PruneAction, - Status: event.Started, - }, - actionGroups: []event.ActionGroup{ - { - Name: "age-1", - Action: event.PruneAction, - }, - { - Name: "age-2", - Action: event.PruneAction, - }, - }, - expected: map[string]interface{}{ - "action": "Prune", - "status": "Started", - "timestamp": "2022-03-24T01:51:36Z", - "type": "group", - }, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled - formatter := NewFormatter(ioStreams, tc.previewStrategy) - err := formatter.FormatActionGroupEvent(tc.event, tc.actionGroups, tc.statsCollector, tc.statusCollector) - assert.NoError(t, err) - - assertOutput(t, tc.expected, out.String()) - }) - } -} - -func TestFormatter_FormatValidationEvent(t *testing.T) { - testCases := map[string]struct { - previewStrategy common.DryRunStrategy - event event.ValidationEvent - expected map[string]interface{} - expectedError error - }{ - "zero objects, return error": { - previewStrategy: common.DryRunNone, - event: event.ValidationEvent{ - Identifiers: object.ObjMetadataSet{}, - Error: errors.New("unexpected"), - }, - expectedError: errors.New("invalid validation event: no identifiers: unexpected"), - }, - "one object, missing namespace": { - previewStrategy: common.DryRunNone, - event: event.ValidationEvent{ - Identifiers: object.ObjMetadataSet{ - { - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - }, - Namespace: "foo", - Name: "bar", - }, - }, - Error: validation.NewError( - field.Required(field.NewPath("metadata", "namespace"), "namespace is required"), - object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - }, - Namespace: "foo", - Name: "bar", - }, - ), - }, - expected: map[string]interface{}{ - "type": "validation", - "timestamp": "", - "objects": []interface{}{ - map[string]interface{}{ - "group": "apps", - "kind": "Deployment", - "name": "bar", - "namespace": "foo", - }, - }, - "error": "metadata.namespace: Required value: namespace is required", - }, - }, - "two objects, cyclic dependency": { - previewStrategy: common.DryRunNone, - event: event.ValidationEvent{ - Identifiers: object.ObjMetadataSet{ - { - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - }, - Namespace: "default", - Name: "bar", - }, - { - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - }, - Namespace: "default", - Name: "foo", - }, - }, - Error: validation.NewError( - graph.CyclicDependencyError{ - Edges: []graph.Edge{ - { - From: object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - }, - Namespace: "default", - Name: "bar", - }, - To: object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - }, - Namespace: "default", - Name: "foo", - }, - }, - { - From: object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - }, - Namespace: "default", - Name: "foo", - }, - To: object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - }, - Namespace: "default", - Name: "bar", - }, - }, - }, - }, - object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - }, - Namespace: "default", - Name: "bar", - }, - object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - }, - Namespace: "default", - Name: "foo", - }, - ), - }, - expected: map[string]interface{}{ - "type": "validation", - "timestamp": "", - "objects": []interface{}{ - map[string]interface{}{ - "group": "apps", - "kind": "Deployment", - "name": "bar", - "namespace": "default", - }, - map[string]interface{}{ - "group": "apps", - "kind": "Deployment", - "name": "foo", - "namespace": "default", - }, - }, - "error": `cyclic dependency: -- apps/namespaces/default/Deployment/bar -> apps/namespaces/default/Deployment/foo -- apps/namespaces/default/Deployment/foo -> apps/namespaces/default/Deployment/bar`, - }, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled - formatter := NewFormatter(ioStreams, tc.previewStrategy) - err := formatter.FormatValidationEvent(tc.event) - if tc.expectedError != nil { - assert.EqualError(t, err, tc.expectedError.Error()) - return - } - assert.NoError(t, err) - assertOutput(t, tc.expected, out.String()) - }) - } -} - -func TestFormatter_FormatSummary(t *testing.T) { - now := time.Now() - nowStr := now.UTC().Format(time.RFC3339) - - testCases := map[string]struct { - statsCollector stats.Stats - expected []map[string]interface{} - }{ - "apply prune wait": { - statsCollector: stats.Stats{ - ApplyStats: stats.ApplyStats{ - Successful: 1, - Skipped: 2, - Failed: 3, - }, - PruneStats: stats.PruneStats{ - Successful: 3, - Skipped: 2, - Failed: 1, - }, - WaitStats: stats.WaitStats{ - Successful: 4, - Skipped: 6, - Failed: 1, - Timeout: 1, - }, - }, - expected: []map[string]interface{}{ - { - "action": "Apply", - "count": float64(6), - "successful": float64(1), - "skipped": float64(2), - "failed": float64(3), - "timestamp": nowStr, - "type": "summary", - }, - { - "action": "Prune", - "count": float64(6), - "successful": float64(3), - "skipped": float64(2), - "failed": float64(1), - "timestamp": nowStr, - "type": "summary", - }, - { - "action": "Wait", - "count": float64(12), - "successful": float64(4), - "skipped": float64(6), - "failed": float64(1), - "timeout": float64(1), - "timestamp": nowStr, - "type": "summary", - }, - }, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled - jf := &formatter{ - ioStreams: ioStreams, - // fake time func - now: func() time.Time { return now }, - } - err := jf.FormatSummary(tc.statsCollector) - assert.NoError(t, err) - - assertOutputLines(t, tc.expected, out.String()) - }) - } -} - -func assertOutputLines(t *testing.T, expectedMaps []map[string]interface{}, actual string) { - actual = strings.TrimRight(actual, "\n") - lines := strings.Split(actual, "\n") - actualMaps := make([]map[string]interface{}, len(lines)) - for i, line := range lines { - err := json.Unmarshal([]byte(line), &actualMaps[i]) - require.NoError(t, err) - } - testutil.AssertEqual(t, expectedMaps, actualMaps) -} - -// nolint:unparam -func assertOutput(t *testing.T, expectedMap map[string]interface{}, actual string) bool { - if len(expectedMap) == 0 { - return assert.Empty(t, actual) - } - - var m map[string]interface{} - err := json.Unmarshal([]byte(actual), &m) - if !assert.NoError(t, err) { - return false - } - - if _, found := expectedMap["timestamp"]; found { - if _, ok := m["timestamp"]; ok { - delete(expectedMap, "timestamp") - delete(m, "timestamp") - } else { - t.Error("expected to find key 'timestamp', but didn't") - return false - } - } - - for key, val := range m { - if floatVal, ok := val.(float64); ok { - m[key] = int(floatVal) - } - } - - return assert.Equal(t, expectedMap, m) -} - -func createIdentifier(group, kind, namespace, name string) object.ObjMetadata { - return object.ObjMetadata{ - Namespace: namespace, - Name: name, - GroupKind: schema.GroupKind{ - Group: group, - Kind: kind, - }, - } -} diff --git a/pkg/printers/json/printer.go b/pkg/printers/json/printer.go deleted file mode 100644 index e978204f..00000000 --- a/pkg/printers/json/printer.go +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package json - -import ( - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/print/list" - "github.com/fluxcd/cli-utils/pkg/printers/printer" - "k8s.io/cli-runtime/pkg/genericclioptions" -) - -func NewPrinter(ioStreams genericclioptions.IOStreams) printer.Printer { - return &list.BaseListPrinter{ - FormatterFactory: func(previewStrategy common.DryRunStrategy) list.Formatter { - return NewFormatter(ioStreams, previewStrategy) - }, - } -} diff --git a/pkg/printers/json/printer_test.go b/pkg/printers/json/printer_test.go deleted file mode 100644 index ccbed366..00000000 --- a/pkg/printers/json/printer_test.go +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package json - -import ( - "testing" - - "github.com/fluxcd/cli-utils/pkg/printers/printer" - printertesting "github.com/fluxcd/cli-utils/pkg/printers/testutil" - "k8s.io/cli-runtime/pkg/genericclioptions" -) - -func TestPrint(t *testing.T) { - printertesting.PrintResultErrorTest(t, func() printer.Printer { - ioStreams, _, _, _ := genericclioptions.NewTestIOStreams() - return NewPrinter(ioStreams) - }) -} diff --git a/pkg/printers/printer/printer.go b/pkg/printers/printer/printer.go deleted file mode 100644 index 36930468..00000000 --- a/pkg/printers/printer/printer.go +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package printer - -import ( - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/common" -) - -type Printer interface { - Print(ch <-chan event.Event, previewStrategy common.DryRunStrategy, printStatus bool) error -} diff --git a/pkg/printers/printers.go b/pkg/printers/printers.go deleted file mode 100644 index 31996771..00000000 --- a/pkg/printers/printers.go +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package printers - -import ( - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/print/list" - "github.com/fluxcd/cli-utils/pkg/printers/events" - "github.com/fluxcd/cli-utils/pkg/printers/json" - "github.com/fluxcd/cli-utils/pkg/printers/printer" - "github.com/fluxcd/cli-utils/pkg/printers/table" - "k8s.io/cli-runtime/pkg/genericclioptions" -) - -const ( - EventsPrinter = "events" - TablePrinter = "table" - JSONPrinter = "json" -) - -func GetPrinter(printerType string, ioStreams genericclioptions.IOStreams) printer.Printer { - switch printerType { //nolint:gocritic - case TablePrinter: - return &table.Printer{ - IOStreams: ioStreams, - } - case JSONPrinter: - return &list.BaseListPrinter{ - FormatterFactory: func(previewStrategy common.DryRunStrategy) list.Formatter { - return json.NewFormatter(ioStreams, previewStrategy) - }, - } - default: - return events.NewPrinter(ioStreams) - } -} - -func SupportedPrinters() []string { - return []string{EventsPrinter, TablePrinter, JSONPrinter} -} - -func DefaultPrinter() string { - return EventsPrinter -} - -func ValidatePrinterType(printerType string) bool { - for _, p := range SupportedPrinters() { - if printerType == p { - return true - } - } - return false -} diff --git a/pkg/printers/table/collector.go b/pkg/printers/table/collector.go deleted file mode 100644 index 9b32e329..00000000 --- a/pkg/printers/table/collector.go +++ /dev/null @@ -1,381 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package table - -import ( - "fmt" - "sort" - "sync" - - "github.com/fluxcd/cli-utils/pkg/apply/event" - pe "github.com/fluxcd/cli-utils/pkg/kstatus/polling/event" - "github.com/fluxcd/cli-utils/pkg/kstatus/status" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/object/validation" - "github.com/fluxcd/cli-utils/pkg/print/stats" - "github.com/fluxcd/cli-utils/pkg/print/table" - "k8s.io/klog/v2" -) - -const InvalidStatus status.Status = "Invalid" - -func newResourceStateCollector(resourceGroups []event.ActionGroup) *resourceStateCollector { - resourceInfos := make(map[object.ObjMetadata]*resourceInfo) - for _, group := range resourceGroups { - action := group.Action - // Keep the action that describes the operation for the resource - // rather than that we will wait for it. - if action == event.WaitAction { - continue - } - for _, identifier := range group.Identifiers { - resourceInfos[identifier] = &resourceInfo{ - identifier: identifier, - resourceStatus: &pe.ResourceStatus{ - Identifier: identifier, - Status: status.UnknownStatus, - }, - ResourceAction: action, - } - } - } - return &resourceStateCollector{ - resourceInfos: resourceInfos, - } -} - -// resourceStateCollector consumes the events from the applier -// eventChannel and keeps track of the latest state for all resources. -// It also provides functionality for fetching the latest seen -// state and return it in format that can be used by the -// BaseTablePrinter. -type resourceStateCollector struct { - mux sync.RWMutex - - // resourceInfos contains a mapping from the unique - // resource identifier to a ResourceInfo object that captures - // the latest state for the given resource. - resourceInfos map[object.ObjMetadata]*resourceInfo - - // stats collect statistics from handled events - stats stats.Stats - - err error -} - -// resourceInfo captures the latest seen state of a single resource. -// This is used for top-level resources that have a ResourceAction -// associated with them. -type resourceInfo struct { - // identifier contains the information that identifies a - // single resource. - identifier object.ObjMetadata - - // resourceStatus contains the latest status information - // about the resource. - resourceStatus *pe.ResourceStatus - - // ResourceAction defines the action we are performing - // on this particular resource. This can be either Apply - // or Prune. - ResourceAction event.ResourceAction - - // Error is set if an error occurred trying to perform - // the desired action on the resource. - Error error - - // ApplyStatus contains the result after - // a resource has been applied to the cluster. - ApplyStatus event.ApplyEventStatus - - // PruneStatus contains the result after - // a prune operation on a resource - PruneStatus event.PruneEventStatus - - // DeleteStatus contains the result after - // a delete operation on a resource - DeleteStatus event.DeleteEventStatus - - // WaitStatus contains the result after - // a wait operation on a resource - WaitStatus event.WaitEventStatus -} - -// Identifier returns the identifier for the given resource. -func (r *resourceInfo) Identifier() object.ObjMetadata { - return r.identifier -} - -// ResourceStatus returns the latest seen status for the -// resource. -func (r *resourceInfo) ResourceStatus() *pe.ResourceStatus { - return r.resourceStatus -} - -// SubResources returns a slice of Resource which contains -// any resources created and managed by this resource. -func (r *resourceInfo) SubResources() []table.Resource { - var resources []table.Resource - for _, res := range r.resourceStatus.GeneratedResources { - resources = append(resources, &subResourceInfo{ - resourceStatus: res, - }) - } - return resources -} - -// subResourceInfo captures the latest seen state of a -// single subResource, i.e. resources that are created and -// managed by one of the top-level resources we either apply -// or prune. -type subResourceInfo struct { - // resourceStatus contains the latest status information - // about the subResource. - resourceStatus *pe.ResourceStatus -} - -// Identifier returns the identifier for the given subResource. -func (r *subResourceInfo) Identifier() object.ObjMetadata { - return r.resourceStatus.Identifier -} - -// ResourceStatus returns the latest seen status for the -// subResource. -func (r *subResourceInfo) ResourceStatus() *pe.ResourceStatus { - return r.resourceStatus -} - -// SubResources returns a slice of Resource which contains -// any resources created and managed by this resource. -func (r *subResourceInfo) SubResources() []table.Resource { - var resources []table.Resource - for _, res := range r.resourceStatus.GeneratedResources { - resources = append(resources, &subResourceInfo{ - resourceStatus: res, - }) - } - return resources -} - -// Listen starts a new goroutine that will listen for events on the -// provided eventChannel and keep track of the latest state for -// the resources. The goroutine will exit when the provided -// eventChannel is closed. -// The function returns a channel. When this channel is closed, the -// goroutine has processed all events in the eventChannel and -// exited. -func (r *resourceStateCollector) Listen(eventChannel <-chan event.Event) <-chan listenerResult { - completed := make(chan listenerResult) - go func() { - defer close(completed) - for ev := range eventChannel { - if err := r.processEvent(ev); err != nil { - completed <- listenerResult{err: err} - return - } - } - }() - return completed -} - -type listenerResult struct { - err error -} - -// processEvent processes an event and updates the state. -func (r *resourceStateCollector) processEvent(ev event.Event) error { - r.mux.Lock() - defer r.mux.Unlock() - switch ev.Type { - case event.ValidationType: - return r.processValidationEvent(ev.ValidationEvent) - case event.StatusType: - r.processStatusEvent(ev.StatusEvent) - case event.ApplyType: - r.processApplyEvent(ev.ApplyEvent) - case event.PruneType: - r.processPruneEvent(ev.PruneEvent) - case event.DeleteType: - r.processDeleteEvent(ev.DeleteEvent) - case event.WaitType: - r.processWaitEvent(ev.WaitEvent) - case event.ErrorType: - return ev.ErrorEvent.Err - } - return nil -} - -// processValidationEvent handles events pertaining to a validation error -// for a resource. -func (r *resourceStateCollector) processValidationEvent(e event.ValidationEvent) error { - klog.V(7).Infoln("processing validation event") - // unwrap validation errors - err := e.Error - if vErr, ok := err.(*validation.Error); ok { - err = vErr.Unwrap() - } - if len(e.Identifiers) == 0 { - // no objects, invalid event - return fmt.Errorf("invalid validation event: no identifiers: %w", err) - } - for _, id := range e.Identifiers { - previous, found := r.resourceInfos[id] - if !found { - klog.V(4).Infof("%s status event not found in ResourceInfos; no processing", id) - continue - } - previous.resourceStatus = &pe.ResourceStatus{ - Identifier: id, - Status: InvalidStatus, - Message: e.Error.Error(), - } - } - return nil -} - -// processStatusEvent handles events pertaining to a status -// update for a resource. -func (r *resourceStateCollector) processStatusEvent(e event.StatusEvent) { - klog.V(7).Infoln("processing status event") - previous, found := r.resourceInfos[e.Identifier] - if !found { - klog.V(4).Infof("%s status event not found in ResourceInfos; no processing", e.Identifier) - return - } - previous.resourceStatus = e.PollResourceInfo -} - -// processApplyEvent handles events relating to apply operations -func (r *resourceStateCollector) processApplyEvent(e event.ApplyEvent) { - identifier := e.Identifier - klog.V(7).Infof("processing apply event for %s", identifier) - previous, found := r.resourceInfos[identifier] - if !found { - klog.V(4).Infof("%s apply event not found in ResourceInfos; no processing", identifier) - return - } - if e.Error != nil { - previous.Error = e.Error - } - previous.ApplyStatus = e.Status - r.stats.ApplyStats.Inc(e.Status) -} - -// processPruneEvent handles event related to prune operations. -func (r *resourceStateCollector) processPruneEvent(e event.PruneEvent) { - identifier := e.Identifier - klog.V(7).Infof("processing prune event for %s", identifier) - previous, found := r.resourceInfos[identifier] - if !found { - klog.V(4).Infof("%s prune event not found in ResourceInfos; no processing", identifier) - return - } - if e.Error != nil { - previous.Error = e.Error - } - previous.PruneStatus = e.Status - r.stats.PruneStats.Inc(e.Status) -} - -// processDeleteEvent handles event related to delete operations. -func (r *resourceStateCollector) processDeleteEvent(e event.DeleteEvent) { - identifier := e.Identifier - klog.V(7).Infof("processing delete event for %s", identifier) - previous, found := r.resourceInfos[identifier] - if !found { - klog.V(4).Infof("%s delete event not found in ResourceInfos; no processing", identifier) - return - } - if e.Error != nil { - previous.Error = e.Error - } - previous.DeleteStatus = e.Status - r.stats.DeleteStats.Inc(e.Status) -} - -// processPruneEvent handles event related to prune operations. -func (r *resourceStateCollector) processWaitEvent(e event.WaitEvent) { - identifier := e.Identifier - klog.V(7).Infof("processing wait event for %s", identifier) - previous, found := r.resourceInfos[identifier] - if !found { - klog.V(4).Infof("%s wait event not found in ResourceInfos; no processing", identifier) - return - } - previous.WaitStatus = e.Status - r.stats.WaitStats.Inc(e.Status) -} - -// ResourceState contains the latest state for all the resources. -type ResourceState struct { - resourceInfos ResourceInfos - - err error -} - -// Resources returns a slice containing the latest state -// for each individual resource. -func (r *ResourceState) Resources() []table.Resource { - var resources []table.Resource - for _, res := range r.resourceInfos { - resources = append(resources, res) - } - return resources -} - -func (r *ResourceState) Error() error { - return r.err -} - -// LatestState returns a ResourceState object that contains -// a copy of the latest state for all resources. -func (r *resourceStateCollector) LatestState() *ResourceState { - r.mux.RLock() - defer r.mux.RUnlock() - - var resourceInfos ResourceInfos - for _, ri := range r.resourceInfos { - resourceInfos = append(resourceInfos, &resourceInfo{ - identifier: ri.identifier, - resourceStatus: ri.resourceStatus, - ResourceAction: ri.ResourceAction, - ApplyStatus: ri.ApplyStatus, - PruneStatus: ri.PruneStatus, - DeleteStatus: ri.DeleteStatus, - WaitStatus: ri.WaitStatus, - }) - } - sort.Sort(resourceInfos) - - return &ResourceState{ - resourceInfos: resourceInfos, - err: r.err, - } -} - -type ResourceInfos []*resourceInfo - -func (g ResourceInfos) Len() int { - return len(g) -} - -func (g ResourceInfos) Less(i, j int) bool { - idI := g[i].identifier - idJ := g[j].identifier - - if idI.Namespace != idJ.Namespace { - return idI.Namespace < idJ.Namespace - } - if idI.GroupKind.Group != idJ.GroupKind.Group { - return idI.GroupKind.Group < idJ.GroupKind.Group - } - if idI.GroupKind.Kind != idJ.GroupKind.Kind { - return idI.GroupKind.Kind < idJ.GroupKind.Kind - } - return idI.Name < idJ.Name -} - -func (g ResourceInfos) Swap(i, j int) { - g[i], g[j] = g[j], g[i] -} diff --git a/pkg/printers/table/collector_test.go b/pkg/printers/table/collector_test.go deleted file mode 100644 index 38134e40..00000000 --- a/pkg/printers/table/collector_test.go +++ /dev/null @@ -1,277 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package table - -import ( - "errors" - "testing" - - "github.com/fluxcd/cli-utils/pkg/apply/event" - pe "github.com/fluxcd/cli-utils/pkg/kstatus/polling/event" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/object/graph" - "github.com/fluxcd/cli-utils/pkg/object/validation" - "github.com/stretchr/testify/assert" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/util/validation/field" -) - -var ( - depID = object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - }, - Name: "foo", - Namespace: "default", - } - depID2 = object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - }, - Name: "bar", - Namespace: "default", - } - customID = object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "custom.io", - Kind: "Custom", - }, - Name: "Custom", - } -) - -const testMessage = "test message for ResourceStatus" - -func TestResourceStateCollector_New(t *testing.T) { - testCases := map[string]struct { - resourceGroups []event.ActionGroup - resourceInfos map[object.ObjMetadata]*resourceInfo - }{ - "no resources": { - resourceGroups: []event.ActionGroup{}, - resourceInfos: map[object.ObjMetadata]*resourceInfo{}, - }, - "several resources for apply": { - resourceGroups: []event.ActionGroup{ - { - Action: event.ApplyAction, - Identifiers: object.ObjMetadataSet{ - depID, customID, - }, - }, - }, - resourceInfos: map[object.ObjMetadata]*resourceInfo{ - depID: { - ResourceAction: event.ApplyAction, - }, - customID: { - ResourceAction: event.ApplyAction, - }, - }, - }, - "several resources for prune": { - resourceGroups: []event.ActionGroup{ - { - Action: event.ApplyAction, - Identifiers: object.ObjMetadataSet{ - customID, - }, - }, - { - Action: event.PruneAction, - Identifiers: object.ObjMetadataSet{ - depID, - }, - }, - }, - resourceInfos: map[object.ObjMetadata]*resourceInfo{ - depID: { - ResourceAction: event.PruneAction, - }, - customID: { - ResourceAction: event.ApplyAction, - }, - }, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - rsc := newResourceStateCollector(tc.resourceGroups) - - assert.Equal(t, len(tc.resourceInfos), len(rsc.resourceInfos)) - for expID, expRi := range tc.resourceInfos { - actRi, found := rsc.resourceInfos[expID] - if !found { - t.Errorf("expected to find id %v, but didn't", expID) - } - assert.Equal(t, expRi.ResourceAction, actRi.ResourceAction) - } - }) - } -} - -func TestResourceStateCollector_ProcessStatusEvent(t *testing.T) { - testCases := map[string]struct { - resourceGroups []event.ActionGroup - statusEvent event.StatusEvent - }{ - "nil StatusEvent.Resource does not crash": { - resourceGroups: []event.ActionGroup{}, - statusEvent: event.StatusEvent{ - Resource: nil, - }, - }, - "unfound Resource identifier does not crash": { - resourceGroups: []event.ActionGroup{ - { - Action: event.ApplyAction, - Identifiers: object.ObjMetadataSet{depID}, - }, - }, - statusEvent: event.StatusEvent{ - PollResourceInfo: &pe.ResourceStatus{ - Identifier: customID, // Does not match identifier in resourceGroups - }, - }, - }, - "basic status event for applying two resources updates resourceStatus": { - resourceGroups: []event.ActionGroup{ - { - Action: event.ApplyAction, - Identifiers: object.ObjMetadataSet{ - depID, customID, - }, - }, - }, - statusEvent: event.StatusEvent{ - PollResourceInfo: &pe.ResourceStatus{ - Identifier: depID, - Message: testMessage, - }, - }, - }, - "several resources for prune": { - resourceGroups: []event.ActionGroup{ - { - Action: event.ApplyAction, - Identifiers: object.ObjMetadataSet{ - customID, - }, - }, - { - Action: event.PruneAction, - Identifiers: object.ObjMetadataSet{ - depID, - }, - }, - }, - statusEvent: event.StatusEvent{ - PollResourceInfo: &pe.ResourceStatus{ - Identifier: depID, - Message: testMessage, - }, - }, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - rsc := newResourceStateCollector(tc.resourceGroups) - rsc.processStatusEvent(tc.statusEvent) - id, found := getID(tc.statusEvent) - if found { - resourceInfo, found := rsc.resourceInfos[id] - if found { - // Validate the ResourceStatus was set from StatusEvent - if resourceInfo.resourceStatus != tc.statusEvent.PollResourceInfo { - t.Errorf("status event not processed for %s", id) - } - } - } - }) - } -} - -func TestResourceStateCollector_ProcessValidationEvent(t *testing.T) { - testCases := map[string]struct { - resourceGroups []event.ActionGroup - event event.ValidationEvent - expectedError error - }{ - "zero objects, return error": { - event: event.ValidationEvent{ - Identifiers: object.ObjMetadataSet{}, - Error: errors.New("unexpected"), - }, - expectedError: errors.New("invalid validation event: no identifiers: unexpected"), - }, - "one object, missing namespace": { - resourceGroups: []event.ActionGroup{ - { - Action: event.ApplyAction, - Identifiers: object.ObjMetadataSet{depID}, - }, - }, - event: event.ValidationEvent{ - Identifiers: object.ObjMetadataSet{depID}, - Error: validation.NewError( - field.Required(field.NewPath("metadata", "namespace"), "namespace is required"), - depID, - ), - }, - }, - "two objects, cyclic dependency": { - event: event.ValidationEvent{ - Identifiers: object.ObjMetadataSet{depID, depID2}, - Error: validation.NewError( - graph.CyclicDependencyError{ - Edges: []graph.Edge{ - { - From: depID, - To: depID2, - }, - { - From: depID2, - To: depID, - }, - }, - }, - depID, - depID2, - ), - }, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - rsc := newResourceStateCollector(tc.resourceGroups) - err := rsc.processValidationEvent(tc.event) - if tc.expectedError != nil { - assert.EqualError(t, err, tc.expectedError.Error()) - return - } - for _, id := range tc.event.Identifiers { - resourceInfo, found := rsc.resourceInfos[id] - if found { - assert.Equal(t, &pe.ResourceStatus{ - Identifier: id, - Status: InvalidStatus, - Message: tc.event.Error.Error(), - }, resourceInfo.resourceStatus) - } - } - }) - } -} - -func getID(e event.StatusEvent) (object.ObjMetadata, bool) { - if e.Resource == nil { - return object.ObjMetadata{}, false - } - return e.Identifier, true -} diff --git a/pkg/printers/table/printer.go b/pkg/printers/table/printer.go deleted file mode 100644 index c3c55914..00000000 --- a/pkg/printers/table/printer.go +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package table - -import ( - "fmt" - "io" - "time" - - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/common" - printcommon "github.com/fluxcd/cli-utils/pkg/print/common" - "github.com/fluxcd/cli-utils/pkg/print/table" - "k8s.io/cli-runtime/pkg/genericclioptions" -) - -type Printer struct { - IOStreams genericclioptions.IOStreams -} - -func (t *Printer) Print(ch <-chan event.Event, _ common.DryRunStrategy, _ bool) error { - // Wait for the init event that will give us the set of - // resources. - var initEvent event.InitEvent - for e := range ch { - if e.Type == event.InitType { - initEvent = e.InitEvent - break - } - // If we get an error event, we just print it and - // exit. The error event signals a fatal error. - if e.Type == event.ErrorType { - return e.ErrorEvent.Err - } - } - // Create a new collector and initialize it with the resources - // we are interested in. - coll := newResourceStateCollector(initEvent.ActionGroups) - - stop := make(chan struct{}) - - // Start the goroutine that is responsible for - // printing the latest state on a regular cadence. - printCompleted := t.runPrintLoop(coll, stop) - - // Make the collector start listening on the eventChannel. - done := coll.Listen(ch) - - // Block until all the collector has shut down. This means the - // eventChannel has been closed and all events have been processed. - var err error - for msg := range done { - err = msg.err - } - - // Close the stop channel to notify the print goroutine that it should - // shut down. - close(stop) - - // Wait until the printCompleted channel is closed. This means - // the printer has updated the UI with the latest state and - // exited from the goroutine. - <-printCompleted - - if err != nil { - return err - } - // If no fatal errors happened, we will return a ResultError if - // one or more resources failed to apply/prune or reconcile. - return printcommon.ResultErrorFromStats(coll.stats) -} - -// columns defines the columns we want to print -// TODO: We should have the number of columns and their widths be -// dependent on the space available. -var ( - actionColumnDef = table.ColumnDef{ - // Column containing the resource type and name. Currently it does not - // print group or version since those are rarely needed to uniquely - // distinguish two resources from each other. Just name and kind should - // be enough in almost all cases and saves space in the output. - ColumnName: "action", - ColumnHeader: "ACTION", - ColumnWidth: 12, - PrintResourceFunc: func(w io.Writer, width int, r table.Resource) (int, - error) { - var resInfo *resourceInfo - switch res := r.(type) { - case *resourceInfo: - resInfo = res - default: - return 0, nil - } - - var text string - switch resInfo.ResourceAction { - case event.ApplyAction: - if resInfo.ApplyStatus != event.ApplyFailed { - text = resInfo.ApplyStatus.String() - } - case event.PruneAction: - if resInfo.PruneStatus != event.PruneFailed { - text = resInfo.PruneStatus.String() - } - } - - if len(text) > width { - text = text[:width] - } - _, err := fmt.Fprint(w, text) - return len(text), err - }, - } - - reconciledColumnDef = table.ColumnDef{ - // Column containing the reconciliation status. - ColumnName: "reconciled", - ColumnHeader: "RECONCILED", - ColumnWidth: 10, - PrintResourceFunc: func(w io.Writer, width int, r table.Resource) ( - int, - error, - ) { - var resInfo *resourceInfo - switch res := r.(type) { - case *resourceInfo: - resInfo = res - default: - return 0, nil - } - - var text string - switch resInfo.ResourceAction { - case event.WaitAction: - text = resInfo.WaitStatus.String() - } - - if len(text) > width { - text = text[:width] - } - _, err := fmt.Fprint(w, text) - return len(text), err - }, - } - - columns = []table.ColumnDefinition{ - table.MustColumn("namespace"), - table.MustColumn("resource"), - actionColumnDef, - table.MustColumn("status"), - reconciledColumnDef, - table.MustColumn("conditions"), - table.MustColumn("age"), - table.MustColumn("message"), - } -) - -// runPrintLoop starts a new goroutine that will regularly fetch the -// latest state from the collector and update the table. -func (t *Printer) runPrintLoop(coll *resourceStateCollector, stop chan struct{}) chan struct{} { - finished := make(chan struct{}) - - baseTablePrinter := table.BaseTablePrinter{ - IOStreams: t.IOStreams, - Columns: columns, - } - - linesPrinted := baseTablePrinter.PrintTable(coll.LatestState(), 0) - - go func() { - defer close(finished) - ticker := time.NewTicker(500 * time.Millisecond) - for { - select { - case <-stop: - ticker.Stop() - latestState := coll.LatestState() - linesPrinted = baseTablePrinter.PrintTable(latestState, linesPrinted) - _, _ = fmt.Fprint(t.IOStreams.Out, "\n") - return - case <-ticker.C: - latestState := coll.LatestState() - linesPrinted = baseTablePrinter.PrintTable(latestState, linesPrinted) - } - } - }() - return finished -} diff --git a/pkg/printers/table/printer_test.go b/pkg/printers/table/printer_test.go deleted file mode 100644 index fd2ef3a1..00000000 --- a/pkg/printers/table/printer_test.go +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package table - -import ( - "bytes" - "testing" - - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/print/table" - "github.com/fluxcd/cli-utils/pkg/printers/printer" - printertesting "github.com/fluxcd/cli-utils/pkg/printers/testutil" - "k8s.io/cli-runtime/pkg/genericclioptions" -) - -func TestActionColumnDef(t *testing.T) { - testCases := map[string]struct { - resource table.Resource - columnWidth int - expectedOutput string - }{ - "unexpected implementation of Resource interface": { - resource: &subResourceInfo{}, - columnWidth: 15, - expectedOutput: "", - }, - "neither applied nor pruned": { - resource: &resourceInfo{}, - columnWidth: 15, - expectedOutput: "Pending", - }, - "applied": { - resource: &resourceInfo{ - ResourceAction: event.ApplyAction, - ApplyStatus: event.ApplySuccessful, - }, - columnWidth: 15, - expectedOutput: "Successful", - }, - "pruned": { - resource: &resourceInfo{ - ResourceAction: event.PruneAction, - PruneStatus: event.PruneSuccessful, - }, - columnWidth: 15, - expectedOutput: "Successful", - }, - "trimmed output": { - resource: &resourceInfo{ - ResourceAction: event.ApplyAction, - ApplyStatus: event.ApplySuccessful, - }, - columnWidth: 5, - expectedOutput: "Succe", - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - var buf bytes.Buffer - _, err := actionColumnDef.PrintResource(&buf, tc.columnWidth, tc.resource) - if err != nil { - t.Error(err) - } - - if want, got := tc.expectedOutput, buf.String(); want != got { - t.Errorf("expected %q, but got %q", want, got) - } - }) - } -} - -func TestPrint(t *testing.T) { - printertesting.PrintResultErrorTest(t, func() printer.Printer { - ioStreams, _, _, _ := genericclioptions.NewTestIOStreams() - return &Printer{ - IOStreams: ioStreams, - } - }) -} diff --git a/pkg/printers/testutil/common.go b/pkg/printers/testutil/common.go deleted file mode 100644 index 40b5c5c5..00000000 --- a/pkg/printers/testutil/common.go +++ /dev/null @@ -1,307 +0,0 @@ -// Copyright 2022 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package testutil - -import ( - "fmt" - "sync" - "testing" - - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/object" - printcommon "github.com/fluxcd/cli-utils/pkg/print/common" - "github.com/fluxcd/cli-utils/pkg/print/stats" - "github.com/fluxcd/cli-utils/pkg/printers/printer" - "github.com/stretchr/testify/assert" - "k8s.io/apimachinery/pkg/runtime/schema" -) - -type PrinterFactoryFunc func() printer.Printer - -func PrintResultErrorTest(t *testing.T, f PrinterFactoryFunc) { - deploymentIdentifier := object.ObjMetadata{ - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - }, - Name: "foo", - Namespace: "bar", - } - - testCases := map[string]struct { - events []event.Event - expectedErr error - }{ - "successful apply, prune and reconcile": { - events: []event.Event{ - { - Type: event.InitType, - InitEvent: event.InitEvent{ - ActionGroups: event.ActionGroupList{ - { - Name: "apply-1", - Action: event.ApplyAction, - Identifiers: []object.ObjMetadata{ - deploymentIdentifier, - }, - }, - { - Name: "wait-1", - Action: event.WaitAction, - Identifiers: []object.ObjMetadata{ - deploymentIdentifier, - }, - }, - }, - }, - }, - { - Type: event.ActionGroupType, - ActionGroupEvent: event.ActionGroupEvent{ - GroupName: "apply-1", - Action: event.ApplyAction, - Status: event.Started, - }, - }, - { - Type: event.ApplyType, - ApplyEvent: event.ApplyEvent{ - GroupName: "apply-1", - Status: event.ApplySuccessful, - Identifier: deploymentIdentifier, - }, - }, - { - Type: event.ActionGroupType, - ActionGroupEvent: event.ActionGroupEvent{ - GroupName: "apply-1", - Action: event.ApplyAction, - Status: event.Finished, - }, - }, - { - Type: event.ActionGroupType, - ActionGroupEvent: event.ActionGroupEvent{ - GroupName: "wait-1", - Action: event.WaitAction, - Status: event.Started, - }, - }, - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: "wait-1", - Status: event.ReconcileSuccessful, - Identifier: deploymentIdentifier, - }, - }, - { - Type: event.ActionGroupType, - ActionGroupEvent: event.ActionGroupEvent{ - GroupName: "wait-1", - Action: event.WaitAction, - Status: event.Finished, - }, - }, - }, - expectedErr: nil, - }, - "successful apply, failed reconcile": { - events: []event.Event{ - { - Type: event.InitType, - InitEvent: event.InitEvent{ - ActionGroups: event.ActionGroupList{ - { - Name: "apply-1", - Action: event.ApplyAction, - Identifiers: []object.ObjMetadata{ - deploymentIdentifier, - }, - }, - { - Name: "wait-1", - Action: event.WaitAction, - Identifiers: []object.ObjMetadata{ - deploymentIdentifier, - }, - }, - }, - }, - }, - { - Type: event.ActionGroupType, - ActionGroupEvent: event.ActionGroupEvent{ - GroupName: "apply-1", - Action: event.ApplyAction, - Status: event.Started, - }, - }, - { - Type: event.ApplyType, - ApplyEvent: event.ApplyEvent{ - GroupName: "apply-1", - Status: event.ApplySuccessful, - Identifier: deploymentIdentifier, - }, - }, - { - Type: event.ActionGroupType, - ActionGroupEvent: event.ActionGroupEvent{ - GroupName: "apply-1", - Action: event.ApplyAction, - Status: event.Finished, - }, - }, - { - Type: event.ActionGroupType, - ActionGroupEvent: event.ActionGroupEvent{ - GroupName: "wait-1", - Action: event.WaitAction, - Status: event.Started, - }, - }, - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: "wait-1", - Status: event.ReconcileFailed, - Identifier: deploymentIdentifier, - }, - }, - { - Type: event.ActionGroupType, - ActionGroupEvent: event.ActionGroupEvent{ - GroupName: "wait-1", - Action: event.WaitAction, - Status: event.Finished, - }, - }, - }, - expectedErr: &printcommon.ResultError{ - Stats: stats.Stats{ - ApplyStats: stats.ApplyStats{ - Successful: 1, - }, - WaitStats: stats.WaitStats{ - Failed: 1, - }, - }, - }, - }, - "failed apply": { - events: []event.Event{ - { - Type: event.InitType, - InitEvent: event.InitEvent{ - ActionGroups: event.ActionGroupList{ - { - Name: "apply-1", - Action: event.ApplyAction, - Identifiers: []object.ObjMetadata{ - deploymentIdentifier, - }, - }, - { - Name: "wait-1", - Action: event.WaitAction, - Identifiers: []object.ObjMetadata{ - deploymentIdentifier, - }, - }, - }, - }, - }, - { - Type: event.ActionGroupType, - ActionGroupEvent: event.ActionGroupEvent{ - GroupName: "apply-1", - Action: event.ApplyAction, - Status: event.Started, - }, - }, - { - Type: event.ApplyType, - ApplyEvent: event.ApplyEvent{ - GroupName: "apply-1", - Status: event.ApplyFailed, - Identifier: deploymentIdentifier, - Error: fmt.Errorf("apply failed"), - }, - }, - { - Type: event.ActionGroupType, - ActionGroupEvent: event.ActionGroupEvent{ - GroupName: "apply-1", - Action: event.ApplyAction, - Status: event.Finished, - }, - }, - { - Type: event.ActionGroupType, - ActionGroupEvent: event.ActionGroupEvent{ - GroupName: "wait-1", - Action: event.WaitAction, - Status: event.Started, - }, - }, - { - Type: event.WaitType, - WaitEvent: event.WaitEvent{ - GroupName: "wait-1", - Status: event.ReconcileSkipped, - Identifier: deploymentIdentifier, - }, - }, - { - Type: event.ActionGroupType, - ActionGroupEvent: event.ActionGroupEvent{ - GroupName: "wait-1", - Action: event.WaitAction, - Status: event.Finished, - }, - }, - }, - expectedErr: &printcommon.ResultError{ - Stats: stats.Stats{ - ApplyStats: stats.ApplyStats{ - Failed: 1, - }, - WaitStats: stats.WaitStats{ - Skipped: 1, - }, - }, - }, - }, - } - - for tn := range testCases { - tc := testCases[tn] - t.Run(tn, func(t *testing.T) { - p := f() - - eventChannel := make(chan event.Event) - - var wg sync.WaitGroup - var err error - - wg.Add(1) - go func() { - err = p.Print(eventChannel, common.DryRunNone, false) - wg.Done() - }() - - for i := range tc.events { - e := tc.events[i] - eventChannel <- e - } - close(eventChannel) - - wg.Wait() - - assert.Equal(t, tc.expectedErr, err) - }) - } -} diff --git a/pkg/testutil/events.go b/pkg/testutil/events.go deleted file mode 100644 index 6c88fd94..00000000 --- a/pkg/testutil/events.go +++ /dev/null @@ -1,487 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package testutil - -import ( - "fmt" - "sort" - - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/kstatus/status" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" -) - -type ExpEvent struct { - EventType event.Type - - InitEvent *ExpInitEvent - ErrorEvent *ExpErrorEvent - ActionGroupEvent *ExpActionGroupEvent - ApplyEvent *ExpApplyEvent - StatusEvent *ExpStatusEvent - PruneEvent *ExpPruneEvent - DeleteEvent *ExpDeleteEvent - WaitEvent *ExpWaitEvent - ValidationEvent *ExpValidationEvent -} - -type ExpInitEvent struct { - // TODO: enable if we want to more thuroughly test InitEvents - // ActionGroups []event.ActionGroup -} - -type ExpErrorEvent struct { - Err error -} - -type ExpActionGroupEvent struct { - GroupName string - Action event.ResourceAction - Type event.ActionGroupEventStatus -} - -type ExpApplyEvent struct { - GroupName string - Status event.ApplyEventStatus - Identifier object.ObjMetadata - Error error -} - -type ExpStatusEvent struct { - Status status.Status - Identifier object.ObjMetadata - Error error -} - -type ExpPruneEvent struct { - GroupName string - Status event.PruneEventStatus - Identifier object.ObjMetadata - Error error -} - -type ExpDeleteEvent struct { - GroupName string - Status event.DeleteEventStatus - Identifier object.ObjMetadata - Error error -} - -type ExpWaitEvent struct { - GroupName string - Status event.WaitEventStatus - Identifier object.ObjMetadata -} - -type ExpValidationEvent struct { - Identifiers object.ObjMetadataSet - Error error -} - -func VerifyEvents(expEvents []ExpEvent, events []event.Event) error { - if len(expEvents) == 0 && len(events) == 0 { - return nil - } - expEventIndex := 0 - for i := range events { - e := events[i] - ee := expEvents[expEventIndex] - if isMatch(ee, e) { - expEventIndex++ - if expEventIndex >= len(expEvents) { - return nil - } - } - } - return fmt.Errorf("event %s not found", expEvents[expEventIndex].EventType) -} - -// nolint:gocyclo -// TODO(mortent): This function is pretty complex and with quite a bit of -// duplication. We should see if there is a better way to provide a flexible -// way to verify that we go the expected events. -func isMatch(ee ExpEvent, e event.Event) bool { - if ee.EventType != e.Type { - return false - } - - // nolint:gocritic - switch e.Type { - case event.ErrorType: - a := ee.ErrorEvent - - if a == nil { - return true - } - - b := e.ErrorEvent - - if a.Err != nil { - if !cmp.Equal(a.Err, b.Err, cmpopts.EquateErrors()) { - return false - } - } - return true - - case event.ActionGroupType: - agee := ee.ActionGroupEvent - - if agee == nil { - return true - } - - age := e.ActionGroupEvent - - if agee.GroupName != age.GroupName { - return false - } - - if agee.Action != age.Action { - return false - } - - if agee.Type != age.Status { - return false - } - return true - - case event.ApplyType: - aee := ee.ApplyEvent - // If no more information is specified, we consider it a match. - if aee == nil { - return true - } - ae := e.ApplyEvent - - if aee.Identifier != object.NilObjMetadata { - if aee.Identifier != ae.Identifier { - return false - } - } - - if aee.GroupName != "" { - if aee.GroupName != ae.GroupName { - return false - } - } - - if aee.Status != ae.Status { - return false - } - - if aee.Error != nil { - return ae.Error != nil - } - return ae.Error == nil - - case event.StatusType: - see := ee.StatusEvent - if see == nil { - return true - } - se := e.StatusEvent - - if see.Identifier != se.Identifier { - return false - } - - if see.Status != se.PollResourceInfo.Status { - return false - } - - if see.Error != nil { - return se.Error != nil - } - return se.Error == nil - - case event.PruneType: - pee := ee.PruneEvent - if pee == nil { - return true - } - pe := e.PruneEvent - - if pee.Identifier != object.NilObjMetadata { - if pee.Identifier != pe.Identifier { - return false - } - } - - if pee.GroupName != "" { - if pee.GroupName != pe.GroupName { - return false - } - } - - if pee.Status != pe.Status { - return false - } - - if pee.Error != nil { - return pe.Error != nil - } - return pe.Error == nil - - case event.DeleteType: - dee := ee.DeleteEvent - if dee == nil { - return true - } - de := e.DeleteEvent - - if dee.Identifier != object.NilObjMetadata { - if dee.Identifier != de.Identifier { - return false - } - } - - if dee.GroupName != "" { - if dee.GroupName != de.GroupName { - return false - } - } - - if dee.Status != de.Status { - return false - } - - if dee.Error != nil { - return de.Error != nil - } - return de.Error == nil - - case event.WaitType: - wee := ee.WaitEvent - if wee == nil { - return true - } - we := e.WaitEvent - - if wee.Identifier != object.NilObjMetadata { - if wee.Identifier != we.Identifier { - return false - } - } - - if wee.GroupName != "" { - if wee.GroupName != we.GroupName { - return false - } - } - - if wee.Status != we.Status { - return false - } - return true - - case event.ValidationType: - vee := ee.ValidationEvent - if vee == nil { - return true - } - ve := e.ValidationEvent - - if vee.Identifiers != nil { - if !vee.Identifiers.Equal(ve.Identifiers) { - return false - } - } - - if vee.Error != nil { - return ve.Error != nil - } - return ve.Error == nil - - default: - return true - } -} - -func EventsToExpEvents(events []event.Event) []ExpEvent { - result := make([]ExpEvent, 0, len(events)) - for _, event := range events { - result = append(result, EventToExpEvent(event)) - } - return result -} - -func EventToExpEvent(e event.Event) ExpEvent { - switch e.Type { - case event.InitType: - return ExpEvent{ - EventType: event.InitType, - InitEvent: &ExpInitEvent{ - // TODO: enable if we want to more thuroughly test InitEvents - // ActionGroups: e.InitEvent.ActionGroups, - }, - } - - case event.ErrorType: - return ExpEvent{ - EventType: event.ErrorType, - ErrorEvent: &ExpErrorEvent{ - Err: e.ErrorEvent.Err, - }, - } - - case event.ActionGroupType: - return ExpEvent{ - EventType: event.ActionGroupType, - ActionGroupEvent: &ExpActionGroupEvent{ - GroupName: e.ActionGroupEvent.GroupName, - Action: e.ActionGroupEvent.Action, - Type: e.ActionGroupEvent.Status, - }, - } - - case event.ApplyType: - return ExpEvent{ - EventType: event.ApplyType, - ApplyEvent: &ExpApplyEvent{ - GroupName: e.ApplyEvent.GroupName, - Identifier: e.ApplyEvent.Identifier, - Status: e.ApplyEvent.Status, - Error: e.ApplyEvent.Error, - }, - } - - case event.StatusType: - return ExpEvent{ - EventType: event.StatusType, - StatusEvent: &ExpStatusEvent{ - Identifier: e.StatusEvent.Identifier, - Status: e.StatusEvent.PollResourceInfo.Status, - Error: e.StatusEvent.Error, - }, - } - - case event.PruneType: - return ExpEvent{ - EventType: event.PruneType, - PruneEvent: &ExpPruneEvent{ - GroupName: e.PruneEvent.GroupName, - Identifier: e.PruneEvent.Identifier, - Status: e.PruneEvent.Status, - Error: e.PruneEvent.Error, - }, - } - - case event.DeleteType: - return ExpEvent{ - EventType: event.DeleteType, - DeleteEvent: &ExpDeleteEvent{ - GroupName: e.DeleteEvent.GroupName, - Identifier: e.DeleteEvent.Identifier, - Status: e.DeleteEvent.Status, - Error: e.DeleteEvent.Error, - }, - } - - case event.WaitType: - return ExpEvent{ - EventType: event.WaitType, - WaitEvent: &ExpWaitEvent{ - GroupName: e.WaitEvent.GroupName, - Identifier: e.WaitEvent.Identifier, - Status: e.WaitEvent.Status, - }, - } - - case event.ValidationType: - return ExpEvent{ - EventType: event.ValidationType, - ValidationEvent: &ExpValidationEvent{ - Identifiers: e.ValidationEvent.Identifiers, - Error: e.ValidationEvent.Error, - }, - } - } - return ExpEvent{} -} - -func RemoveEqualEvents(in []ExpEvent, expected ExpEvent) ([]ExpEvent, int) { - matches := 0 - for i := 0; i < len(in); i++ { - if cmp.Equal(in[i], expected, cmpopts.EquateErrors()) { - // remove event at index i - in = append(in[:i], in[i+1:]...) - matches++ - i-- - } - } - return in, matches -} - -// SortExpEvents sorts a list of ExpEvents so they can be compared for equality. -// -// This is a stable sort which only sorts nearly identical contiguous events by -// object identifier, to make the full list easier to validate. -// -// You may need to remove StatusEvents from the list before comparing, because -// these events are fully asynchronous and non-contiguous. -// -// Comparison Options: -// A) Expect(received).To(testutil.Equal(expected)) -// B) testutil.assertEqual(t, expected, received) -func SortExpEvents(events []ExpEvent) { - sort.SliceStable(events, GroupedEventsByID(events).Less) -} - -// GroupedEventsByID implements sort.Interface for []ExpEvent based on -// the serialized ObjMetadata of Apply, Prune, and Delete events within the same -// task group. -// This makes testing events easier, because apply/prune/delete order is -// non-deterministic within each task group. -// This is only needed if you expect to have multiple apply/prune/delete events -// in the same task group. -type GroupedEventsByID []ExpEvent - -func (ape GroupedEventsByID) Len() int { return len(ape) } -func (ape GroupedEventsByID) Swap(i, j int) { ape[i], ape[j] = ape[j], ape[i] } -func (ape GroupedEventsByID) Less(i, j int) bool { - if ape[i].EventType != ape[j].EventType { - // don't change order if not the same type - return i < j - } - switch ape[i].EventType { - case event.ValidationType: - // Validation events are predictable ordered by input object set order. - case event.ApplyType: - // Apply events are are predictably ordered by ordering.SortableMetas. - case event.PruneType: - // Prune events are predictably ordered in reverse apply order. - case event.DeleteType: - // Delete events are predictably ordered in reverse apply order. - case event.WaitType: - // Wait events are unpredictably ordered, because the status may - // reconcile before or after the WaitTask starts, and status event - // order after starting is dependent on remote controller behavior. - // So here we sort status groups explicitly: - // Pending > Skipped > Successful > Failed > Timeout. - // Each status group is then sorted by Identifier: - // Group > Kind > Namespace > Name. - // Note that the Pending status is always optional. - if ape[i].WaitEvent.GroupName == ape[j].WaitEvent.GroupName { - if ape[i].WaitEvent.Status != ape[j].WaitEvent.Status { - return lessWaitStatus(ape[i].WaitEvent.Status, ape[j].WaitEvent.Status) - } - return ape[i].WaitEvent.Identifier.String() < ape[j].WaitEvent.Identifier.String() - } - } - return i < j -} - -var waitStatusWeight = map[event.WaitEventStatus]int{ - event.ReconcilePending: 0, - event.ReconcileSkipped: 1, - event.ReconcileSuccessful: 2, - event.ReconcileFailed: 3, - event.ReconcileTimeout: 4, -} - -func lessWaitStatus(x, y event.WaitEventStatus) bool { - return waitStatusWeight[x] < waitStatusWeight[y] -} diff --git a/pkg/testutil/object.go b/pkg/testutil/object.go index 0c1bfe63..390d8054 100644 --- a/pkg/testutil/object.go +++ b/pkg/testutil/object.go @@ -9,7 +9,6 @@ import ( "testing" "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/object/dependson" "github.com/stretchr/testify/assert" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -132,31 +131,3 @@ func (a deleteOwningInvMutator) Mutate(u *unstructured.Unstructured) { unstructured.RemoveNestedField(u.Object, "metadata", "annotations") } } - -// AddDependsOn returns a testutil.Mutator which adds the passed objects as a -// depends-on annotation to the object which is mutated. Multiple objects -// passed in means multiple depends on objects in the annotation separated -// by a comma. -func AddDependsOn(t *testing.T, deps ...object.ObjMetadata) Mutator { - return dependsOnMutator{ - t: t, - deps: dependson.DependencySet(deps), - } -} - -// dependsOnMutator encapsulates fields for adding depends-on annotation -// to a test object. Implements the Mutator interface. -type dependsOnMutator struct { - t *testing.T - deps dependson.DependencySet -} - -// Mutate writes a depends-on annotation on the supplied object. The value of -// the annotation is a set of dependencies referencing the dependsOnMutator's -// depObjs. -func (d dependsOnMutator) Mutate(u *unstructured.Unstructured) { - err := dependson.WriteAnnotation(u, d.deps) - if !assert.NoError(d.t, err) { - d.t.FailNow() - } -} diff --git a/release/README.md b/release/README.md deleted file mode 100644 index ac3bb822..00000000 --- a/release/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# Releasing - -Currently (2020/05/06), we are only releasing minor versions of -the cli-utils. We will update these notes, when we begin -updating major/minor/patch versions. - -To cut a new cli-utils release perform the following: - -- Fetch the latest master changes to a clean branch - - (Assuming remote fork is named `upstream`) - - `git checkout -b release` - - `git fetch upstream` - - `git reset --hard upstream/master` -- Run `git tag` to determine the latest tag -- Create/Push new tag for release - - (Assuming updating minor version) - - `git tag v0.MINOR.0` - - `git push upstream v0.MINOR.0` diff --git a/release/goreleaser.yml b/release/goreleaser.yml deleted file mode 100644 index 0c76bcf6..00000000 --- a/release/goreleaser.yml +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2020 The Kubernetes Authors. -# SPDX-License-Identifier: Apache-2.0 - -builds: -- skip: true -checksum: - name_template: 'checksums.txt' -changelog: - sort: asc - filters: - exclude: - - '^docs:' - - '^test:' - - Merge pull request - - Merge branch diff --git a/scripts/check-everything.sh b/scripts/check-everything.sh deleted file mode 100755 index e3ad0a90..00000000 --- a/scripts/check-everything.sh +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env bash - -# Copyright 2019 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 -e - -hack_dir=$(dirname ${BASH_SOURCE}) -source ${hack_dir}/common.sh - -k8s_version=1.13.1 -goarch=amd64 -goos="unknown" - -if [[ "$OSTYPE" == "linux-gnu" ]]; then - goos="linux" -elif [[ "$OSTYPE" == "darwin"* ]]; then - goos="darwin" -fi - -if [[ "$goos" == "unknown" ]]; then - echo "OS '$OSTYPE' not supported. Aborting." >&2 - exit 1 -fi - -tmp_root=/tmp -kb_root_dir=$tmp_root/kubebuilder - -# Skip fetching and untaring the tools by setting the SKIP_FETCH_TOOLS variable -# in your environment to any value: -# -# $ SKIP_FETCH_TOOLS=1 ./test.sh -# -# If you skip fetching tools, this script will use the tools already on your -# machine, but rebuild the kubebuilder and kubebuilder-bin binaries. -SKIP_FETCH_TOOLS=${SKIP_FETCH_TOOLS:-""} - -# fetch k8s API gen tools and make it available under kb_root_dir/bin. -function fetch_kb_tools { - if [ -n "$SKIP_FETCH_TOOLS" ]; then - return 0 - fi - - header_text "fetching tools" - kb_tools_archive_name="kubebuilder-tools-$k8s_version-$goos-$goarch.tar.gz" - kb_tools_download_url="https://storage.googleapis.com/kubebuilder-tools/$kb_tools_archive_name" - - kb_tools_archive_path="$tmp_root/$kb_tools_archive_name" - if [ ! -f $kb_tools_archive_path ]; then - curl -sL ${kb_tools_download_url} -o "$kb_tools_archive_path" - fi - tar -zvxf "$kb_tools_archive_path" -C "$tmp_root/" -} - -# Install metalinter -function install_metalinter { - which gometalinter.v2 || go install gopkg.in/alecthomas/gometalinter.v2 -} - - -# # Install wire -# function install_wire { -# which wire|| go install github.com/google/wire/cmd/wire -# } - -# header_text "using tools" - -# install_wire - -fetch_kb_tools -setup_envs - -# ${hack_dir}/verify.sh -# ${hack_dir}/test-all.sh - -echo "passed" -exit 0 diff --git a/scripts/common.sh b/scripts/common.sh deleted file mode 100755 index 6b26a381..00000000 --- a/scripts/common.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env bash - -# Copyright 2018 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 -e - -# Enable tracing in this script off by setting the TRACE variable in your -# environment to any value: -# -# $ TRACE=1 test.sh -TRACE=${TRACE:-""} -if [ -n "$TRACE" ]; then - set -x -fi - -# Turn colors in this script off by setting the NO_COLOR variable in your -# environment to any value: -# -# $ NO_COLOR=1 test.sh -NO_COLOR=${NO_COLOR:-""} -if [ -z "$NO_COLOR" ]; then - header=$'\e[1;33m' - reset=$'\e[0m' -else - header='' - reset='' -fi - -function header_text { - echo "$header$*$reset" -} - -function setup_envs { - header_text "setting up env vars" - - # Setup env vars - if [[ -z "${KUBEBUILDER_ASSETS}" ]]; then - export KUBEBUILDER_ASSETS=$kb_root_dir/bin - fi -} diff --git a/scripts/manual-test.sh b/scripts/manual-test.sh deleted file mode 100755 index 1ebf4498..00000000 --- a/scripts/manual-test.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env bash - -# Copyright 2019 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 -e -./cli-utils --kubeconfig ~/.kube/config apply cmd/test-manifests/hello -./cli-utils --kubeconfig ~/.kube/config apply status cmd/test-manifests/hello --wait --every 2 --count 30 -./cli-utils --kubeconfig ~/.kube/config delete cmd/test-manifests/hello -./cli-utils --kubeconfig ~/.kube/config apply status cmd/test-manifests/hello diff --git a/scripts/test-all.sh b/scripts/test-all.sh deleted file mode 100755 index 24904029..00000000 --- a/scripts/test-all.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash - -# Copyright 2018 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 -e - -source $(dirname ${BASH_SOURCE})/common.sh - -setup_envs - -header_text "running go test" - -go test ./... - diff --git a/scripts/verify.sh b/scripts/verify.sh deleted file mode 100755 index 4b924bdd..00000000 --- a/scripts/verify.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash - -# Copyright 2019 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 -e - -source $(dirname ${BASH_SOURCE})/common.sh - -header_text "running go vet" - -go vet ./internal/... ./pkg/... ./cmd/... ./util/... - -header_text "running golangci-lint" - -# TODO: enable typecheck -golangci-lint run ./... -D typecheck diff --git a/test/e2e/apply_and_destroy_test.go b/test/e2e/apply_and_destroy_test.go deleted file mode 100644 index 299913d9..00000000 --- a/test/e2e/apply_and_destroy_test.go +++ /dev/null @@ -1,298 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package e2e - -import ( - "context" - "fmt" - "time" - - "github.com/fluxcd/cli-utils/pkg/apply" - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/pkg/kstatus/status" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/testutil" - "github.com/fluxcd/cli-utils/test/e2e/e2eutil" - "github.com/fluxcd/cli-utils/test/e2e/invconfig" - . "github.com/onsi/ginkgo/v2" //nolint:revive - . "github.com/onsi/gomega" //nolint:revive - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -func applyAndDestroyTest(ctx context.Context, c client.Client, invConfig invconfig.InventoryConfig, inventoryName, namespaceName string) { - By("Apply resources") - applier := invConfig.ApplierFactoryFunc() - inventoryID := fmt.Sprintf("%s-%s", inventoryName, namespaceName) - - inventoryInfo := invconfig.CreateInventoryInfo(invConfig, inventoryName, namespaceName, inventoryID) - - deployment1Obj := e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(deployment1), namespaceName) - resources := []*unstructured.Unstructured{ - deployment1Obj, - } - - applierEvents := e2eutil.RunCollect(applier.Run(ctx, inventoryInfo, resources, apply.ApplierOptions{ - ReconcileTimeout: 2 * time.Minute, - EmitStatusEvents: true, - })) - - expEvents := []testutil.ExpEvent{ - { - // InitTask - EventType: event.InitType, - InitEvent: &testutil.ExpInitEvent{}, - }, - { - // InvAddTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-add-0", - Type: event.Started, - }, - }, - { - // InvAddTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-add-0", - Type: event.Finished, - }, - }, - { - // ApplyTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-0", - Type: event.Started, - }, - }, - { - // Create deployment - EventType: event.ApplyType, - ApplyEvent: &testutil.ExpApplyEvent{ - GroupName: "apply-0", - Status: event.ApplySuccessful, - Identifier: object.UnstructuredToObjMetadata(deployment1Obj), - Error: nil, - }, - }, - { - // ApplyTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-0", - Type: event.Finished, - }, - }, - { - // WaitTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Started, - }, - }, - { - // Deployment reconcile Pending . - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcilePending, - Identifier: object.UnstructuredToObjMetadata(deployment1Obj), - }, - }, - { - // Deployment confirmed Current. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcileSuccessful, - Identifier: object.UnstructuredToObjMetadata(deployment1Obj), - }, - }, - { - // WaitTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Finished, - }, - }, - { - // InvSetTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-set-0", - Type: event.Started, - }, - }, - { - // InvSetTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-set-0", - Type: event.Finished, - }, - }, - } - receivedEvents := testutil.EventsToExpEvents(applierEvents) - - // handle optional async NotFound StatusEvents - expected := testutil.ExpEvent{ - EventType: event.StatusType, - StatusEvent: &testutil.ExpStatusEvent{ - Identifier: object.UnstructuredToObjMetadata(deployment1Obj), - Status: status.NotFoundStatus, - Error: nil, - }, - } - receivedEvents, _ = testutil.RemoveEqualEvents(receivedEvents, expected) - - // handle optional async InProgress StatusEvents - expected = testutil.ExpEvent{ - EventType: event.StatusType, - StatusEvent: &testutil.ExpStatusEvent{ - Identifier: object.UnstructuredToObjMetadata(deployment1Obj), - Status: status.InProgressStatus, - Error: nil, - }, - } - receivedEvents, _ = testutil.RemoveEqualEvents(receivedEvents, expected) - - // handle required async Current StatusEvents - expected = testutil.ExpEvent{ - EventType: event.StatusType, - StatusEvent: &testutil.ExpStatusEvent{ - Identifier: object.UnstructuredToObjMetadata(deployment1Obj), - Status: status.CurrentStatus, - Error: nil, - }, - } - receivedEvents, matches := testutil.RemoveEqualEvents(receivedEvents, expected) - Expect(matches).To(BeNumerically(">=", 1), "unexpected number of %q status events", status.CurrentStatus) - - expEvents, receivedEvents = e2eutil.FilterOptionalEvents(expEvents, receivedEvents) - - Expect(receivedEvents).To(testutil.Equal(expEvents)) - - By("Verify deployment created") - e2eutil.AssertUnstructuredExists(ctx, c, deployment1Obj) - - By("Verify inventory") - invConfig.InvSizeVerifyFunc(ctx, c, inventoryName, namespaceName, inventoryID, 1, 1) - - By("Destroy resources") - destroyer := invConfig.DestroyerFactoryFunc() - - options := apply.DestroyerOptions{InventoryPolicy: inventory.PolicyAdoptIfNoInventory} - destroyerEvents := e2eutil.RunCollect(destroyer.Run(ctx, inventoryInfo, options)) - - expEvents = []testutil.ExpEvent{ - { - // InitTask - EventType: event.InitType, - InitEvent: &testutil.ExpInitEvent{}, - }, - { - // PruneTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.DeleteAction, - GroupName: "prune-0", - Type: event.Started, - }, - }, - { - // Delete deployment - EventType: event.DeleteType, - DeleteEvent: &testutil.ExpDeleteEvent{ - GroupName: "prune-0", - Status: event.DeleteSuccessful, - Identifier: object.UnstructuredToObjMetadata(deployment1Obj), - Error: nil, - }, - }, - { - // PruneTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.DeleteAction, - GroupName: "prune-0", - Type: event.Finished, - }, - }, - { - // WaitTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Started, - }, - }, - { - // Deployment reconcile Pending . - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcilePending, - Identifier: object.UnstructuredToObjMetadata(deployment1Obj), - }, - }, - { - // Deployment confirmed NotFound. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcileSuccessful, - Identifier: object.UnstructuredToObjMetadata(deployment1Obj), - }, - }, - { - // WaitTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Finished, - }, - }, - { - // DeleteInvTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-delete-or-update-0", - Type: event.Started, - }, - }, - { - // DeleteInvTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-delete-or-update-0", - Type: event.Finished, - }, - }, - } - receivedEvents = testutil.EventsToExpEvents(destroyerEvents) - - expEvents, receivedEvents = e2eutil.FilterOptionalEvents(expEvents, receivedEvents) - - Expect(receivedEvents).To(testutil.Equal(expEvents)) - - By("Verify deployment deleted") - e2eutil.AssertUnstructuredDoesNotExist(ctx, c, deployment1Obj) -} diff --git a/test/e2e/artifacts_test.go b/test/e2e/artifacts_test.go deleted file mode 100644 index 9da3a301..00000000 --- a/test/e2e/artifacts_test.go +++ /dev/null @@ -1,201 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package e2e - -import ( - "strings" -) - -var deployment1 = []byte(strings.TrimSpace(` -apiVersion: apps/v1 -kind: Deployment -metadata: - name: nginx-deployment -spec: - replicas: 4 - selector: - matchLabels: - app: nginx - template: - metadata: - labels: - app: nginx - spec: - containers: - - name: nginx - image: nginx:1.19.6 - ports: - - containerPort: 80 -`)) - -var apiservice1 = []byte(strings.TrimSpace(` -apiVersion: apiregistration.k8s.io/v1 -kind: APIService -metadata: - name: v1beta1.custom.metrics.k8s.io -spec: - insecureSkipTLSVerify: true - group: custom.metrics.k8s.io - groupPriorityMinimum: 100 - versionPriority: 100 - service: - name: custom-metrics-stackdriver-adapter - namespace: custom-metrics - version: v1beta1 -`)) - -var invalidCrd = []byte(strings.TrimSpace(` -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: invalidexamples.cli-utils.example.io -spec: - conversion: - strategy: None - group: cli-utils.example.io - names: - kind: InvalidExample - listKind: InvalidExampleList - plural: invalidexamples - singular: invalidexample - scope: Cluster -`)) - -var pod1 = []byte(strings.TrimSpace(` -kind: Pod -apiVersion: v1 -metadata: - name: pod1 -spec: - containers: - - name: kubernetes-pause - image: registry.k8s.io/pause:2.0 -`)) - -var pod2 = []byte(strings.TrimSpace(` -kind: Pod -apiVersion: v1 -metadata: - name: pod2 -spec: - containers: - - name: kubernetes-pause - image: registry.k8s.io/pause:2.0 -`)) - -var pod3 = []byte(strings.TrimSpace(` -kind: Pod -apiVersion: v1 -metadata: - name: pod3 -spec: - containers: - - name: kubernetes-pause - image: registry.k8s.io/pause:2.0 -`)) - -var podATemplate = ` -kind: Pod -apiVersion: v1 -metadata: - name: pod-a - namespace: {{.Namespace}} - annotations: - config.kubernetes.io/apply-time-mutation: | - - sourceRef: - kind: Pod - name: pod-b - namespace: {{.Namespace}} - sourcePath: $.status.podIP - targetPath: $.spec.containers[?(@.name=="nginx")].env[?(@.name=="SERVICE_HOST")].value - token: ${pob-b-ip} - - sourceRef: - kind: Pod - name: pod-b - namespace: {{.Namespace}} - sourcePath: $.spec.containers[?(@.name=="nginx")].ports[?(@.name=="tcp")].containerPort - targetPath: $.spec.containers[?(@.name=="nginx")].env[?(@.name=="SERVICE_HOST")].value - token: ${pob-b-port} -spec: - containers: - - name: nginx - image: nginx:1.21 - ports: - - name: tcp - containerPort: 80 - env: - - name: SERVICE_HOST - value: "${pob-b-ip}:${pob-b-port}" -` - -var podBTemplate = ` -kind: Pod -apiVersion: v1 -metadata: - name: pod-b - namespace: {{.Namespace}} -spec: - containers: - - name: nginx - image: nginx:1.21 - ports: - - name: tcp - containerPort: 80 -` - -var invalidMutationPodBTemplate = ` -kind: Pod -apiVersion: v1 -metadata: - name: pod-b - namespace: {{.Namespace}} - annotations: - config.kubernetes.io/apply-time-mutation: | - - sourceRef: - kind: Pod - name: pod-a # cyclic dependency - namespace: {{.Namespace}} - sourcePath: $.status.podIP - targetPath: $.spec.containers[?(@.name=="nginx")].env[?(@.name=="SERVICE_HOST")].value - token: ${pob-b-ip} - - sourceRef: - kind: Pod - name: pod-a - namespace: "" # empty namespace on a namespaced type - sourcePath: $.spec.containers[?(@.name=="nginx")].ports[?(@.name=="tcp")].containerPort - targetPath: $.spec.containers[?(@.name=="nginx")].env[?(@.name=="SERVICE_HOST")].value - token: ${pob-b-port} -spec: - containers: - - name: nginx - image: nginx:1.21 - ports: - - name: tcp - containerPort: 80 - env: - - name: SERVICE_HOST - value: "${pob-b-ip}:${pob-b-port}" -` - -var invalidPodTemplate = ` -kind: Pod -apiVersion: v1 -metadata: - # missing name - namespace: {{.Namespace}} -spec: - containers: - - name: nginx - image: nginx:1.21 - ports: - - name: tcp - containerPort: 80 -` - -var namespaceTemplate = ` -apiVersion: v1 -kind: Namespace -metadata: - name: {{.Namespace}} -` diff --git a/test/e2e/continue_on_error_test.go b/test/e2e/continue_on_error_test.go deleted file mode 100644 index 7c1256b2..00000000 --- a/test/e2e/continue_on_error_test.go +++ /dev/null @@ -1,207 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package e2e - -import ( - "context" - - . "github.com/onsi/ginkgo/v2" //nolint:revive - . "github.com/onsi/gomega" //nolint:revive - - "github.com/fluxcd/cli-utils/pkg/apply" - applyerror "github.com/fluxcd/cli-utils/pkg/apply/error" - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/testutil" - "github.com/fluxcd/cli-utils/test/e2e/e2eutil" - "github.com/fluxcd/cli-utils/test/e2e/invconfig" - "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/util/validation/field" - cmdutil "k8s.io/kubectl/pkg/cmd/util" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -func continueOnErrorTest(ctx context.Context, c client.Client, invConfig invconfig.InventoryConfig, inventoryName, namespaceName string) { - By("apply an invalid CRD") - applier := invConfig.ApplierFactoryFunc() - - inv := invConfig.InvWrapperFunc(invConfig.FactoryFunc(inventoryName, namespaceName, "test")) - - invalidCrdObj := e2eutil.ManifestToUnstructured(invalidCrd) - pod1Obj := e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(pod1), namespaceName) - resources := []*unstructured.Unstructured{ - invalidCrdObj, - pod1Obj, - } - - applierEvents := e2eutil.RunCollect(applier.Run(ctx, inv, resources, apply.ApplierOptions{})) - - expEvents := []testutil.ExpEvent{ - { - // InitTask - EventType: event.InitType, - InitEvent: &testutil.ExpInitEvent{}, - }, - { - // InvAddTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-add-0", - Type: event.Started, - }, - }, - { - // InvAddTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-add-0", - Type: event.Finished, - }, - }, - { - // ApplyTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-0", - Type: event.Started, - }, - }, - { - // Apply invalidCrd fails - EventType: event.ApplyType, - ApplyEvent: &testutil.ExpApplyEvent{ - GroupName: "apply-0", - Status: event.ApplyFailed, - Identifier: object.UnstructuredToObjMetadata(invalidCrdObj), - Error: testutil.EqualError( - applyerror.NewApplyRunError( - cmdutil.AddSourceToErr( - "creating", - "unstructured", - apierrors.NewInvalid( - invalidCrdObj.GroupVersionKind().GroupKind(), - invalidCrdObj.GetName(), - field.ErrorList{ - &field.Error{ - Type: field.ErrorTypeInvalid, - Field: "spec.versions", - BadValue: []apiextensions.CustomResourceDefinitionVersion(nil), - Detail: "must have exactly one version marked as storage version", - }, - &field.Error{ - Type: field.ErrorTypeInvalid, - Field: "status.storedVersions", - BadValue: []string(nil), - Detail: "must have at least one stored version", - }, - }, - ), - ), - ), - ), - }, - }, - { - // Create pod1 - EventType: event.ApplyType, - ApplyEvent: &testutil.ExpApplyEvent{ - GroupName: "apply-0", - Status: event.ApplySuccessful, - Identifier: object.UnstructuredToObjMetadata(pod1Obj), - Error: nil, - }, - }, - { - // ApplyTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-0", - Type: event.Finished, - }, - }, - { - // WaitTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Started, - }, - }, - { - // Pod1 reconcile Pending. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcilePending, - Identifier: object.UnstructuredToObjMetadata(pod1Obj), - }, - }, - { - // CRD reconcile Skipped. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcileSkipped, - Identifier: object.UnstructuredToObjMetadata(invalidCrdObj), - }, - }, - { - // Pod1 reconcile Successful. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcileSuccessful, - Identifier: object.UnstructuredToObjMetadata(pod1Obj), - }, - }, - { - // WaitTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Finished, - }, - }, - { - // InvSetTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-set-0", - Type: event.Started, - }, - }, - { - // InvSetTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-set-0", - Type: event.Finished, - }, - }, - } - receivedEvents := testutil.EventsToExpEvents(applierEvents) - - expEvents, receivedEvents = e2eutil.FilterOptionalEvents(expEvents, receivedEvents) - - // sort to allow comparison of multiple ApplyTasks in the same task group - testutil.SortExpEvents(receivedEvents) - - Expect(receivedEvents).To(testutil.Equal(expEvents)) - - By("Verify pod1 created") - e2eutil.AssertUnstructuredExists(ctx, c, pod1Obj) - - By("Verify CRD not created") - e2eutil.AssertUnstructuredDoesNotExist(ctx, c, invalidCrdObj) -} diff --git a/test/e2e/crd_test.go b/test/e2e/crd_test.go deleted file mode 100644 index 120c5ad3..00000000 --- a/test/e2e/crd_test.go +++ /dev/null @@ -1,435 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package e2e - -import ( - "context" - "strings" - "time" - - "github.com/fluxcd/cli-utils/pkg/apply" - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/testutil" - "github.com/fluxcd/cli-utils/test/e2e/e2eutil" - "github.com/fluxcd/cli-utils/test/e2e/invconfig" - . "github.com/onsi/ginkgo/v2" //nolint:revive - . "github.com/onsi/gomega" //nolint:revive - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -//nolint:dupl // expEvents similar to mutation tests -func crdTest(ctx context.Context, _ client.Client, invConfig invconfig.InventoryConfig, inventoryName, namespaceName string) { - By("apply a set of resources that includes both a crd and a cr") - applier := invConfig.ApplierFactoryFunc() - - inv := invConfig.InvWrapperFunc(invConfig.FactoryFunc(inventoryName, namespaceName, "test")) - - crdObj := e2eutil.ManifestToUnstructured(crd) - crObj := e2eutil.ManifestToUnstructured(cr) - - resources := []*unstructured.Unstructured{ - crObj, - crdObj, - } - - applierEvents := e2eutil.RunCollect(applier.Run(ctx, inv, resources, apply.ApplierOptions{ - ReconcileTimeout: 2 * time.Minute, - EmitStatusEvents: false, - })) - - expEvents := []testutil.ExpEvent{ - { - // InitTask - EventType: event.InitType, - InitEvent: &testutil.ExpInitEvent{}, - }, - { - // InvAddTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-add-0", - Type: event.Started, - }, - }, - { - // InvAddTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-add-0", - Type: event.Finished, - }, - }, - { - // ApplyTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-0", - Type: event.Started, - }, - }, - { - // Apply CRD before custom resource - EventType: event.ApplyType, - ApplyEvent: &testutil.ExpApplyEvent{ - GroupName: "apply-0", - Status: event.ApplySuccessful, - Identifier: object.UnstructuredToObjMetadata(crdObj), - Error: nil, - }, - }, - { - // ApplyTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-0", - Type: event.Finished, - }, - }, - { - // WaitTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Started, - }, - }, - { - // CRD reconcile Pending - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcilePending, - Identifier: object.UnstructuredToObjMetadata(crdObj), - }, - }, - { - // CRD confirmed Current. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcileSuccessful, - Identifier: object.UnstructuredToObjMetadata(crdObj), - }, - }, - { - // WaitTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Finished, - }, - }, - { - // ApplyTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-1", - Type: event.Started, - }, - }, - { - // Apply CRD before custom resource - EventType: event.ApplyType, - ApplyEvent: &testutil.ExpApplyEvent{ - GroupName: "apply-1", - Status: event.ApplySuccessful, - Identifier: object.UnstructuredToObjMetadata(crObj), - Error: nil, - }, - }, - { - // ApplyTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-1", - Type: event.Finished, - }, - }, - { - // WaitTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-1", - Type: event.Started, - }, - }, - { - // CR reconcile Pending - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-1", - Status: event.ReconcilePending, - Identifier: object.UnstructuredToObjMetadata(crObj), - }, - }, - { - // CR confirmed Current. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-1", - Status: event.ReconcileSuccessful, - Identifier: object.UnstructuredToObjMetadata(crObj), - }, - }, - { - // WaitTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-1", - Type: event.Finished, - }, - }, - { - // InvSetTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-set-0", - Type: event.Started, - }, - }, - { - // InvSetTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-set-0", - Type: event.Finished, - }, - }, - } - receivedEvents := testutil.EventsToExpEvents(applierEvents) - - expEvents, receivedEvents = e2eutil.FilterOptionalEvents(expEvents, receivedEvents) - - Expect(receivedEvents).To(testutil.Equal(expEvents)) - - By("destroy the resources, including the crd") - destroyer := invConfig.DestroyerFactoryFunc() - options := apply.DestroyerOptions{InventoryPolicy: inventory.PolicyAdoptIfNoInventory} - destroyerEvents := e2eutil.RunCollect(destroyer.Run(ctx, inv, options)) - - expEvents = []testutil.ExpEvent{ - { - // InitTask - EventType: event.InitType, - InitEvent: &testutil.ExpInitEvent{}, - }, - { - // PruneTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.DeleteAction, - GroupName: "prune-0", - Type: event.Started, - }, - }, - { - // Delete custom resource first - EventType: event.DeleteType, - DeleteEvent: &testutil.ExpDeleteEvent{ - GroupName: "prune-0", - Status: event.DeleteSuccessful, - Identifier: object.UnstructuredToObjMetadata(crObj), - Error: nil, - }, - }, - { - // PruneTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.DeleteAction, - GroupName: "prune-0", - Type: event.Finished, - }, - }, - { - // WaitTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Started, - }, - }, - { - // CR reconcile Pending - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcilePending, - Identifier: object.UnstructuredToObjMetadata(crObj), - }, - }, - { - // CR confirmed NotFound. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcileSuccessful, - Identifier: object.UnstructuredToObjMetadata(crObj), - }, - }, - { - // WaitTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Finished, - }, - }, - { - // PruneTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.DeleteAction, - GroupName: "prune-1", - Type: event.Started, - }, - }, - { - // Delete CRD after custom resource - EventType: event.DeleteType, - DeleteEvent: &testutil.ExpDeleteEvent{ - GroupName: "prune-1", - Status: event.DeleteSuccessful, - Identifier: object.UnstructuredToObjMetadata(crdObj), - Error: nil, - }, - }, - { - // PruneTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.DeleteAction, - GroupName: "prune-1", - Type: event.Finished, - }, - }, - { - // WaitTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-1", - Type: event.Started, - }, - }, - { - // CRD reconcile Pending. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-1", - Status: event.ReconcilePending, - Identifier: object.UnstructuredToObjMetadata(crdObj), - }, - }, - { - // CRD confirmed NotFound. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-1", - Status: event.ReconcileSuccessful, - Identifier: object.UnstructuredToObjMetadata(crdObj), - }, - }, - { - // WaitTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-1", - Type: event.Finished, - }, - }, - { - // DeleteInvTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-delete-or-update-0", - Type: event.Started, - }, - }, - { - // DeleteInvTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-delete-or-update-0", - Type: event.Finished, - }, - }, - } - receivedEvents = testutil.EventsToExpEvents(destroyerEvents) - - expEvents, receivedEvents = e2eutil.FilterOptionalEvents(expEvents, receivedEvents) - - Expect(receivedEvents).To(testutil.Equal(expEvents)) -} - -var crd = []byte(strings.TrimSpace(` -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: examples.cli-utils.example.io -spec: - conversion: - strategy: None - group: cli-utils.example.io - names: - kind: Example - listKind: ExampleList - plural: examples - singular: example - scope: Cluster - versions: - - name: v1alpha1 - schema: - openAPIV3Schema: - description: Example for cli-utils e2e tests - properties: - apiVersion: - type: string - kind: - type: string - metadata: - type: object - spec: - description: Example for cli-utils e2e tests - properties: - replicas: - description: Number of replicas - type: integer - required: - - replicas - type: object - type: object - served: true - storage: true - subresources: {} -`)) - -var cr = []byte(strings.TrimSpace(` -apiVersion: cli-utils.example.io/v1alpha1 -kind: Example -metadata: - name: example-cr -spec: - replicas: 4 -`)) diff --git a/test/e2e/current_uid_filter_test.go b/test/e2e/current_uid_filter_test.go deleted file mode 100644 index 834809fb..00000000 --- a/test/e2e/current_uid_filter_test.go +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package e2e - -import ( - "context" - "fmt" - - "github.com/fluxcd/cli-utils/pkg/apply" - "github.com/fluxcd/cli-utils/test/e2e/e2eutil" - "github.com/fluxcd/cli-utils/test/e2e/invconfig" - . "github.com/onsi/ginkgo/v2" //nolint:revive - . "github.com/onsi/gomega" //nolint:revive - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -const v1EventTemplate = ` -apiVersion: v1 -involvedObject: - apiVersion: v1 - kind: Pod - name: pod - namespace: {{.Namespace}} -kind: Event -message: Back-off restarting failed container -metadata: - name: test - namespace: {{.Namespace}} -reason: BackOff -type: Warning -` - -const v1EventsEventTemplate = ` -apiVersion: events.k8s.io/v1 -eventTime: null -kind: Event -metadata: - name: test - namespace: {{.Namespace}} -note: Back-off restarting failed container -reason: BackOff -regarding: - apiVersion: v1 - kind: Pod - name: pod - namespace: {{.Namespace}} -type: Warning -` - -// Note this tests the scenario of "cohabitating" k8s objects (an object available via multiple apiGroups), but having the same UID. -// As of k8s 1.25 an example of such "cohabitating" kinds is Event which is available via both "v1" and "events.k8s.io/v1". -// See the full list of cohabitating resources on the storage level here: -// - https://github.com/kubernetes/kubernetes/blob/v1.25.0/pkg/kubeapiserver/default_storage_factory_builder.go#L124-L131 -// We test that when the user upgrades their manifest from one cohabitated apiGroup to the other, then: -// - it should not result in object being pruned -// - object pruning should be skipped due to CurrentUIDFilter (even though a diff is found) -// - inventory should not double-track the object i.e. we should hold reference only to the object with the groupKind that was most recently applied -func currentUIDFilterTest(ctx context.Context, c client.Client, invConfig invconfig.InventoryConfig, inventoryName, namespaceName string) { - applier := invConfig.ApplierFactoryFunc() - inventoryID := fmt.Sprintf("%s-%s", inventoryName, namespaceName) - inventoryInfo := invconfig.CreateInventoryInfo(invConfig, inventoryName, namespaceName, inventoryID) - - templateFields := struct{ Namespace string }{Namespace: namespaceName} - v1Event := e2eutil.TemplateToUnstructured(v1EventTemplate, templateFields) - v1EventsEvent := e2eutil.TemplateToUnstructured(v1EventsEventTemplate, templateFields) - - By("Apply resource with deprecated groupKind") - resources := []*unstructured.Unstructured{ - v1Event, - } - err := e2eutil.Run(applier.Run(ctx, inventoryInfo, resources, apply.ApplierOptions{})) - Expect(err).ToNot(HaveOccurred()) - - By("Verify resource available in both apiGroups") - objDeprecated := e2eutil.AssertUnstructuredExists(ctx, c, v1Event) - objNew := e2eutil.AssertUnstructuredExists(ctx, c, v1EventsEvent) - - By("Verify UID matches for cohabitating resources") - uid := objDeprecated.GetUID() - Expect(uid).ToNot(BeEmpty()) - Expect(objDeprecated.GetUID()).To(Equal(objNew.GetUID())) - - By("Verify only 1 item in inventory") - invConfig.InvSizeVerifyFunc(ctx, c, inventoryName, namespaceName, inventoryID, 1, 1) - - By("Apply resource with new groupKind") - resources = []*unstructured.Unstructured{ - v1EventsEvent, - } - err = e2eutil.Run(applier.Run(ctx, inventoryInfo, resources, apply.ApplierOptions{})) - Expect(err).ToNot(HaveOccurred()) - - By("Verify resource still available in both apiGroups") - objDeprecated = e2eutil.AssertUnstructuredExists(ctx, c, v1Event) - objNew = e2eutil.AssertUnstructuredExists(ctx, c, v1EventsEvent) - - By("Verify UID matches for cohabitating resources") - Expect(objDeprecated.GetUID()).To(Equal(objNew.GetUID())) - - By("Verify UID matches the UID from previous apply") - Expect(objDeprecated.GetUID()).To(Equal(uid)) - - By("Verify still only 1 item in inventory") - // Expecting statusCount=2: - // one object applied and one prune skipped - invConfig.InvSizeVerifyFunc(ctx, c, inventoryName, namespaceName, inventoryID, 1, 2) -} diff --git a/test/e2e/customprovider/provider.go b/test/e2e/customprovider/provider.go deleted file mode 100644 index 39d50160..00000000 --- a/test/e2e/customprovider/provider.go +++ /dev/null @@ -1,306 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package customprovider - -import ( - "context" - "fmt" - "strings" - - "github.com/fluxcd/cli-utils/pkg/apis/actuation" - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/pkg/object" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/dynamic" - "k8s.io/kubectl/pkg/cmd/util" -) - -var InventoryCRD = []byte(strings.TrimSpace(` -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: inventories.cli-utils.example.io -spec: - conversion: - strategy: None - group: cli-utils.example.io - names: - kind: Inventory - listKind: InventoryList - plural: inventories - singular: inventory - scope: Namespaced - versions: - - name: v1alpha1 - schema: - openAPIV3Schema: - description: Example for cli-utils e2e tests - properties: - spec: - properties: - objects: - items: - properties: - group: - type: string - kind: - type: string - name: - type: string - namespace: - type: string - required: - - group - - kind - - name - - namespace - type: object - type: array - type: object - status: - properties: - objects: - items: - properties: - group: - type: string - kind: - type: string - name: - type: string - namespace: - type: string - strategy: - type: string - actuation: - type: string - reconcile: - type: string - required: - - group - - kind - - name - - namespace - - strategy - - actuation - - reconcile - type: object - type: array - type: object - type: object - served: true - storage: true - subresources: - status: {} -`)) - -var InventoryGVK = schema.GroupVersionKind{ - Group: "cli-utils.example.io", - Version: "v1alpha1", - Kind: "Inventory", -} - -var _ inventory.ClientFactory = CustomClientFactory{} - -type CustomClientFactory struct { -} - -func (CustomClientFactory) NewClient(factory util.Factory) (inventory.Client, error) { - return inventory.NewClient(factory, - WrapInventoryObj, invToUnstructuredFunc, inventory.StatusPolicyAll, inventory.ConfigMapGVK) -} - -func invToUnstructuredFunc(inv inventory.Info) *unstructured.Unstructured { - switch invInfo := inv.(type) { - case *InventoryCustomType: - return invInfo.inv - default: - return nil - } -} - -func WrapInventoryObj(obj *unstructured.Unstructured) inventory.Storage { - return &InventoryCustomType{inv: obj} -} - -func WrapInventoryInfoObj(obj *unstructured.Unstructured) inventory.Info { - return &InventoryCustomType{inv: obj} -} - -var _ inventory.Storage = &InventoryCustomType{} -var _ inventory.Info = &InventoryCustomType{} - -type InventoryCustomType struct { - inv *unstructured.Unstructured -} - -func (i InventoryCustomType) Namespace() string { - return i.inv.GetNamespace() -} - -func (i InventoryCustomType) Name() string { - return i.inv.GetName() -} - -func (i InventoryCustomType) Strategy() inventory.Strategy { - return inventory.NameStrategy -} - -func (i InventoryCustomType) ID() string { - labels := i.inv.GetLabels() - id, found := labels[common.InventoryLabel] - if !found { - return "" - } - return id -} - -func (i InventoryCustomType) Load() (object.ObjMetadataSet, error) { - var inv object.ObjMetadataSet - s, found, err := unstructured.NestedSlice(i.inv.Object, "spec", "objects") - if err != nil { - return inv, err - } - if !found { - return inv, nil - } - for _, item := range s { - m := item.(map[string]interface{}) - namespace, _, _ := unstructured.NestedString(m, "namespace") - name, _, _ := unstructured.NestedString(m, "name") - group, _, _ := unstructured.NestedString(m, "group") - kind, _, _ := unstructured.NestedString(m, "kind") - id := object.ObjMetadata{ - Namespace: namespace, - Name: name, - GroupKind: schema.GroupKind{ - Group: group, - Kind: kind, - }, - } - inv = append(inv, id) - } - return inv, nil -} - -func (i InventoryCustomType) Store(objs object.ObjMetadataSet, status []actuation.ObjectStatus) error { - var specObjs []interface{} - for _, obj := range objs { - specObjs = append(specObjs, map[string]interface{}{ - "group": obj.GroupKind.Group, - "kind": obj.GroupKind.Kind, - "namespace": obj.Namespace, - "name": obj.Name, - }) - } - var statusObjs []interface{} - for _, objStatus := range status { - statusObjs = append(statusObjs, map[string]interface{}{ - "group": objStatus.Group, - "kind": objStatus.Kind, - "namespace": objStatus.Namespace, - "name": objStatus.Name, - "strategy": objStatus.Strategy.String(), - "actuation": objStatus.Actuation.String(), - "reconcile": objStatus.Reconcile.String(), - }) - } - if len(specObjs) > 0 { - err := unstructured.SetNestedSlice(i.inv.Object, specObjs, "spec", "objects") - if err != nil { - return err - } - } else { - unstructured.RemoveNestedField(i.inv.Object, "spec") - } - if len(statusObjs) > 0 { - err := unstructured.SetNestedSlice(i.inv.Object, statusObjs, "status", "objects") - if err != nil { - return err - } - } else { - unstructured.RemoveNestedField(i.inv.Object, "status") - } - return nil -} - -func (i InventoryCustomType) GetObject() (*unstructured.Unstructured, error) { - return i.inv, nil -} - -// Apply is an Inventory interface function implemented to apply the inventory -// object. -func (i InventoryCustomType) Apply(dc dynamic.Interface, mapper meta.RESTMapper, _ inventory.StatusPolicy) error { - invInfo, namespacedClient, err := i.getNamespacedClient(dc, mapper) - if err != nil { - return err - } - - // Get cluster object, if exsists. - clusterObj, err := namespacedClient.Get(context.TODO(), invInfo.GetName(), metav1.GetOptions{}) - if err != nil && !apierrors.IsNotFound(err) { - return err - } - - var appliedObj *unstructured.Unstructured - - if clusterObj == nil { - // Create cluster inventory object, if it does not exist on cluster. - appliedObj, err = namespacedClient.Create(context.TODO(), invInfo, metav1.CreateOptions{}) - } else { - // Update the cluster inventory object instead. - appliedObj, err = namespacedClient.Update(context.TODO(), invInfo, metav1.UpdateOptions{}) - } - if err != nil { - return err - } - - // Update status. - invInfo.SetResourceVersion(appliedObj.GetResourceVersion()) - _, err = namespacedClient.UpdateStatus(context.TODO(), invInfo, metav1.UpdateOptions{}) - return err -} - -func (i InventoryCustomType) ApplyWithPrune(dc dynamic.Interface, mapper meta.RESTMapper, _ inventory.StatusPolicy, _ object.ObjMetadataSet) error { - invInfo, namespacedClient, err := i.getNamespacedClient(dc, mapper) - if err != nil { - return err - } - - // Update the cluster inventory object. - appliedObj, err := namespacedClient.Update(context.TODO(), invInfo, metav1.UpdateOptions{}) - if err != nil { - return err - } - - // Update status. - invInfo.SetResourceVersion(appliedObj.GetResourceVersion()) - _, err = namespacedClient.UpdateStatus(context.TODO(), invInfo, metav1.UpdateOptions{}) - return err -} - -func (i InventoryCustomType) getNamespacedClient(dc dynamic.Interface, mapper meta.RESTMapper) (*unstructured.Unstructured, dynamic.ResourceInterface, error) { - invInfo, err := i.GetObject() - if err != nil { - return nil, nil, err - } - if invInfo == nil { - return nil, nil, fmt.Errorf("attempting to create a nil inventory object") - } - - mapping, err := mapper.RESTMapping(invInfo.GroupVersionKind().GroupKind(), invInfo.GroupVersionKind().Version) - if err != nil { - return nil, nil, err - } - - // Create client to interact with cluster. - namespacedClient := dc.Resource(mapping.Resource).Namespace(invInfo.GetNamespace()) - - return invInfo, namespacedClient, nil -} diff --git a/test/e2e/deletion_prevention_test.go b/test/e2e/deletion_prevention_test.go deleted file mode 100644 index cdf0fe82..00000000 --- a/test/e2e/deletion_prevention_test.go +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package e2e - -import ( - "context" - "fmt" - "time" - - "github.com/fluxcd/cli-utils/pkg/apply" - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/test/e2e/e2eutil" - "github.com/fluxcd/cli-utils/test/e2e/invconfig" - . "github.com/onsi/ginkgo/v2" //nolint:revive - . "github.com/onsi/gomega" //nolint:revive - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -func deletionPreventionTest(ctx context.Context, c client.Client, invConfig invconfig.InventoryConfig, inventoryName, namespaceName string) { - By("Apply resources") - applier := invConfig.ApplierFactoryFunc() - inventoryID := fmt.Sprintf("%s-%s", inventoryName, namespaceName) - - inventoryInfo := invconfig.CreateInventoryInfo(invConfig, inventoryName, namespaceName, inventoryID) - - resources := []*unstructured.Unstructured{ - e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(deployment1), namespaceName), - e2eutil.WithAnnotation(e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(pod1), namespaceName), common.OnRemoveAnnotation, common.OnRemoveKeep), - e2eutil.WithAnnotation(e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(pod2), namespaceName), common.LifecycleDeleteAnnotation, common.PreventDeletion), - } - - e2eutil.RunCollect(applier.Run(ctx, inventoryInfo, resources, apply.ApplierOptions{ - ReconcileTimeout: 2 * time.Minute, - })) - - By("Verify deployment created") - obj := e2eutil.AssertUnstructuredExists(ctx, c, e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(deployment1), namespaceName)) - Expect(obj.GetAnnotations()[inventory.OwningInventoryKey]).To(Equal(inventoryInfo.ID())) - - By("Verify pod1 created") - obj = e2eutil.AssertUnstructuredExists(ctx, c, e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(pod1), namespaceName)) - Expect(obj.GetAnnotations()[inventory.OwningInventoryKey]).To(Equal(inventoryInfo.ID())) - - By("Verify pod2 created") - obj = e2eutil.AssertUnstructuredExists(ctx, c, e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(pod2), namespaceName)) - Expect(obj.GetAnnotations()[inventory.OwningInventoryKey]).To(Equal(inventoryInfo.ID())) - - By("Verify the inventory size is 3") - invConfig.InvSizeVerifyFunc(ctx, c, inventoryName, namespaceName, inventoryID, 3, 3) - - By("Dry-run apply resources") - resources = []*unstructured.Unstructured{ - e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(deployment1), namespaceName), - } - - e2eutil.RunCollect(applier.Run(ctx, inventoryInfo, resources, apply.ApplierOptions{ - ReconcileTimeout: 2 * time.Minute, - DryRunStrategy: common.DryRunClient, - })) - - By("Verify deployment still exists and has the config.k8s.io/owning-inventory annotation") - obj = e2eutil.AssertUnstructuredExists(ctx, c, e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(deployment1), namespaceName)) - Expect(obj.GetAnnotations()[inventory.OwningInventoryKey]).To(Equal(inventoryInfo.ID())) - - By("Verify pod1 still exits and does not have the config.k8s.io/owning-inventory annotation") - obj = e2eutil.AssertUnstructuredExists(ctx, c, e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(pod1), namespaceName)) - Expect(obj.GetAnnotations()[inventory.OwningInventoryKey]).To(Equal(inventoryInfo.ID())) - - By("Verify pod2 still exits and does not have the config.k8s.io/owning-inventory annotation") - obj = e2eutil.AssertUnstructuredExists(ctx, c, e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(pod2), namespaceName)) - Expect(obj.GetAnnotations()[inventory.OwningInventoryKey]).To(Equal(inventoryInfo.ID())) - - By("Verify the inventory size is still 3") - invConfig.InvSizeVerifyFunc(ctx, c, inventoryName, namespaceName, inventoryID, 3, 3) - - By("Apply resources") - resources = []*unstructured.Unstructured{ - e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(deployment1), namespaceName), - } - - e2eutil.RunCollect(applier.Run(ctx, inventoryInfo, resources, apply.ApplierOptions{ - ReconcileTimeout: 2 * time.Minute, - })) - - By("Verify deployment still exists and has the config.k8s.io/owning-inventory annotation") - obj = e2eutil.AssertUnstructuredExists(ctx, c, e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(deployment1), namespaceName)) - Expect(obj.GetAnnotations()[inventory.OwningInventoryKey]).To(Equal(inventoryInfo.ID())) - - By("Verify pod1 still exits and does not have the config.k8s.io/owning-inventory annotation") - obj = e2eutil.AssertUnstructuredExists(ctx, c, e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(pod1), namespaceName)) - Expect(obj.GetAnnotations()[inventory.OwningInventoryKey]).To(Equal("")) - - By("Verify pod2 still exits and does not have the config.k8s.io/owning-inventory annotation") - obj = e2eutil.AssertUnstructuredExists(ctx, c, e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(pod2), namespaceName)) - Expect(obj.GetAnnotations()[inventory.OwningInventoryKey]).To(Equal("")) - - By("Verify the inventory size is 1") - invConfig.InvSizeVerifyFunc(ctx, c, inventoryName, namespaceName, inventoryID, 1, 3) -} diff --git a/test/e2e/dependency_filter_test.go b/test/e2e/dependency_filter_test.go deleted file mode 100644 index b84a85cf..00000000 --- a/test/e2e/dependency_filter_test.go +++ /dev/null @@ -1,435 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package e2e - -import ( - "context" - "fmt" - - "github.com/fluxcd/cli-utils/pkg/apis/actuation" - "github.com/fluxcd/cli-utils/pkg/apply" - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/apply/filter" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/object/validation" - "github.com/fluxcd/cli-utils/pkg/testutil" - "github.com/fluxcd/cli-utils/test/e2e/e2eutil" - "github.com/fluxcd/cli-utils/test/e2e/invconfig" - . "github.com/onsi/ginkgo/v2" //nolint:revive - . "github.com/onsi/gomega" //nolint:revive - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -//nolint:dupl // expEvents similar to other tests -func dependencyFilterTest(ctx context.Context, c client.Client, invConfig invconfig.InventoryConfig, inventoryName, namespaceName string) { - By("apply resources in order based on depends-on annotation") - applier := invConfig.ApplierFactoryFunc() - - inv := invConfig.InvWrapperFunc(invConfig.FactoryFunc(inventoryName, namespaceName, "test")) - - pod1Obj := e2eutil.WithDependsOn(e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(pod1), namespaceName), fmt.Sprintf("/namespaces/%s/Pod/pod2", namespaceName)) - pod2Obj := e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(pod2), namespaceName) - - // Dependency order: pod1 -> pod2 - // Apply order: pod2, pod1 - resources := []*unstructured.Unstructured{ - pod1Obj, - pod2Obj, - } - - // Cleanup - defer func(ctx context.Context, c client.Client) { - e2eutil.DeleteUnstructuredIfExists(ctx, c, pod1Obj) - e2eutil.DeleteUnstructuredIfExists(ctx, c, pod2Obj) - }(ctx, c) - - applierEvents := e2eutil.RunCollect(applier.Run(ctx, inv, resources, apply.ApplierOptions{ - EmitStatusEvents: false, - })) - - expEvents := []testutil.ExpEvent{ - { - // InitTask - EventType: event.InitType, - InitEvent: &testutil.ExpInitEvent{}, - }, - { - // InvAddTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-add-0", - Type: event.Started, - }, - }, - { - // InvAddTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-add-0", - Type: event.Finished, - }, - }, - { - // ApplyTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-0", - Type: event.Started, - }, - }, - { - // Apply pod2 first - EventType: event.ApplyType, - ApplyEvent: &testutil.ExpApplyEvent{ - GroupName: "apply-0", - Status: event.ApplySuccessful, - Identifier: object.UnstructuredToObjMetadata(pod2Obj), - Error: nil, - }, - }, - { - // ApplyTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-0", - Type: event.Finished, - }, - }, - { - // WaitTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Started, - }, - }, - { - // pod2 reconcile Pending. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcilePending, - Identifier: object.UnstructuredToObjMetadata(pod2Obj), - }, - }, - { - // pod2 confirmed Current. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcileSuccessful, - Identifier: object.UnstructuredToObjMetadata(pod2Obj), - }, - }, - { - // WaitTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Finished, - }, - }, - { - // ApplyTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-1", - Type: event.Started, - }, - }, - { - // Apply pod1 second - EventType: event.ApplyType, - ApplyEvent: &testutil.ExpApplyEvent{ - GroupName: "apply-1", - Status: event.ApplySuccessful, - Identifier: object.UnstructuredToObjMetadata(pod1Obj), - Error: nil, - }, - }, - { - // ApplyTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-1", - Type: event.Finished, - }, - }, - { - // WaitTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-1", - Type: event.Started, - }, - }, - { - // pod1 reconcile Pending. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-1", - Status: event.ReconcilePending, - Identifier: object.UnstructuredToObjMetadata(pod1Obj), - }, - }, - { - // pod1 confirmed Current. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-1", - Status: event.ReconcileSuccessful, - Identifier: object.UnstructuredToObjMetadata(pod1Obj), - }, - }, - { - // WaitTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-1", - Type: event.Finished, - }, - }, - { - // InvSetTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-set-0", - Type: event.Started, - }, - }, - { - // InvSetTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-set-0", - Type: event.Finished, - }, - }, - } - receivedEvents := testutil.EventsToExpEvents(applierEvents) - - expEvents, receivedEvents = e2eutil.FilterOptionalEvents(expEvents, receivedEvents) - - Expect(receivedEvents).To(testutil.Equal(expEvents)) - - By("verify pod1 created and ready") - result := e2eutil.AssertUnstructuredExists(ctx, c, pod1Obj) - podIP, found, err := object.NestedField(result.Object, "status", "podIP") - Expect(err).NotTo(HaveOccurred()) - Expect(found).To(BeTrue()) - Expect(podIP).NotTo(BeEmpty()) // use podIP as proxy for readiness - - By("verify pod2 created and ready") - result = e2eutil.AssertUnstructuredExists(ctx, c, pod2Obj) - podIP, found, err = object.NestedField(result.Object, "status", "podIP") - Expect(err).NotTo(HaveOccurred()) - Expect(found).To(BeTrue()) - Expect(podIP).NotTo(BeEmpty()) // use podIP as proxy for readiness - - // Attempt to Prune pod2 - resources = []*unstructured.Unstructured{ - pod1Obj, - } - - applierEvents = e2eutil.RunCollect(applier.Run(ctx, inv, resources, apply.ApplierOptions{ - EmitStatusEvents: false, - ValidationPolicy: validation.SkipInvalid, - })) - - expEvents = []testutil.ExpEvent{ - { - // InitTask - EventType: event.InitType, - InitEvent: &testutil.ExpInitEvent{}, - }, - { - // InvAddTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-add-0", - Type: event.Started, - }, - }, - { - // InvAddTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-add-0", - Type: event.Finished, - }, - }, - { - // ApplyTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-0", - Type: event.Started, - }, - }, - { - // Apply pod1 Skipped (dependency actuation strategy mismatch) - EventType: event.ApplyType, - ApplyEvent: &testutil.ExpApplyEvent{ - GroupName: "apply-0", - Status: event.ApplySkipped, - Identifier: object.UnstructuredToObjMetadata(pod1Obj), - Error: testutil.EqualError(&filter.DependencyActuationMismatchError{ - Object: object.UnstructuredToObjMetadata(pod1Obj), - Strategy: actuation.ActuationStrategyApply, - Relationship: filter.RelationshipDependency, - Relation: object.UnstructuredToObjMetadata(pod2Obj), - RelationStrategy: actuation.ActuationStrategyDelete, - }), - }, - }, - { - // ApplyTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-0", - Type: event.Finished, - }, - }, - { - // WaitTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Started, - }, - }, - { - // pod1 reconcile Skipped (because apply skipped) - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcileSkipped, - Identifier: object.UnstructuredToObjMetadata(pod1Obj), - }, - }, - { - // WaitTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Finished, - }, - }, - { - // PruneTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.PruneAction, - GroupName: "prune-0", - Type: event.Started, - }, - }, - { - // Prune pod2 Skipped (dependency actuation strategy mismatch) - EventType: event.PruneType, - PruneEvent: &testutil.ExpPruneEvent{ - GroupName: "prune-0", - Status: event.PruneSkipped, - Identifier: object.UnstructuredToObjMetadata(pod2Obj), - Error: testutil.EqualError(&filter.DependencyActuationMismatchError{ - Object: object.UnstructuredToObjMetadata(pod2Obj), - Strategy: actuation.ActuationStrategyDelete, - Relationship: filter.RelationshipDependent, - Relation: object.UnstructuredToObjMetadata(pod1Obj), - RelationStrategy: actuation.ActuationStrategyApply, - }), - }, - }, - { - // PruneTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.PruneAction, - GroupName: "prune-0", - Type: event.Finished, - }, - }, - { - // WaitTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-1", - Type: event.Started, - }, - }, - { - // pod2 reconcile Skipped (because prune skipped) - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-1", - Status: event.ReconcileSkipped, - Identifier: object.UnstructuredToObjMetadata(pod2Obj), - }, - }, - { - // WaitTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-1", - Type: event.Finished, - }, - }, - { - // InvSetTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-set-0", - Type: event.Started, - }, - }, - { - // InvSetTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-set-0", - Type: event.Finished, - }, - }, - } - receivedEvents = testutil.EventsToExpEvents(applierEvents) - - expEvents, receivedEvents = e2eutil.FilterOptionalEvents(expEvents, receivedEvents) - - Expect(receivedEvents).To(testutil.Equal(expEvents)) - - By("verify pod1 not deleted") - result = e2eutil.AssertUnstructuredExists(ctx, c, pod1Obj) - ts, found, err := object.NestedField(result.Object, "metadata", "deletionTimestamp") - Expect(err).NotTo(HaveOccurred()) - Expect(found).To(BeFalse(), "deletionTimestamp found: ", ts) - - By("verify pod2 not deleted") - result = e2eutil.AssertUnstructuredExists(ctx, c, pod2Obj) - ts, found, err = object.NestedField(result.Object, "metadata", "deletionTimestamp") - Expect(err).NotTo(HaveOccurred()) - Expect(found).To(BeFalse(), "deletionTimestamp found: ", ts) -} diff --git a/test/e2e/depends_on_test.go b/test/e2e/depends_on_test.go deleted file mode 100644 index 19e1ce9b..00000000 --- a/test/e2e/depends_on_test.go +++ /dev/null @@ -1,756 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package e2e - -import ( - "context" - "fmt" - - "github.com/fluxcd/cli-utils/pkg/apply" - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/testutil" - "github.com/fluxcd/cli-utils/test/e2e/e2eutil" - "github.com/fluxcd/cli-utils/test/e2e/invconfig" - . "github.com/onsi/ginkgo/v2" //nolint:revive - . "github.com/onsi/gomega" //nolint:revive - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -func dependsOnTest(ctx context.Context, c client.Client, invConfig invconfig.InventoryConfig, inventoryName, namespaceName string) { - By("apply resources in order based on depends-on annotation") - applier := invConfig.ApplierFactoryFunc() - - inv := invConfig.InvWrapperFunc(invConfig.FactoryFunc(inventoryName, namespaceName, "test")) - - namespace1Name := fmt.Sprintf("%s-ns1", namespaceName) - namespace1Obj := e2eutil.UnstructuredNamespace(namespace1Name) - - namespace2Name := fmt.Sprintf("%s-ns2", namespaceName) - namespace2Obj := e2eutil.UnstructuredNamespace(namespace2Name) - - pod1Obj := e2eutil.ManifestToUnstructured(pod1) - pod1Obj = e2eutil.WithNamespace(pod1Obj, namespace1Name) - pod1Obj = e2eutil.WithDependsOn(pod1Obj, fmt.Sprintf("/namespaces/%s/Pod/pod3", namespace1Name)) - - pod2Obj := e2eutil.ManifestToUnstructured(pod2) - pod2Obj = e2eutil.WithNamespace(pod2Obj, namespace2Name) - - pod3Obj := e2eutil.ManifestToUnstructured(pod3) - pod3Obj = e2eutil.WithNamespace(pod3Obj, namespace1Name) - pod3Obj = e2eutil.WithDependsOn(pod3Obj, fmt.Sprintf("/namespaces/%s/Pod/pod2", namespace2Name)) - - // Dependency order: pod1 -> pod3 -> pod2 - // Apply order: pod2, pod3, pod1 - resources := []*unstructured.Unstructured{ - namespace1Obj, - namespace2Obj, - pod1Obj, - pod2Obj, - pod3Obj, - } - - applierEvents := e2eutil.RunCollect(applier.Run(ctx, inv, resources, apply.ApplierOptions{ - EmitStatusEvents: false, - })) - - expEvents := []testutil.ExpEvent{ - { - // InitTask - EventType: event.InitType, - InitEvent: &testutil.ExpInitEvent{}, - }, - { - // InvAddTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-add-0", - Type: event.Started, - }, - }, - { - // InvAddTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-add-0", - Type: event.Finished, - }, - }, - { - // ApplyTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-0", - Type: event.Started, - }, - }, - { - // Apply Namespace1 first - EventType: event.ApplyType, - ApplyEvent: &testutil.ExpApplyEvent{ - GroupName: "apply-0", - Status: event.ApplySuccessful, - Identifier: object.UnstructuredToObjMetadata(namespace1Obj), - }, - }, - { - // Apply Namespace2 first - EventType: event.ApplyType, - ApplyEvent: &testutil.ExpApplyEvent{ - GroupName: "apply-0", - Status: event.ApplySuccessful, - Identifier: object.UnstructuredToObjMetadata(namespace2Obj), - }, - }, - { - // ApplyTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-0", - Type: event.Finished, - }, - }, - { - // WaitTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Started, - }, - }, - { - // Namespace1 reconcile Pending. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcilePending, - Identifier: object.UnstructuredToObjMetadata(namespace1Obj), - }, - }, - { - // Namespace2 reconcile Pending. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcilePending, - Identifier: object.UnstructuredToObjMetadata(namespace2Obj), - }, - }, - { - // Namespace1 confirmed Current. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcileSuccessful, - Identifier: object.UnstructuredToObjMetadata(namespace1Obj), - }, - }, - { - // Namespace2 confirmed Current. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcileSuccessful, - Identifier: object.UnstructuredToObjMetadata(namespace2Obj), - }, - }, - { - // WaitTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Finished, - }, - }, - { - // ApplyTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-1", - Type: event.Started, - }, - }, - { - // Apply Pod2 first - EventType: event.ApplyType, - ApplyEvent: &testutil.ExpApplyEvent{ - GroupName: "apply-1", - Status: event.ApplySuccessful, - Identifier: object.UnstructuredToObjMetadata(pod2Obj), - Error: nil, - }, - }, - { - // ApplyTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-1", - Type: event.Finished, - }, - }, - { - // WaitTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-1", - Type: event.Started, - }, - }, - { - // Pod2 reconcile Pending. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-1", - Status: event.ReconcilePending, - Identifier: object.UnstructuredToObjMetadata(pod2Obj), - }, - }, - { - // Pod2 confirmed Current. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-1", - Status: event.ReconcileSuccessful, - Identifier: object.UnstructuredToObjMetadata(pod2Obj), - }, - }, - { - // WaitTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-1", - Type: event.Finished, - }, - }, - { - // ApplyTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-2", - Type: event.Started, - }, - }, - { - // Apply Pod3 second - EventType: event.ApplyType, - ApplyEvent: &testutil.ExpApplyEvent{ - GroupName: "apply-2", - Status: event.ApplySuccessful, - Identifier: object.UnstructuredToObjMetadata(pod3Obj), - Error: nil, - }, - }, - { - // ApplyTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-2", - Type: event.Finished, - }, - }, - { - // WaitTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-2", - Type: event.Started, - }, - }, - { - // Pod3 reconcile Pending. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-2", - Status: event.ReconcilePending, - Identifier: object.UnstructuredToObjMetadata(pod3Obj), - }, - }, - { - // Pod3 confirmed Current. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-2", - Status: event.ReconcileSuccessful, - Identifier: object.UnstructuredToObjMetadata(pod3Obj), - }, - }, - { - // WaitTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-2", - Type: event.Finished, - }, - }, - { - // ApplyTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-3", - Type: event.Started, - }, - }, - { - // Apply Pod1 third - EventType: event.ApplyType, - ApplyEvent: &testutil.ExpApplyEvent{ - GroupName: "apply-3", - Status: event.ApplySuccessful, - Identifier: object.UnstructuredToObjMetadata(pod1Obj), - Error: nil, - }, - }, - { - // ApplyTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-3", - Type: event.Finished, - }, - }, - { - // WaitTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-3", - Type: event.Started, - }, - }, - { - // Pod1 reconcile Pending. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-3", - Status: event.ReconcilePending, - Identifier: object.UnstructuredToObjMetadata(pod1Obj), - }, - }, - { - // Pod1 confirmed Current. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-3", - Status: event.ReconcileSuccessful, - Identifier: object.UnstructuredToObjMetadata(pod1Obj), - }, - }, - { - // WaitTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-3", - Type: event.Finished, - }, - }, - { - // InvSetTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-set-0", - Type: event.Started, - }, - }, - { - // InvSetTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-set-0", - Type: event.Finished, - }, - }, - } - receivedEvents := testutil.EventsToExpEvents(applierEvents) - - expEvents, receivedEvents = e2eutil.FilterOptionalEvents(expEvents, receivedEvents) - - // sort to compensate for wait task reconcile ordering variations - testutil.SortExpEvents(receivedEvents) - - Expect(receivedEvents).To(testutil.Equal(expEvents)) - - By("verify namespace1 created") - e2eutil.AssertUnstructuredExists(ctx, c, namespace1Obj) - - By("verify namespace2 created") - e2eutil.AssertUnstructuredExists(ctx, c, namespace2Obj) - - By("verify pod1 created and ready") - result := e2eutil.AssertUnstructuredExists(ctx, c, pod1Obj) - podIP, found, err := object.NestedField(result.Object, "status", "podIP") - Expect(err).NotTo(HaveOccurred()) - Expect(found).To(BeTrue()) - Expect(podIP).NotTo(BeEmpty()) // use podIP as proxy for readiness - - By("verify pod2 created and ready") - result = e2eutil.AssertUnstructuredExists(ctx, c, pod2Obj) - podIP, found, err = object.NestedField(result.Object, "status", "podIP") - Expect(err).NotTo(HaveOccurred()) - Expect(found).To(BeTrue()) - Expect(podIP).NotTo(BeEmpty()) // use podIP as proxy for readiness - - By("verify pod3 created and ready") - result = e2eutil.AssertUnstructuredExists(ctx, c, pod3Obj) - podIP, found, err = object.NestedField(result.Object, "status", "podIP") - Expect(err).NotTo(HaveOccurred()) - Expect(found).To(BeTrue()) - Expect(podIP).NotTo(BeEmpty()) // use podIP as proxy for readiness - - By("destroy resources in opposite order") - destroyer := invConfig.DestroyerFactoryFunc() - options := apply.DestroyerOptions{InventoryPolicy: inventory.PolicyAdoptIfNoInventory} - destroyerEvents := e2eutil.RunCollect(destroyer.Run(ctx, inv, options)) - - expEvents = []testutil.ExpEvent{ - { - // InitTask - EventType: event.InitType, - InitEvent: &testutil.ExpInitEvent{}, - }, - { - // PruneTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.DeleteAction, - GroupName: "prune-0", - Type: event.Started, - }, - }, - { - // Delete pod1 first - EventType: event.DeleteType, - DeleteEvent: &testutil.ExpDeleteEvent{ - GroupName: "prune-0", - Status: event.DeleteSuccessful, - Identifier: object.UnstructuredToObjMetadata(pod1Obj), - Error: nil, - }, - }, - { - // PruneTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.DeleteAction, - GroupName: "prune-0", - Type: event.Finished, - }, - }, - { - // WaitTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Started, - }, - }, - { - // Pod1 reconcile Pending. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcilePending, - Identifier: object.UnstructuredToObjMetadata(pod1Obj), - }, - }, - { - // Pod1 confirmed NotFound. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcileSuccessful, - Identifier: object.UnstructuredToObjMetadata(pod1Obj), - }, - }, - { - // WaitTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Finished, - }, - }, - { - // PruneTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.DeleteAction, - GroupName: "prune-1", - Type: event.Started, - }, - }, - { - // Delete pod3 second - EventType: event.DeleteType, - DeleteEvent: &testutil.ExpDeleteEvent{ - GroupName: "prune-1", - Status: event.DeleteSuccessful, - Identifier: object.UnstructuredToObjMetadata(pod3Obj), - Error: nil, - }, - }, - { - // PruneTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.DeleteAction, - GroupName: "prune-1", - Type: event.Finished, - }, - }, - { - // WaitTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-1", - Type: event.Started, - }, - }, - { - // Pod3 reconcile Pending. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-1", - Status: event.ReconcilePending, - Identifier: object.UnstructuredToObjMetadata(pod3Obj), - }, - }, - { - // Pod3 confirmed NotFound. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-1", - Status: event.ReconcileSuccessful, - Identifier: object.UnstructuredToObjMetadata(pod3Obj), - }, - }, - { - // WaitTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-1", - Type: event.Finished, - }, - }, - { - // PruneTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.DeleteAction, - GroupName: "prune-2", - Type: event.Started, - }, - }, - { - // Delete pod2 third - EventType: event.DeleteType, - DeleteEvent: &testutil.ExpDeleteEvent{ - GroupName: "prune-2", - Status: event.DeleteSuccessful, - Identifier: object.UnstructuredToObjMetadata(pod2Obj), - Error: nil, - }, - }, - { - // PruneTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.DeleteAction, - GroupName: "prune-2", - Type: event.Finished, - }, - }, - { - // WaitTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-2", - Type: event.Started, - }, - }, - { - // Pod2 reconcile Pending. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-2", - Status: event.ReconcilePending, - Identifier: object.UnstructuredToObjMetadata(pod2Obj), - }, - }, - { - // Pod2 confirmed NotFound. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-2", - Status: event.ReconcileSuccessful, - Identifier: object.UnstructuredToObjMetadata(pod2Obj), - }, - }, - { - // WaitTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-2", - Type: event.Finished, - }, - }, - { - // PruneTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.DeleteAction, - GroupName: "prune-3", - Type: event.Started, - }, - }, - { - // Delete Namespace1 last - EventType: event.DeleteType, - DeleteEvent: &testutil.ExpDeleteEvent{ - GroupName: "prune-3", - Status: event.DeleteSuccessful, - Identifier: object.UnstructuredToObjMetadata(namespace2Obj), - }, - }, - { - // Delete Namespace2 last - EventType: event.DeleteType, - DeleteEvent: &testutil.ExpDeleteEvent{ - GroupName: "prune-3", - Status: event.DeleteSuccessful, - Identifier: object.UnstructuredToObjMetadata(namespace1Obj), - }, - }, - { - // PruneTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.DeleteAction, - GroupName: "prune-3", - Type: event.Finished, - }, - }, - { - // WaitTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-3", - Type: event.Started, - }, - }, - { - // Namespace1 reconcile Pending. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-3", - Status: event.ReconcilePending, - Identifier: object.UnstructuredToObjMetadata(namespace1Obj), - }, - }, - { - // Namespace2 reconcile Pending. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-3", - Status: event.ReconcilePending, - Identifier: object.UnstructuredToObjMetadata(namespace2Obj), - }, - }, - { - // Namespace1 confirmed NotFound. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-3", - Status: event.ReconcileSuccessful, - Identifier: object.UnstructuredToObjMetadata(namespace1Obj), - }, - }, - { - // Namespace2 confirmed NotFound. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-3", - Status: event.ReconcileSuccessful, - Identifier: object.UnstructuredToObjMetadata(namespace2Obj), - }, - }, - { - // WaitTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-3", - Type: event.Finished, - }, - }, - { - // DeleteInvTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-delete-or-update-0", - Type: event.Started, - }, - }, - { - // DeleteInvTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-delete-or-update-0", - Type: event.Finished, - }, - }, - } - receivedEvents = testutil.EventsToExpEvents(destroyerEvents) - - expEvents, receivedEvents = e2eutil.FilterOptionalEvents(expEvents, receivedEvents) - - // sort to handle objects reconciling in random order - testutil.SortExpEvents(receivedEvents) - - Expect(receivedEvents).To(testutil.Equal(expEvents)) - - By("verify pod1 deleted") - e2eutil.AssertUnstructuredDoesNotExist(ctx, c, pod1Obj) - - By("verify pod2 deleted") - e2eutil.AssertUnstructuredDoesNotExist(ctx, c, pod2Obj) - - By("verify pod3 deleted") - e2eutil.AssertUnstructuredDoesNotExist(ctx, c, pod3Obj) - - By("verify namespace1 deleted") - e2eutil.AssertUnstructuredDoesNotExist(ctx, c, namespace1Obj) - - By("verify namespace2 deleted") - e2eutil.AssertUnstructuredDoesNotExist(ctx, c, namespace2Obj) -} diff --git a/test/e2e/destroy_reconciliation_failure_test.go b/test/e2e/destroy_reconciliation_failure_test.go deleted file mode 100644 index 117c6363..00000000 --- a/test/e2e/destroy_reconciliation_failure_test.go +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package e2e - -import ( - "context" - "fmt" - "time" - - "github.com/fluxcd/cli-utils/pkg/apply" - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/test/e2e/e2eutil" - "github.com/fluxcd/cli-utils/test/e2e/invconfig" - . "github.com/onsi/ginkgo/v2" //nolint:revive - . "github.com/onsi/gomega" //nolint:revive - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -func destroyReconciliationFailureTest(ctx context.Context, c client.Client, invConfig invconfig.InventoryConfig, inventoryName, namespaceName string) { - By("apply a single resource, which is referenced in the inventory") - applier := invConfig.ApplierFactoryFunc() - - inventoryID := fmt.Sprintf("%s-%s", inventoryName, namespaceName) - - inv := invconfig.CreateInventoryInfo(invConfig, inventoryName, namespaceName, inventoryID) - - podObject := e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(pod1), namespaceName) - podWithFinalizerObject := e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(pod2), namespaceName) - // inject an arbitrary finalizer to prevent garbage collection - podWithFinalizerObject = e2eutil.WithFinalizer(podWithFinalizerObject, "cli-utils/e2e-test") - - resource1 := []*unstructured.Unstructured{ - podObject, - podWithFinalizerObject, - } - - _ = e2eutil.RunCollect(applier.Run(ctx, inv, resource1, apply.ApplierOptions{ - EmitStatusEvents: false, - })) - - By("Verify all pods created and ready") - for _, pod := range resource1 { - result := e2eutil.AssertUnstructuredExists(ctx, c, pod) - podIP, found, err := object.NestedField(result.Object, "status", "podIP") - Expect(err).NotTo(HaveOccurred()) - Expect(found).To(BeTrue()) - Expect(podIP).NotTo(BeEmpty()) // use podIP as proxy for readiness - } - - By("Verify inventory") - // The inventory should have both Pods. - invConfig.InvSizeVerifyFunc(ctx, c, inventoryName, namespaceName, inventoryID, 2, 2) - - // Destroy the resources, with one resource having a finalizer that blocks - // garbage collection - By("Destroy resources") - destroyer := invConfig.DestroyerFactoryFunc() - - options := apply.DestroyerOptions{ - InventoryPolicy: inventory.PolicyAdoptIfNoInventory, - DeleteTimeout: 10 * time.Second, // one pod is expected to be not pruned, so set a short timeout - } - // we should be able to run destroy multiple times and continue tracking the - // object in the inventory - expectedCounts := []struct { - specCount int - statusCount int - }{ - { - specCount: 1, // one object failed to reconcile, so is retained - statusCount: 2, // status for two objects, one deleted successfully - }, - { - specCount: 1, - statusCount: 1, // only one object in inventory now, still failing to reconcile - }, - { - specCount: 1, - statusCount: 1, - }, - } - for _, ec := range expectedCounts { - _ = e2eutil.RunCollect(destroyer.Run(ctx, inv, options)) - - By("Verify pod1 is deleted") - e2eutil.AssertUnstructuredDoesNotExist(ctx, c, podObject) - - By("Verify podWithFinalizerObject is not deleted but has deletion timestamp") - podWithFinalizerObject = e2eutil.AssertHasDeletionTimestamp(ctx, c, podWithFinalizerObject) - - By("Verify inventory") - // The inventory should still have the Pod with the finalizer. - invConfig.InvSizeVerifyFunc(ctx, c, inventoryName, namespaceName, inventoryID, ec.specCount, ec.statusCount) - } - // remove the finalizer - podWithFinalizerObject = e2eutil.WithoutFinalizers(podWithFinalizerObject) - e2eutil.ApplyUnstructured(ctx, c, podWithFinalizerObject) - // re-run the destroyer and verify the object is removed from the inventory - _ = e2eutil.RunCollect(destroyer.Run(ctx, inv, options)) - - By("Verify pod1 is deleted") - e2eutil.AssertUnstructuredDoesNotExist(ctx, c, podObject) - - By("Verify podWithFinalizerObject is deleted") - e2eutil.AssertUnstructuredDoesNotExist(ctx, c, podWithFinalizerObject) - - By("Verify inventory") - // The inventory should be deleted. - invConfig.InvNotExistsFunc(ctx, c, inventoryName, namespaceName, inventoryID) -} diff --git a/test/e2e/dry_run_test.go b/test/e2e/dry_run_test.go deleted file mode 100644 index 07733200..00000000 --- a/test/e2e/dry_run_test.go +++ /dev/null @@ -1,301 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package e2e - -import ( - "context" - "fmt" - "time" - - "github.com/fluxcd/cli-utils/pkg/apply" - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/pkg/kstatus/status" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/testutil" - "github.com/fluxcd/cli-utils/test/e2e/e2eutil" - "github.com/fluxcd/cli-utils/test/e2e/invconfig" - . "github.com/onsi/ginkgo/v2" //nolint:revive - . "github.com/onsi/gomega" //nolint:revive - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -func dryRunTest(ctx context.Context, c client.Client, invConfig invconfig.InventoryConfig, inventoryName, namespaceName string) { - By("Apply with DryRun") - applier := invConfig.ApplierFactoryFunc() - inventoryID := fmt.Sprintf("%s-%s", inventoryName, namespaceName) - - inventoryInfo := invconfig.CreateInventoryInfo(invConfig, inventoryName, namespaceName, inventoryID) - - namespace1Name := fmt.Sprintf("%s-ns1", namespaceName) - - fields := struct{ Namespace string }{Namespace: namespace1Name} - namespace1Obj := e2eutil.TemplateToUnstructured(namespaceTemplate, fields) - podBObj := e2eutil.TemplateToUnstructured(podBTemplate, fields) - - // Dependency order: podB -> namespace1 - // Apply order: namespace1, podB - resources := []*unstructured.Unstructured{ - namespace1Obj, - podBObj, - } - - applierEvents := e2eutil.RunCollect(applier.Run(ctx, inventoryInfo, resources, apply.ApplierOptions{ - ReconcileTimeout: 2 * time.Minute, - EmitStatusEvents: true, - DryRunStrategy: common.DryRunClient, - })) - - expEvents := []testutil.ExpEvent{ - { - // InitTask - EventType: event.InitType, - InitEvent: &testutil.ExpInitEvent{}, - }, - { - // InvAddTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-add-0", - Type: event.Started, - }, - }, - { - // InvAddTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-add-0", - Type: event.Finished, - }, - }, - { - // ApplyTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-0", - Type: event.Started, - }, - }, - { - // Create namespace - EventType: event.ApplyType, - ApplyEvent: &testutil.ExpApplyEvent{ - GroupName: "apply-0", - Status: event.ApplySuccessful, - Identifier: object.UnstructuredToObjMetadata(namespace1Obj), - Error: nil, - }, - }, - { - // ApplyTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-0", - Type: event.Finished, - }, - }, - { - // ApplyTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-1", - Type: event.Started, - }, - }, - { - // Create pod - EventType: event.ApplyType, - ApplyEvent: &testutil.ExpApplyEvent{ - GroupName: "apply-1", - Status: event.ApplySuccessful, - Identifier: object.UnstructuredToObjMetadata(podBObj), - Error: nil, - }, - }, - { - // ApplyTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-1", - Type: event.Finished, - }, - }, - // No Wait Tasks for Dry Run - { - // InvSetTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-set-0", - Type: event.Started, - }, - }, - { - // InvSetTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-set-0", - Type: event.Finished, - }, - }, - } - receivedEvents := testutil.EventsToExpEvents(applierEvents) - - // handle optional async NotFound StatusEvent for pod - expected := testutil.ExpEvent{ - EventType: event.StatusType, - StatusEvent: &testutil.ExpStatusEvent{ - Identifier: object.UnstructuredToObjMetadata(podBObj), - Status: status.NotFoundStatus, - Error: nil, - }, - } - receivedEvents, _ = testutil.RemoveEqualEvents(receivedEvents, expected) - - // handle optional async NotFound StatusEvent for namespace - expected = testutil.ExpEvent{ - EventType: event.StatusType, - StatusEvent: &testutil.ExpStatusEvent{ - Identifier: object.UnstructuredToObjMetadata(namespace1Obj), - Status: status.NotFoundStatus, - Error: nil, - }, - } - receivedEvents, _ = testutil.RemoveEqualEvents(receivedEvents, expected) - - Expect(receivedEvents).To(testutil.Equal(expEvents)) - - By("Verify pod NotFound") - e2eutil.AssertUnstructuredDoesNotExist(ctx, c, podBObj) - - By("Verify inventory NotFound") - invConfig.InvNotExistsFunc(ctx, c, inventoryName, namespaceName, inventoryID) - - By("Apply") - e2eutil.RunWithNoErr(applier.Run(ctx, inventoryInfo, resources, apply.ApplierOptions{ - ReconcileTimeout: 2 * time.Minute, - })) - - By("Verify pod created") - e2eutil.AssertUnstructuredExists(ctx, c, podBObj) - - By("Verify inventory size") - invConfig.InvSizeVerifyFunc(ctx, c, inventoryName, namespaceName, inventoryID, 2, 2) - - By("Destroy with DryRun") - destroyer := invConfig.DestroyerFactoryFunc() - - destroyerEvents := e2eutil.RunCollect(destroyer.Run(ctx, inventoryInfo, apply.DestroyerOptions{ - InventoryPolicy: inventory.PolicyAdoptIfNoInventory, - EmitStatusEvents: true, - DryRunStrategy: common.DryRunClient, - })) - - expEvents = []testutil.ExpEvent{ - { - // InitTask - EventType: event.InitType, - InitEvent: &testutil.ExpInitEvent{}, - }, - { - // PruneTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.DeleteAction, - GroupName: "prune-0", - Type: event.Started, - }, - }, - { - // Delete pod - EventType: event.DeleteType, - DeleteEvent: &testutil.ExpDeleteEvent{ - GroupName: "prune-0", - Status: event.DeleteSuccessful, - Identifier: object.UnstructuredToObjMetadata(podBObj), - Error: nil, - }, - }, - { - // PruneTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.DeleteAction, - GroupName: "prune-0", - Type: event.Finished, - }, - }, - { - // PruneTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.DeleteAction, - GroupName: "prune-1", - Type: event.Started, - }, - }, - { - // Delete namespace - EventType: event.DeleteType, - DeleteEvent: &testutil.ExpDeleteEvent{ - GroupName: "prune-1", - Status: event.DeleteSuccessful, - Identifier: object.UnstructuredToObjMetadata(namespace1Obj), - Error: nil, - }, - }, - { - // PruneTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.DeleteAction, - GroupName: "prune-1", - Type: event.Finished, - }, - }, - // No Wait Tasks for Dry Run - { - // DeleteInvTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-delete-or-update-0", - Type: event.Started, - }, - }, - { - // DeleteInvTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-delete-or-update-0", - Type: event.Finished, - }, - }, - } - Expect(testutil.EventsToExpEvents(destroyerEvents)).To(testutil.Equal(expEvents)) - - By("Verify pod still exists") - e2eutil.AssertUnstructuredExists(ctx, c, podBObj) - - By("Destroy") - e2eutil.RunWithNoErr(destroyer.Run(ctx, inventoryInfo, apply.DestroyerOptions{ - InventoryPolicy: inventory.PolicyAdoptIfNoInventory, - })) - - By("Verify pod deleted") - e2eutil.AssertUnstructuredDoesNotExist(ctx, c, podBObj) - - By("Verify inventory deleted") - invConfig.InvNotExistsFunc(ctx, c, inventoryName, namespaceName, inventoryID) -} diff --git a/test/e2e/e2e_suite_test.go b/test/e2e/e2e_suite_test.go deleted file mode 100644 index 94f94272..00000000 --- a/test/e2e/e2e_suite_test.go +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package e2e - -import ( - "testing" - - . "github.com/onsi/ginkgo/v2" //nolint:revive - . "github.com/onsi/gomega" //nolint:revive -) - -func TestE2e(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "E2e Suite") -} diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go deleted file mode 100644 index 436d3960..00000000 --- a/test/e2e/e2e_test.go +++ /dev/null @@ -1,279 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package e2e - -import ( - "context" - "fmt" - "time" - - "github.com/fluxcd/cli-utils/test/e2e/e2eutil" - "github.com/fluxcd/cli-utils/test/e2e/invconfig" - . "github.com/onsi/ginkgo/v2" //nolint:revive - . "github.com/onsi/gomega" //nolint:revive - "github.com/onsi/gomega/format" - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/client-go/rest" - "k8s.io/klog/v2" - "k8s.io/kubectl/pkg/scheme" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/client/apiutil" -) - -const ( - ConfigMapTypeInvConfig = "ConfigMap" - CustomTypeInvConfig = "Custom" -) - -var inventoryConfigs = map[string]invconfig.InventoryConfig{} -var inventoryConfigTypes = []string{ - ConfigMapTypeInvConfig, - CustomTypeInvConfig, -} - -// Parse optional logging flags -// Ex: ginkgo ./test/e2e/... -- -v=5 -// Allow init for e2e test (not imported by external code) -// nolint:gochecknoinits -func init() { - klog.InitFlags(nil) - klog.SetOutput(GinkgoWriter) -} - -var defaultTestTimeout = 5 * time.Minute -var defaultBeforeTestTimeout = 30 * time.Second -var defaultAfterTestTimeout = 30 * time.Second - -var c client.Client - -var _ = BeforeSuite(func() { - // increase from 4000 to handle long event lists - format.MaxLength = 10000 - - cfg, err := ctrl.GetConfig() - Expect(err).NotTo(HaveOccurred()) - - cfg.UserAgent = e2eutil.UserAgent("test/e2e") - - if e2eutil.IsFlowControlEnabled(cfg) { - // Disable client-side throttling. - klog.V(3).Infof("Client-side throttling disabled") - cfg.QPS = -1 - cfg.Burst = -1 - } - - inventoryConfigs[ConfigMapTypeInvConfig] = invconfig.NewConfigMapTypeInvConfig(cfg) - inventoryConfigs[CustomTypeInvConfig] = invconfig.NewCustomTypeInvConfig(cfg) - - httpClient, err := rest.HTTPClientFor(cfg) - Expect(err).NotTo(HaveOccurred()) - mapper, err := apiutil.NewDynamicRESTMapper(cfg, httpClient) - Expect(err).NotTo(HaveOccurred()) - - c, err = client.New(cfg, client.Options{ - Scheme: scheme.Scheme, - Mapper: mapper, - }) - Expect(err).NotTo(HaveOccurred()) - - ctx, cancel := context.WithTimeout(context.Background(), defaultBeforeTestTimeout) - defer cancel() - e2eutil.CreateInventoryCRD(ctx, c) - Expect(ctx.Err()).To(BeNil(), "BeforeSuite context cancelled or timed out") -}) - -var _ = AfterSuite(func() { - ctx, cancel := context.WithTimeout(context.Background(), defaultAfterTestTimeout) - defer cancel() - if c != nil { - // If BeforeSuite() failed, c might be nil. Skip deletion to avoid red herring panic. - e2eutil.DeleteInventoryCRD(ctx, c) - } - Expect(ctx.Err()).To(BeNil(), "AfterSuite context cancelled or timed out") -}) - -var _ = Describe("E2E", func() { - for i := range inventoryConfigTypes { - invType := inventoryConfigTypes[i] - Context(fmt.Sprintf("Inventory%s", invType), func() { - var invConfig invconfig.InventoryConfig - - BeforeEach(func() { - invConfig = inventoryConfigs[invType] - }) - - Context("Basic", func() { - var namespace *v1.Namespace - var inventoryName string - var ctx context.Context - var cancel context.CancelFunc - - BeforeEach(func() { - ctx, cancel = context.WithTimeout(context.Background(), defaultTestTimeout) - inventoryName = e2eutil.RandomString("test-inv-") - namespace = e2eutil.CreateRandomNamespace(ctx, c) - }) - - AfterEach(func() { - Expect(ctx.Err()).To(BeNil(), "test context cancelled or timed out") - cancel() - // new timeout for cleanup - ctx, cancel = context.WithTimeout(context.Background(), defaultAfterTestTimeout) - defer cancel() - // clean up resources created by the tests - fields := struct{ Namespace string }{Namespace: namespace.GetName()} - objs := []*unstructured.Unstructured{ - e2eutil.ManifestToUnstructured(cr), - e2eutil.ManifestToUnstructured(crd), - e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(pod1), namespace.GetName()), - e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(pod2), namespace.GetName()), - e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(pod3), namespace.GetName()), - e2eutil.TemplateToUnstructured(podATemplate, fields), - e2eutil.TemplateToUnstructured(podBTemplate, fields), - e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(deployment1), namespace.GetName()), - e2eutil.ManifestToUnstructured(apiservice1), - } - for _, obj := range objs { - e2eutil.DeleteUnstructuredIfExists(ctx, c, obj) - } - e2eutil.DeleteNamespace(ctx, c, namespace) - }) - - It("ApplyDestroy", func() { - applyAndDestroyTest(ctx, c, invConfig, inventoryName, namespace.GetName()) - }) - - It("DryRun", func() { - dryRunTest(ctx, c, invConfig, inventoryName, namespace.GetName()) - }) - - It("EmptySet", func() { - emptySetTest(ctx, c, invConfig, inventoryName, namespace.GetName()) - }) - - It("DeletionPrevention", func() { - deletionPreventionTest(ctx, c, invConfig, inventoryName, namespace.GetName()) - }) - - It("CustomResource", func() { - crdTest(ctx, c, invConfig, inventoryName, namespace.GetName()) - }) - - It("ContinueOnError", func() { - continueOnErrorTest(ctx, c, invConfig, inventoryName, namespace.GetName()) - }) - - It("ServerSideApply", func() { - serversideApplyTest(ctx, c, invConfig, inventoryName, namespace.GetName()) - }) - - It("DependsOn", func() { - dependsOnTest(ctx, c, invConfig, inventoryName, namespace.GetName()) - }) - - It("ApplyTimeMutation", func() { - mutationTest(ctx, c, invConfig, inventoryName, namespace.GetName()) - }) - - It("DependencyFilter", func() { - dependencyFilterTest(ctx, c, invConfig, inventoryName, namespace.GetName()) - }) - - It("LocalNamespacesFilter", func() { - namespaceFilterTest(ctx, c, invConfig, inventoryName, namespace.GetName()) - }) - - It("CurrentUIDFilter", func() { - currentUIDFilterTest(ctx, c, invConfig, inventoryName, namespace.GetName()) - }) - - It("PruneRetrievalError", func() { - pruneRetrieveErrorTest(ctx, c, invConfig, inventoryName, namespace.GetName()) - }) - - It("DestroyReconciliationFailure", func() { - destroyReconciliationFailureTest(ctx, c, invConfig, inventoryName, namespace.GetName()) - }) - - It("ReconciliationFailure", func() { - reconciliationFailed(ctx, invConfig, inventoryName, namespace.GetName()) - }) - - It("ReconciliationTimeout", func() { - reconciliationTimeout(ctx, invConfig, inventoryName, namespace.GetName()) - }) - - It("SkipInvalid", func() { - skipInvalidTest(ctx, c, invConfig, inventoryName, namespace.GetName()) - }) - - It("ExitEarly", func() { - exitEarlyTest(ctx, c, invConfig, inventoryName, namespace.GetName()) - }) - }) - - Context("InventoryPolicy", func() { - var namespace *v1.Namespace - var ctx context.Context - var cancel context.CancelFunc - - BeforeEach(func() { - ctx, cancel = context.WithTimeout(context.Background(), defaultTestTimeout) - namespace = e2eutil.CreateRandomNamespace(ctx, c) - }) - - AfterEach(func() { - Expect(ctx.Err()).To(BeNil(), "test context cancelled or timed out") - cancel() - // new timeout for cleanup - ctx, cancel = context.WithTimeout(context.Background(), defaultAfterTestTimeout) - defer cancel() - e2eutil.DeleteUnstructuredIfExists(ctx, c, e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(deployment1), namespace.GetName())) - e2eutil.DeleteNamespace(ctx, c, namespace) - }) - - It("MustMatch", func() { - inventoryPolicyMustMatchTest(ctx, c, invConfig, namespace.GetName()) - }) - - It("AdoptIfNoInventory", func() { - inventoryPolicyAdoptIfNoInventoryTest(ctx, c, invConfig, namespace.GetName()) - }) - - It("AdoptAll", func() { - inventoryPolicyAdoptAllTest(ctx, c, invConfig, namespace.GetName()) - }) - }) - }) - } - - Context("NameStrategy", func() { - var namespace *v1.Namespace - var inventoryName string - var ctx context.Context - var cancel context.CancelFunc - - BeforeEach(func() { - ctx, cancel = context.WithTimeout(context.Background(), defaultTestTimeout) - inventoryName = e2eutil.RandomString("test-inv-") - namespace = e2eutil.CreateRandomNamespace(ctx, c) - }) - - AfterEach(func() { - Expect(ctx.Err()).To(BeNil(), "test context cancelled or timed out") - cancel() - // new timeout for cleanup - ctx, cancel = context.WithTimeout(context.Background(), defaultAfterTestTimeout) - defer cancel() - e2eutil.DeleteUnstructuredIfExists(ctx, c, e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(deployment1), namespace.GetName())) - e2eutil.DeleteNamespace(ctx, c, namespace) - }) - - It("InventoryIDMismatch", func() { - applyWithExistingInvTest(ctx, c, inventoryConfigs[CustomTypeInvConfig], inventoryName, namespace.GetName()) - }) - }) -}) diff --git a/test/e2e/e2eutil/common.go b/test/e2e/e2eutil/common.go deleted file mode 100644 index e4fd2def..00000000 --- a/test/e2e/e2eutil/common.go +++ /dev/null @@ -1,540 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package e2eutil - -import ( - "bytes" - "context" - "fmt" - "text/template" - "time" - - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/flowcontrol" - "github.com/fluxcd/cli-utils/pkg/kstatus/status" - "github.com/fluxcd/cli-utils/pkg/object/dependson" - "github.com/fluxcd/cli-utils/pkg/object/mutation" - "github.com/fluxcd/cli-utils/pkg/testutil" - "github.com/fluxcd/cli-utils/test/e2e/customprovider" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - "github.com/onsi/ginkgo/v2" - "github.com/onsi/gomega" - "github.com/onsi/gomega/gstruct" - v1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/yaml" - "k8s.io/client-go/rest" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -const TestIDLabel = "test-id" - -func WithReplicas(obj *unstructured.Unstructured, replicas int) *unstructured.Unstructured { - err := unstructured.SetNestedField(obj.Object, int64(replicas), "spec", "replicas") - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - return obj -} - -func WithNamespace(obj *unstructured.Unstructured, namespace string) *unstructured.Unstructured { - obj.SetNamespace(namespace) - return obj -} - -func PodWithImage(obj *unstructured.Unstructured, containerName, image string) *unstructured.Unstructured { - containers, found, err := unstructured.NestedSlice(obj.Object, "spec", "containers") - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - gomega.Expect(found).To(gomega.BeTrue()) - - containerFound := false - for i := range containers { - container := containers[i].(map[string]interface{}) - name := container["name"].(string) - if name != containerName { - continue - } - containerFound = true - container["image"] = image - } - gomega.Expect(containerFound).To(gomega.BeTrue()) - err = unstructured.SetNestedSlice(obj.Object, containers, "spec", "containers") - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - return obj -} - -func WithNodeSelector(obj *unstructured.Unstructured, key, value string) *unstructured.Unstructured { - selectors, found, err := unstructured.NestedMap(obj.Object, "spec", "nodeSelector") - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - - if !found { - selectors = make(map[string]interface{}) - } - selectors[key] = value - err = unstructured.SetNestedMap(obj.Object, selectors, "spec", "nodeSelector") - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - return obj -} - -func WithAnnotation(obj *unstructured.Unstructured, key, value string) *unstructured.Unstructured { - annotations := obj.GetAnnotations() - if annotations == nil { - annotations = make(map[string]string) - } - annotations[key] = value - obj.SetAnnotations(annotations) - return obj -} - -func WithDependsOn(obj *unstructured.Unstructured, dep string) *unstructured.Unstructured { - a := obj.GetAnnotations() - if a == nil { - a = make(map[string]string, 1) - } - a[dependson.Annotation] = dep - obj.SetAnnotations(a) - return obj -} - -func WithFinalizer(obj *unstructured.Unstructured, finalizer string) *unstructured.Unstructured { - finalizers := obj.GetFinalizers() - finalizers = append(finalizers, finalizer) - obj.SetFinalizers(finalizers) - return obj -} - -func WithoutFinalizers(obj *unstructured.Unstructured) *unstructured.Unstructured { - obj.SetFinalizers([]string{}) - return obj -} - -func DeleteUnstructuredAndWait(ctx context.Context, c client.Client, obj *unstructured.Unstructured) { - ref := mutation.ResourceReferenceFromUnstructured(obj) - - err := c.Delete(ctx, obj, - client.PropagationPolicy(metav1.DeletePropagationForeground)) - gomega.Expect(err).NotTo(gomega.HaveOccurred(), - "expected DELETE to not error (%s): %s", ref, err) - - WaitForDeletion(ctx, c, obj) -} - -func WaitForDeletion(ctx context.Context, c client.Client, obj *unstructured.Unstructured) { - ref := mutation.ResourceReferenceFromUnstructured(obj) - resultObj := ref.ToUnstructured() - - timeout := 30 * time.Second - retry := 2 * time.Second - - t := time.NewTimer(timeout) - s := time.NewTimer(0) - defer t.Stop() - - for { - select { - case <-t.C: - ginkgo.Fail("timed out waiting for resource to be fully deleted") - return - case <-s.C: - err := c.Get(ctx, types.NamespacedName{ - Namespace: obj.GetNamespace(), - Name: obj.GetName(), - }, resultObj) - if err != nil { - gomega.Expect(apierrors.ReasonForError(err)).To(gomega.Equal(metav1.StatusReasonNotFound), - "expected GET to error with NotFound (%s): %s", ref, err) - return - } - s = time.NewTimer(retry) - } - } -} - -func CreateUnstructuredAndWait(ctx context.Context, c client.Client, obj *unstructured.Unstructured) { - ref := mutation.ResourceReferenceFromUnstructured(obj) - - err := c.Create(ctx, obj) - gomega.Expect(err).NotTo(gomega.HaveOccurred(), - "expected CREATE to not error (%s): %s", ref, err) - - WaitForCreation(ctx, c, obj) -} - -func WaitForCreation(ctx context.Context, c client.Client, obj *unstructured.Unstructured) { - ref := mutation.ResourceReferenceFromUnstructured(obj) - resultObj := ref.ToUnstructured() - - timeout := 30 * time.Second - retry := 2 * time.Second - - t := time.NewTimer(timeout) - s := time.NewTimer(0) - defer t.Stop() - - for { - select { - case <-t.C: - ginkgo.Fail("timed out waiting for resource to be fully created") - return - case <-s.C: - err := c.Get(ctx, types.NamespacedName{ - Namespace: obj.GetNamespace(), - Name: obj.GetName(), - }, resultObj) - if err == nil { - return - } - gomega.Expect(apierrors.ReasonForError(err)).To(gomega.Equal(metav1.StatusReasonNotFound), - "expected GET to error with NotFound (%s): %s", ref, err) - // if NotFound, sleep and retry - s = time.NewTimer(retry) - } - } -} - -func AssertUnstructuredExists(ctx context.Context, c client.Client, obj *unstructured.Unstructured) *unstructured.Unstructured { - ref := mutation.ResourceReferenceFromUnstructured(obj) - resultObj := ref.ToUnstructured() - - err := c.Get(ctx, types.NamespacedName{ - Namespace: obj.GetNamespace(), - Name: obj.GetName(), - }, resultObj) - gomega.Expect(err).NotTo(gomega.HaveOccurred(), - "expected GET not to error (%s): %s", ref, err) - return resultObj -} - -func AssertHasDeletionTimestamp(ctx context.Context, c client.Client, obj *unstructured.Unstructured) *unstructured.Unstructured { - ref := mutation.ResourceReferenceFromUnstructured(obj) - resultObj := ref.ToUnstructured() - - err := c.Get(ctx, types.NamespacedName{ - Namespace: obj.GetNamespace(), - Name: obj.GetName(), - }, resultObj) - gomega.Expect(err).NotTo(gomega.HaveOccurred(), - "expected GET not to error (%s): %s", ref, err) - gomega.Expect(resultObj.GetDeletionTimestamp().IsZero()).To(gomega.BeFalse(), - "expected deletion timestamp to be non-zero (%s)", ref) - return resultObj -} - -func AssertUnstructuredDoesNotExist(ctx context.Context, c client.Client, obj *unstructured.Unstructured) { - ref := mutation.ResourceReferenceFromUnstructured(obj) - resultObj := ref.ToUnstructured() - - err := c.Get(ctx, types.NamespacedName{ - Namespace: obj.GetNamespace(), - Name: obj.GetName(), - }, resultObj) - gomega.Expect(err).To(gomega.HaveOccurred(), - "expected GET to error (%s)", ref) - gomega.Expect(apierrors.ReasonForError(err)).To(gomega.Equal(metav1.StatusReasonNotFound), - "expected GET to error with NotFound (%s): %s", ref, err) -} - -func ApplyUnstructured(ctx context.Context, c client.Client, obj *unstructured.Unstructured) { - ref := mutation.ResourceReferenceFromUnstructured(obj) - resultObj := ref.ToUnstructured() - - err := c.Get(ctx, types.NamespacedName{ - Namespace: obj.GetNamespace(), - Name: obj.GetName(), - }, resultObj) - gomega.Expect(err).NotTo(gomega.HaveOccurred(), - "expected GET not to error (%s)", ref) - - err = c.Patch(ctx, obj, client.MergeFrom(resultObj)) - gomega.Expect(err).NotTo(gomega.HaveOccurred(), - "expected PATCH not to error (%s): %s", ref, err) -} - -func AssertUnstructuredAvailable(obj *unstructured.Unstructured) { - ref := mutation.ResourceReferenceFromUnstructured(obj) - objc, err := status.GetObjectWithConditions(obj.Object) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - available := false - for _, c := range objc.Status.Conditions { - // appsv1.DeploymentAvailable && corev1.ConditionTrue - if c.Type == "Available" && c.Status == "True" { - available = true - break - } - } - gomega.Expect(available).To(gomega.BeTrue(), - "expected Available condition to be True (%s)", ref) -} - -func AssertUnstructuredCount(ctx context.Context, c client.Client, obj *unstructured.Unstructured, count int) { - var u unstructured.UnstructuredList - u.SetGroupVersionKind(obj.GetObjectKind().GroupVersionKind()) - err := c.List(ctx, &u, - client.InNamespace(obj.GetNamespace()), - client.MatchingLabels(obj.GetLabels())) - if err != nil && count == 0 { - expectNotFoundError(err) - return - } - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - gomega.Expect(len(u.Items)).To(gomega.Equal(count), "unexpected number of %s", obj.GetKind()) -} - -func RandomString(prefix string) string { - randomSuffix := common.RandomStr() - return fmt.Sprintf("%s%s", prefix, randomSuffix) -} - -func Run(ch <-chan event.Event) error { - var err error - for e := range ch { - if e.Type == event.ErrorType { - err = e.ErrorEvent.Err - } - } - return err -} - -var RunWithNoErr = RunCollectNoErr - -func RunCollect(ch <-chan event.Event) []event.Event { - var events []event.Event - for e := range ch { - events = append(events, e) - } - return events -} - -func RunCollectNoErr(ch <-chan event.Event, callerSkip ...int) []event.Event { - skip := 0 - if len(callerSkip) > 0 { - skip = callerSkip[0] - } - - events := RunCollect(ch) - ExpectNoEventErrors(events, skip+1) - ExpectNoReconcileTimeouts(events, skip+1) - return events -} - -func ExpectNoEventErrors(events []event.Event, callerSkip ...int) { - skip := 0 - if len(callerSkip) > 0 { - skip = callerSkip[0] - } - - gomega.Expect(events).WithOffset(skip + 1).NotTo( - gomega.ContainElement(gstruct.MatchFields(gstruct.IgnoreExtras, - gstruct.Fields{ - "Type": gomega.Equal(event.ErrorType), - }))) - gomega.Expect(events).WithOffset(skip + 1).NotTo( - gomega.ContainElement(gstruct.MatchFields(gstruct.IgnoreExtras, - gstruct.Fields{ - "Type": gomega.Equal(event.ApplyType), - "ApplyEvent": gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{ - "Status": gomega.Equal(event.ApplyFailed), - }), - }))) - gomega.Expect(events).WithOffset(skip + 1).NotTo( - gomega.ContainElement(gstruct.MatchFields(gstruct.IgnoreExtras, - gstruct.Fields{ - "Type": gomega.Equal(event.PruneType), - "PruneEvent": gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{ - "Status": gomega.Equal(event.PruneFailed), - }), - }))) - gomega.Expect(events).WithOffset(skip + 1).NotTo( - gomega.ContainElement(gstruct.MatchFields(gstruct.IgnoreExtras, - gstruct.Fields{ - "Type": gomega.Equal(event.DeleteType), - "DeleteEvent": gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{ - "Status": gomega.Equal(event.DeleteFailed), - }), - }))) -} - -func ExpectNoReconcileTimeouts(events []event.Event, callerSkip ...int) { - skip := 0 - if len(callerSkip) > 0 { - skip = callerSkip[0] - } - - gomega.Expect(events).WithOffset(skip + 1).NotTo( - gomega.ContainElement(gstruct.MatchFields(gstruct.IgnoreExtras, - gstruct.Fields{ - "Type": gomega.Equal(event.WaitType), - "WaitEvent": gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{ - "Status": gomega.Equal(event.ReconcileTimeout), - }), - }))) -} - -func ManifestToUnstructured(manifest []byte) *unstructured.Unstructured { - u := make(map[string]interface{}) - err := yaml.Unmarshal(manifest, &u) - if err != nil { - panic(fmt.Errorf("failed to parse manifest yaml: %w", err)) - } - return &unstructured.Unstructured{ - Object: u, - } -} - -func TemplateToUnstructured(tmpl string, data interface{}) *unstructured.Unstructured { - t, err := template.New("manifest").Parse(tmpl) - if err != nil { - panic(fmt.Errorf("failed to parse manifest go-template: %w", err)) - } - var buffer bytes.Buffer - err = t.Execute(&buffer, data) - if err != nil { - panic(fmt.Errorf("failed to execute manifest go-template: %w", err)) - } - return ManifestToUnstructured(buffer.Bytes()) -} - -func CreateInventoryCRD(ctx context.Context, c client.Client) { - invCRD := ManifestToUnstructured(customprovider.InventoryCRD) - var u unstructured.Unstructured - u.SetGroupVersionKind(invCRD.GroupVersionKind()) - err := c.Get(ctx, types.NamespacedName{ - Name: invCRD.GetName(), - }, &u) - if apierrors.IsNotFound(err) { - err = c.Create(ctx, invCRD) - } - gomega.Expect(err).NotTo(gomega.HaveOccurred()) -} - -func CreateRandomNamespace(ctx context.Context, c client.Client) *v1.Namespace { - namespaceName := RandomString("e2e-test-") - namespace := &v1.Namespace{ - TypeMeta: metav1.TypeMeta{ - APIVersion: v1.SchemeGroupVersion.String(), - Kind: "Namespace", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: namespaceName, - }, - } - - err := c.Create(ctx, namespace) - gomega.Expect(err).ToNot(gomega.HaveOccurred()) - return namespace -} - -func DeleteInventoryCRD(ctx context.Context, c client.Client) { - invCRD := ManifestToUnstructured(customprovider.InventoryCRD) - DeleteUnstructuredIfExists(ctx, c, invCRD) -} - -func DeleteUnstructuredIfExists(ctx context.Context, c client.Client, obj *unstructured.Unstructured) { - err := c.Delete(ctx, obj) - if err != nil { - expectNotFoundError(err) - } -} - -func DeleteAllUnstructuredIfExists(ctx context.Context, c client.Client, obj *unstructured.Unstructured) { - err := c.DeleteAllOf(ctx, obj, - client.InNamespace(obj.GetNamespace()), - client.MatchingLabels(obj.GetLabels())) - if err != nil { - expectNotFoundError(err) - } -} - -func DeleteNamespace(ctx context.Context, c client.Client, namespace *v1.Namespace) { - err := c.Delete(ctx, namespace) - gomega.Expect(err).ToNot(gomega.HaveOccurred()) -} - -func UnstructuredExistsAndIsNotTerminating(ctx context.Context, c client.Client, obj *unstructured.Unstructured) bool { - serverObj := obj.DeepCopy() - err := c.Get(ctx, types.NamespacedName{ - Namespace: obj.GetNamespace(), - Name: obj.GetName(), - }, serverObj) - if err != nil { - expectNotFoundError(err) - return false - } - return !UnstructuredIsTerminating(serverObj) -} - -func expectNotFoundError(err error) { - gomega.Expect(err).To(gomega.Or( - gomega.BeAssignableToTypeOf(&meta.NoKindMatchError{}), - gomega.BeAssignableToTypeOf(&apierrors.StatusError{}), - )) - if se, ok := err.(*apierrors.StatusError); ok { - gomega.Expect(se.ErrStatus.Reason).To(gomega.Or( - gomega.Equal(metav1.StatusReasonNotFound), - // custom resources dissalow deletion if the CRD is terminating - gomega.Equal(metav1.StatusReasonMethodNotAllowed), - )) - } -} - -func UnstructuredIsTerminating(obj *unstructured.Unstructured) bool { - objc, err := status.GetObjectWithConditions(obj.Object) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - for _, c := range objc.Status.Conditions { - if c.Type == "Terminating" && c.Status == "True" { - return true - } - } - return false -} - -func UnstructuredNamespace(name string) *unstructured.Unstructured { - u := &unstructured.Unstructured{} - u.SetAPIVersion("v1") - u.SetKind("Namespace") - u.SetName(name) - return u -} - -func IsFlowControlEnabled(config *rest.Config) bool { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - enabled, err := flowcontrol.IsEnabled(ctx, config) - gomega.Expect(err).ToNot(gomega.HaveOccurred()) - - return enabled -} - -// FilterOptionalEvents looks for optional events in the expected list and -// removes them from both lists. This allows the output to be compared for -// equality. -// -// Optional events include: -// - WaitEvent with ReconcilePending -func FilterOptionalEvents(expected, received []testutil.ExpEvent) ([]testutil.ExpEvent, []testutil.ExpEvent) { - expectedCopy := make([]testutil.ExpEvent, 0, len(expected)) - for _, ee := range expected { - if ee.EventType == event.WaitType && - ee.WaitEvent != nil && - ee.WaitEvent.Status == event.ReconcilePending { - // Pending WaitEvent is optional. - // Remove first event match, if exists. - for i, re := range received { - if cmp.Equal(re, ee, cmpopts.EquateErrors()) { - // remove event at index i - received = append(received[:i], received[i+1:]...) - break - } - } - } else { - expectedCopy = append(expectedCopy, ee) - } - } - return expectedCopy, received -} diff --git a/test/e2e/e2eutil/rest.go b/test/e2e/e2eutil/rest.go deleted file mode 100644 index 2b5ce8a1..00000000 --- a/test/e2e/e2eutil/rest.go +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright 2022 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package e2eutil - -import ( - "bytes" - "context" - "fmt" - "os" - "os/exec" - "path/filepath" - "runtime" - "strings" - "time" - - "github.com/onsi/gomega" -) - -const unknown = "unknown" - -// UserAgent returns the a User-Agent for use with HTTP clients. -// The string corresponds to the current version of the binary being executed, -// using metadata from git and go. -func UserAgent(suffix string) string { - return fmt.Sprintf("%s/%s (%s/%s) cli-utils/%s/%s", - adjustCommand(os.Args[0]), - adjustVersion(gitVersion()), - runtime.GOOS, - runtime.GOARCH, - adjustCommit(gitCommit()), - suffix) -} - -// gitVersion returns the output from `git describe` -func gitVersion() string { - ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) - defer cancel() - - cmd := exec.CommandContext(ctx, "git", "describe") - - // Ginkgo sets the working directory to the current test dir - cwd, err := os.Getwd() - gomega.Expect(err).ToNot(gomega.HaveOccurred()) - cmd.Dir = cwd - - var outBuf bytes.Buffer - var errBuf bytes.Buffer - cmd.Stdout = &outBuf - cmd.Stderr = &errBuf - - err = cmd.Run() - gomega.Expect(err).ToNot(gomega.HaveOccurred(), "STDERR:\n%v\nSTDOUT:\n%v", errBuf, outBuf) - - return strings.TrimSpace(outBuf.String()) -} - -// gitCommit returns the output from `git rev-parse HEAD` -func gitCommit() string { - ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) - defer cancel() - - cmd := exec.CommandContext(ctx, "git", "rev-parse", "HEAD") - - // Ginkgo sets the working directory to the current test dir - cwd, err := os.Getwd() - gomega.Expect(err).ToNot(gomega.HaveOccurred()) - cmd.Dir = cwd - - var outBuf bytes.Buffer - var errBuf bytes.Buffer - cmd.Stdout = &outBuf - cmd.Stderr = &errBuf - - err = cmd.Run() - gomega.Expect(err).ToNot(gomega.HaveOccurred(), "STDERR: %s", errBuf.String()) - - return strings.TrimSpace(outBuf.String()) -} - -// adjustCommand returns the last component of the -// OS-specific command path for use in User-Agent. -func adjustCommand(p string) string { - // Unlikely, but better than returning "". - if len(p) == 0 { - return unknown - } - return filepath.Base(p) -} - -// adjustVersion strips "alpha", "beta", etc. from version in form -// major.minor.patch-[alpha|beta|etc]. -func adjustVersion(v string) string { - if len(v) == 0 { - return unknown - } - seg := strings.SplitN(v, "-", 2) - return seg[0] -} - -// adjustCommit returns sufficient significant figures of the commit's git hash. -func adjustCommit(c string) string { - if len(c) == 0 { - return unknown - } - if len(c) > 7 { - return c[:7] - } - return c -} diff --git a/test/e2e/empty_set_test.go b/test/e2e/empty_set_test.go deleted file mode 100644 index f5fc3ddf..00000000 --- a/test/e2e/empty_set_test.go +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package e2e - -import ( - "context" - "fmt" - - "github.com/fluxcd/cli-utils/pkg/apply" - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/pkg/testutil" - "github.com/fluxcd/cli-utils/test/e2e/e2eutil" - "github.com/fluxcd/cli-utils/test/e2e/invconfig" - . "github.com/onsi/ginkgo/v2" //nolint:revive - . "github.com/onsi/gomega" //nolint:revive - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -//nolint:dupl // expEvents similar to other tests -func emptySetTest(ctx context.Context, c client.Client, invConfig invconfig.InventoryConfig, inventoryName, namespaceName string) { - By("Apply zero resources") - applier := invConfig.ApplierFactoryFunc() - - inventoryID := fmt.Sprintf("%s-%s", inventoryName, namespaceName) - inventoryInfo := invConfig.InvWrapperFunc(invConfig.FactoryFunc(inventoryName, namespaceName, inventoryID)) - - resources := []*unstructured.Unstructured{} - - applierEvents := e2eutil.RunCollect(applier.Run(ctx, inventoryInfo, resources, apply.ApplierOptions{ - EmitStatusEvents: true, - })) - - expEvents := []testutil.ExpEvent{ - { - // InitTask - EventType: event.InitType, - InitEvent: &testutil.ExpInitEvent{}, - }, - { - // InvAddTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-add-0", - Type: event.Started, - }, - }, - { - // InvAddTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-add-0", - Type: event.Finished, - }, - }, - { - // InvSetTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-set-0", - Type: event.Started, - }, - }, - { - // InvSetTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-set-0", - Type: event.Finished, - }, - }, - } - Expect(testutil.EventsToExpEvents(applierEvents)).To(testutil.Equal(expEvents)) - - By("Verify inventory created") - invConfig.InvSizeVerifyFunc(ctx, c, inventoryName, namespaceName, inventoryID, 0, 0) - - By("Destroy zero resources") - destroyer := invConfig.DestroyerFactoryFunc() - - options := apply.DestroyerOptions{InventoryPolicy: inventory.PolicyAdoptIfNoInventory} - destroyerEvents := e2eutil.RunCollect(destroyer.Run(ctx, inventoryInfo, options)) - - expEvents = []testutil.ExpEvent{ - { - // InitTask - EventType: event.InitType, - InitEvent: &testutil.ExpInitEvent{}, - }, - { - // DeleteInvTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-delete-or-update-0", - Type: event.Started, - }, - }, - { - // DeleteInvTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-delete-or-update-0", - Type: event.Finished, - }, - }, - } - - Expect(testutil.EventsToExpEvents(destroyerEvents)).To(testutil.Equal(expEvents)) - - By("Verify inventory deleted") - invConfig.InvNotExistsFunc(ctx, c, inventoryName, namespaceName, inventoryID) -} diff --git a/test/e2e/exit_early_test.go b/test/e2e/exit_early_test.go deleted file mode 100644 index 143c3209..00000000 --- a/test/e2e/exit_early_test.go +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright 2022 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package e2e - -import ( - "context" - "fmt" - - "github.com/fluxcd/cli-utils/pkg/apply" - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/object/validation" - "github.com/fluxcd/cli-utils/pkg/testutil" - "github.com/fluxcd/cli-utils/test/e2e/e2eutil" - "github.com/fluxcd/cli-utils/test/e2e/invconfig" - . "github.com/onsi/ginkgo/v2" //nolint:revive - . "github.com/onsi/gomega" //nolint:revive - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/util/validation/field" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -func exitEarlyTest(ctx context.Context, c client.Client, invConfig invconfig.InventoryConfig, inventoryName, namespaceName string) { - By("exit early on invalid object") - applier := invConfig.ApplierFactoryFunc() - - inv := invConfig.InvWrapperFunc(invConfig.FactoryFunc(inventoryName, namespaceName, "test")) - - fields := struct{ Namespace string }{Namespace: namespaceName} - // valid pod - pod1Obj := e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(pod1), namespaceName) - // valid deployment with dependency - deployment1Obj := e2eutil.WithDependsOn(e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(deployment1), namespaceName), - fmt.Sprintf("/namespaces/%s/Pod/%s", namespaceName, pod1Obj.GetName())) - // missing name - invalidPodObj := e2eutil.TemplateToUnstructured(invalidPodTemplate, fields) - - resources := []*unstructured.Unstructured{ - pod1Obj, - deployment1Obj, - invalidPodObj, - } - - applierEvents := e2eutil.RunCollect(applier.Run(ctx, inv, resources, apply.ApplierOptions{ - EmitStatusEvents: false, - ValidationPolicy: validation.ExitEarly, - })) - - expEvents := []testutil.ExpEvent{ - { - // invalid pod validation error - EventType: event.ErrorType, - ErrorEvent: &testutil.ExpErrorEvent{ - Err: testutil.EqualError( - validation.NewError( - field.Required(field.NewPath("metadata", "name"), "name is required"), - object.UnstructuredToObjMetadata(invalidPodObj), - ), - ), - }, - }, - } - Expect(testutil.EventsToExpEvents(applierEvents)).To(testutil.Equal(expEvents)) - - By("verify pod1 not found") - e2eutil.AssertUnstructuredDoesNotExist(ctx, c, pod1Obj) - - By("verify deployment1 not found") - e2eutil.AssertUnstructuredDoesNotExist(ctx, c, deployment1Obj) -} diff --git a/test/e2e/invconfig/configmap.go b/test/e2e/invconfig/configmap.go deleted file mode 100644 index cdb72d64..00000000 --- a/test/e2e/invconfig/configmap.go +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package invconfig - -import ( - "context" - - "github.com/fluxcd/cli-utils/pkg/apply" - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/onsi/gomega" - v1 "k8s.io/api/core/v1" - "k8s.io/client-go/rest" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -func NewConfigMapTypeInvConfig(cfg *rest.Config) InventoryConfig { - return InventoryConfig{ - ClientConfig: cfg, - Strategy: inventory.LabelStrategy, - FactoryFunc: cmInventoryManifest, - InvWrapperFunc: inventory.WrapInventoryInfoObj, - ApplierFactoryFunc: newDefaultInvApplierFactory(cfg), - DestroyerFactoryFunc: newDefaultInvDestroyerFactory(cfg), - InvSizeVerifyFunc: defaultInvSizeVerifyFunc, - InvCountVerifyFunc: defaultInvCountVerifyFunc, - InvNotExistsFunc: defaultInvNotExistsFunc, - } -} - -func newDefaultInvApplierFactory(cfg *rest.Config) applierFactoryFunc { - cfgPtrCopy := cfg - return func() *apply.Applier { - return newApplier(inventory.ClusterClientFactory{ - StatusPolicy: inventory.StatusPolicyAll, - }, cfgPtrCopy) - } -} - -func newDefaultInvDestroyerFactory(cfg *rest.Config) destroyerFactoryFunc { - cfgPtrCopy := cfg - return func() *apply.Destroyer { - return newDestroyer(inventory.ClusterClientFactory{ - StatusPolicy: inventory.StatusPolicyAll, - }, cfgPtrCopy) - } -} - -func defaultInvNotExistsFunc(ctx context.Context, c client.Client, name, namespace, id string) { - var cmList v1.ConfigMapList - err := c.List(ctx, &cmList, - client.MatchingLabels(map[string]string{common.InventoryLabel: id}), - client.InNamespace(namespace)) - gomega.Expect(err).ToNot(gomega.HaveOccurred()) - gomega.Expect(cmList.Items).To(gomega.HaveLen(0), "expected inventory list to be empty") -} - -func defaultInvSizeVerifyFunc(ctx context.Context, c client.Client, name, namespace, id string, specCount, _ int) { - var cmList v1.ConfigMapList - err := c.List(ctx, &cmList, - client.MatchingLabels(map[string]string{common.InventoryLabel: id}), - client.InNamespace(namespace)) - gomega.Expect(err).WithOffset(1).ToNot(gomega.HaveOccurred(), "listing ConfigMap inventory from cluster") - - gomega.Expect(len(cmList.Items)).WithOffset(1).To(gomega.Equal(1), "number of inventory objects by label") - - data := cmList.Items[0].Data - gomega.Expect(len(data)).WithOffset(1).To(gomega.Equal(specCount), "inventory spec.data length") - - // Don't validate status size. - // ConfigMap provider uses inventory.StatusPolicyNone. -} - -func defaultInvCountVerifyFunc(ctx context.Context, c client.Client, namespace string, count int) { - var cmList v1.ConfigMapList - err := c.List(ctx, &cmList, client.InNamespace(namespace), client.HasLabels{common.InventoryLabel}) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - gomega.Expect(len(cmList.Items)).To(gomega.Equal(count)) -} diff --git a/test/e2e/invconfig/custom.go b/test/e2e/invconfig/custom.go deleted file mode 100644 index c99aa246..00000000 --- a/test/e2e/invconfig/custom.go +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package invconfig - -import ( - "context" - "strings" - - "github.com/fluxcd/cli-utils/pkg/apply" - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/test/e2e/customprovider" - "github.com/fluxcd/cli-utils/test/e2e/e2eutil" - "github.com/onsi/gomega" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/rest" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -func NewCustomTypeInvConfig(cfg *rest.Config) InventoryConfig { - return InventoryConfig{ - ClientConfig: cfg, - Strategy: inventory.NameStrategy, - FactoryFunc: customInventoryManifest, - InvWrapperFunc: customprovider.WrapInventoryInfoObj, - ApplierFactoryFunc: newCustomInvApplierFactory(cfg), - DestroyerFactoryFunc: newCustomInvDestroyerFactory(cfg), - InvSizeVerifyFunc: customInvSizeVerifyFunc, - InvCountVerifyFunc: customInvCountVerifyFunc, - InvNotExistsFunc: customInvNotExistsFunc, - } -} - -func newCustomInvApplierFactory(cfg *rest.Config) applierFactoryFunc { - cfgPtrCopy := cfg - return func() *apply.Applier { - return newApplier(customprovider.CustomClientFactory{}, cfgPtrCopy) - } -} - -func newCustomInvDestroyerFactory(cfg *rest.Config) destroyerFactoryFunc { - cfgPtrCopy := cfg - return func() *apply.Destroyer { - return newDestroyer(customprovider.CustomClientFactory{}, cfgPtrCopy) - } -} - -func customInvNotExistsFunc(ctx context.Context, c client.Client, name, namespace, id string) { - var u unstructured.Unstructured - u.SetGroupVersionKind(customprovider.InventoryGVK) - u.SetName(name) - u.SetNamespace(namespace) - e2eutil.AssertUnstructuredDoesNotExist(ctx, c, &u) -} - -func customInvSizeVerifyFunc(ctx context.Context, c client.Client, name, namespace, _ string, specCount, statusCount int) { - var u unstructured.Unstructured - u.SetGroupVersionKind(customprovider.InventoryGVK) - err := c.Get(ctx, types.NamespacedName{ - Name: name, - Namespace: namespace, - }, &u) - gomega.Expect(err).WithOffset(1).ToNot(gomega.HaveOccurred(), "getting custom inventory from cluster") - - s, found, err := unstructured.NestedSlice(u.Object, "spec", "objects") - gomega.Expect(err).WithOffset(1).ToNot(gomega.HaveOccurred(), "reading inventory spec.objects") - if found { - gomega.Expect(len(s)).WithOffset(1).To(gomega.Equal(specCount), "inventory status.objects length") - } else { - gomega.Expect(specCount).WithOffset(1).To(gomega.Equal(0), "inventory spec.objects not found") - } - - s, found, err = unstructured.NestedSlice(u.Object, "status", "objects") - gomega.Expect(err).WithOffset(1).ToNot(gomega.HaveOccurred(), "reading inventory status.objects") - if found { - gomega.Expect(len(s)).WithOffset(1).To(gomega.Equal(statusCount), "inventory status.objects length") - } else { - gomega.Expect(statusCount).WithOffset(1).To(gomega.Equal(0), "inventory status.objects not found") - } -} - -func customInvCountVerifyFunc(ctx context.Context, c client.Client, namespace string, count int) { - var u unstructured.UnstructuredList - u.SetGroupVersionKind(customprovider.InventoryGVK) - err := c.List(ctx, &u, client.InNamespace(namespace)) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - gomega.Expect(len(u.Items)).To(gomega.Equal(count)) -} - -func cmInventoryManifest(name, namespace, id string) *unstructured.Unstructured { - cm := &v1.ConfigMap{ - TypeMeta: metav1.TypeMeta{ - APIVersion: v1.SchemeGroupVersion.String(), - Kind: "ConfigMap", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Labels: map[string]string{ - common.InventoryLabel: id, - }, - }, - } - u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(cm) - if err != nil { - panic(err) - } - return &unstructured.Unstructured{ - Object: u, - } -} - -func customInventoryManifest(name, namespace, id string) *unstructured.Unstructured { - u := e2eutil.ManifestToUnstructured([]byte(strings.TrimSpace(` -apiVersion: cli-utils.example.io/v1alpha1 -kind: Inventory -metadata: - name: PLACEHOLDER -`))) - u.SetName(name) - u.SetNamespace(namespace) - u.SetLabels(map[string]string{ - common.InventoryLabel: id, - }) - return u -} diff --git a/test/e2e/invconfig/invconfig.go b/test/e2e/invconfig/invconfig.go deleted file mode 100644 index a8a03460..00000000 --- a/test/e2e/invconfig/invconfig.go +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package invconfig - -import ( - "context" - "fmt" - - "github.com/fluxcd/cli-utils/pkg/apply" - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/test/e2e/e2eutil" - "github.com/onsi/gomega" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/cli-runtime/pkg/genericclioptions" - "k8s.io/client-go/rest" - "k8s.io/kubectl/pkg/cmd/util" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -type inventoryFactoryFunc func(name, namespace, id string) *unstructured.Unstructured -type invWrapperFunc func(*unstructured.Unstructured) inventory.Info -type applierFactoryFunc func() *apply.Applier -type destroyerFactoryFunc func() *apply.Destroyer -type invSizeVerifyFunc func(ctx context.Context, c client.Client, name, namespace, id string, specCount, statusCount int) -type invCountVerifyFunc func(ctx context.Context, c client.Client, namespace string, count int) -type invNotExistsFunc func(ctx context.Context, c client.Client, name, namespace, id string) - -type InventoryConfig struct { - ClientConfig *rest.Config - Strategy inventory.Strategy - FactoryFunc inventoryFactoryFunc - InvWrapperFunc invWrapperFunc - ApplierFactoryFunc applierFactoryFunc - DestroyerFactoryFunc destroyerFactoryFunc - InvSizeVerifyFunc invSizeVerifyFunc - InvCountVerifyFunc invCountVerifyFunc - InvNotExistsFunc invNotExistsFunc -} - -func CreateInventoryInfo(invConfig InventoryConfig, inventoryName, namespaceName, inventoryID string) inventory.Info { - switch invConfig.Strategy { - case inventory.NameStrategy: - return invConfig.InvWrapperFunc(invConfig.FactoryFunc(inventoryName, namespaceName, e2eutil.RandomString("inventory-"))) - case inventory.LabelStrategy: - return invConfig.InvWrapperFunc(invConfig.FactoryFunc(e2eutil.RandomString("inventory-"), namespaceName, inventoryID)) - default: - panic(fmt.Errorf("unknown inventory strategy %q", invConfig.Strategy)) - } -} - -func newFactory(cfg *rest.Config) util.Factory { - kubeConfigFlags := genericclioptions.NewConfigFlags(true).WithDeprecatedPasswordFlag() - cfgPtrCopy := cfg - kubeConfigFlags.WrapConfigFn = func(c *rest.Config) *rest.Config { - // update rest.Config to pick up QPS & timeout changes - deepCopyRESTConfig(cfgPtrCopy, c) - return c - } - matchVersionKubeConfigFlags := util.NewMatchVersionFlags(kubeConfigFlags) - return util.NewFactory(matchVersionKubeConfigFlags) -} - -func newApplier(invFactory inventory.ClientFactory, cfg *rest.Config) *apply.Applier { - f := newFactory(cfg) - invClient, err := invFactory.NewClient(f) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - - a, err := apply.NewApplierBuilder(). - WithFactory(f). - WithInventoryClient(invClient). - Build() - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - return a -} - -func newDestroyer(invFactory inventory.ClientFactory, cfg *rest.Config) *apply.Destroyer { - f := newFactory(cfg) - invClient, err := invFactory.NewClient(f) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - - d, err := apply.NewDestroyerBuilder(). - WithFactory(f). - WithInventoryClient(invClient). - Build() - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - return d -} - -func deepCopyRESTConfig(from, to *rest.Config) { - to.Host = from.Host - to.APIPath = from.APIPath - to.ContentConfig = from.ContentConfig - to.Username = from.Username - to.Password = from.Password - to.BearerToken = from.BearerToken - to.BearerTokenFile = from.BearerTokenFile - to.Impersonate = rest.ImpersonationConfig{ - UserName: from.Impersonate.UserName, - UID: from.Impersonate.UID, - Groups: from.Impersonate.Groups, - Extra: from.Impersonate.Extra, - } - to.AuthProvider = from.AuthProvider - to.AuthConfigPersister = from.AuthConfigPersister - to.ExecProvider = from.ExecProvider - if from.ExecProvider != nil && from.ExecProvider.Config != nil { - to.ExecProvider.Config = from.ExecProvider.Config.DeepCopyObject() - } - to.TLSClientConfig = rest.TLSClientConfig{ - Insecure: from.Insecure, - ServerName: from.ServerName, - CertFile: from.CertFile, - KeyFile: from.KeyFile, - CAFile: from.CAFile, - CertData: from.CertData, - KeyData: from.KeyData, - CAData: from.CAData, - NextProtos: from.NextProtos, - } - to.UserAgent = from.UserAgent - to.DisableCompression = from.DisableCompression - to.Transport = from.Transport - to.WrapTransport = from.WrapTransport - to.QPS = from.QPS - to.Burst = from.Burst - to.RateLimiter = from.RateLimiter - to.WarningHandler = from.WarningHandler - to.Timeout = from.Timeout - to.Dial = from.Dial - to.Proxy = from.Proxy -} diff --git a/test/e2e/inventory_policy_test.go b/test/e2e/inventory_policy_test.go deleted file mode 100644 index bfe228da..00000000 --- a/test/e2e/inventory_policy_test.go +++ /dev/null @@ -1,551 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package e2e - -import ( - "context" - "time" - - "github.com/fluxcd/cli-utils/pkg/apis/actuation" - "github.com/fluxcd/cli-utils/pkg/apply" - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/pkg/kstatus/status" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/testutil" - "github.com/fluxcd/cli-utils/test/e2e/e2eutil" - "github.com/fluxcd/cli-utils/test/e2e/invconfig" - . "github.com/onsi/ginkgo/v2" //nolint:revive - . "github.com/onsi/gomega" //nolint:revive - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -func inventoryPolicyMustMatchTest(ctx context.Context, c client.Client, invConfig invconfig.InventoryConfig, namespaceName string) { - By("Apply first set of resources") - applier := invConfig.ApplierFactoryFunc() - - firstInvName := e2eutil.RandomString("first-inv-") - firstInv := invConfig.InvWrapperFunc(invConfig.FactoryFunc(firstInvName, namespaceName, firstInvName)) - deployment1Obj := e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(deployment1), namespaceName) - firstResources := []*unstructured.Unstructured{ - deployment1Obj, - } - - e2eutil.RunWithNoErr(applier.Run(ctx, firstInv, firstResources, apply.ApplierOptions{ - ReconcileTimeout: 2 * time.Minute, - EmitStatusEvents: true, - })) - - By("Apply second set of resources") - secondInvName := e2eutil.RandomString("second-inv-") - secondInv := invConfig.InvWrapperFunc(invConfig.FactoryFunc(secondInvName, namespaceName, secondInvName)) - deployment1Obj = e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(deployment1), namespaceName) - secondResources := []*unstructured.Unstructured{ - e2eutil.WithReplicas(deployment1Obj, 6), - } - - applierEvents := e2eutil.RunCollect(applier.Run(ctx, secondInv, secondResources, apply.ApplierOptions{ - ReconcileTimeout: 2 * time.Minute, - EmitStatusEvents: true, - InventoryPolicy: inventory.PolicyMustMatch, - })) - - By("Verify the events") - expEvents := []testutil.ExpEvent{ - { - // InitTask - EventType: event.InitType, - InitEvent: &testutil.ExpInitEvent{}, - }, - { - // InvAddTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-add-0", - Type: event.Started, - }, - }, - { - // InvAddTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-add-0", - Type: event.Finished, - }, - }, - { - // ApplyTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-0", - Type: event.Started, - }, - }, - { - // ApplyTask error: resource managed by another inventory - EventType: event.ApplyType, - ApplyEvent: &testutil.ExpApplyEvent{ - GroupName: "apply-0", - Status: event.ApplySkipped, - Identifier: object.UnstructuredToObjMetadata(deployment1Obj), - Error: &inventory.PolicyPreventedActuationError{ - Strategy: actuation.ActuationStrategyApply, - Policy: inventory.PolicyMustMatch, - Status: inventory.NoMatch, - }, - }, - }, - { - // ApplyTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-0", - Type: event.Finished, - }, - }, - { - // WaitTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Started, - }, - }, - { - // Wait skipped because apply failed - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcileSkipped, - Identifier: object.UnstructuredToObjMetadata(deployment1Obj), - }, - }, - { - // WaitTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Finished, - }, - }, - { - // InvSetTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-set-0", - Type: event.Started, - }, - }, - { - // InvSetTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-set-0", - Type: event.Finished, - }, - }, - } - received := testutil.EventsToExpEvents(applierEvents) - - // handle optional async InProgress StatusEvents - expected := testutil.ExpEvent{ - EventType: event.StatusType, - StatusEvent: &testutil.ExpStatusEvent{ - Identifier: object.UnstructuredToObjMetadata(deployment1Obj), - Status: status.InProgressStatus, - Error: nil, - }, - } - received, _ = testutil.RemoveEqualEvents(received, expected) - - // handle required async Current StatusEvents - expected = testutil.ExpEvent{ - EventType: event.StatusType, - StatusEvent: &testutil.ExpStatusEvent{ - Identifier: object.UnstructuredToObjMetadata(deployment1Obj), - Status: status.CurrentStatus, - Error: nil, - }, - } - received, matches := testutil.RemoveEqualEvents(received, expected) - Expect(matches).To(BeNumerically(">=", 1), "unexpected number of %q status events", status.CurrentStatus) - - Expect(received).To(testutil.Equal(expEvents)) - - By("Verify resource wasn't updated") - result := e2eutil.AssertUnstructuredExists(ctx, c, deployment1Obj) - replicas, found, err := object.NestedField(result.Object, "spec", "replicas") - Expect(err).NotTo(HaveOccurred()) - Expect(found).To(BeTrue()) - Expect(replicas).To(Equal(int64(4))) - - invConfig.InvCountVerifyFunc(ctx, c, namespaceName, 2) -} - -func inventoryPolicyAdoptIfNoInventoryTest(ctx context.Context, c client.Client, invConfig invconfig.InventoryConfig, namespaceName string) { - By("Create unmanaged resource") - deployment1Obj := e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(deployment1), namespaceName) - e2eutil.CreateUnstructuredAndWait(ctx, c, deployment1Obj) - - By("Apply resources") - applier := invConfig.ApplierFactoryFunc() - - invName := e2eutil.RandomString("test-inv-") - inv := invConfig.InvWrapperFunc(invConfig.FactoryFunc(invName, namespaceName, invName)) - deployment1Obj = e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(deployment1), namespaceName) - resources := []*unstructured.Unstructured{ - e2eutil.WithReplicas(deployment1Obj, 6), - } - - applierEvents := e2eutil.RunCollect(applier.Run(ctx, inv, resources, apply.ApplierOptions{ - ReconcileTimeout: 2 * time.Minute, - EmitStatusEvents: true, - InventoryPolicy: inventory.PolicyAdoptIfNoInventory, - })) - - By("Verify the events") - expEvents := []testutil.ExpEvent{ - { - // InitTask - EventType: event.InitType, - InitEvent: &testutil.ExpInitEvent{}, - }, - { - // InvAddTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-add-0", - Type: event.Started, - }, - }, - { - // InvAddTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-add-0", - Type: event.Finished, - }, - }, - { - // ApplyTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-0", - Type: event.Started, - }, - }, - { - // Apply deployment - EventType: event.ApplyType, - ApplyEvent: &testutil.ExpApplyEvent{ - GroupName: "apply-0", - Status: event.ApplySuccessful, - Identifier: object.UnstructuredToObjMetadata(deployment1Obj), - Error: nil, - }, - }, - { - // ApplyTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-0", - Type: event.Finished, - }, - }, - { - // WaitTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Started, - }, - }, - { - // Deployment reconcile Pending. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcilePending, - Identifier: object.UnstructuredToObjMetadata(deployment1Obj), - }, - }, - { - // Deployment confirmed Current. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcileSuccessful, - Identifier: object.UnstructuredToObjMetadata(deployment1Obj), - }, - }, - { - // WaitTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Finished, - }, - }, - { - // InvSetTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-set-0", - Type: event.Started, - }, - }, - { - // InvSetTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-set-0", - Type: event.Finished, - }, - }, - } - - // handle optional async InProgress StatusEvents - receivedEvents := testutil.EventsToExpEvents(applierEvents) - expected := testutil.ExpEvent{ - EventType: event.StatusType, - StatusEvent: &testutil.ExpStatusEvent{ - Identifier: object.UnstructuredToObjMetadata(deployment1Obj), - Status: status.InProgressStatus, - Error: nil, - }, - } - receivedEvents, _ = testutil.RemoveEqualEvents(receivedEvents, expected) - - // handle required async Current StatusEvents - expected = testutil.ExpEvent{ - EventType: event.StatusType, - StatusEvent: &testutil.ExpStatusEvent{ - Identifier: object.UnstructuredToObjMetadata(deployment1Obj), - Status: status.CurrentStatus, - Error: nil, - }, - } - receivedEvents, matches := testutil.RemoveEqualEvents(receivedEvents, expected) - Expect(matches).To(BeNumerically(">=", 1), "unexpected number of %q status events", status.CurrentStatus) - - expEvents, receivedEvents = e2eutil.FilterOptionalEvents(expEvents, receivedEvents) - - Expect(receivedEvents).To(testutil.Equal(expEvents)) - - By("Verify resource was updated and added to inventory") - result := e2eutil.AssertUnstructuredExists(ctx, c, deployment1Obj) - - replicas, found, err := object.NestedField(result.Object, "spec", "replicas") - Expect(err).NotTo(HaveOccurred()) - Expect(found).To(BeTrue()) - Expect(replicas).To(Equal(int64(6))) - - value, found, err := object.NestedField(result.Object, "metadata", "annotations", "config.k8s.io/owning-inventory") - Expect(err).NotTo(HaveOccurred()) - Expect(found).To(BeTrue()) - Expect(value).To(Equal(invName)) - - invConfig.InvCountVerifyFunc(ctx, c, namespaceName, 1) - invConfig.InvSizeVerifyFunc(ctx, c, invName, namespaceName, invName, 1, 1) -} - -func inventoryPolicyAdoptAllTest(ctx context.Context, c client.Client, invConfig invconfig.InventoryConfig, namespaceName string) { - By("Apply an initial set of resources") - applier := invConfig.ApplierFactoryFunc() - - firstInvName := e2eutil.RandomString("first-inv-") - firstInv := invConfig.InvWrapperFunc(invConfig.FactoryFunc(firstInvName, namespaceName, firstInvName)) - deployment1Obj := e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(deployment1), namespaceName) - firstResources := []*unstructured.Unstructured{ - deployment1Obj, - } - - e2eutil.RunWithNoErr(applier.Run(ctx, firstInv, firstResources, apply.ApplierOptions{ - ReconcileTimeout: 2 * time.Minute, - EmitStatusEvents: true, - })) - - By("Apply resources") - secondInvName := e2eutil.RandomString("test-inv-") - secondInv := invConfig.InvWrapperFunc(invConfig.FactoryFunc(secondInvName, namespaceName, secondInvName)) - deployment1Obj = e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(deployment1), namespaceName) - secondResources := []*unstructured.Unstructured{ - e2eutil.WithReplicas(deployment1Obj, 6), - } - - applierEvents := e2eutil.RunCollect(applier.Run(ctx, secondInv, secondResources, apply.ApplierOptions{ - ReconcileTimeout: 2 * time.Minute, - EmitStatusEvents: true, - InventoryPolicy: inventory.PolicyAdoptAll, - })) - - By("Verify the events") - expEvents := []testutil.ExpEvent{ - { - // InitTask - EventType: event.InitType, - InitEvent: &testutil.ExpInitEvent{}, - }, - { - // InvAddTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-add-0", - Type: event.Started, - }, - }, - { - // InvAddTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-add-0", - Type: event.Finished, - }, - }, - { - // ApplyTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-0", - Type: event.Started, - }, - }, - { - // Apply deployment - EventType: event.ApplyType, - ApplyEvent: &testutil.ExpApplyEvent{ - GroupName: "apply-0", - Status: event.ApplySuccessful, - Identifier: object.UnstructuredToObjMetadata(deployment1Obj), - Error: nil, - }, - }, - { - // ApplyTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-0", - Type: event.Finished, - }, - }, - { - // WaitTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Started, - }, - }, - { - // Deployment reconcile Pending. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcilePending, - Identifier: object.UnstructuredToObjMetadata(deployment1Obj), - }, - }, - { - // Deployment confirmed Current. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcileSuccessful, - Identifier: object.UnstructuredToObjMetadata(deployment1Obj), - }, - }, - { - // WaitTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Finished, - }, - }, - { - // InvSetTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-set-0", - Type: event.Started, - }, - }, - { - // InvSetTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-set-0", - Type: event.Finished, - }, - }, - } - - // handle optional async InProgress StatusEvents - receivedEvents := testutil.EventsToExpEvents(applierEvents) - expected := testutil.ExpEvent{ - EventType: event.StatusType, - StatusEvent: &testutil.ExpStatusEvent{ - Identifier: object.UnstructuredToObjMetadata(deployment1Obj), - Status: status.InProgressStatus, - Error: nil, - }, - } - receivedEvents, _ = testutil.RemoveEqualEvents(receivedEvents, expected) - - // handle required async Current StatusEvents - expected = testutil.ExpEvent{ - EventType: event.StatusType, - StatusEvent: &testutil.ExpStatusEvent{ - Identifier: object.UnstructuredToObjMetadata(deployment1Obj), - Status: status.CurrentStatus, - Error: nil, - }, - } - receivedEvents, matches := testutil.RemoveEqualEvents(receivedEvents, expected) - Expect(matches).To(BeNumerically(">=", 1), "unexpected number of %q status events", status.CurrentStatus) - - expEvents, receivedEvents = e2eutil.FilterOptionalEvents(expEvents, receivedEvents) - - Expect(receivedEvents).To(testutil.Equal(expEvents)) - - By("Verify resource was updated and added to inventory") - result := e2eutil.AssertUnstructuredExists(ctx, c, deployment1Obj) - - replicas, found, err := object.NestedField(result.Object, "spec", "replicas") - Expect(err).NotTo(HaveOccurred()) - Expect(found).To(BeTrue()) - Expect(replicas).To(Equal(int64(6))) - - value, found, err := object.NestedField(result.Object, "metadata", "annotations", "config.k8s.io/owning-inventory") - Expect(err).NotTo(HaveOccurred()) - Expect(found).To(BeTrue()) - Expect(value).To(Equal(secondInvName)) - - invConfig.InvCountVerifyFunc(ctx, c, namespaceName, 2) -} diff --git a/test/e2e/mutation_test.go b/test/e2e/mutation_test.go deleted file mode 100644 index b70ada40..00000000 --- a/test/e2e/mutation_test.go +++ /dev/null @@ -1,432 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package e2e - -import ( - "context" - "fmt" - - "github.com/fluxcd/cli-utils/pkg/apply" - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/testutil" - "github.com/fluxcd/cli-utils/test/e2e/e2eutil" - "github.com/fluxcd/cli-utils/test/e2e/invconfig" - . "github.com/onsi/ginkgo/v2" //nolint:revive - . "github.com/onsi/gomega" //nolint:revive - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -// High priority use cases: -// - IAMPolicyMember .spec.member injected with apply-time-mutation using a name -// that contains Project .status.number -// https://github.com/GoogleCloudPlatform/k8s-config-connector/issues/340 -// - Service .spec.loadBalancerIP injected with apply-time-mutation from -// ComputeAddress .spec.address -// https://github.com/GoogleCloudPlatform/k8s-config-connector/issues/334 -// -// However, since both of these use Config Connector resources, which use CRDs -// that are copyright to Google, we can't use them as e2e tests here. Instead, -// we test a toy example with a pod-a depending on pod-b, injecting the ip and -// port from pod-b into an environment variable of pod-a. - -//nolint:dupl // expEvents similar to CRD tests -func mutationTest(ctx context.Context, c client.Client, invConfig invconfig.InventoryConfig, inventoryName, namespaceName string) { - By("apply resources in order with substitutions based on apply-time-mutation annotation") - applier := invConfig.ApplierFactoryFunc() - - inv := invConfig.InvWrapperFunc(invConfig.FactoryFunc(inventoryName, namespaceName, "test")) - - fields := struct{ Namespace string }{Namespace: namespaceName} - podAObj := e2eutil.TemplateToUnstructured(podATemplate, fields) - podBObj := e2eutil.TemplateToUnstructured(podBTemplate, fields) - - // Dependency order: podA -> podB - // Apply order: podB, podA - resources := []*unstructured.Unstructured{ - podAObj, - podBObj, - } - - applierEvents := e2eutil.RunCollect(applier.Run(ctx, inv, resources, apply.ApplierOptions{ - EmitStatusEvents: false, - })) - - expEvents := []testutil.ExpEvent{ - { - // InitTask - EventType: event.InitType, - InitEvent: &testutil.ExpInitEvent{}, - }, - { - // InvAddTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-add-0", - Type: event.Started, - }, - }, - { - // InvAddTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-add-0", - Type: event.Finished, - }, - }, - { - // ApplyTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-0", - Type: event.Started, - }, - }, - { - // Apply PodB first - EventType: event.ApplyType, - ApplyEvent: &testutil.ExpApplyEvent{ - GroupName: "apply-0", - Status: event.ApplySuccessful, - Identifier: object.UnstructuredToObjMetadata(podBObj), - Error: nil, - }, - }, - { - // ApplyTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-0", - Type: event.Finished, - }, - }, - { - // WaitTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Started, - }, - }, - { - // PodB reconcile Pending. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcilePending, - Identifier: object.UnstructuredToObjMetadata(podBObj), - }, - }, - { - // PodB confirmed Current. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcileSuccessful, - Identifier: object.UnstructuredToObjMetadata(podBObj), - }, - }, - { - // WaitTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Finished, - }, - }, - { - // ApplyTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-1", - Type: event.Started, - }, - }, - { - // Apply PodA second - EventType: event.ApplyType, - ApplyEvent: &testutil.ExpApplyEvent{ - GroupName: "apply-1", - Status: event.ApplySuccessful, - Identifier: object.UnstructuredToObjMetadata(podAObj), - Error: nil, - }, - }, - { - // ApplyTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-1", - Type: event.Finished, - }, - }, - { - // WaitTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-1", - Type: event.Started, - }, - }, - { - // PodA reconcile Pending. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-1", - Status: event.ReconcilePending, - Identifier: object.UnstructuredToObjMetadata(podAObj), - }, - }, - { - // PodA confirmed Current. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-1", - Status: event.ReconcileSuccessful, - Identifier: object.UnstructuredToObjMetadata(podAObj), - }, - }, - { - // WaitTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-1", - Type: event.Finished, - }, - }, - { - // InvSetTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-set-0", - Type: event.Started, - }, - }, - { - // InvSetTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-set-0", - Type: event.Finished, - }, - }, - } - receivedEvents := testutil.EventsToExpEvents(applierEvents) - - expEvents, receivedEvents = e2eutil.FilterOptionalEvents(expEvents, receivedEvents) - - Expect(receivedEvents).To(testutil.Equal(expEvents)) - - By("verify podB is created and ready") - result := e2eutil.AssertUnstructuredExists(ctx, c, podBObj) - - podIP, found, err := object.NestedField(result.Object, "status", "podIP") - Expect(err).NotTo(HaveOccurred()) - Expect(found).To(BeTrue()) - Expect(podIP).NotTo(BeEmpty()) // use podIP as proxy for readiness - - containerPort, found, err := object.NestedField(result.Object, "spec", "containers", 0, "ports", 0, "containerPort") - Expect(err).NotTo(HaveOccurred()) - Expect(found).To(BeTrue()) - Expect(containerPort).To(Equal(int64(80))) - - host := fmt.Sprintf("%s:%d", podIP, containerPort) - - By("verify podA is mutated, created, and ready") - result = e2eutil.AssertUnstructuredExists(ctx, c, podAObj) - - podIP, found, err = object.NestedField(result.Object, "status", "podIP") - Expect(err).NotTo(HaveOccurred()) - Expect(found).To(BeTrue()) - Expect(podIP).NotTo(BeEmpty()) // use podIP as proxy for readiness - - envValue, found, err := object.NestedField(result.Object, "spec", "containers", 0, "env", 0, "value") - Expect(err).NotTo(HaveOccurred()) - Expect(found).To(BeTrue()) - Expect(envValue).To(Equal(host)) - - By("destroy resources in opposite order") - destroyer := invConfig.DestroyerFactoryFunc() - options := apply.DestroyerOptions{InventoryPolicy: inventory.PolicyAdoptIfNoInventory} - destroyerEvents := e2eutil.RunCollect(destroyer.Run(ctx, inv, options)) - - expEvents = []testutil.ExpEvent{ - { - // InitTask - EventType: event.InitType, - InitEvent: &testutil.ExpInitEvent{}, - }, - { - // PruneTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.DeleteAction, - GroupName: "prune-0", - Type: event.Started, - }, - }, - { - // Delete PodA first - EventType: event.DeleteType, - DeleteEvent: &testutil.ExpDeleteEvent{ - GroupName: "prune-0", - Status: event.DeleteSuccessful, - Identifier: object.UnstructuredToObjMetadata(podAObj), - Error: nil, - }, - }, - { - // PruneTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.DeleteAction, - GroupName: "prune-0", - Type: event.Finished, - }, - }, - { - // WaitTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Started, - }, - }, - { - // PodA reconcile Pending. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcilePending, - Identifier: object.UnstructuredToObjMetadata(podAObj), - }, - }, - { - // PodA confirmed NotFound. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcileSuccessful, - Identifier: object.UnstructuredToObjMetadata(podAObj), - }, - }, - { - // WaitTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Finished, - }, - }, - { - // PruneTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.DeleteAction, - GroupName: "prune-1", - Type: event.Started, - }, - }, - { - // Delete PodB second - EventType: event.DeleteType, - DeleteEvent: &testutil.ExpDeleteEvent{ - GroupName: "prune-1", - Status: event.DeleteSuccessful, - Identifier: object.UnstructuredToObjMetadata(podBObj), - Error: nil, - }, - }, - { - // PruneTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.DeleteAction, - GroupName: "prune-1", - Type: event.Finished, - }, - }, - { - // WaitTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-1", - Type: event.Started, - }, - }, - { - // PodB reconcile Pending. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-1", - Status: event.ReconcilePending, - Identifier: object.UnstructuredToObjMetadata(podBObj), - }, - }, - { - // PodB confirmed NotFound. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-1", - Status: event.ReconcileSuccessful, - Identifier: object.UnstructuredToObjMetadata(podBObj), - }, - }, - { - // WaitTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-1", - Type: event.Finished, - }, - }, - { - // DeleteInvTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-delete-or-update-0", - Type: event.Started, - }, - }, - { - // DeleteInvTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-delete-or-update-0", - Type: event.Finished, - }, - }, - } - receivedEvents = testutil.EventsToExpEvents(destroyerEvents) - - expEvents, receivedEvents = e2eutil.FilterOptionalEvents(expEvents, receivedEvents) - - Expect(receivedEvents).To(testutil.Equal(expEvents)) - - By("verify podB deleted") - e2eutil.AssertUnstructuredDoesNotExist(ctx, c, podBObj) - - By("verify podA deleted") - e2eutil.AssertUnstructuredDoesNotExist(ctx, c, podAObj) -} diff --git a/test/e2e/name_inv_strategy_test.go b/test/e2e/name_inv_strategy_test.go deleted file mode 100644 index f4be8b01..00000000 --- a/test/e2e/name_inv_strategy_test.go +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2021 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package e2e - -import ( - "context" - "fmt" - "time" - - "github.com/fluxcd/cli-utils/pkg/apply" - "github.com/fluxcd/cli-utils/test/e2e/e2eutil" - "github.com/fluxcd/cli-utils/test/e2e/invconfig" - . "github.com/onsi/ginkgo/v2" //nolint:revive - . "github.com/onsi/gomega" //nolint:revive - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -func applyWithExistingInvTest(ctx context.Context, c client.Client, invConfig invconfig.InventoryConfig, inventoryName, namespaceName string) { - By("Apply first set of resources") - applier := invConfig.ApplierFactoryFunc() - orgInventoryID := fmt.Sprintf("%s-%s", inventoryName, namespaceName) - - orgApplyInv := invConfig.InvWrapperFunc(invConfig.FactoryFunc(inventoryName, namespaceName, orgInventoryID)) - - resources := []*unstructured.Unstructured{ - e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(deployment1), namespaceName), - } - - e2eutil.RunWithNoErr(applier.Run(ctx, orgApplyInv, resources, apply.ApplierOptions{ - ReconcileTimeout: 2 * time.Minute, - EmitStatusEvents: true, - })) - - By("Verify inventory") - invConfig.InvSizeVerifyFunc(ctx, c, inventoryName, namespaceName, orgInventoryID, 1, 1) - - By("Apply second set of resources, using same inventory name but different ID") - secondInventoryID := fmt.Sprintf("%s-%s-2", inventoryName, namespaceName) - secondApplyInv := invConfig.InvWrapperFunc(invConfig.FactoryFunc(inventoryName, namespaceName, secondInventoryID)) - - err := e2eutil.Run(applier.Run(ctx, secondApplyInv, resources, apply.ApplierOptions{ - ReconcileTimeout: 2 * time.Minute, - EmitStatusEvents: true, - })) - - By("Verify that we get the correct error") - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("inventory-id of inventory object in cluster doesn't match provided id")) -} diff --git a/test/e2e/namespace_filter_test.go b/test/e2e/namespace_filter_test.go deleted file mode 100644 index a10cd465..00000000 --- a/test/e2e/namespace_filter_test.go +++ /dev/null @@ -1,424 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package e2e - -import ( - "context" - "fmt" - - "github.com/fluxcd/cli-utils/pkg/apis/actuation" - "github.com/fluxcd/cli-utils/pkg/apply" - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/apply/filter" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/testutil" - "github.com/fluxcd/cli-utils/test/e2e/e2eutil" - "github.com/fluxcd/cli-utils/test/e2e/invconfig" - . "github.com/onsi/ginkgo/v2" //nolint:revive - . "github.com/onsi/gomega" //nolint:revive - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -//nolint:dupl // expEvents similar to other tests -func namespaceFilterTest(ctx context.Context, c client.Client, invConfig invconfig.InventoryConfig, inventoryName, namespaceName string) { - By("apply resources in order based on depends-on annotation") - applier := invConfig.ApplierFactoryFunc() - - inv := invConfig.InvWrapperFunc(invConfig.FactoryFunc(inventoryName, namespaceName, "test")) - - namespace1Name := fmt.Sprintf("%s-ns1", namespaceName) - - fields := struct{ Namespace string }{Namespace: namespace1Name} - namespace1Obj := e2eutil.TemplateToUnstructured(namespaceTemplate, fields) - podBObj := e2eutil.TemplateToUnstructured(podBTemplate, fields) - - // Dependency order: podB -> namespace1 - // Apply order: namespace1, podB - resources := []*unstructured.Unstructured{ - namespace1Obj, - podBObj, - } - - // Cleanup - defer func(ctx context.Context, c client.Client) { - e2eutil.DeleteUnstructuredIfExists(ctx, c, podBObj) - e2eutil.DeleteUnstructuredIfExists(ctx, c, namespace1Obj) - }(ctx, c) - - applierEvents := e2eutil.RunCollect(applier.Run(ctx, inv, resources, apply.ApplierOptions{ - EmitStatusEvents: false, - })) - - expEvents := []testutil.ExpEvent{ - { - // InitTask - EventType: event.InitType, - InitEvent: &testutil.ExpInitEvent{}, - }, - { - // InvAddTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-add-0", - Type: event.Started, - }, - }, - { - // InvAddTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-add-0", - Type: event.Finished, - }, - }, - { - // ApplyTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-0", - Type: event.Started, - }, - }, - { - // Apply namespace1 first - EventType: event.ApplyType, - ApplyEvent: &testutil.ExpApplyEvent{ - GroupName: "apply-0", - Status: event.ApplySuccessful, - Identifier: object.UnstructuredToObjMetadata(namespace1Obj), - Error: nil, - }, - }, - { - // ApplyTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-0", - Type: event.Finished, - }, - }, - { - // WaitTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Started, - }, - }, - { - // namespace1 reconcile Pending - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcilePending, - Identifier: object.UnstructuredToObjMetadata(namespace1Obj), - }, - }, - { - // namespace1 confirmed Current. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcileSuccessful, - Identifier: object.UnstructuredToObjMetadata(namespace1Obj), - }, - }, - { - // WaitTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Finished, - }, - }, - { - // ApplyTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-1", - Type: event.Started, - }, - }, - { - // Apply podB second - EventType: event.ApplyType, - ApplyEvent: &testutil.ExpApplyEvent{ - GroupName: "apply-1", - Status: event.ApplySuccessful, - Identifier: object.UnstructuredToObjMetadata(podBObj), - Error: nil, - }, - }, - { - // ApplyTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-1", - Type: event.Finished, - }, - }, - { - // WaitTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-1", - Type: event.Started, - }, - }, - { - // podB reconcile Pending. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-1", - Status: event.ReconcilePending, - Identifier: object.UnstructuredToObjMetadata(podBObj), - }, - }, - { - // podB confirmed Current. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-1", - Status: event.ReconcileSuccessful, - Identifier: object.UnstructuredToObjMetadata(podBObj), - }, - }, - { - // WaitTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-1", - Type: event.Finished, - }, - }, - { - // InvSetTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-set-0", - Type: event.Started, - }, - }, - { - // InvSetTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-set-0", - Type: event.Finished, - }, - }, - } - receivedEvents := testutil.EventsToExpEvents(applierEvents) - - expEvents, receivedEvents = e2eutil.FilterOptionalEvents(expEvents, receivedEvents) - - Expect(receivedEvents).To(testutil.Equal(expEvents)) - - By("verify namespace1 created") - e2eutil.AssertUnstructuredExists(ctx, c, namespace1Obj) - - By("verify podB created and ready") - result := e2eutil.AssertUnstructuredExists(ctx, c, podBObj) - podIP, found, err := object.NestedField(result.Object, "status", "podIP") - Expect(err).NotTo(HaveOccurred()) - Expect(found).To(BeTrue()) - Expect(podIP).NotTo(BeEmpty()) // use podIP as proxy for readiness - - // Attempt to Prune namespace - resources = []*unstructured.Unstructured{ - podBObj, - } - - applierEvents = e2eutil.RunCollect(applier.Run(ctx, inv, resources, apply.ApplierOptions{ - EmitStatusEvents: false, - })) - - expEvents = []testutil.ExpEvent{ - { - // InitTask - EventType: event.InitType, - InitEvent: &testutil.ExpInitEvent{}, - }, - { - // InvAddTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-add-0", - Type: event.Started, - }, - }, - { - // InvAddTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-add-0", - Type: event.Finished, - }, - }, - { - // ApplyTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-0", - Type: event.Started, - }, - }, - { - // Apply podB Skipped (because depends on namespace being deleted) - EventType: event.ApplyType, - ApplyEvent: &testutil.ExpApplyEvent{ - GroupName: "apply-0", - Status: event.ApplySkipped, - Identifier: object.UnstructuredToObjMetadata(podBObj), - Error: testutil.EqualError(&filter.DependencyActuationMismatchError{ - Object: object.UnstructuredToObjMetadata(podBObj), - Strategy: actuation.ActuationStrategyApply, - Relationship: filter.RelationshipDependency, - Relation: object.UnstructuredToObjMetadata(namespace1Obj), - RelationStrategy: actuation.ActuationStrategyDelete, - }), - }, - }, - { - // ApplyTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-0", - Type: event.Finished, - }, - }, - { - // WaitTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Started, - }, - }, - { - // podB Reconcile Skipped (because apply skipped) - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcileSkipped, - Identifier: object.UnstructuredToObjMetadata(podBObj), - }, - }, - { - // WaitTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Finished, - }, - }, - { - // PruneTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.PruneAction, - GroupName: "prune-0", - Type: event.Started, - }, - }, - { - // Prune namespace1 Skipped (because namespace still in use) - EventType: event.PruneType, - PruneEvent: &testutil.ExpPruneEvent{ - GroupName: "prune-0", - Status: event.PruneSkipped, - Identifier: object.UnstructuredToObjMetadata(namespace1Obj), - Error: testutil.EqualError(&filter.NamespaceInUseError{ - Namespace: namespace1Name, - }), - }, - }, - { - // PruneTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.PruneAction, - GroupName: "prune-0", - Type: event.Finished, - }, - }, - { - // WaitTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-1", - Type: event.Started, - }, - }, - { - // namespace1 reconcile Skipped (because prune skipped). - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-1", - Status: event.ReconcileSkipped, - Identifier: object.UnstructuredToObjMetadata(namespace1Obj), - }, - }, - { - // WaitTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-1", - Type: event.Finished, - }, - }, - { - // InvSetTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-set-0", - Type: event.Started, - }, - }, - { - // InvSetTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-set-0", - Type: event.Finished, - }, - }, - } - Expect(testutil.EventsToExpEvents(applierEvents)).To(testutil.Equal(expEvents)) - - By("verify namespace1 not deleted") - result = e2eutil.AssertUnstructuredExists(ctx, c, namespace1Obj) - ts, found, err := object.NestedField(result.Object, "metadata", "deletionTimestamp") - Expect(err).NotTo(HaveOccurred()) - Expect(found).To(BeFalse(), "deletionTimestamp found: ", ts) - - By("verify podB not deleted") - result = e2eutil.AssertUnstructuredExists(ctx, c, podBObj) - ts, found, err = object.NestedField(result.Object, "metadata", "deletionTimestamp") - Expect(err).NotTo(HaveOccurred()) - Expect(found).To(BeFalse(), "deletionTimestamp found: ", ts) -} diff --git a/test/e2e/prune_retrieve_error_test.go b/test/e2e/prune_retrieve_error_test.go deleted file mode 100644 index fc5fac8c..00000000 --- a/test/e2e/prune_retrieve_error_test.go +++ /dev/null @@ -1,412 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package e2e - -import ( - "context" - "fmt" - - "github.com/fluxcd/cli-utils/pkg/apply" - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/testutil" - "github.com/fluxcd/cli-utils/test/e2e/e2eutil" - "github.com/fluxcd/cli-utils/test/e2e/invconfig" - . "github.com/onsi/ginkgo/v2" //nolint:revive - . "github.com/onsi/gomega" //nolint:revive - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -func pruneRetrieveErrorTest(ctx context.Context, c client.Client, invConfig invconfig.InventoryConfig, inventoryName, namespaceName string) { - By("apply a single resource, which is referenced in the inventory") - applier := invConfig.ApplierFactoryFunc() - - inventoryID := fmt.Sprintf("%s-%s", inventoryName, namespaceName) - - inv := invconfig.CreateInventoryInfo(invConfig, inventoryName, namespaceName, inventoryID) - - pod1Obj := e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(pod1), namespaceName) - resource1 := []*unstructured.Unstructured{ - pod1Obj, - } - - applierEvents := e2eutil.RunCollect(applier.Run(ctx, inv, resource1, apply.ApplierOptions{ - EmitStatusEvents: false, - })) - - expEvents := []testutil.ExpEvent{ - { - // InitTask - EventType: event.InitType, - InitEvent: &testutil.ExpInitEvent{}, - }, - { - // InvAddTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-add-0", - Type: event.Started, - }, - }, - { - // InvAddTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-add-0", - Type: event.Finished, - }, - }, - { - // ApplyTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-0", - Type: event.Started, - }, - }, - { - // Create Pod1 - EventType: event.ApplyType, - ApplyEvent: &testutil.ExpApplyEvent{ - GroupName: "apply-0", - Status: event.ApplySuccessful, - Identifier: object.UnstructuredToObjMetadata(pod1Obj), - Error: nil, - }, - }, - { - // ApplyTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-0", - Type: event.Finished, - }, - }, - { - // WaitTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Started, - }, - }, - { - // Pod1 reconcile Pending. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcilePending, - Identifier: object.UnstructuredToObjMetadata(pod1Obj), - }, - }, - { - // Pod1 confirmed Current. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcileSuccessful, - Identifier: object.UnstructuredToObjMetadata(pod1Obj), - }, - }, - { - // WaitTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Finished, - }, - }, - { - // InvSetTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-set-0", - Type: event.Started, - }, - }, - { - // InvSetTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-set-0", - Type: event.Finished, - }, - }, - } - receivedEvents := testutil.EventsToExpEvents(applierEvents) - - expEvents, receivedEvents = e2eutil.FilterOptionalEvents(expEvents, receivedEvents) - - Expect(receivedEvents).To(testutil.Equal(expEvents)) - - By("Verify pod1 created and ready") - result := e2eutil.AssertUnstructuredExists(ctx, c, pod1Obj) - podIP, found, err := object.NestedField(result.Object, "status", "podIP") - Expect(err).NotTo(HaveOccurred()) - Expect(found).To(BeTrue()) - Expect(podIP).NotTo(BeEmpty()) // use podIP as proxy for readiness - - // Delete the previously applied resource, which is referenced in the inventory. - By("delete resource, which is referenced in the inventory") - e2eutil.DeleteUnstructuredAndWait(ctx, c, pod1Obj) - - By("Verify inventory") - // The inventory should still have the previously deleted item. - invConfig.InvSizeVerifyFunc(ctx, c, inventoryName, namespaceName, inventoryID, 1, 1) - - By("apply a different resource, and validate the inventory accurately reflects only this object") - pod2Obj := e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(pod2), namespaceName) - resource2 := []*unstructured.Unstructured{ - pod2Obj, - } - - applierEvents = e2eutil.RunCollect(applier.Run(ctx, inv, resource2, apply.ApplierOptions{ - EmitStatusEvents: false, - })) - - expEvents = []testutil.ExpEvent{ - { - // InitTask - EventType: event.InitType, - InitEvent: &testutil.ExpInitEvent{}, - }, - { - // InvAddTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-add-0", - Type: event.Started, - }, - }, - { - // InvAddTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-add-0", - Type: event.Finished, - }, - }, - { - // ApplyTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-0", - Type: event.Started, - }, - }, - { - // Create pod2 - EventType: event.ApplyType, - ApplyEvent: &testutil.ExpApplyEvent{ - GroupName: "apply-0", - Status: event.ApplySuccessful, - Identifier: object.UnstructuredToObjMetadata(pod2Obj), - Error: nil, - }, - }, - { - // ApplyTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-0", - Type: event.Finished, - }, - }, - { - // WaitTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Started, - }, - }, - { - // Pod2 reconcile Pending. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcilePending, - Identifier: object.UnstructuredToObjMetadata(pod2Obj), - }, - }, - { - // Pod2 confirmed Current. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcileSuccessful, - Identifier: object.UnstructuredToObjMetadata(pod2Obj), - }, - }, - { - // WaitTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Finished, - }, - }, - // Don't prune pod1, it should already be deleted. - { - // InvSetTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-set-0", - Type: event.Started, - }, - }, - { - // InvSetTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-set-0", - Type: event.Finished, - }, - }, - } - receivedEvents = testutil.EventsToExpEvents(applierEvents) - - expEvents, receivedEvents = e2eutil.FilterOptionalEvents(expEvents, receivedEvents) - - Expect(receivedEvents).To(testutil.Equal(expEvents)) - - By("Verify pod2 created and ready") - result = e2eutil.AssertUnstructuredExists(ctx, c, pod2Obj) - podIP, found, err = object.NestedField(result.Object, "status", "podIP") - Expect(err).NotTo(HaveOccurred()) - Expect(found).To(BeTrue()) - Expect(podIP).NotTo(BeEmpty()) // use podIP as proxy for readiness - - By("Verify pod1 still deleted") - e2eutil.AssertUnstructuredDoesNotExist(ctx, c, pod1Obj) - - By("Verify inventory") - // The inventory should only have the currently applied item. - invConfig.InvSizeVerifyFunc(ctx, c, inventoryName, namespaceName, inventoryID, 1, 1) - - By("Destroy resources") - destroyer := invConfig.DestroyerFactoryFunc() - - options := apply.DestroyerOptions{InventoryPolicy: inventory.PolicyAdoptIfNoInventory} - destroyerEvents := e2eutil.RunCollect(destroyer.Run(ctx, inv, options)) - - expEvents = []testutil.ExpEvent{ - { - // InitTask - EventType: event.InitType, - InitEvent: &testutil.ExpInitEvent{}, - }, - { - // PruneTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.DeleteAction, - GroupName: "prune-0", - Type: event.Started, - }, - }, - { - // Delete pod2 - EventType: event.DeleteType, - DeleteEvent: &testutil.ExpDeleteEvent{ - GroupName: "prune-0", - Status: event.DeleteSuccessful, - Identifier: object.UnstructuredToObjMetadata(pod2Obj), - Error: nil, - }, - }, - { - // PruneTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.DeleteAction, - GroupName: "prune-0", - Type: event.Finished, - }, - }, - { - // WaitTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Started, - }, - }, - { - // Pod2 reconcile Pending. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcilePending, - Identifier: object.UnstructuredToObjMetadata(pod2Obj), - }, - }, - { - // Pod2 confirmed NotFound. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcileSuccessful, - Identifier: object.UnstructuredToObjMetadata(pod2Obj), - }, - }, - { - // WaitTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Finished, - }, - }, - { - // DeleteInvTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-delete-or-update-0", - Type: event.Started, - }, - }, - { - // DeleteInvTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-delete-or-update-0", - Type: event.Finished, - }, - }, - } - receivedEvents = testutil.EventsToExpEvents(destroyerEvents) - - expEvents, receivedEvents = e2eutil.FilterOptionalEvents(expEvents, receivedEvents) - - Expect(receivedEvents).To(testutil.Equal(expEvents)) - - By("Verify pod1 is deleted") - e2eutil.AssertUnstructuredDoesNotExist(ctx, c, pod1Obj) - - By("Verify pod2 is deleted") - e2eutil.AssertUnstructuredDoesNotExist(ctx, c, pod2Obj) -} diff --git a/test/e2e/reconcile_failed_timeout_test.go b/test/e2e/reconcile_failed_timeout_test.go deleted file mode 100644 index fc6ac64c..00000000 --- a/test/e2e/reconcile_failed_timeout_test.go +++ /dev/null @@ -1,178 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package e2e - -import ( - "context" - "fmt" - "time" - - "github.com/fluxcd/cli-utils/pkg/apply" - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/testutil" - "github.com/fluxcd/cli-utils/test/e2e/e2eutil" - "github.com/fluxcd/cli-utils/test/e2e/invconfig" - . "github.com/onsi/ginkgo/v2" //nolint:revive - . "github.com/onsi/gomega" //nolint:revive - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -func reconciliationFailed(ctx context.Context, invConfig invconfig.InventoryConfig, inventoryName, namespaceName string) { - By("Apply resources") - applier := invConfig.ApplierFactoryFunc() - inventoryID := fmt.Sprintf("%s-%s", inventoryName, namespaceName) - - inventoryInfo := invconfig.CreateInventoryInfo(invConfig, inventoryName, namespaceName, inventoryID) - - podObj := e2eutil.WithNodeSelector(e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(pod1), namespaceName), "foo", "bar") - resources := []*unstructured.Unstructured{ - podObj, - } - - applierEvents := e2eutil.RunCollect(applier.Run(ctx, inventoryInfo, resources, apply.ApplierOptions{ - ReconcileTimeout: 2 * time.Minute, - EmitStatusEvents: false, - })) - - expEvents := expectedPodEvents(podObj, event.ReconcileFailed) - received := testutil.EventsToExpEvents(applierEvents) - - Expect(received).To(testutil.Equal(expEvents)) -} - -func reconciliationTimeout(ctx context.Context, invConfig invconfig.InventoryConfig, inventoryName, namespaceName string) { - By("Apply resources") - applier := invConfig.ApplierFactoryFunc() - inventoryID := fmt.Sprintf("%s-%s", inventoryName, namespaceName) - - inventoryInfo := invconfig.CreateInventoryInfo(invConfig, inventoryName, namespaceName, inventoryID) - - podObj := e2eutil.PodWithImage(e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(pod1), namespaceName), "kubernetes-pause", "does-not-exist") - resources := []*unstructured.Unstructured{ - podObj, - } - - applierEvents := e2eutil.RunCollect(applier.Run(ctx, inventoryInfo, resources, apply.ApplierOptions{ - ReconcileTimeout: 30 * time.Second, - EmitStatusEvents: false, - })) - - expEvents := expectedPodEvents(podObj, event.ReconcileTimeout) - receivedEvents := testutil.EventsToExpEvents(applierEvents) - - expEvents, receivedEvents = e2eutil.FilterOptionalEvents(expEvents, receivedEvents) - - Expect(receivedEvents).To(testutil.Equal(expEvents)) -} - -func expectedPodEvents(pod *unstructured.Unstructured, waitStatus event.WaitEventStatus) []testutil.ExpEvent { - return []testutil.ExpEvent{ - { - // InitTask - EventType: event.InitType, - InitEvent: &testutil.ExpInitEvent{}, - }, - { - // InvAddTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-add-0", - Type: event.Started, - }, - }, - { - // InvAddTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-add-0", - Type: event.Finished, - }, - }, - { - // ApplyTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-0", - Type: event.Started, - }, - }, - { - // Create deployment - EventType: event.ApplyType, - ApplyEvent: &testutil.ExpApplyEvent{ - GroupName: "apply-0", - Status: event.ApplySuccessful, - Identifier: object.UnstructuredToObjMetadata(pod), - Error: nil, - }, - }, - { - // ApplyTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-0", - Type: event.Finished, - }, - }, - { - // WaitTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Started, - }, - }, - { - // Deployment reconcile Pending . - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcilePending, - Identifier: object.UnstructuredToObjMetadata(pod), - }, - }, - { - // Deployment confirmed Current. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: waitStatus, - Identifier: object.UnstructuredToObjMetadata(pod), - }, - }, - { - // WaitTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Finished, - }, - }, - { - // InvSetTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-set-0", - Type: event.Started, - }, - }, - { - // InvSetTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-set-0", - Type: event.Finished, - }, - }, - } -} diff --git a/test/e2e/serverside_apply_test.go b/test/e2e/serverside_apply_test.go deleted file mode 100644 index 975c9333..00000000 --- a/test/e2e/serverside_apply_test.go +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package e2e - -import ( - "context" - "time" - - "github.com/fluxcd/cli-utils/pkg/apply" - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/test/e2e/e2eutil" - "github.com/fluxcd/cli-utils/test/e2e/invconfig" - . "github.com/onsi/ginkgo/v2" //nolint:revive - . "github.com/onsi/gomega" //nolint:revive - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -func serversideApplyTest(ctx context.Context, c client.Client, invConfig invconfig.InventoryConfig, inventoryName, namespaceName string) { - By("Apply a Deployment and an APIService by server-side apply") - applier := invConfig.ApplierFactoryFunc() - - inv := invConfig.InvWrapperFunc(invConfig.FactoryFunc(inventoryName, namespaceName, "test")) - firstResources := []*unstructured.Unstructured{ - e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(deployment1), namespaceName), - e2eutil.ManifestToUnstructured(apiservice1), - } - - e2eutil.RunWithNoErr(applier.Run(ctx, inv, firstResources, apply.ApplierOptions{ - ReconcileTimeout: 2 * time.Minute, - EmitStatusEvents: true, - ServerSideOptions: common.ServerSideOptions{ - ServerSideApply: true, - ForceConflicts: true, - FieldManager: "test", - }, - })) - - By("Verify deployment is server-side applied") - result := e2eutil.AssertUnstructuredExists(ctx, c, e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(deployment1), namespaceName)) - - // LastAppliedConfigAnnotation annotation is only set for client-side apply and we've server-side applied here. - _, found, err := object.NestedField(result.Object, "metadata", "annotations", v1.LastAppliedConfigAnnotation) - Expect(err).NotTo(HaveOccurred()) - Expect(found).To(BeFalse()) - - manager, found, err := object.NestedField(result.Object, "metadata", "managedFields", 0, "manager") - Expect(err).NotTo(HaveOccurred()) - Expect(found).To(BeTrue()) - Expect(manager).To(Equal("test")) - - By("Verify APIService is server-side applied") - result = e2eutil.AssertUnstructuredExists(ctx, c, e2eutil.ManifestToUnstructured(apiservice1)) - - // LastAppliedConfigAnnotation annotation is only set for client-side apply and we've server-side applied here. - _, found, err = object.NestedField(result.Object, "metadata", "annotations", v1.LastAppliedConfigAnnotation) - Expect(err).NotTo(HaveOccurred()) - Expect(found).To(BeFalse()) - - manager, found, err = object.NestedField(result.Object, "metadata", "managedFields", 0, "manager") - Expect(err).NotTo(HaveOccurred()) - Expect(found).To(BeTrue()) - Expect(manager).To(Equal("test")) -} diff --git a/test/e2e/skip_invalid_test.go b/test/e2e/skip_invalid_test.go deleted file mode 100644 index 29e326e0..00000000 --- a/test/e2e/skip_invalid_test.go +++ /dev/null @@ -1,487 +0,0 @@ -// Copyright 2022 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package e2e - -import ( - "context" - "fmt" - - "github.com/fluxcd/cli-utils/pkg/apply" - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/pkg/object" - "github.com/fluxcd/cli-utils/pkg/object/dependson" - "github.com/fluxcd/cli-utils/pkg/object/graph" - "github.com/fluxcd/cli-utils/pkg/object/mutation" - "github.com/fluxcd/cli-utils/pkg/object/validation" - "github.com/fluxcd/cli-utils/pkg/testutil" - "github.com/fluxcd/cli-utils/test/e2e/e2eutil" - "github.com/fluxcd/cli-utils/test/e2e/invconfig" - . "github.com/onsi/ginkgo/v2" //nolint:revive - . "github.com/onsi/gomega" //nolint:revive - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/util/validation/field" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -func skipInvalidTest(ctx context.Context, c client.Client, invConfig invconfig.InventoryConfig, inventoryName, namespaceName string) { - By("apply valid objects and skip invalid objects") - applier := invConfig.ApplierFactoryFunc() - - inv := invConfig.InvWrapperFunc(invConfig.FactoryFunc(inventoryName, namespaceName, "test")) - - fields := struct{ Namespace string }{Namespace: namespaceName} - // valid pod - pod1Obj := e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(pod1), namespaceName) - // valid deployment with dependency - deployment1Obj := e2eutil.WithDependsOn(e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(deployment1), namespaceName), - fmt.Sprintf("/namespaces/%s/Pod/%s", namespaceName, pod1Obj.GetName())) - // external/missing dependency - pod3Obj := e2eutil.WithDependsOn(e2eutil.WithNamespace(e2eutil.ManifestToUnstructured(pod3), namespaceName), - fmt.Sprintf("/namespaces/%s/Pod/pod0", namespaceName)) - // cyclic dependency (podB) - podAObj := e2eutil.TemplateToUnstructured(podATemplate, fields) - // cyclic dependency (podA) & invalid source reference (dependency not in object set) - podBObj := e2eutil.TemplateToUnstructured(invalidMutationPodBTemplate, fields) - // missing name - invalidPodObj := e2eutil.TemplateToUnstructured(invalidPodTemplate, fields) - - resources := []*unstructured.Unstructured{ - pod1Obj, - deployment1Obj, - pod3Obj, - podAObj, - podBObj, - invalidPodObj, - } - - applierEvents := e2eutil.RunCollect(applier.Run(ctx, inv, resources, apply.ApplierOptions{ - EmitStatusEvents: false, - ValidationPolicy: validation.SkipInvalid, - })) - - expEvents := []testutil.ExpEvent{ - { - // invalid pod validation error - EventType: event.ValidationType, - ValidationEvent: &testutil.ExpValidationEvent{ - Identifiers: object.ObjMetadataSet{ - object.UnstructuredToObjMetadata(invalidPodObj), - }, - Error: testutil.EqualError( - validation.NewError( - field.Required(field.NewPath("metadata", "name"), "name is required"), - object.UnstructuredToObjMetadata(invalidPodObj), - ), - ), - }, - }, - { - // Pod3 validation error - EventType: event.ValidationType, - ValidationEvent: &testutil.ExpValidationEvent{ - Identifiers: object.ObjMetadataSet{ - object.UnstructuredToObjMetadata(pod3Obj), - }, - Error: testutil.EqualError( - validation.NewError( - object.InvalidAnnotationError{ - Annotation: dependson.Annotation, - Cause: graph.ExternalDependencyError{ - Edge: graph.Edge{ - From: object.UnstructuredToObjMetadata(pod3Obj), - To: object.ObjMetadata{ - GroupKind: schema.GroupKind{Kind: "Pod"}, - Name: "pod0", - Namespace: namespaceName, - }, - }, - }, - }, - object.UnstructuredToObjMetadata(pod3Obj), - ), - ), - }, - }, - { - // PodB validation error - EventType: event.ValidationType, - ValidationEvent: &testutil.ExpValidationEvent{ - Identifiers: object.ObjMetadataSet{ - object.UnstructuredToObjMetadata(podBObj), - }, - Error: testutil.EqualError( - validation.NewError( - object.InvalidAnnotationError{ - Annotation: mutation.Annotation, - Cause: graph.ExternalDependencyError{ - Edge: graph.Edge{ - From: object.UnstructuredToObjMetadata(podBObj), - To: object.ObjMetadata{ - GroupKind: schema.GroupKind{Kind: "Pod"}, - Name: "pod-a", - }, - }, - }, - }, - object.UnstructuredToObjMetadata(podBObj), - ), - ), - }, - }, - { - // Cyclic Dependency validation error - EventType: event.ValidationType, - ValidationEvent: &testutil.ExpValidationEvent{ - Identifiers: object.ObjMetadataSet{ - object.UnstructuredToObjMetadata(podAObj), - object.UnstructuredToObjMetadata(podBObj), - }, - Error: testutil.EqualError( - validation.NewError( - graph.CyclicDependencyError{ - Edges: []graph.Edge{ - { - From: object.UnstructuredToObjMetadata(podAObj), - To: object.UnstructuredToObjMetadata(podBObj), - }, - { - From: object.UnstructuredToObjMetadata(podBObj), - To: object.UnstructuredToObjMetadata(podAObj), - }, - }, - }, - object.UnstructuredToObjMetadata(podAObj), - object.UnstructuredToObjMetadata(podBObj), - ), - ), - }, - }, - { - // InitTask - EventType: event.InitType, - InitEvent: &testutil.ExpInitEvent{}, - }, - { - // InvAddTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-add-0", - Type: event.Started, - }, - }, - { - // InvAddTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-add-0", - Type: event.Finished, - }, - }, - { - // ApplyTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-0", - Type: event.Started, - }, - }, - { - // Apply Pod1 - EventType: event.ApplyType, - ApplyEvent: &testutil.ExpApplyEvent{ - GroupName: "apply-0", - Status: event.ApplySuccessful, - Identifier: object.UnstructuredToObjMetadata(pod1Obj), - }, - }, - { - // ApplyTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-0", - Type: event.Finished, - }, - }, - { - // WaitTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Started, - }, - }, - { - // Pod1 reconcile Pending. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcilePending, - Identifier: object.UnstructuredToObjMetadata(pod1Obj), - }, - }, - { - // Pod1 confirmed Current. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcileSuccessful, - Identifier: object.UnstructuredToObjMetadata(pod1Obj), - }, - }, - { - // WaitTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Finished, - }, - }, - { - // ApplyTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-1", - Type: event.Started, - }, - }, - { - // Apply Deployment1 - EventType: event.ApplyType, - ApplyEvent: &testutil.ExpApplyEvent{ - GroupName: "apply-1", - Status: event.ApplySuccessful, - Identifier: object.UnstructuredToObjMetadata(deployment1Obj), - }, - }, - { - // ApplyTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.ApplyAction, - GroupName: "apply-1", - Type: event.Finished, - }, - }, - { - // WaitTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-1", - Type: event.Started, - }, - }, - { - // Deployment1 reconcile Pending. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-1", - Status: event.ReconcilePending, - Identifier: object.UnstructuredToObjMetadata(deployment1Obj), - }, - }, - { - // Deployment1 confirmed Current. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-1", - Status: event.ReconcileSuccessful, - Identifier: object.UnstructuredToObjMetadata(deployment1Obj), - }, - }, - { - // WaitTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-1", - Type: event.Finished, - }, - }, - { - // InvSetTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-set-0", - Type: event.Started, - }, - }, - { - // InvSetTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-set-0", - Type: event.Finished, - }, - }, - } - Expect(testutil.EventsToExpEvents(applierEvents)).To(testutil.Equal(expEvents)) - - By("verify pod1 created and ready") - result := e2eutil.AssertUnstructuredExists(ctx, c, pod1Obj) - podIP, found, err := object.NestedField(result.Object, "status", "podIP") - Expect(err).NotTo(HaveOccurred()) - Expect(found).To(BeTrue()) - Expect(podIP).NotTo(BeEmpty()) // use podIP as proxy for readiness - - By("verify deployment1 created and ready") - result = e2eutil.AssertUnstructuredExists(ctx, c, deployment1Obj) - e2eutil.AssertUnstructuredAvailable(result) - - By("verify pod3 not found") - e2eutil.AssertUnstructuredDoesNotExist(ctx, c, pod3Obj) - - By("verify podA not found") - e2eutil.AssertUnstructuredDoesNotExist(ctx, c, podAObj) - - By("verify podB not found") - e2eutil.AssertUnstructuredDoesNotExist(ctx, c, podBObj) - - By("modify deployment1 depends-on annotation to be invalid") - e2eutil.ApplyUnstructured(ctx, c, e2eutil.WithDependsOn(deployment1Obj, "invalid")) - - By("destroy valid objects and skip invalid objects") - destroyer := invConfig.DestroyerFactoryFunc() - destroyerEvents := e2eutil.RunCollect(destroyer.Run(ctx, inv, apply.DestroyerOptions{ - InventoryPolicy: inventory.PolicyAdoptIfNoInventory, - ValidationPolicy: validation.SkipInvalid, - })) - - expEvents = []testutil.ExpEvent{ - { - // Deployment1 validation error - EventType: event.ValidationType, - ValidationEvent: &testutil.ExpValidationEvent{ - Identifiers: object.ObjMetadataSet{ - object.UnstructuredToObjMetadata(deployment1Obj), - }, - Error: testutil.EqualError( - validation.NewError( - object.InvalidAnnotationError{ - Annotation: dependson.Annotation, - Cause: fmt.Errorf("failed to parse object reference (index: 0): %w", - fmt.Errorf("expected 3 or 5 fields, found 1: %q", "invalid")), - }, - object.UnstructuredToObjMetadata(deployment1Obj), - ), - ), - }, - }, - { - // InitTask - EventType: event.InitType, - InitEvent: &testutil.ExpInitEvent{}, - }, - { - // PruneTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.DeleteAction, - GroupName: "prune-0", - Type: event.Started, - }, - }, - // TODO: Filter deletes so dependencies don't get deleted when the objects that used to depend on them are invalid? - { - // Delete pod1 - EventType: event.DeleteType, - DeleteEvent: &testutil.ExpDeleteEvent{ - GroupName: "prune-0", - Status: event.DeleteSuccessful, - Identifier: object.UnstructuredToObjMetadata(pod1Obj), - }, - }, - { - // PruneTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.DeleteAction, - GroupName: "prune-0", - Type: event.Finished, - }, - }, - { - // WaitTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Started, - }, - }, - { - // Pod1 reconcile Pending. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcilePending, - Identifier: object.UnstructuredToObjMetadata(pod1Obj), - }, - }, - { - // Pod1 confirmed NotFound. - EventType: event.WaitType, - WaitEvent: &testutil.ExpWaitEvent{ - GroupName: "wait-0", - Status: event.ReconcileSuccessful, - Identifier: object.UnstructuredToObjMetadata(pod1Obj), - }, - }, - { - // WaitTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.WaitAction, - GroupName: "wait-0", - Type: event.Finished, - }, - }, - { - // DeleteInvTask start - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-delete-or-update-0", - Type: event.Started, - }, - }, - { - // DeleteInvTask finished - EventType: event.ActionGroupType, - ActionGroupEvent: &testutil.ExpActionGroupEvent{ - Action: event.InventoryAction, - GroupName: "inventory-delete-or-update-0", - Type: event.Finished, - }, - }, - } - Expect(testutil.EventsToExpEvents(destroyerEvents)).To(testutil.Equal(expEvents)) - - By("verify pod1 deleted") - e2eutil.AssertUnstructuredDoesNotExist(ctx, c, pod1Obj) - - By("verify deployment1 not deleted") - e2eutil.AssertUnstructuredExists(ctx, c, deployment1Obj) - e2eutil.DeleteUnstructuredIfExists(ctx, c, deployment1Obj) - - By("verify pod3 not found") - e2eutil.AssertUnstructuredDoesNotExist(ctx, c, pod3Obj) - - By("verify podA not found") - e2eutil.AssertUnstructuredDoesNotExist(ctx, c, podAObj) - - By("verify podB not found") - e2eutil.AssertUnstructuredDoesNotExist(ctx, c, podBObj) -} diff --git a/test/stress/artifacts_test.go b/test/stress/artifacts_test.go deleted file mode 100644 index 55c0c51a..00000000 --- a/test/stress/artifacts_test.go +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package stress - -var namespaceYaml = ` -apiVersion: v1 -kind: Namespace -metadata: - name: "" -` - -var configMapYaml = ` -apiVersion: v1 -kind: ConfigMap -metadata: - name: "" - namespace: "" -data: {} -` - -var cronTabCRDYaml = ` -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: crontabs.stable.example.com -spec: - group: stable.example.com - versions: - - name: v1 - served: true - storage: true - schema: - openAPIV3Schema: - type: object - properties: - apiVersion: - type: string - kind: - type: string - metadata: - type: object - spec: - type: object - properties: - cronSpec: - type: string - image: - type: string - scope: Namespaced - names: - plural: crontabs - singular: crontab - kind: CronTab - shortNames: - - ct -` - -var cronTabYaml = ` -apiVersion: stable.example.com/v1 -kind: CronTab -metadata: - name: "" - namespace: "" -spec: - cronSpec: "* * * * */5" -` - -var deploymentYaml = ` -apiVersion: apps/v1 -kind: Deployment -metadata: - name: "" - namespace: "" -spec: - replicas: 1 - selector: - matchLabels: - app: pause - template: - metadata: - labels: - app: pause - spec: - containers: - - name: kubernetes-pause - image: registry.k8s.io/pause:2.0 - resources: - requests: - cpu: 1m - memory: 1Mi -` diff --git a/test/stress/kind-cluster.yaml b/test/stress/kind-cluster.yaml deleted file mode 100644 index 16ad5d20..00000000 --- a/test/stress/kind-cluster.yaml +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright 2022 The Kubernetes Authors. -# SPDX-License-Identifier: Apache-2.0 - -# The ThousandDeployments stress test needs to spin up 1,000 pods, so this kind -# cluster config uses 10 nodes, which each default to allowing 110 pods. -# -# The API-server and other control plane components will be -# on the control-plane node to make sure the other nodes have enough capacity. -kind: Cluster -apiVersion: kind.x-k8s.io/v1alpha4 -nodes: -- role: control-plane -- role: worker -- role: worker -- role: worker -- role: worker -- role: worker -- role: worker -- role: worker -- role: worker -- role: worker -- role: worker diff --git a/test/stress/stress_suite_test.go b/test/stress/stress_suite_test.go deleted file mode 100644 index 733da85c..00000000 --- a/test/stress/stress_suite_test.go +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package stress - -import ( - "testing" - - . "github.com/onsi/ginkgo/v2" //nolint:revive - . "github.com/onsi/gomega" //nolint:revive -) - -func TestStress(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Stress Test Suite") -} diff --git a/test/stress/stress_test.go b/test/stress/stress_test.go deleted file mode 100644 index 4688ded2..00000000 --- a/test/stress/stress_test.go +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package stress - -import ( - "context" - "time" - - "github.com/fluxcd/cli-utils/test/e2e/e2eutil" - "github.com/fluxcd/cli-utils/test/e2e/invconfig" - . "github.com/onsi/ginkgo/v2" //nolint:revive - . "github.com/onsi/gomega" //nolint:revive - "github.com/onsi/gomega/format" - v1 "k8s.io/api/core/v1" - "k8s.io/client-go/rest" - "k8s.io/klog/v2" - "k8s.io/kubectl/pkg/scheme" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/client/apiutil" -) - -// Parse optional logging flags -// Ex: ginkgo ./test/e2e/... -- -v=5 -// Allow init for e2e test (not imported by external code) -// nolint:gochecknoinits -func init() { - klog.InitFlags(nil) - klog.SetOutput(GinkgoWriter) -} - -var defaultTestTimeout = 1 * time.Hour -var defaultBeforeTestTimeout = 30 * time.Second -var defaultAfterTestTimeout = 30 * time.Second - -var c client.Client -var invConfig invconfig.InventoryConfig - -var _ = BeforeSuite(func() { - // increase from 4000 to handle long event lists - format.MaxLength = 10000 - - cfg, err := ctrl.GetConfig() - Expect(err).NotTo(HaveOccurred()) - - cfg.UserAgent = e2eutil.UserAgent("test/stress") - - if e2eutil.IsFlowControlEnabled(cfg) { - // Disable client-side throttling. - klog.V(3).Infof("Client-side throttling disabled") - cfg.QPS = -1 - cfg.Burst = -1 - } - - invConfig = invconfig.NewCustomTypeInvConfig(cfg) - - httpClient, err := rest.HTTPClientFor(cfg) - Expect(err).NotTo(HaveOccurred()) - mapper, err := apiutil.NewDynamicRESTMapper(cfg, httpClient) - Expect(err).NotTo(HaveOccurred()) - - c, err = client.New(cfg, client.Options{ - Scheme: scheme.Scheme, - Mapper: mapper, - }) - Expect(err).NotTo(HaveOccurred()) - - ctx, cancel := context.WithTimeout(context.Background(), defaultBeforeTestTimeout) - defer cancel() - e2eutil.CreateInventoryCRD(ctx, c) - Expect(ctx.Err()).To(BeNil(), "BeforeSuite context cancelled or timed out") -}) - -var _ = AfterSuite(func() { - ctx, cancel := context.WithTimeout(context.Background(), defaultAfterTestTimeout) - defer cancel() - if c != nil { - // If BeforeSuite() failed, c might be nil. Skip deletion to avoid red herring panic. - e2eutil.DeleteInventoryCRD(ctx, c) - } - Expect(ctx.Err()).To(BeNil(), "AfterSuite context cancelled or timed out") -}) - -var _ = Describe("Stress", func() { - var namespace *v1.Namespace - var inventoryName string - var ctx context.Context - var cancel context.CancelFunc - - BeforeEach(func() { - ctx, cancel = context.WithTimeout(context.Background(), defaultTestTimeout) - inventoryName = e2eutil.RandomString("test-inv-") - namespace = e2eutil.CreateRandomNamespace(ctx, c) - }) - - AfterEach(func() { - Expect(ctx.Err()).To(BeNil(), "test context cancelled or timed out") - cancel() - ctx, cancel = context.WithTimeout(context.Background(), defaultAfterTestTimeout) - defer cancel() - // clean up resources created by the tests - e2eutil.DeleteNamespace(ctx, c, namespace) - }) - - It("ThousandDeployments", func() { - thousandDeploymentsTest(ctx, c, invConfig, inventoryName, namespace.GetName()) - }) - - It("ThousandDeploymentsRetry", func() { - thousandDeploymentsRetryTest(ctx, c, invConfig, inventoryName, namespace.GetName()) - }) - - It("ThousandNamespaces", func() { - thousandNamespacesTest(ctx, c, invConfig, inventoryName, namespace.GetName()) - }) -}) diff --git a/test/stress/thousand_deployments_retry_test.go b/test/stress/thousand_deployments_retry_test.go deleted file mode 100644 index 73833ff8..00000000 --- a/test/stress/thousand_deployments_retry_test.go +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright 2022 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package stress - -import ( - "context" - "fmt" - "time" - - "github.com/fluxcd/cli-utils/pkg/apply" - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/test/e2e/e2eutil" - "github.com/fluxcd/cli-utils/test/e2e/invconfig" - . "github.com/onsi/ginkgo/v2" //nolint:revive - . "github.com/onsi/gomega" //nolint:revive - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/klog/v2" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -// thousandDeploymentsRetryTest tests one pre-existing namespace with 1,000 -// Deployments in it. The wait timeout is set too short to confirm -// reconciliation, but the apply/destroy is retried until success. -// -// The Deployments themselves are easy to get status on, but with the retrieval -// of generated resource status (ReplicaSets & Pods), this becomes expensive. -func thousandDeploymentsRetryTest(ctx context.Context, c client.Client, invConfig invconfig.InventoryConfig, inventoryName, namespaceName string) { - By("Apply LOTS of resources") - applier := invConfig.ApplierFactoryFunc() - inventoryID := fmt.Sprintf("%s-%s", inventoryName, namespaceName) - - inventoryInfo := invconfig.CreateInventoryInfo(invConfig, inventoryName, namespaceName, inventoryID) - - resources := []*unstructured.Unstructured{} - - deploymentObjTemplate := e2eutil.ManifestToUnstructured([]byte(deploymentYaml)) - deploymentObjTemplate.SetLabels(map[string]string{e2eutil.TestIDLabel: inventoryID}) - - objectCount := 1000 - - for i := 1; i <= objectCount; i++ { - deploymentObj := deploymentObjTemplate.DeepCopy() - deploymentObj.SetNamespace(namespaceName) - - // change name & selector labels to avoid overlap between deployments - name := fmt.Sprintf("pause-%d", i) - deploymentObj.SetName(name) - err := unstructured.SetNestedField(deploymentObj.Object, name, "spec", "selector", "matchLabels", "app") - Expect(err).ToNot(HaveOccurred()) - err = unstructured.SetNestedField(deploymentObj.Object, name, "spec", "template", "metadata", "labels", "app") - Expect(err).ToNot(HaveOccurred()) - - resources = append(resources, deploymentObj) - } - - defer func() { - By("Cleanup Deployments") - e2eutil.DeleteAllUnstructuredIfExists(ctx, c, deploymentObjTemplate) - }() - - startTotal := time.Now() - - var applierEvents []event.Event - - maxAttempts := 15 - reconcileTimeout := 2 * time.Minute - - for attempt := 1; attempt <= maxAttempts; attempt++ { - start := time.Now() - - applierEvents = e2eutil.RunCollect(applier.Run(ctx, inventoryInfo, resources, apply.ApplierOptions{ - // SSA reduces GET+PATCH to just PATCH, which is faster - ServerSideOptions: common.ServerSideOptions{ - ServerSideApply: true, - ForceConflicts: true, - FieldManager: "cli-utils.kubernetes.io", - }, - ReconcileTimeout: reconcileTimeout, - EmitStatusEvents: false, - })) - - duration := time.Since(start) - klog.Infof("Applier.Run execution time (attempt: %d): %v", attempt, duration) - - e2eutil.ExpectNoEventErrors(applierEvents) - - // Retry if ReconcileTimeout - retry := false - for _, e := range applierEvents { - if e.Type == event.WaitType && e.WaitEvent.Status == event.ReconcileTimeout { - retry = true - } - } - if !retry { - break - } - } - - durationTotal := time.Since(startTotal) - klog.Infof("Applier.Run total execution time (attempts: %d): %v", maxAttempts, durationTotal) - - e2eutil.ExpectNoReconcileTimeouts(applierEvents) - - By("Verify inventory created") - invConfig.InvSizeVerifyFunc(ctx, c, inventoryName, namespaceName, inventoryID, len(resources), len(resources)) - - By(fmt.Sprintf("Verify %d Deployments created", objectCount)) - e2eutil.AssertUnstructuredCount(ctx, c, deploymentObjTemplate, objectCount) - - By("Destroy LOTS of resources") - destroyer := invConfig.DestroyerFactoryFunc() - - startTotal = time.Now() - - var destroyerEvents []event.Event - - for attempt := 1; attempt <= maxAttempts; attempt++ { - start := time.Now() - - destroyerEvents = e2eutil.RunCollect(destroyer.Run(ctx, inventoryInfo, apply.DestroyerOptions{ - InventoryPolicy: inventory.PolicyAdoptIfNoInventory, - DeleteTimeout: reconcileTimeout, - })) - - duration := time.Since(start) - klog.Infof("Destroyer.Run execution time (attempt: %d): %v", attempt, duration) - - e2eutil.ExpectNoEventErrors(destroyerEvents) - - // Retry if ReconcileTimeout - retry := false - for _, e := range applierEvents { - if e.Type == event.WaitType && e.WaitEvent.Status == event.ReconcileTimeout { - retry = true - } - } - if !retry { - break - } - } - - durationTotal = time.Since(startTotal) - klog.Infof("Destroyer.Run total execution time (attempts: %d): %v", maxAttempts, durationTotal) - - e2eutil.ExpectNoReconcileTimeouts(applierEvents) - - By("Verify inventory deleted") - invConfig.InvNotExistsFunc(ctx, c, inventoryName, namespaceName, inventoryID) - - By(fmt.Sprintf("Verify %d Deployments deleted", objectCount)) - e2eutil.AssertUnstructuredCount(ctx, c, deploymentObjTemplate, 0) -} diff --git a/test/stress/thousand_deployments_test.go b/test/stress/thousand_deployments_test.go deleted file mode 100644 index 0aaffe2b..00000000 --- a/test/stress/thousand_deployments_test.go +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright 2022 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package stress - -import ( - "context" - "fmt" - "time" - - "github.com/fluxcd/cli-utils/pkg/apply" - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/test/e2e/e2eutil" - "github.com/fluxcd/cli-utils/test/e2e/invconfig" - . "github.com/onsi/ginkgo/v2" //nolint:revive - . "github.com/onsi/gomega" //nolint:revive - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/klog/v2" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -// thousandDeploymentsTest tests one pre-existing namespace with 1,000 -// Deployments in it. -// -// The Deployments themselves are easy to get status on, but with the retrieval -// of generated resource status (ReplicaSets & Pods), this becomes expensive. -func thousandDeploymentsTest(ctx context.Context, c client.Client, invConfig invconfig.InventoryConfig, inventoryName, namespaceName string) { - By("Apply LOTS of resources") - applier := invConfig.ApplierFactoryFunc() - inventoryID := fmt.Sprintf("%s-%s", inventoryName, namespaceName) - - inventoryInfo := invconfig.CreateInventoryInfo(invConfig, inventoryName, namespaceName, inventoryID) - - resources := []*unstructured.Unstructured{} - - deploymentObjTemplate := e2eutil.ManifestToUnstructured([]byte(deploymentYaml)) - deploymentObjTemplate.SetLabels(map[string]string{e2eutil.TestIDLabel: inventoryID}) - - objectCount := 1000 - - for i := 1; i <= objectCount; i++ { - deploymentObj := deploymentObjTemplate.DeepCopy() - deploymentObj.SetNamespace(namespaceName) - - // change name & selector labels to avoid overlap between deployments - name := fmt.Sprintf("pause-%d", i) - deploymentObj.SetName(name) - err := unstructured.SetNestedField(deploymentObj.Object, name, "spec", "selector", "matchLabels", "app") - Expect(err).ToNot(HaveOccurred()) - err = unstructured.SetNestedField(deploymentObj.Object, name, "spec", "template", "metadata", "labels", "app") - Expect(err).ToNot(HaveOccurred()) - - resources = append(resources, deploymentObj) - } - - defer func() { - By("Cleanup Deployments") - e2eutil.DeleteAllUnstructuredIfExists(ctx, c, deploymentObjTemplate) - }() - - start := time.Now() - - applierEvents := e2eutil.RunCollect(applier.Run(ctx, inventoryInfo, resources, apply.ApplierOptions{ - // SSA reduces GET+PATCH to just PATCH, which is faster - ServerSideOptions: common.ServerSideOptions{ - ServerSideApply: true, - ForceConflicts: true, - FieldManager: "cli-utils.kubernetes.io", - }, - ReconcileTimeout: 30 * time.Minute, - EmitStatusEvents: false, - })) - - duration := time.Since(start) - klog.Infof("Applier.Run execution time: %v", duration) - - e2eutil.ExpectNoEventErrors(applierEvents) - e2eutil.ExpectNoReconcileTimeouts(applierEvents) - - By("Verify inventory created") - invConfig.InvSizeVerifyFunc(ctx, c, inventoryName, namespaceName, inventoryID, len(resources), len(resources)) - - By(fmt.Sprintf("Verify %d Deployments created", objectCount)) - e2eutil.AssertUnstructuredCount(ctx, c, deploymentObjTemplate, objectCount) - - By("Destroy LOTS of resources") - destroyer := invConfig.DestroyerFactoryFunc() - - start = time.Now() - - destroyerEvents := e2eutil.RunCollect(destroyer.Run(ctx, inventoryInfo, apply.DestroyerOptions{ - InventoryPolicy: inventory.PolicyAdoptIfNoInventory, - DeleteTimeout: 30 * time.Minute, - })) - - duration = time.Since(start) - klog.Infof("Destroyer.Run execution time: %v", duration) - - e2eutil.ExpectNoEventErrors(destroyerEvents) - e2eutil.ExpectNoReconcileTimeouts(destroyerEvents) - - By("Verify inventory deleted") - invConfig.InvNotExistsFunc(ctx, c, inventoryName, namespaceName, inventoryID) - - By(fmt.Sprintf("Verify %d Deployments deleted", objectCount)) - e2eutil.AssertUnstructuredCount(ctx, c, deploymentObjTemplate, 0) -} diff --git a/test/stress/thousand_namespaces_test.go b/test/stress/thousand_namespaces_test.go deleted file mode 100644 index d0a30179..00000000 --- a/test/stress/thousand_namespaces_test.go +++ /dev/null @@ -1,169 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package stress - -import ( - "context" - "fmt" - "time" - - "github.com/fluxcd/cli-utils/pkg/apply" - "github.com/fluxcd/cli-utils/pkg/apply/event" - "github.com/fluxcd/cli-utils/pkg/common" - "github.com/fluxcd/cli-utils/pkg/inventory" - "github.com/fluxcd/cli-utils/test/e2e/e2eutil" - "github.com/fluxcd/cli-utils/test/e2e/invconfig" - . "github.com/onsi/ginkgo/v2" //nolint:revive - . "github.com/onsi/gomega" //nolint:revive - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/klog/v2" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -// thousandNamespacesTest tests a CRD and 1,000 new namespaces, each with -// 1 ConfigMap and 1 CronTab in it. This uses implicit dependencies and many -// namespaces with custom resources (json storage) as well and built-in -// resources (proto storage). -// -// With the StatusWatcher, this should only needs FOUR root-scoped informers -// (CRD, Namespace, ConfigMap, CronTab), For comparison, the StatusPoller used -// 2,002 LISTs for each attempt (two root-scoped and two namespace-scoped per -// namespace). -func thousandNamespacesTest(ctx context.Context, c client.Client, invConfig invconfig.InventoryConfig, inventoryName, namespaceName string) { - By("Apply LOTS of resources") - applier := invConfig.ApplierFactoryFunc() - inventoryID := fmt.Sprintf("%s-%s", inventoryName, namespaceName) - - inventoryInfo := invconfig.CreateInventoryInfo(invConfig, inventoryName, namespaceName, inventoryID) - - crdObj := e2eutil.ManifestToUnstructured([]byte(cronTabCRDYaml)) - - resources := []*unstructured.Unstructured{crdObj} - - namespaceObjTemplate := e2eutil.ManifestToUnstructured([]byte(namespaceYaml)) - namespaceObjTemplate.SetLabels(map[string]string{e2eutil.TestIDLabel: inventoryID}) - - configMapObjTemplate := e2eutil.ManifestToUnstructured([]byte(configMapYaml)) - configMapObjTemplate.SetLabels(map[string]string{e2eutil.TestIDLabel: inventoryID}) - - cronTabObjTemplate := e2eutil.ManifestToUnstructured([]byte(cronTabYaml)) - cronTabObjTemplate.SetLabels(map[string]string{e2eutil.TestIDLabel: inventoryID}) - - objectCount := 1000 - - for i := 1; i <= objectCount; i++ { - ns := fmt.Sprintf("%s-%d", namespaceName, i) - namespaceObj := namespaceObjTemplate.DeepCopy() - namespaceObj.SetName(ns) - resources = append(resources, namespaceObj) - - configMapObj := configMapObjTemplate.DeepCopy() - configMapObj.SetName(fmt.Sprintf("configmap-%d", i)) - configMapObj.SetNamespace(ns) - resources = append(resources, configMapObj) - - cronTabObj := cronTabObjTemplate.DeepCopy() - cronTabObj.SetName(fmt.Sprintf("crontab-%d", i)) - cronTabObj.SetNamespace(ns) - resources = append(resources, cronTabObj) - } - - defer func() { - // Can't delete custom resources if the CRD is still terminating - if e2eutil.UnstructuredExistsAndIsNotTerminating(ctx, c, crdObj) { - By("Cleanup CronTabs") - e2eutil.DeleteAllUnstructuredIfExists(ctx, c, cronTabObjTemplate) - By("Cleanup CRD") - e2eutil.DeleteUnstructuredIfExists(ctx, c, crdObj) - } - - By("Cleanup ConfigMaps") - e2eutil.DeleteAllUnstructuredIfExists(ctx, c, configMapObjTemplate) - By("Cleanup Namespaces") - e2eutil.DeleteAllUnstructuredIfExists(ctx, c, namespaceObjTemplate) - }() - - start := time.Now() - - applierEvents := e2eutil.RunCollect(applier.Run(ctx, inventoryInfo, resources, apply.ApplierOptions{ - // SSA reduces GET+PATCH to just PATCH, which is faster - ServerSideOptions: common.ServerSideOptions{ - ServerSideApply: true, - ForceConflicts: true, - FieldManager: "cli-utils.kubernetes.io", - }, - ReconcileTimeout: 30 * time.Minute, - EmitStatusEvents: false, - })) - - duration := time.Since(start) - klog.Infof("Applier.Run execution time: %v", duration) - - for _, e := range applierEvents { - Expect(e.ErrorEvent.Err).To(BeNil()) - } - for _, e := range applierEvents { - Expect(e.ApplyEvent.Error).To(BeNil(), "ApplyEvent: %v", e.ApplyEvent) - } - for _, e := range applierEvents { - if e.Type == event.WaitType { - Expect(e.WaitEvent.Status).To(BeElementOf(event.ReconcilePending, event.ReconcileSuccessful), "WaitEvent: %v", e.WaitEvent) - } - } - - By("Verify inventory created") - invConfig.InvSizeVerifyFunc(ctx, c, inventoryName, namespaceName, inventoryID, len(resources), len(resources)) - - By("Verify CRD created") - e2eutil.AssertUnstructuredExists(ctx, c, crdObj) - - By(fmt.Sprintf("Verify %d Namespaces created", objectCount)) - e2eutil.AssertUnstructuredCount(ctx, c, namespaceObjTemplate, objectCount) - - By(fmt.Sprintf("Verify %d ConfigMaps created", objectCount)) - e2eutil.AssertUnstructuredCount(ctx, c, configMapObjTemplate, objectCount) - - By(fmt.Sprintf("Verify %d CronTabs created", objectCount)) - e2eutil.AssertUnstructuredCount(ctx, c, cronTabObjTemplate, objectCount) - - By("Destroy LOTS of resources") - destroyer := invConfig.DestroyerFactoryFunc() - - start = time.Now() - - destroyerEvents := e2eutil.RunCollect(destroyer.Run(ctx, inventoryInfo, apply.DestroyerOptions{ - InventoryPolicy: inventory.PolicyAdoptIfNoInventory, - DeleteTimeout: 30 * time.Minute, - })) - - duration = time.Since(start) - klog.Infof("Destroyer.Run execution time: %v", duration) - - for _, e := range destroyerEvents { - Expect(e.ErrorEvent.Err).To(BeNil()) - } - for _, e := range destroyerEvents { - Expect(e.PruneEvent.Error).To(BeNil(), "PruneEvent: %v", e.PruneEvent) - } - for _, e := range destroyerEvents { - if e.Type == event.WaitType { - Expect(e.WaitEvent.Status).To(BeElementOf(event.ReconcilePending, event.ReconcileSuccessful), "WaitEvent: %v", e.WaitEvent) - } - } - - By("Verify inventory deleted") - invConfig.InvNotExistsFunc(ctx, c, inventoryName, namespaceName, inventoryID) - - By(fmt.Sprintf("Verify %d CronTabs deleted", objectCount)) - e2eutil.AssertUnstructuredCount(ctx, c, cronTabObjTemplate, 0) - - By(fmt.Sprintf("Verify %d ConfigMaps deleted", objectCount)) - e2eutil.AssertUnstructuredCount(ctx, c, configMapObjTemplate, 0) - - By(fmt.Sprintf("Verify %d Namespaces deleted", objectCount)) - e2eutil.AssertUnstructuredCount(ctx, c, namespaceObjTemplate, 0) - - By("Verify CRD deleted") - e2eutil.AssertUnstructuredDoesNotExist(ctx, c, crdObj) -}