├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ └── docker-ci.yml ├── .gitignore ├── .travis.yml ├── .travis ├── docker_push.sh └── install_operator_sdk.sh ├── Dockerfile ├── LICENSE ├── Makefile ├── PROJECT ├── README.md ├── api └── v1alpha1 │ ├── groupversion_info.go │ ├── lastpass_types.go │ ├── lastpassgroup_types.go │ ├── types.go │ └── zz_generated.deepcopy.go ├── chart ├── Chart.yaml ├── templates │ ├── deployment.yaml │ ├── edgelevel.com_lastpasses.yaml │ ├── edgelevel.com_lastpassgroups.yaml │ ├── rbac.yaml │ ├── role.yaml │ └── secret.yaml └── values.yaml ├── cmd └── main.go ├── config ├── crd │ ├── bases │ │ ├── edgelevel.com_lastpasses.yaml │ │ └── edgelevel.com_lastpassgroups.yaml │ ├── kustomization.yaml │ └── kustomizeconfig.yaml ├── default │ ├── kustomization.yaml │ ├── manager_auth_proxy_patch.yaml │ └── manager_config_patch.yaml ├── manager │ ├── kustomization.yaml │ └── manager.yaml ├── manifests │ └── kustomization.yaml ├── prometheus │ ├── kustomization.yaml │ └── monitor.yaml ├── rbac │ ├── auth_proxy_client_clusterrole.yaml │ ├── auth_proxy_role.yaml │ ├── auth_proxy_role_binding.yaml │ ├── auth_proxy_service.yaml │ ├── kustomization.yaml │ ├── lastpass_editor_role.yaml │ ├── lastpass_viewer_role.yaml │ ├── lastpassgroup_editor_role.yaml │ ├── lastpassgroup_viewer_role.yaml │ ├── leader_election_role.yaml │ ├── leader_election_role_binding.yaml │ ├── role.yaml │ ├── role_binding.yaml │ └── service_account.yaml ├── samples │ ├── kustomization.yaml │ ├── v1alpha1_lastpass.yaml │ └── v1alpha1_lastpassgroup.yaml └── scorecard │ ├── bases │ └── config.yaml │ ├── kustomization.yaml │ └── patches │ ├── basic.config.yaml │ └── olm.config.yaml ├── docs ├── dev.txt ├── golang.md ├── img │ ├── lastpass-operator.drawio │ └── reconcile-loop.png ├── lastpass.md ├── operator.md └── setup.md ├── example ├── edgelevel_v1alpha1_lastpass_cr.yaml ├── edgelevel_v1alpha1_lastpassgroup_cr.yaml ├── lastpass-alpine ├── lastpass-ubuntu ├── lpass-examples.txt └── metrics.txt ├── go.mod ├── go.sum ├── hack └── boilerplate.go.txt ├── internal └── controller │ ├── lastpass_controller.go │ ├── lastpass_controller_test.go │ ├── lastpassgroup_controller.go │ ├── lastpassgroup_controller_test.go │ └── suite_test.go ├── pkg ├── lastpass │ └── cli.go └── utils │ └── os.go ├── test ├── e2e │ ├── e2e_suite_test.go │ └── e2e_test.go └── utils │ └── utils.go └── version └── version.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | 9 | - package-ecosystem: "gomod" 10 | directory: "/" 11 | schedule: 12 | interval: "daily" 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | ci: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | 18 | - name: Setup Go 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version-file: 'go.mod' 22 | 23 | - name: Build 24 | run: make 25 | -------------------------------------------------------------------------------- /.github/workflows/docker-ci.yml: -------------------------------------------------------------------------------- 1 | name: docker-ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - 'v[0-9]+.[0-9]+.[0-9]+' 9 | 10 | env: 11 | DOCKER_REPOSITORY: edgelevel 12 | DOCKER_IMAGE_NAME: lastpass-operator 13 | 14 | jobs: 15 | docker: 16 | name: Docker 17 | runs-on: ubuntu-latest 18 | # makes sure it doesn't finish the minutes quota if stalls 19 | timeout-minutes: 10 20 | steps: 21 | - name: Checkout repository 22 | uses: actions/checkout@v4 23 | 24 | # extracts tag from ref, returns semver tag or sha suffix 25 | - name: Get Docker tag 26 | id: get-docker-tag 27 | env: 28 | GITHUB_REF: ${{ github.ref }} 29 | COMMIT_SHA: ${{ github.sha }} 30 | run: | 31 | if [[ ${{ github.ref_type }} == "tag" ]]; then 32 | echo "tag=${GITHUB_REF#refs/tags/v}" >> ${GITHUB_OUTPUT} 33 | else 34 | echo "tag=${COMMIT_SHA:0:7}" >> ${GITHUB_OUTPUT} 35 | fi 36 | 37 | - name: Output Docker tag 38 | run: echo ${{ steps.get-docker-tag.outputs.tag }} 39 | 40 | - name: Set up Docker Buildx 41 | uses: docker/setup-buildx-action@v3 42 | 43 | # gh-actions-rw 44 | - name: Login to Docker Hub 45 | uses: docker/login-action@v3 46 | with: 47 | username: ${{ secrets.DOCKERHUB_USERNAME }} 48 | password: ${{ secrets.DOCKERHUB_TOKEN }} 49 | 50 | - name: Build and push [${{ env.DOCKER_IMAGE_NAME }}] 51 | uses: docker/build-push-action@v6 52 | with: 53 | build-args: | 54 | VERSION_COMMIT=${{ steps.get-docker-tag.outputs.tag }} 55 | # if false it will only build 56 | push: true 57 | tags: | 58 | ${{ env.DOCKER_REPOSITORY }}/${{ env.DOCKER_IMAGE_NAME }}:latest 59 | ${{ env.DOCKER_REPOSITORY }}/${{ env.DOCKER_IMAGE_NAME }}:${{ steps.get-docker-tag.outputs.tag }} 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | bin 9 | 10 | # Test binary, build with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # Kubernetes Generated files - skip generated files, except for vendored files 17 | 18 | !vendor/**/zz_generated.* 19 | 20 | # editor and IDE paraphernalia 21 | .idea 22 | *.swp 23 | *.swo 24 | *~ 25 | 26 | .vscode/ 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | services: 4 | - docker 5 | 6 | script: 7 | - make test 8 | 9 | before_deploy: 10 | - .travis/install_operator_sdk.sh 11 | 12 | deploy: 13 | provider: script 14 | skip_cleanup: true 15 | # environment variables are defined in travis ui 16 | script: bash .travis/docker_push.sh $TRAVIS_TAG $DOCKER_PASSWORD 17 | on: 18 | tags: true 19 | -------------------------------------------------------------------------------- /.travis/docker_push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DOCKER_TAG=${1:?"Missing DOCKER_TAG"} 4 | DOCKER_PASSWORD=${2:?"Missing DOCKER_PASSWORD"} 5 | 6 | make docker-push tag=$DOCKER_TAG docker-password=$DOCKER_PASSWORD 7 | -------------------------------------------------------------------------------- /.travis/install_operator_sdk.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # see https://github.com/operator-framework/operator-sdk/blob/master/doc/user/install-operator-sdk.md 4 | 5 | set -e 6 | 7 | export OPERATOR_SDK_VERSION=v1.5.2 8 | 9 | echo "[+] Setup operator-sdk" 10 | 11 | curl -OJL https://github.com/operator-framework/operator-sdk/releases/download/${OPERATOR_SDK_VERSION}/operator-sdk_linux_amd64 12 | chmod +x operator-sdk_linux_amd64 13 | sudo mv operator-sdk_linux_amd64 /usr/local/bin/operator-sdk 14 | 15 | which operator-sdk 16 | operator-sdk version 17 | 18 | echo "[-] Setup operator-sdk" 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM golang:1.21 as builder 3 | 4 | WORKDIR /workspace 5 | # Copy the Go Modules manifests 6 | COPY go.mod go.mod 7 | COPY go.sum go.sum 8 | # cache deps before building and copying source so that we don't need to re-download as much 9 | # and so that source changes don't invalidate our downloaded layer 10 | RUN go mod download -x 11 | 12 | # Copy the go source 13 | COPY cmd/main.go cmd/main.go 14 | COPY api/ api/ 15 | COPY internal/controller/ internal/controller/ 16 | COPY pkg/ pkg/ 17 | COPY version/ version/ 18 | 19 | # Build 20 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -a -o manager cmd/main.go 21 | 22 | # Download lastpass-cli 23 | FROM ubuntu:22.04 as lastpass-cli 24 | 25 | ENV LASTPASS_VERSION=1.5.0 26 | 27 | # https://github.com/lastpass/lastpass-cli?tab=readme-ov-file#building 28 | RUN apt update && apt --no-install-recommends -yqq install \ 29 | bash-completion \ 30 | build-essential \ 31 | cmake \ 32 | libcurl4 \ 33 | libcurl4-openssl-dev \ 34 | libssl-dev \ 35 | libxml2 \ 36 | libxml2-dev \ 37 | libssl3 \ 38 | pkg-config \ 39 | ca-certificates \ 40 | xclip \ 41 | wget 42 | 43 | RUN wget -O lastpass-cli.tar.gz https://github.com/lastpass/lastpass-cli/releases/download/v${LASTPASS_VERSION}/lastpass-cli-${LASTPASS_VERSION}.tar.gz; \ 44 | tar -xf lastpass-cli.tar.gz 45 | 46 | WORKDIR /lastpass-cli-${LASTPASS_VERSION} 47 | 48 | RUN make && make install 49 | 50 | RUN mkdir -p /usr/lib/lastpass-cli \ 51 | && ldd /usr/bin/lpass | grep '=>' | awk '{ print $3 }' | xargs cp -t /usr/lib/lastpass-cli 52 | 53 | # Use distroless as minimal base image to package the manager binary 54 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 55 | FROM gcr.io/distroless/base:nonroot 56 | 57 | ENV USER_UID=1001 \ 58 | USER_NAME=lastpass-operator 59 | 60 | WORKDIR / 61 | COPY --from=builder /workspace/manager . 62 | COPY --from=lastpass-cli /usr/bin/lpass /usr/bin/which /usr/bin/ 63 | COPY --from=lastpass-cli /usr/lib/lastpass-cli /lib 64 | COPY --from=lastpass-cli /bin/sh /bin/echo /bin/ 65 | 66 | USER nonroot:nonroot 67 | 68 | ENTRYPOINT ["/manager"] 69 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 edgelevel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # VERSION defines the project version for the bundle. 2 | # Update this value when you upgrade the version of your project. 3 | # To re-generate a bundle for another specific version without changing the standard setup, you can: 4 | # - use the VERSION as arg of the bundle target (e.g make bundle VERSION=0.0.2) 5 | # - use environment variables to overwrite this value (e.g export VERSION=0.0.2) 6 | VERSION ?= 0.0.1 7 | 8 | # CHANNELS define the bundle channels used in the bundle. 9 | # Add a new line here if you would like to change its default config. (E.g CHANNELS = "candidate,fast,stable") 10 | # To re-generate a bundle for other specific channels without changing the standard setup, you can: 11 | # - use the CHANNELS as arg of the bundle target (e.g make bundle CHANNELS=candidate,fast,stable) 12 | # - use environment variables to overwrite this value (e.g export CHANNELS="candidate,fast,stable") 13 | ifneq ($(origin CHANNELS), undefined) 14 | BUNDLE_CHANNELS := --channels=$(CHANNELS) 15 | endif 16 | 17 | # DEFAULT_CHANNEL defines the default channel used in the bundle. 18 | # Add a new line here if you would like to change its default config. (E.g DEFAULT_CHANNEL = "stable") 19 | # To re-generate a bundle for any other default channel without changing the default setup, you can: 20 | # - use the DEFAULT_CHANNEL as arg of the bundle target (e.g make bundle DEFAULT_CHANNEL=stable) 21 | # - use environment variables to overwrite this value (e.g export DEFAULT_CHANNEL="stable") 22 | ifneq ($(origin DEFAULT_CHANNEL), undefined) 23 | BUNDLE_DEFAULT_CHANNEL := --default-channel=$(DEFAULT_CHANNEL) 24 | endif 25 | BUNDLE_METADATA_OPTS ?= $(BUNDLE_CHANNELS) $(BUNDLE_DEFAULT_CHANNEL) 26 | 27 | # IMAGE_TAG_BASE defines the docker.io namespace and part of the image name for remote images. 28 | # This variable is used to construct full image tags for bundle and catalog images. 29 | # 30 | # For example, running 'make bundle-build bundle-push catalog-build catalog-push' will build and push both 31 | # edgelevel.com/lastpass-operator-bundle:$VERSION and edgelevel.com/lastpass-operator-catalog:$VERSION. 32 | IMAGE_TAG_BASE ?= edgelevel.com/lastpass-operator 33 | 34 | # BUNDLE_IMG defines the image:tag used for the bundle. 35 | # You can use it as an arg. (E.g make bundle-build BUNDLE_IMG=/:) 36 | BUNDLE_IMG ?= $(IMAGE_TAG_BASE)-bundle:v$(VERSION) 37 | 38 | # BUNDLE_GEN_FLAGS are the flags passed to the operator-sdk generate bundle command 39 | BUNDLE_GEN_FLAGS ?= -q --overwrite --version $(VERSION) $(BUNDLE_METADATA_OPTS) 40 | 41 | # USE_IMAGE_DIGESTS defines if images are resolved via tags or digests 42 | # You can enable this value if you would like to use SHA Based Digests 43 | # To enable set flag to true 44 | USE_IMAGE_DIGESTS ?= false 45 | ifeq ($(USE_IMAGE_DIGESTS), true) 46 | BUNDLE_GEN_FLAGS += --use-image-digests 47 | endif 48 | 49 | # Set the Operator SDK version to use. By default, what is installed on the system is used. 50 | # This is useful for CI or a project to utilize a specific version of the operator-sdk toolkit. 51 | OPERATOR_SDK_VERSION ?= v1.36.0 52 | # Image URL to use all building/pushing image targets 53 | IMG ?= controller:latest 54 | # ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. 55 | ENVTEST_K8S_VERSION = 1.29.0 56 | 57 | # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) 58 | ifeq (,$(shell go env GOBIN)) 59 | GOBIN=$(shell go env GOPATH)/bin 60 | else 61 | GOBIN=$(shell go env GOBIN) 62 | endif 63 | 64 | # CONTAINER_TOOL defines the container tool to be used for building images. 65 | # Be aware that the target commands are only tested with Docker which is 66 | # scaffolded by default. However, you might want to replace it to use other 67 | # tools. (i.e. podman) 68 | CONTAINER_TOOL ?= docker 69 | 70 | # Setting SHELL to bash allows bash commands to be executed by recipes. 71 | # Options are set to exit when a recipe line exits non-zero or a piped command fails. 72 | SHELL = /usr/bin/env bash -o pipefail 73 | .SHELLFLAGS = -ec 74 | 75 | .PHONY: all 76 | all: build 77 | 78 | ##@ General 79 | 80 | # The help target prints out all targets with their descriptions organized 81 | # beneath their categories. The categories are represented by '##@' and the 82 | # target descriptions by '##'. The awk command is responsible for reading the 83 | # entire set of makefiles included in this invocation, looking for lines of the 84 | # file as xyz: ## something, and then pretty-format the target and help. Then, 85 | # if there's a line with ##@ something, that gets pretty-printed as a category. 86 | # More info on the usage of ANSI control characters for terminal formatting: 87 | # https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters 88 | # More info on the awk command: 89 | # http://linuxcommand.org/lc3_adv_awk.php 90 | 91 | .PHONY: help 92 | help: ## Display this help. 93 | @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) 94 | 95 | ##@ Development 96 | 97 | .PHONY: manifests 98 | manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. 99 | $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases 100 | 101 | .PHONY: generate 102 | generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. 103 | $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." 104 | 105 | .PHONY: fmt 106 | fmt: ## Run go fmt against code. 107 | go fmt ./... 108 | 109 | .PHONY: vet 110 | vet: ## Run go vet against code. 111 | go vet ./... 112 | 113 | .PHONY: test 114 | test: manifests generate fmt vet envtest ## Run tests. 115 | KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test $$(go list ./... | grep -v /e2e) -coverprofile cover.out 116 | 117 | # Utilize Kind or modify the e2e tests to load the image locally, enabling compatibility with other vendors. 118 | .PHONY: test-e2e # Run the e2e tests against a Kind k8s instance that is spun up. 119 | test-e2e: 120 | go test ./test/e2e/ -v -ginkgo.v 121 | 122 | .PHONY: lint 123 | lint: golangci-lint ## Run golangci-lint linter & yamllint 124 | $(GOLANGCI_LINT) run 125 | 126 | .PHONY: lint-fix 127 | lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes 128 | $(GOLANGCI_LINT) run --fix 129 | 130 | ##@ Build 131 | 132 | .PHONY: build 133 | build: manifests generate fmt vet ## Build manager binary. 134 | go build -o bin/manager cmd/main.go 135 | 136 | .PHONY: run 137 | run: manifests generate fmt vet ## Run a controller from your host. 138 | go run ./cmd/main.go 139 | 140 | # If you wish to build the manager image targeting other platforms you can use the --platform flag. 141 | # (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it. 142 | # More info: https://docs.docker.com/develop/develop-images/build_enhancements/ 143 | .PHONY: docker-build 144 | docker-build: ## Build docker image with the manager. 145 | $(CONTAINER_TOOL) build -t ${IMG} . 146 | 147 | .PHONY: docker-push 148 | docker-push: ## Push docker image with the manager. 149 | $(CONTAINER_TOOL) push ${IMG} 150 | 151 | # PLATFORMS defines the target platforms for the manager image be built to provide support to multiple 152 | # architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to: 153 | # - be able to use docker buildx. More info: https://docs.docker.com/build/buildx/ 154 | # - have enabled BuildKit. More info: https://docs.docker.com/develop/develop-images/build_enhancements/ 155 | # - be able to push the image to your registry (i.e. if you do not set a valid value via IMG=> then the export will fail) 156 | # To adequately provide solutions that are compatible with multiple platforms, you should consider using this option. 157 | PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le 158 | .PHONY: docker-buildx 159 | docker-buildx: ## Build and push docker image for the manager for cross-platform support 160 | # copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile 161 | sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross 162 | - $(CONTAINER_TOOL) buildx create --name project-v3-builder 163 | $(CONTAINER_TOOL) buildx use project-v3-builder 164 | - $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross . 165 | - $(CONTAINER_TOOL) buildx rm project-v3-builder 166 | rm Dockerfile.cross 167 | 168 | .PHONY: build-installer 169 | build-installer: manifests generate kustomize ## Generate a consolidated YAML with CRDs and deployment. 170 | mkdir -p dist 171 | cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} 172 | $(KUSTOMIZE) build config/default > dist/install.yaml 173 | 174 | ##@ Deployment 175 | 176 | ifndef ignore-not-found 177 | ignore-not-found = false 178 | endif 179 | 180 | .PHONY: install 181 | install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. 182 | $(KUSTOMIZE) build config/crd | $(KUBECTL) apply -f - 183 | 184 | .PHONY: uninstall 185 | uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. 186 | $(KUSTOMIZE) build config/crd | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - 187 | 188 | .PHONY: deploy 189 | deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. 190 | cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} 191 | $(KUSTOMIZE) build config/default | $(KUBECTL) apply -f - 192 | 193 | .PHONY: undeploy 194 | undeploy: kustomize ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. 195 | $(KUSTOMIZE) build config/default | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - 196 | 197 | ##@ Dependencies 198 | 199 | ## Location to install dependencies to 200 | LOCALBIN ?= $(shell pwd)/bin 201 | $(LOCALBIN): 202 | mkdir -p $(LOCALBIN) 203 | 204 | ## Tool Binaries 205 | KUBECTL ?= kubectl 206 | KUSTOMIZE ?= $(LOCALBIN)/kustomize-$(KUSTOMIZE_VERSION) 207 | CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen-$(CONTROLLER_TOOLS_VERSION) 208 | ENVTEST ?= $(LOCALBIN)/setup-envtest-$(ENVTEST_VERSION) 209 | GOLANGCI_LINT = $(LOCALBIN)/golangci-lint-$(GOLANGCI_LINT_VERSION) 210 | 211 | ## Tool Versions 212 | KUSTOMIZE_VERSION ?= v5.3.0 213 | CONTROLLER_TOOLS_VERSION ?= v0.14.0 214 | ENVTEST_VERSION ?= release-0.17 215 | GOLANGCI_LINT_VERSION ?= v1.57.2 216 | 217 | .PHONY: kustomize 218 | kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. 219 | $(KUSTOMIZE): $(LOCALBIN) 220 | $(call go-install-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v5,$(KUSTOMIZE_VERSION)) 221 | 222 | .PHONY: controller-gen 223 | controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. 224 | $(CONTROLLER_GEN): $(LOCALBIN) 225 | $(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen,$(CONTROLLER_TOOLS_VERSION)) 226 | 227 | .PHONY: envtest 228 | envtest: $(ENVTEST) ## Download setup-envtest locally if necessary. 229 | $(ENVTEST): $(LOCALBIN) 230 | $(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest,$(ENVTEST_VERSION)) 231 | 232 | .PHONY: golangci-lint 233 | golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary. 234 | $(GOLANGCI_LINT): $(LOCALBIN) 235 | $(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/cmd/golangci-lint,${GOLANGCI_LINT_VERSION}) 236 | 237 | # go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist 238 | # $1 - target path with name of binary (ideally with version) 239 | # $2 - package url which can be installed 240 | # $3 - specific version of package 241 | define go-install-tool 242 | @[ -f $(1) ] || { \ 243 | set -e; \ 244 | package=$(2)@$(3) ;\ 245 | echo "Downloading $${package}" ;\ 246 | GOBIN=$(LOCALBIN) go install $${package} ;\ 247 | mv "$$(echo "$(1)" | sed "s/-$(3)$$//")" $(1) ;\ 248 | } 249 | endef 250 | 251 | .PHONY: operator-sdk 252 | OPERATOR_SDK ?= $(LOCALBIN)/operator-sdk 253 | operator-sdk: ## Download operator-sdk locally if necessary. 254 | ifeq (,$(wildcard $(OPERATOR_SDK))) 255 | ifeq (, $(shell which operator-sdk 2>/dev/null)) 256 | @{ \ 257 | set -e ;\ 258 | mkdir -p $(dir $(OPERATOR_SDK)) ;\ 259 | OS=$(shell go env GOOS) && ARCH=$(shell go env GOARCH) && \ 260 | curl -sSLo $(OPERATOR_SDK) https://github.com/operator-framework/operator-sdk/releases/download/$(OPERATOR_SDK_VERSION)/operator-sdk_$${OS}_$${ARCH} ;\ 261 | chmod +x $(OPERATOR_SDK) ;\ 262 | } 263 | else 264 | OPERATOR_SDK = $(shell which operator-sdk) 265 | endif 266 | endif 267 | 268 | .PHONY: bundle 269 | bundle: manifests kustomize operator-sdk ## Generate bundle manifests and metadata, then validate generated files. 270 | $(OPERATOR_SDK) generate kustomize manifests -q 271 | cd config/manager && $(KUSTOMIZE) edit set image controller=$(IMG) 272 | $(KUSTOMIZE) build config/manifests | $(OPERATOR_SDK) generate bundle $(BUNDLE_GEN_FLAGS) 273 | $(OPERATOR_SDK) bundle validate ./bundle 274 | 275 | .PHONY: bundle-build 276 | bundle-build: ## Build the bundle image. 277 | docker build -f bundle.Dockerfile -t $(BUNDLE_IMG) . 278 | 279 | .PHONY: bundle-push 280 | bundle-push: ## Push the bundle image. 281 | $(MAKE) docker-push IMG=$(BUNDLE_IMG) 282 | 283 | .PHONY: opm 284 | OPM = $(LOCALBIN)/opm 285 | opm: ## Download opm locally if necessary. 286 | ifeq (,$(wildcard $(OPM))) 287 | ifeq (,$(shell which opm 2>/dev/null)) 288 | @{ \ 289 | set -e ;\ 290 | mkdir -p $(dir $(OPM)) ;\ 291 | OS=$(shell go env GOOS) && ARCH=$(shell go env GOARCH) && \ 292 | curl -sSLo $(OPM) https://github.com/operator-framework/operator-registry/releases/download/v1.23.0/$${OS}-$${ARCH}-opm ;\ 293 | chmod +x $(OPM) ;\ 294 | } 295 | else 296 | OPM = $(shell which opm) 297 | endif 298 | endif 299 | 300 | # A comma-separated list of bundle images (e.g. make catalog-build BUNDLE_IMGS=example.com/operator-bundle:v0.1.0,example.com/operator-bundle:v0.2.0). 301 | # These images MUST exist in a registry and be pull-able. 302 | BUNDLE_IMGS ?= $(BUNDLE_IMG) 303 | 304 | # The image tag given to the resulting catalog image (e.g. make catalog-build CATALOG_IMG=example.com/operator-catalog:v0.2.0). 305 | CATALOG_IMG ?= $(IMAGE_TAG_BASE)-catalog:v$(VERSION) 306 | 307 | # Set CATALOG_BASE_IMG to an existing catalog image tag to add $BUNDLE_IMGS to that image. 308 | ifneq ($(origin CATALOG_BASE_IMG), undefined) 309 | FROM_INDEX_OPT := --from-index $(CATALOG_BASE_IMG) 310 | endif 311 | 312 | # Build a catalog image by adding bundle images to an empty catalog using the operator package manager tool, 'opm'. 313 | # This recipe invokes 'opm' in 'semver' bundle add mode. For more information on add modes, see: 314 | # https://github.com/operator-framework/community-operators/blob/7f1438c/docs/packaging-operator.md#updating-your-existing-operator 315 | .PHONY: catalog-build 316 | catalog-build: opm ## Build a catalog image. 317 | $(OPM) index add --container-tool docker --mode semver --tag $(CATALOG_IMG) --bundles $(BUNDLE_IMGS) $(FROM_INDEX_OPT) 318 | 319 | # Push the catalog image. 320 | .PHONY: catalog-push 321 | catalog-push: ## Push a catalog image. 322 | $(MAKE) docker-push IMG=$(CATALOG_IMG) 323 | 324 | 325 | HELMIFY ?= $(LOCALBIN)/helmify 326 | 327 | .PHONY: helmify 328 | helmify: $(HELMIFY) ## Download helmify locally if necessary. 329 | $(HELMIFY): $(LOCALBIN) 330 | test -s $(LOCALBIN)/helmify || GOBIN=$(LOCALBIN) go install github.com/arttor/helmify/cmd/helmify@latest 331 | 332 | helm: manifests kustomize helmify 333 | $(KUSTOMIZE) build config/default | $(HELMIFY) -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | # Code generated by tool. DO NOT EDIT. 2 | # This file is used to track the info used to scaffold your project 3 | # and allow the plugins properly work. 4 | # More info: https://book.kubebuilder.io/reference/project-config.html 5 | domain: edgelevel.com 6 | layout: 7 | - go.kubebuilder.io/v4 8 | plugins: 9 | manifests.sdk.operatorframework.io/v2: {} 10 | scorecard.sdk.operatorframework.io/v2: {} 11 | projectName: lastpass-operator 12 | repo: github.com/edgelevel/lastpass-operator 13 | resources: 14 | - api: 15 | crdVersion: v1 16 | namespaced: true 17 | controller: true 18 | domain: edgelevel.com 19 | kind: LastPass 20 | path: github.com/edgelevel/lastpass-operator/api/v1alpha1 21 | version: v1alpha1 22 | - api: 23 | crdVersion: v1 24 | namespaced: true 25 | controller: true 26 | domain: edgelevel.com 27 | kind: LastPassGroup 28 | path: github.com/edgelevel/lastpass-operator/api/v1alpha1 29 | version: v1alpha1 30 | version: "3" 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lastpass-operator 2 | 3 | [![Build Status][build-image]][build-url] 4 | [![Docker Tag][tag-image]][tag-url] 5 | [![Docker Pulls][pulls-image]][pulls-url] 6 | 7 | [build-image]: https://github.com/edgelevel/lastpass-operator/actions/workflows/ci.yml/badge.svg 8 | [build-url]: https://github.com/edgelevel/lastpass-operator/actions/workflows/ci.yml 9 | [tag-image]: https://img.shields.io/github/tag/edgelevel/lastpass-operator.svg?style=popout-square&color=blue 10 | [tag-url]: https://hub.docker.com/r/edgelevel/lastpass-operator 11 | [pulls-image]: https://img.shields.io/docker/pulls/edgelevel/lastpass-operator?color=orange&style=flat-square 12 | [pulls-url]: https://hub.docker.com/r/edgelevel/lastpass-operator 13 | 14 | A Kubernetes Operator to manage [secrets](https://kubernetes.io/docs/concepts/configuration/secret) stored in [LastPass](https://www.lastpass.com) password manager 15 | 16 | ## How it works 17 | 18 | Suppose you have some credentials stored in LastPass 19 | ```bash 20 | $ lpass show example/my-secret --json 21 | [ 22 | { 23 | "id": "8190226423897406876", 24 | "name": "my-secret", 25 | "fullname": "example/my-secret", 26 | "username": "whoami", 27 | "password": "s3cr3t", 28 | "last_modified_gmt": "1562690587", 29 | "last_touch": "0", 30 | "group": "example", 31 | "url": "https://lastpass.com", 32 | "note": "{\"myKey\":\"myValue\"}" 33 | } 34 | ] 35 | ``` 36 | 37 | Define a `LastPass` or `LastPassGroup` [Custom Resource](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources) to automatically manage the lifecycle of your secrets in Kubernetes 38 | ```bash 39 | $ cat example/edgelevel_v1alpha1_lastpass_cr.yaml 40 | apiVersion: edgelevel.com/v1alpha1 41 | kind: LastPass 42 | metadata: 43 | name: example-lastpass 44 | spec: 45 | secretRef: 46 | group: example 47 | name: my-secret 48 | withUsername: true 49 | withPassword: true 50 | withUrl: true 51 | withNote: true 52 | syncPolicy: 53 | enabled: true 54 | refresh: 10 55 | 56 | # create a custom resource 57 | $ kubectl apply -f example/edgelevel_v1alpha1_lastpass_cr.yaml 58 | ``` 59 | _NOTE: The `LastPassGroup` custom resource will sync all the secrets in a lastpass folder to kubernetes. The lastpass group will **not** sync subfolders._ 60 | ```bash 61 | $ cat example/edgelevel_v1alpha1_lastpassgroup_cr.yaml 62 | apiVersion: edgelevel.com/v1alpha1 63 | kind: LastPassGroup 64 | metadata: 65 | name: example-lastpassgruop 66 | spec: 67 | secretRef: 68 | group: example 69 | withUsername: true 70 | withPassword: true 71 | withUrl: true 72 | withNote: true 73 | syncPolicy: 74 | enabled: true 75 | refresh: 10 76 | 77 | # create a custom resource 78 | $ kubectl apply -f example/edgelevel_v1alpha1_lastpassgroup_cr.yaml 79 | ``` 80 | 81 | The operator will take care of create native Kubernetes secrets and keep them up to date that if they change 82 | ```bash 83 | # verify 84 | $ kubectl get lastpass 85 | $ kubectl get secrets 86 | 87 | # inspect 88 | $ kubectl get secret example-lastpass-8190226423897406876 -o yaml 89 | apiVersion: v1 90 | data: 91 | NOTE: eyJteUtleSI6Im15VmFsdWUifQ== 92 | PASSWORD: czNjcjN0 93 | URL: aHR0cHM6Ly9sYXN0cGFzcy5jb20= 94 | USERNAME: d2hvYW1p 95 | kind: Secret 96 | metadata: 97 | annotations: 98 | fullname: example/my-secret 99 | group: example 100 | id: "8190226423897406876" 101 | lastModifiedGmt: "1562690587" 102 | lastTouch: "0" 103 | name: my-secret 104 | creationTimestamp: "2019-07-09T15:00:13Z" 105 | labels: 106 | app: lastpass-operator 107 | name: example-lastpass-8190226423897406876 108 | namespace: default 109 | ownerReferences: 110 | - apiVersion: edgelevel.com/v1alpha1 111 | blockOwnerDeletion: true 112 | controller: true 113 | kind: LastPass 114 | name: example-lastpass 115 | uid: 0687d5a7-5f02-4ee4-a6c4-011c734f4149 116 | resourceVersion: "113312" 117 | selfLink: /api/v1/namespaces/default/secrets/example-lastpass-8190226423897406876 118 | uid: 382008d2-8999-444d-86c8-e4f29eecbe9f 119 | type: Opaque 120 | 121 | # check values 122 | $ echo 'czNjcjN0' | base64 --decode 123 | s3cr3t 124 | $ echo 'eyJteUtleSI6Im15VmFsdWUifQ==' | base64 --decode | jq -c 125 | {"myKey":"myValue"} 126 | ``` 127 | 128 | Metrics are exposed by default in Prometheus [format](https://prometheus.io/docs/instrumenting/exposition_formats), see an [example](example/metrics.txt) 129 | ```bash 130 | # port forward 131 | kubectl port-forward service/lastpass-operator -n lastpass 8080:8383 132 | 133 | # request metrics 134 | http :8080/metrics 135 | ``` 136 | 137 | ## Considerations 138 | 139 | * If you want to understand how the operator works, you should have a look at the `Reconcile` method defined in [lastpass_controller](https://github.com/edgelevel/lastpass-operator/blob/master/pkg/controller/lastpass/lastpass_controller.go) and at the [CustomResourceDefinition](https://github.com/edgelevel/lastpass-operator/blob/master/chart/templates/crd.yaml) 140 | * The diagram below explains the core logic of the reconcile loop 141 | 142 |

143 | reconcile-loop 144 |

145 | 146 | * The recommended way to install the operator in a cluster is by applying the provided Helm [chart](chart/) 147 | * *TODO for a working example you should have a look at [niqdev/do-k8s](https://github.com/niqdev/do-k8s)* 148 | * This operator has been mainly developed to simplify the secret management of low security environments, if you are a security paranoid you should audit this project and assess if it meets the security standard of your organization 149 | * The operator, for obvious reasons, won't work if you have MFA enabled on LastPass or your credentials "Require Password Reprompt" 150 | * Once [this](https://github.com/argoproj/argo-cd/issues/1786) Argo CD feature will be implemented it should allow to bind secrets directly to an `Application` 151 | 152 | ## Development 153 | 154 | * [Setup](docs/setup.md) 155 | * [golang](docs/golang.md) 156 | * [operator-sdk](docs/operator.md) 157 | * [LastPass](docs/lastpass.md) 158 | 159 | ```bash 160 | # download source 161 | mkdir -p $GOPATH/src/github.com/edgelevel && cd $_ 162 | git clone git@github.com:edgelevel/lastpass-operator.git 163 | cd lastpass-operator 164 | 165 | # install operator-sdk 166 | .travis/install_operator_sdk.sh 167 | 168 | # install dependencies 169 | go mod download -x 170 | ``` 171 | 172 | Run locally outside the cluster on [minkube](https://github.com/kubernetes/minikube) 173 | ```bash 174 | # requires virtualbox 175 | minikube start 176 | 177 | # run locally 178 | export OPERATOR_NAME=lastpass-operator 179 | export LASTPASS_USERNAME=myUsername 180 | export LASTPASS_PASSWORD=myPassword 181 | 182 | # Install CRDs into cluster 183 | make install 184 | 185 | # Start lastpass operator 186 | make run 187 | 188 | # Alternatively you can install and run with 189 | make install run 190 | 191 | ``` 192 | 193 | Run as a Deployment inside the cluster 194 | ```bash 195 | # apply chart 196 | helm template \ 197 | --values chart/values.yaml \ 198 | --set lastpass.username="myUsername" \ 199 | --set lastpass.password="myPassword" \ 200 | chart/ | kubectl apply -n lastpass -f - 201 | ``` 202 | 203 | Debug issues 204 | ```bash 205 | # verify logs 206 | kubectl logs deployment/lastpass-operator -n lastpass -f 207 | ``` 208 | 209 | Publish a new version on [DockerHub](https://hub.docker.com/r/edgelevel/lastpass-operator) 210 | ```bash 211 | # build and publish manually (unsafe) 212 | make docker-build IMG=edgelevel/lastpass-operator:X.Y.Z 213 | make docker-push IMG=edgelevel/lastpass-operator:X.Y.Z 214 | 215 | # build and publish using travis 216 | git tag vX.Y.Z 217 | git push origin --tags 218 | ``` 219 | 220 | --- 221 | 222 | TODO 223 | * [ ] add extra Prometheus [metrics](https://prometheus.io/docs/guides/go-application) 224 | * [ ] publish to [OperatorHub](https://operatorhub.io/contribute) 225 | -------------------------------------------------------------------------------- /api/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | // Package v1alpha1 contains API Schema definitions for the edgelevel.com v1alpha1 API group 2 | // +kubebuilder:object:generate=true 3 | // +groupName=edgelevel.com 4 | package v1alpha1 5 | 6 | import ( 7 | "k8s.io/apimachinery/pkg/runtime/schema" 8 | "sigs.k8s.io/controller-runtime/pkg/scheme" 9 | ) 10 | 11 | var ( 12 | // GroupVersion is group version used to register these objects 13 | GroupVersion = schema.GroupVersion{Group: "edgelevel.com", Version: "v1alpha1"} 14 | 15 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 16 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 17 | 18 | // AddToScheme adds the types in this group-version to the given scheme. 19 | AddToScheme = SchemeBuilder.AddToScheme 20 | ) 21 | -------------------------------------------------------------------------------- /api/v1alpha1/lastpass_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | ) 22 | 23 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 24 | // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. 25 | 26 | // LastPassSpec defines the desired state of LastPass 27 | // +k8s:openapi-gen=true 28 | // +kubebuilder:resource:path=lastpass,scope=Namespaced 29 | type LastPassSpec struct { 30 | // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster 31 | // Important: Run "make" to regenerate code after modifying this file 32 | 33 | SecretRef SecretRef `json:"secretRef,required"` 34 | SyncPolicy SyncPolicy `json:"syncPolicy,omitempty"` 35 | } 36 | 37 | type SecretRef struct { 38 | Group string `json:"group,omitempty"` 39 | Name string `json:"name,required"` 40 | WithUsername bool `json:"withUsername,omitempty"` 41 | WithPassword bool `json:"withPassword,omitempty"` 42 | WithUrl bool `json:"withUrl,omitempty"` 43 | WithNote bool `json:"withNote,omitempty"` 44 | } 45 | 46 | // LastPassStatus defines the observed state of LastPass 47 | // +k8s:openapi-gen=true 48 | type LastPassStatus struct { 49 | // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster 50 | // Important: Run "make" to regenerate code after modifying this file 51 | } 52 | 53 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 54 | //+kubebuilder:object:root=true 55 | //+kubebuilder:subresource:status 56 | 57 | // LastPass is the Schema for the lastpasses API 58 | // +k8s:openapi-gen=true 59 | // +kubebuilder:subresource:status 60 | type LastPass struct { 61 | metav1.TypeMeta `json:",inline"` 62 | metav1.ObjectMeta `json:"metadata,omitempty"` 63 | 64 | Spec LastPassSpec `json:"spec,omitempty"` 65 | Status LastPassStatus `json:"status,omitempty"` 66 | } 67 | 68 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 69 | //+kubebuilder:object:root=true 70 | 71 | // LastPassList contains a list of LastPass 72 | type LastPassList struct { 73 | metav1.TypeMeta `json:",inline"` 74 | metav1.ListMeta `json:"metadata,omitempty"` 75 | Items []LastPass `json:"items"` 76 | } 77 | 78 | func init() { 79 | SchemeBuilder.Register(&LastPass{}, &LastPassList{}) 80 | } 81 | -------------------------------------------------------------------------------- /api/v1alpha1/lastpassgroup_types.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | ) 6 | 7 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 8 | // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. 9 | 10 | // LastPassGroupSpec defines the desired state of LastPassGroup 11 | type LastPassGroupSpec struct { 12 | // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster 13 | // Important: Run "make" to regenerate code after modifying this file 14 | 15 | GroupRef GroupRef `json:"groupRef,required"` 16 | SyncPolicy SyncPolicy `json:"syncPolicy,omitempty"` 17 | } 18 | 19 | type GroupRef struct { 20 | Group string `json:"group,omitempty"` 21 | WithUsername bool `json:"withUsername,omitempty"` 22 | WithPassword bool `json:"withPassword,omitempty"` 23 | WithUrl bool `json:"withUrl,omitempty"` 24 | WithNote bool `json:"withNote,omitempty"` 25 | } 26 | 27 | // LastPassGroupStatus defines the observed state of LastPassGroup 28 | type LastPassGroupStatus struct { 29 | // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster 30 | // Important: Run "make" to regenerate code after modifying this file 31 | } 32 | 33 | //+kubebuilder:object:root=true 34 | //+kubebuilder:subresource:status 35 | 36 | // LastPassGroup is the Schema for the lastpassgroups API 37 | type LastPassGroup struct { 38 | metav1.TypeMeta `json:",inline"` 39 | metav1.ObjectMeta `json:"metadata,omitempty"` 40 | 41 | Spec LastPassGroupSpec `json:"spec,omitempty"` 42 | Status LastPassGroupStatus `json:"status,omitempty"` 43 | } 44 | 45 | //+kubebuilder:object:root=true 46 | 47 | // LastPassGroupList contains a list of LastPassGroup 48 | type LastPassGroupList struct { 49 | metav1.TypeMeta `json:",inline"` 50 | metav1.ListMeta `json:"metadata,omitempty"` 51 | Items []LastPassGroup `json:"items"` 52 | } 53 | 54 | func init() { 55 | SchemeBuilder.Register(&LastPassGroup{}, &LastPassGroupList{}) 56 | } 57 | -------------------------------------------------------------------------------- /api/v1alpha1/types.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import "time" 4 | 5 | type SyncPolicy struct { 6 | Enabled bool `json:"enabled,required"` 7 | Refresh time.Duration `json:"refresh,required"` 8 | } 9 | -------------------------------------------------------------------------------- /api/v1alpha1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | 3 | // Code generated by controller-gen. DO NOT EDIT. 4 | 5 | package v1alpha1 6 | 7 | import ( 8 | runtime "k8s.io/apimachinery/pkg/runtime" 9 | ) 10 | 11 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 12 | func (in *GroupRef) DeepCopyInto(out *GroupRef) { 13 | *out = *in 14 | } 15 | 16 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GroupRef. 17 | func (in *GroupRef) DeepCopy() *GroupRef { 18 | if in == nil { 19 | return nil 20 | } 21 | out := new(GroupRef) 22 | in.DeepCopyInto(out) 23 | return out 24 | } 25 | 26 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 27 | func (in *LastPass) DeepCopyInto(out *LastPass) { 28 | *out = *in 29 | out.TypeMeta = in.TypeMeta 30 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 31 | out.Spec = in.Spec 32 | out.Status = in.Status 33 | } 34 | 35 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LastPass. 36 | func (in *LastPass) DeepCopy() *LastPass { 37 | if in == nil { 38 | return nil 39 | } 40 | out := new(LastPass) 41 | in.DeepCopyInto(out) 42 | return out 43 | } 44 | 45 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 46 | func (in *LastPass) DeepCopyObject() runtime.Object { 47 | if c := in.DeepCopy(); c != nil { 48 | return c 49 | } 50 | return nil 51 | } 52 | 53 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 54 | func (in *LastPassGroup) DeepCopyInto(out *LastPassGroup) { 55 | *out = *in 56 | out.TypeMeta = in.TypeMeta 57 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 58 | out.Spec = in.Spec 59 | out.Status = in.Status 60 | } 61 | 62 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LastPassGroup. 63 | func (in *LastPassGroup) DeepCopy() *LastPassGroup { 64 | if in == nil { 65 | return nil 66 | } 67 | out := new(LastPassGroup) 68 | in.DeepCopyInto(out) 69 | return out 70 | } 71 | 72 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 73 | func (in *LastPassGroup) DeepCopyObject() runtime.Object { 74 | if c := in.DeepCopy(); c != nil { 75 | return c 76 | } 77 | return nil 78 | } 79 | 80 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 81 | func (in *LastPassGroupList) DeepCopyInto(out *LastPassGroupList) { 82 | *out = *in 83 | out.TypeMeta = in.TypeMeta 84 | in.ListMeta.DeepCopyInto(&out.ListMeta) 85 | if in.Items != nil { 86 | in, out := &in.Items, &out.Items 87 | *out = make([]LastPassGroup, len(*in)) 88 | for i := range *in { 89 | (*in)[i].DeepCopyInto(&(*out)[i]) 90 | } 91 | } 92 | } 93 | 94 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LastPassGroupList. 95 | func (in *LastPassGroupList) DeepCopy() *LastPassGroupList { 96 | if in == nil { 97 | return nil 98 | } 99 | out := new(LastPassGroupList) 100 | in.DeepCopyInto(out) 101 | return out 102 | } 103 | 104 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 105 | func (in *LastPassGroupList) DeepCopyObject() runtime.Object { 106 | if c := in.DeepCopy(); c != nil { 107 | return c 108 | } 109 | return nil 110 | } 111 | 112 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 113 | func (in *LastPassGroupSpec) DeepCopyInto(out *LastPassGroupSpec) { 114 | *out = *in 115 | out.GroupRef = in.GroupRef 116 | out.SyncPolicy = in.SyncPolicy 117 | } 118 | 119 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LastPassGroupSpec. 120 | func (in *LastPassGroupSpec) DeepCopy() *LastPassGroupSpec { 121 | if in == nil { 122 | return nil 123 | } 124 | out := new(LastPassGroupSpec) 125 | in.DeepCopyInto(out) 126 | return out 127 | } 128 | 129 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 130 | func (in *LastPassGroupStatus) DeepCopyInto(out *LastPassGroupStatus) { 131 | *out = *in 132 | } 133 | 134 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LastPassGroupStatus. 135 | func (in *LastPassGroupStatus) DeepCopy() *LastPassGroupStatus { 136 | if in == nil { 137 | return nil 138 | } 139 | out := new(LastPassGroupStatus) 140 | in.DeepCopyInto(out) 141 | return out 142 | } 143 | 144 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 145 | func (in *LastPassList) DeepCopyInto(out *LastPassList) { 146 | *out = *in 147 | out.TypeMeta = in.TypeMeta 148 | in.ListMeta.DeepCopyInto(&out.ListMeta) 149 | if in.Items != nil { 150 | in, out := &in.Items, &out.Items 151 | *out = make([]LastPass, len(*in)) 152 | for i := range *in { 153 | (*in)[i].DeepCopyInto(&(*out)[i]) 154 | } 155 | } 156 | } 157 | 158 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LastPassList. 159 | func (in *LastPassList) DeepCopy() *LastPassList { 160 | if in == nil { 161 | return nil 162 | } 163 | out := new(LastPassList) 164 | in.DeepCopyInto(out) 165 | return out 166 | } 167 | 168 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 169 | func (in *LastPassList) DeepCopyObject() runtime.Object { 170 | if c := in.DeepCopy(); c != nil { 171 | return c 172 | } 173 | return nil 174 | } 175 | 176 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 177 | func (in *LastPassSpec) DeepCopyInto(out *LastPassSpec) { 178 | *out = *in 179 | out.SecretRef = in.SecretRef 180 | out.SyncPolicy = in.SyncPolicy 181 | } 182 | 183 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LastPassSpec. 184 | func (in *LastPassSpec) DeepCopy() *LastPassSpec { 185 | if in == nil { 186 | return nil 187 | } 188 | out := new(LastPassSpec) 189 | in.DeepCopyInto(out) 190 | return out 191 | } 192 | 193 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 194 | func (in *LastPassStatus) DeepCopyInto(out *LastPassStatus) { 195 | *out = *in 196 | } 197 | 198 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LastPassStatus. 199 | func (in *LastPassStatus) DeepCopy() *LastPassStatus { 200 | if in == nil { 201 | return nil 202 | } 203 | out := new(LastPassStatus) 204 | in.DeepCopyInto(out) 205 | return out 206 | } 207 | 208 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 209 | func (in *SecretRef) DeepCopyInto(out *SecretRef) { 210 | *out = *in 211 | } 212 | 213 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretRef. 214 | func (in *SecretRef) DeepCopy() *SecretRef { 215 | if in == nil { 216 | return nil 217 | } 218 | out := new(SecretRef) 219 | in.DeepCopyInto(out) 220 | return out 221 | } 222 | 223 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 224 | func (in *SyncPolicy) DeepCopyInto(out *SyncPolicy) { 225 | *out = *in 226 | } 227 | 228 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SyncPolicy. 229 | func (in *SyncPolicy) DeepCopy() *SyncPolicy { 230 | if in == nil { 231 | return nil 232 | } 233 | out := new(SyncPolicy) 234 | in.DeepCopyInto(out) 235 | return out 236 | } 237 | -------------------------------------------------------------------------------- /chart/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | name: lastpass-operator 3 | version: 1.0.1 4 | -------------------------------------------------------------------------------- /chart/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: {{ .Values.name }} 6 | spec: 7 | replicas: {{ .Values.replicas }} 8 | selector: 9 | matchLabels: 10 | name: {{ .Values.name }} 11 | template: 12 | metadata: 13 | labels: 14 | name: {{ .Values.name }} 15 | spec: 16 | serviceAccountName: {{ .Values.name }} 17 | containers: 18 | - name: {{ .Values.name }} 19 | image: {{ printf "%s:%s" .Values.image.repository .Values.image.tag | quote }} 20 | imagePullPolicy: {{ .Values.image.pullPolicy }} 21 | {{- if ne .Values.secretNameTemplate "" }} 22 | args: 23 | - --secret-name-template={{ .Values.secretNameTemplate }} 24 | {{- end }} 25 | env: 26 | # default 27 | - name: WATCH_NAMESPACE 28 | valueFrom: 29 | fieldRef: 30 | fieldPath: metadata.namespace 31 | - name: POD_NAME 32 | valueFrom: 33 | fieldRef: 34 | fieldPath: metadata.name 35 | - name: OPERATOR_NAME 36 | value: {{ .Values.name }} 37 | # custom 38 | - name: LASTPASS_USERNAME 39 | valueFrom: 40 | secretKeyRef: 41 | name: lastpass-master-secret 42 | key: username 43 | - name: LASTPASS_PASSWORD 44 | valueFrom: 45 | secretKeyRef: 46 | name: lastpass-master-secret 47 | key: password 48 | -------------------------------------------------------------------------------- /chart/templates/edgelevel.com_lastpasses.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.14.0 7 | name: lastpasses.edgelevel.com 8 | spec: 9 | group: edgelevel.com 10 | names: 11 | kind: LastPass 12 | listKind: LastPassList 13 | plural: lastpasses 14 | singular: lastpass 15 | scope: Namespaced 16 | versions: 17 | - name: v1alpha1 18 | schema: 19 | openAPIV3Schema: 20 | description: LastPass is the Schema for the lastpasses API 21 | properties: 22 | apiVersion: 23 | description: |- 24 | APIVersion defines the versioned schema of this representation of an object. 25 | Servers should convert recognized schemas to the latest internal value, and 26 | may reject unrecognized values. 27 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 28 | type: string 29 | kind: 30 | description: |- 31 | Kind is a string value representing the REST resource this object represents. 32 | Servers may infer this from the endpoint the client submits requests to. 33 | Cannot be updated. 34 | In CamelCase. 35 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 36 | type: string 37 | metadata: 38 | type: object 39 | spec: 40 | description: LastPassSpec defines the desired state of LastPass 41 | properties: 42 | secretRef: 43 | properties: 44 | group: 45 | type: string 46 | name: 47 | type: string 48 | withNote: 49 | type: boolean 50 | withPassword: 51 | type: boolean 52 | withUrl: 53 | type: boolean 54 | withUsername: 55 | type: boolean 56 | required: 57 | - name 58 | type: object 59 | syncPolicy: 60 | properties: 61 | enabled: 62 | type: boolean 63 | refresh: 64 | description: |- 65 | A Duration represents the elapsed time between two instants 66 | as an int64 nanosecond count. The representation limits the 67 | largest representable duration to approximately 290 years. 68 | format: int64 69 | type: integer 70 | required: 71 | - enabled 72 | - refresh 73 | type: object 74 | required: 75 | - secretRef 76 | type: object 77 | status: 78 | description: LastPassStatus defines the observed state of LastPass 79 | type: object 80 | type: object 81 | served: true 82 | storage: true 83 | subresources: 84 | status: {} 85 | -------------------------------------------------------------------------------- /chart/templates/edgelevel.com_lastpassgroups.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.14.0 7 | name: lastpassgroups.edgelevel.com 8 | spec: 9 | group: edgelevel.com 10 | names: 11 | kind: LastPassGroup 12 | listKind: LastPassGroupList 13 | plural: lastpassgroups 14 | singular: lastpassgroup 15 | scope: Namespaced 16 | versions: 17 | - name: v1alpha1 18 | schema: 19 | openAPIV3Schema: 20 | description: LastPassGroup is the Schema for the lastpassgroups API 21 | properties: 22 | apiVersion: 23 | description: |- 24 | APIVersion defines the versioned schema of this representation of an object. 25 | Servers should convert recognized schemas to the latest internal value, and 26 | may reject unrecognized values. 27 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 28 | type: string 29 | kind: 30 | description: |- 31 | Kind is a string value representing the REST resource this object represents. 32 | Servers may infer this from the endpoint the client submits requests to. 33 | Cannot be updated. 34 | In CamelCase. 35 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 36 | type: string 37 | metadata: 38 | type: object 39 | spec: 40 | description: LastPassGroupSpec defines the desired state of LastPassGroup 41 | properties: 42 | groupRef: 43 | properties: 44 | group: 45 | type: string 46 | withNote: 47 | type: boolean 48 | withPassword: 49 | type: boolean 50 | withUrl: 51 | type: boolean 52 | withUsername: 53 | type: boolean 54 | type: object 55 | syncPolicy: 56 | properties: 57 | enabled: 58 | type: boolean 59 | refresh: 60 | description: |- 61 | A Duration represents the elapsed time between two instants 62 | as an int64 nanosecond count. The representation limits the 63 | largest representable duration to approximately 290 years. 64 | format: int64 65 | type: integer 66 | required: 67 | - enabled 68 | - refresh 69 | type: object 70 | required: 71 | - groupRef 72 | type: object 73 | status: 74 | description: LastPassGroupStatus defines the observed state of LastPassGroup 75 | type: object 76 | type: object 77 | served: true 78 | storage: true 79 | subresources: 80 | status: {} 81 | -------------------------------------------------------------------------------- /chart/templates/rbac.yaml: -------------------------------------------------------------------------------- 1 | 2 | 3 | --- 4 | apiVersion: v1 5 | kind: ServiceAccount 6 | metadata: 7 | name: {{ .Values.name }} 8 | 9 | --- 10 | kind: ClusterRoleBinding 11 | apiVersion: rbac.authorization.k8s.io/v1 12 | metadata: 13 | name: {{ .Values.name }} 14 | subjects: 15 | - kind: ServiceAccount 16 | name: {{ .Values.name }} 17 | namespace: {{ .Release.Namespace }} 18 | roleRef: 19 | kind: ClusterRole 20 | name: {{ .Values.name }} 21 | apiGroup: rbac.authorization.k8s.io 22 | -------------------------------------------------------------------------------- /chart/templates/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: '{{ .Values.name }}' 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - secrets 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - edgelevel.com 21 | resources: 22 | - lastpasses 23 | verbs: 24 | - create 25 | - delete 26 | - get 27 | - list 28 | - patch 29 | - update 30 | - watch 31 | - apiGroups: 32 | - edgelevel.com 33 | resources: 34 | - lastpasses/status 35 | verbs: 36 | - get 37 | - patch 38 | - update 39 | -------------------------------------------------------------------------------- /chart/templates/secret.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: lastpass-master-secret 6 | type: Opaque 7 | stringData: 8 | username: {{ .Values.lastpass.username }} 9 | password: {{ .Values.lastpass.password }} 10 | -------------------------------------------------------------------------------- /chart/values.yaml: -------------------------------------------------------------------------------- 1 | name: lastpass-operator 2 | 3 | image: 4 | repository: edgelevel/lastpass-operator 5 | tag: latest 6 | pullPolicy: Never 7 | 8 | replicas: 1 9 | 10 | lastpass: 11 | username: "" 12 | password: "" 13 | 14 | secretNameTemplate: "" 15 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | goruntime "runtime" 8 | "text/template" 9 | 10 | "k8s.io/apimachinery/pkg/runtime" 11 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 12 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 13 | _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" 14 | ctrl "sigs.k8s.io/controller-runtime" 15 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 16 | metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" 17 | 18 | edgelevelcomv1alpha1 "github.com/edgelevel/lastpass-operator/api/v1alpha1" 19 | "github.com/edgelevel/lastpass-operator/internal/controller" 20 | "github.com/edgelevel/lastpass-operator/version" 21 | "github.com/rs/zerolog/log" 22 | // +kubebuilder:scaffold:imports 23 | ) 24 | 25 | var ( 26 | scheme = runtime.NewScheme() 27 | setupLog = ctrl.Log.WithName("setup") 28 | ) 29 | 30 | func init() { 31 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 32 | 33 | utilruntime.Must(edgelevelcomv1alpha1.AddToScheme(scheme)) 34 | // +kubebuilder:scaffold:scheme 35 | } 36 | 37 | func printVersion() { 38 | log.Info().Msgf("Go Version: %s", goruntime.Version()) 39 | log.Info().Msgf(fmt.Sprintf("Go OS/Arch: %s/%s", goruntime.GOOS, goruntime.GOARCH)) 40 | log.Info().Msgf(fmt.Sprintf("Version of lastpass-operator: %s", version.Version)) 41 | } 42 | 43 | func main() { 44 | var metricsAddr string 45 | var secretNameTemplateStr string 46 | var enableLeaderElection bool 47 | flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.") 48 | flag.StringVar(&secretNameTemplateStr, "secret-name-template", "{{.LastPass.ObjectMeta.Name}}-{{.LastPassSecret.ID}}", "The go template to generate secrets name from LastPass and LastPassSecret objects.") 49 | flag.BoolVar(&enableLeaderElection, "enable-leader-election", false, 50 | "Enable leader election for controller manager. "+ 51 | "Enabling this will ensure there is only one active controller manager.") 52 | flag.Parse() 53 | 54 | ctrl.SetLogger(zap.New(zap.UseDevMode(true))) 55 | 56 | printVersion() 57 | 58 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 59 | Scheme: scheme, 60 | Metrics: metricsserver.Options{ 61 | BindAddress: metricsAddr, 62 | }, 63 | LeaderElection: enableLeaderElection, 64 | LeaderElectionID: "e9330328.edgelevel.com", 65 | }) 66 | if err != nil { 67 | setupLog.Error(err, "unable to start manager") 68 | os.Exit(1) 69 | } 70 | 71 | secretNameTemplate, err := template.New("secretName").Parse(secretNameTemplateStr) 72 | if err != nil { 73 | panic(err) 74 | } 75 | 76 | if err = (&controller.LastPassReconciler{ 77 | Client: mgr.GetClient(), 78 | Log: ctrl.Log.WithName("controllers").WithName("LastPass"), 79 | Scheme: mgr.GetScheme(), 80 | SecretNameTemplate: secretNameTemplate, 81 | }).SetupWithManager(mgr); err != nil { 82 | setupLog.Error(err, "unable to create controller", "controller", "LastPass") 83 | os.Exit(1) 84 | } 85 | if err = (&controller.LastPassGroupReconciler{ 86 | Client: mgr.GetClient(), 87 | Log: ctrl.Log.WithName("controllers").WithName("LastPass"), 88 | Scheme: mgr.GetScheme(), 89 | SecretNameTemplate: secretNameTemplate, 90 | }).SetupWithManager(mgr); err != nil { 91 | setupLog.Error(err, "unable to create controller", "controller", "LastPassGroup") 92 | os.Exit(1) 93 | } 94 | // +kubebuilder:scaffold:builder 95 | 96 | setupLog.Info("starting manager") 97 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 98 | setupLog.Error(err, "problem running manager") 99 | os.Exit(1) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /config/crd/bases/edgelevel.com_lastpasses.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.14.0 7 | name: lastpasses.edgelevel.com 8 | spec: 9 | group: edgelevel.com 10 | names: 11 | kind: LastPass 12 | listKind: LastPassList 13 | plural: lastpasses 14 | singular: lastpass 15 | scope: Namespaced 16 | versions: 17 | - name: v1alpha1 18 | schema: 19 | openAPIV3Schema: 20 | description: LastPass is the Schema for the lastpasses API 21 | properties: 22 | apiVersion: 23 | description: |- 24 | APIVersion defines the versioned schema of this representation of an object. 25 | Servers should convert recognized schemas to the latest internal value, and 26 | may reject unrecognized values. 27 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 28 | type: string 29 | kind: 30 | description: |- 31 | Kind is a string value representing the REST resource this object represents. 32 | Servers may infer this from the endpoint the client submits requests to. 33 | Cannot be updated. 34 | In CamelCase. 35 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 36 | type: string 37 | metadata: 38 | type: object 39 | spec: 40 | description: LastPassSpec defines the desired state of LastPass 41 | properties: 42 | secretRef: 43 | properties: 44 | group: 45 | type: string 46 | name: 47 | type: string 48 | withNote: 49 | type: boolean 50 | withPassword: 51 | type: boolean 52 | withUrl: 53 | type: boolean 54 | withUsername: 55 | type: boolean 56 | required: 57 | - name 58 | type: object 59 | syncPolicy: 60 | properties: 61 | enabled: 62 | type: boolean 63 | refresh: 64 | description: |- 65 | A Duration represents the elapsed time between two instants 66 | as an int64 nanosecond count. The representation limits the 67 | largest representable duration to approximately 290 years. 68 | format: int64 69 | type: integer 70 | required: 71 | - enabled 72 | - refresh 73 | type: object 74 | required: 75 | - secretRef 76 | type: object 77 | status: 78 | description: LastPassStatus defines the observed state of LastPass 79 | type: object 80 | type: object 81 | served: true 82 | storage: true 83 | subresources: 84 | status: {} 85 | -------------------------------------------------------------------------------- /config/crd/bases/edgelevel.com_lastpassgroups.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.14.0 7 | name: lastpassgroups.edgelevel.com 8 | spec: 9 | group: edgelevel.com 10 | names: 11 | kind: LastPassGroup 12 | listKind: LastPassGroupList 13 | plural: lastpassgroups 14 | singular: lastpassgroup 15 | scope: Namespaced 16 | versions: 17 | - name: v1alpha1 18 | schema: 19 | openAPIV3Schema: 20 | description: LastPassGroup is the Schema for the lastpassgroups API 21 | properties: 22 | apiVersion: 23 | description: |- 24 | APIVersion defines the versioned schema of this representation of an object. 25 | Servers should convert recognized schemas to the latest internal value, and 26 | may reject unrecognized values. 27 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 28 | type: string 29 | kind: 30 | description: |- 31 | Kind is a string value representing the REST resource this object represents. 32 | Servers may infer this from the endpoint the client submits requests to. 33 | Cannot be updated. 34 | In CamelCase. 35 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 36 | type: string 37 | metadata: 38 | type: object 39 | spec: 40 | description: LastPassGroupSpec defines the desired state of LastPassGroup 41 | properties: 42 | groupRef: 43 | properties: 44 | group: 45 | type: string 46 | withNote: 47 | type: boolean 48 | withPassword: 49 | type: boolean 50 | withUrl: 51 | type: boolean 52 | withUsername: 53 | type: boolean 54 | type: object 55 | syncPolicy: 56 | properties: 57 | enabled: 58 | type: boolean 59 | refresh: 60 | description: |- 61 | A Duration represents the elapsed time between two instants 62 | as an int64 nanosecond count. The representation limits the 63 | largest representable duration to approximately 290 years. 64 | format: int64 65 | type: integer 66 | required: 67 | - enabled 68 | - refresh 69 | type: object 70 | required: 71 | - groupRef 72 | type: object 73 | status: 74 | description: LastPassGroupStatus defines the observed state of LastPassGroup 75 | type: object 76 | type: object 77 | served: true 78 | storage: true 79 | subresources: 80 | status: {} 81 | -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # This kustomization.yaml is not intended to be run by itself, 2 | # since it depends on service name and namespace that are out of this kustomize package. 3 | # It should be run by config/default 4 | resources: 5 | - bases/edgelevel.com_lastpasses.yaml 6 | - bases/edgelevel.com_lastpassgroups.yaml 7 | #+kubebuilder:scaffold:crdkustomizeresource 8 | 9 | patches: 10 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 11 | # patches here are for enabling the conversion webhook for each CRD 12 | #+kubebuilder:scaffold:crdkustomizewebhookpatch 13 | 14 | # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. 15 | # patches here are for enabling the CA injection for each CRD 16 | #- path: patches/cainjection_in_lastpasses.yaml 17 | #- path: patches/cainjection_in_lastpassgroups.yaml 18 | #+kubebuilder:scaffold:crdkustomizecainjectionpatch 19 | 20 | # [WEBHOOK] To enable webhook, uncomment the following section 21 | # the following config is for teaching kustomize how to do kustomization for CRDs. 22 | 23 | #configurations: 24 | #- kustomizeconfig.yaml 25 | -------------------------------------------------------------------------------- /config/crd/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This file is for teaching kustomize how to substitute name and namespace reference in CRD 2 | nameReference: 3 | - kind: Service 4 | version: v1 5 | fieldSpecs: 6 | - kind: CustomResourceDefinition 7 | version: v1 8 | group: apiextensions.k8s.io 9 | path: spec/conversion/webhook/clientConfig/service/name 10 | 11 | namespace: 12 | - kind: CustomResourceDefinition 13 | version: v1 14 | group: apiextensions.k8s.io 15 | path: spec/conversion/webhook/clientConfig/service/namespace 16 | create: false 17 | 18 | varReference: 19 | - path: metadata/annotations 20 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Adds namespace to all resources. 2 | namespace: lastpass-operator-system 3 | 4 | # Value of this field is prepended to the 5 | # names of all resources, e.g. a deployment named 6 | # "wordpress" becomes "alices-wordpress". 7 | # Note that it should also match with the prefix (text before '-') of the namespace 8 | # field above. 9 | namePrefix: lastpass-operator- 10 | 11 | # Labels to add to all resources and selectors. 12 | #labels: 13 | #- includeSelectors: true 14 | # pairs: 15 | # someName: someValue 16 | 17 | resources: 18 | - ../crd 19 | - ../rbac 20 | - ../manager 21 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 22 | # crd/kustomization.yaml 23 | #- ../webhook 24 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. 25 | #- ../certmanager 26 | # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. 27 | #- ../prometheus 28 | 29 | patches: 30 | # Protect the /metrics endpoint by putting it behind auth. 31 | # If you want your controller-manager to expose the /metrics 32 | # endpoint w/o any authn/z, please comment the following line. 33 | # - path: manager_auth_proxy_patch.yaml 34 | 35 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 36 | # crd/kustomization.yaml 37 | #- path: manager_webhook_patch.yaml 38 | 39 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 40 | # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. 41 | # 'CERTMANAGER' needs to be enabled to use ca injection 42 | #- path: webhookcainjection_patch.yaml 43 | 44 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. 45 | # Uncomment the following replacements to add the cert-manager CA injection annotations 46 | #replacements: 47 | # - source: # Add cert-manager annotation to ValidatingWebhookConfiguration, MutatingWebhookConfiguration and CRDs 48 | # kind: Certificate 49 | # group: cert-manager.io 50 | # version: v1 51 | # name: serving-cert # this name should match the one in certificate.yaml 52 | # fieldPath: .metadata.namespace # namespace of the certificate CR 53 | # targets: 54 | # - select: 55 | # kind: ValidatingWebhookConfiguration 56 | # fieldPaths: 57 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 58 | # options: 59 | # delimiter: '/' 60 | # index: 0 61 | # create: true 62 | # - select: 63 | # kind: MutatingWebhookConfiguration 64 | # fieldPaths: 65 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 66 | # options: 67 | # delimiter: '/' 68 | # index: 0 69 | # create: true 70 | # - select: 71 | # kind: CustomResourceDefinition 72 | # fieldPaths: 73 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 74 | # options: 75 | # delimiter: '/' 76 | # index: 0 77 | # create: true 78 | # - source: 79 | # kind: Certificate 80 | # group: cert-manager.io 81 | # version: v1 82 | # name: serving-cert # this name should match the one in certificate.yaml 83 | # fieldPath: .metadata.name 84 | # targets: 85 | # - select: 86 | # kind: ValidatingWebhookConfiguration 87 | # fieldPaths: 88 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 89 | # options: 90 | # delimiter: '/' 91 | # index: 1 92 | # create: true 93 | # - select: 94 | # kind: MutatingWebhookConfiguration 95 | # fieldPaths: 96 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 97 | # options: 98 | # delimiter: '/' 99 | # index: 1 100 | # create: true 101 | # - select: 102 | # kind: CustomResourceDefinition 103 | # fieldPaths: 104 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 105 | # options: 106 | # delimiter: '/' 107 | # index: 1 108 | # create: true 109 | # - source: # Add cert-manager annotation to the webhook Service 110 | # kind: Service 111 | # version: v1 112 | # name: webhook-service 113 | # fieldPath: .metadata.name # namespace of the service 114 | # targets: 115 | # - select: 116 | # kind: Certificate 117 | # group: cert-manager.io 118 | # version: v1 119 | # fieldPaths: 120 | # - .spec.dnsNames.0 121 | # - .spec.dnsNames.1 122 | # options: 123 | # delimiter: '.' 124 | # index: 0 125 | # create: true 126 | # - source: 127 | # kind: Service 128 | # version: v1 129 | # name: webhook-service 130 | # fieldPath: .metadata.namespace # namespace of the service 131 | # targets: 132 | # - select: 133 | # kind: Certificate 134 | # group: cert-manager.io 135 | # version: v1 136 | # fieldPaths: 137 | # - .spec.dnsNames.0 138 | # - .spec.dnsNames.1 139 | # options: 140 | # delimiter: '.' 141 | # index: 1 142 | # create: true 143 | -------------------------------------------------------------------------------- /config/default/manager_auth_proxy_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch inject a sidecar container which is a HTTP proxy for the 2 | # controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: controller-manager 7 | namespace: system 8 | spec: 9 | template: 10 | spec: 11 | containers: 12 | - name: kube-rbac-proxy 13 | securityContext: 14 | allowPrivilegeEscalation: false 15 | capabilities: 16 | drop: 17 | - "ALL" 18 | image: gcr.io/kubebuilder/kube-rbac-proxy:v0.16.0 19 | args: 20 | - "--secure-listen-address=0.0.0.0:8443" 21 | - "--upstream=http://127.0.0.1:8080/" 22 | - "--logtostderr=true" 23 | - "--v=0" 24 | ports: 25 | - containerPort: 8443 26 | protocol: TCP 27 | name: https 28 | resources: 29 | limits: 30 | cpu: 500m 31 | memory: 128Mi 32 | requests: 33 | cpu: 5m 34 | memory: 64Mi 35 | - name: manager 36 | args: 37 | - "--health-probe-bind-address=:8081" 38 | - "--metrics-bind-address=127.0.0.1:8080" 39 | - "--leader-elect" 40 | -------------------------------------------------------------------------------- /config/default/manager_config_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: manager 11 | -------------------------------------------------------------------------------- /config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manager.yaml 3 | -------------------------------------------------------------------------------- /config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/name: lastpass-operator 7 | app.kubernetes.io/managed-by: kustomize 8 | name: system 9 | --- 10 | apiVersion: apps/v1 11 | kind: Deployment 12 | metadata: 13 | name: controller-manager 14 | namespace: system 15 | labels: 16 | control-plane: controller-manager 17 | app.kubernetes.io/name: lastpass-operator 18 | app.kubernetes.io/managed-by: kustomize 19 | spec: 20 | selector: 21 | matchLabels: 22 | control-plane: controller-manager 23 | replicas: 1 24 | template: 25 | metadata: 26 | annotations: 27 | kubectl.kubernetes.io/default-container: manager 28 | labels: 29 | control-plane: controller-manager 30 | spec: 31 | # TODO(user): Uncomment the following code to configure the nodeAffinity expression 32 | # according to the platforms which are supported by your solution. 33 | # It is considered best practice to support multiple architectures. You can 34 | # build your manager image using the makefile target docker-buildx. 35 | # affinity: 36 | # nodeAffinity: 37 | # requiredDuringSchedulingIgnoredDuringExecution: 38 | # nodeSelectorTerms: 39 | # - matchExpressions: 40 | # - key: kubernetes.io/arch 41 | # operator: In 42 | # values: 43 | # - amd64 44 | # - arm64 45 | # - ppc64le 46 | # - s390x 47 | # - key: kubernetes.io/os 48 | # operator: In 49 | # values: 50 | # - linux 51 | securityContext: 52 | runAsNonRoot: true 53 | # TODO(user): For common cases that do not require escalating privileges 54 | # it is recommended to ensure that all your Pods/Containers are restrictive. 55 | # More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted 56 | # Please uncomment the following code if your project does NOT have to work on old Kubernetes 57 | # versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ). 58 | # seccompProfile: 59 | # type: RuntimeDefault 60 | containers: 61 | - command: 62 | - /manager 63 | args: 64 | - --leader-elect 65 | image: controller:latest 66 | name: manager 67 | securityContext: 68 | allowPrivilegeEscalation: false 69 | capabilities: 70 | drop: 71 | - "ALL" 72 | livenessProbe: 73 | httpGet: 74 | path: /healthz 75 | port: 8081 76 | initialDelaySeconds: 15 77 | periodSeconds: 20 78 | readinessProbe: 79 | httpGet: 80 | path: /readyz 81 | port: 8081 82 | initialDelaySeconds: 5 83 | periodSeconds: 10 84 | # TODO(user): Configure the resources accordingly based on the project requirements. 85 | # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ 86 | resources: 87 | limits: 88 | cpu: 500m 89 | memory: 128Mi 90 | requests: 91 | cpu: 10m 92 | memory: 64Mi 93 | serviceAccountName: controller-manager 94 | terminationGracePeriodSeconds: 10 95 | -------------------------------------------------------------------------------- /config/manifests/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # These resources constitute the fully configured set of manifests 2 | # used to generate the 'manifests/' directory in a bundle. 3 | resources: 4 | - bases/lastpass-operator.clusterserviceversion.yaml 5 | - ../default 6 | - ../samples 7 | - ../scorecard 8 | 9 | # [WEBHOOK] To enable webhooks, uncomment all the sections with [WEBHOOK] prefix. 10 | # Do NOT uncomment sections with prefix [CERTMANAGER], as OLM does not support cert-manager. 11 | # These patches remove the unnecessary "cert" volume and its manager container volumeMount. 12 | #patchesJson6902: 13 | #- target: 14 | # group: apps 15 | # version: v1 16 | # kind: Deployment 17 | # name: controller-manager 18 | # namespace: system 19 | # patch: |- 20 | # # Remove the manager container's "cert" volumeMount, since OLM will create and mount a set of certs. 21 | # # Update the indices in this path if adding or removing containers/volumeMounts in the manager's Deployment. 22 | # - op: remove 23 | 24 | # path: /spec/template/spec/containers/0/volumeMounts/0 25 | # # Remove the "cert" volume, since OLM will create and mount a set of certs. 26 | # # Update the indices in this path if adding or removing volumes in the manager's Deployment. 27 | # - op: remove 28 | # path: /spec/template/spec/volumes/0 29 | -------------------------------------------------------------------------------- /config/prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - monitor.yaml 3 | -------------------------------------------------------------------------------- /config/prometheus/monitor.yaml: -------------------------------------------------------------------------------- 1 | # Prometheus Monitor Service (Metrics) 2 | apiVersion: monitoring.coreos.com/v1 3 | kind: ServiceMonitor 4 | metadata: 5 | labels: 6 | control-plane: controller-manager 7 | app.kubernetes.io/name: lastpass-operator 8 | app.kubernetes.io/managed-by: kustomize 9 | name: controller-manager-metrics-monitor 10 | namespace: system 11 | spec: 12 | endpoints: 13 | - path: /metrics 14 | port: https 15 | scheme: https 16 | bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token 17 | tlsConfig: 18 | insecureSkipVerify: true 19 | selector: 20 | matchLabels: 21 | control-plane: controller-manager 22 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_client_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: lastpass-operator 6 | app.kubernetes.io/managed-by: kustomize 7 | name: metrics-reader 8 | rules: 9 | - nonResourceURLs: 10 | - "/metrics" 11 | verbs: 12 | - get 13 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: lastpass-operator 6 | app.kubernetes.io/managed-by: kustomize 7 | name: proxy-role 8 | rules: 9 | - apiGroups: 10 | - authentication.k8s.io 11 | resources: 12 | - tokenreviews 13 | verbs: 14 | - create 15 | - apiGroups: 16 | - authorization.k8s.io 17 | resources: 18 | - subjectaccessreviews 19 | verbs: 20 | - create 21 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: lastpass-operator 6 | app.kubernetes.io/managed-by: kustomize 7 | name: proxy-rolebinding 8 | roleRef: 9 | apiGroup: rbac.authorization.k8s.io 10 | kind: ClusterRole 11 | name: proxy-role 12 | subjects: 13 | - kind: ServiceAccount 14 | name: controller-manager 15 | namespace: system 16 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/name: lastpass-operator 7 | app.kubernetes.io/managed-by: kustomize 8 | name: controller-manager-metrics-service 9 | namespace: system 10 | spec: 11 | ports: 12 | - name: https 13 | port: 8443 14 | protocol: TCP 15 | targetPort: https 16 | selector: 17 | control-plane: controller-manager 18 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | # All RBAC will be applied under this service account in 3 | # the deployment namespace. You may comment out this resource 4 | # if your manager will use a service account that exists at 5 | # runtime. Be sure to update RoleBinding and ClusterRoleBinding 6 | # subjects if changing service account names. 7 | - service_account.yaml 8 | - role.yaml 9 | - role_binding.yaml 10 | - leader_election_role.yaml 11 | - leader_election_role_binding.yaml 12 | # Comment the following 4 lines if you want to disable 13 | # the auth proxy (https://github.com/brancz/kube-rbac-proxy) 14 | # which protects your /metrics endpoint. 15 | # - auth_proxy_service.yaml 16 | # - auth_proxy_role.yaml 17 | # - auth_proxy_role_binding.yaml 18 | # - auth_proxy_client_clusterrole.yaml 19 | # For each CRD, "Editor" and "Viewer" roles are scaffolded by 20 | # default, aiding admins in cluster management. Those roles are 21 | # not used by the Project itself. You can comment the following lines 22 | # if you do not want those helpers be installed with your Project. 23 | # - lastpassgroup_editor_role.yaml 24 | # - lastpassgroup_viewer_role.yaml 25 | # - lastpass_editor_role.yaml 26 | # - lastpass_viewer_role.yaml 27 | -------------------------------------------------------------------------------- /config/rbac/lastpass_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit lastpasses. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: lastpass-operator 7 | app.kubernetes.io/managed-by: kustomize 8 | name: lastpass-editor-role 9 | rules: 10 | - apiGroups: 11 | - edgelevel.com 12 | resources: 13 | - lastpasses 14 | verbs: 15 | - create 16 | - delete 17 | - get 18 | - list 19 | - patch 20 | - update 21 | - watch 22 | - apiGroups: 23 | - edgelevel.com 24 | resources: 25 | - lastpasses/status 26 | verbs: 27 | - get 28 | -------------------------------------------------------------------------------- /config/rbac/lastpass_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view lastpasses. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: lastpass-operator 7 | app.kubernetes.io/managed-by: kustomize 8 | name: lastpass-viewer-role 9 | rules: 10 | - apiGroups: 11 | - edgelevel.com 12 | resources: 13 | - lastpasses 14 | verbs: 15 | - get 16 | - list 17 | - watch 18 | - apiGroups: 19 | - edgelevel.com 20 | resources: 21 | - lastpasses/status 22 | verbs: 23 | - get 24 | -------------------------------------------------------------------------------- /config/rbac/lastpassgroup_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit lastpassgroups. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: lastpass-operator 7 | app.kubernetes.io/managed-by: kustomize 8 | name: lastpassgroup-editor-role 9 | rules: 10 | - apiGroups: 11 | - edgelevel.com 12 | resources: 13 | - lastpassgroups 14 | verbs: 15 | - create 16 | - delete 17 | - get 18 | - list 19 | - patch 20 | - update 21 | - watch 22 | - apiGroups: 23 | - edgelevel.com 24 | resources: 25 | - lastpassgroups/status 26 | verbs: 27 | - get 28 | -------------------------------------------------------------------------------- /config/rbac/lastpassgroup_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view lastpassgroups. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: lastpass-operator 7 | app.kubernetes.io/managed-by: kustomize 8 | name: lastpassgroup-viewer-role 9 | rules: 10 | - apiGroups: 11 | - edgelevel.com 12 | resources: 13 | - lastpassgroups 14 | verbs: 15 | - get 16 | - list 17 | - watch 18 | - apiGroups: 19 | - edgelevel.com 20 | resources: 21 | - lastpassgroups/status 22 | verbs: 23 | - get 24 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: lastpass-operator 7 | app.kubernetes.io/managed-by: kustomize 8 | name: leader-election-role 9 | rules: 10 | - apiGroups: 11 | - "" 12 | resources: 13 | - configmaps 14 | verbs: 15 | - get 16 | - list 17 | - watch 18 | - create 19 | - update 20 | - patch 21 | - delete 22 | - apiGroups: 23 | - coordination.k8s.io 24 | resources: 25 | - leases 26 | verbs: 27 | - get 28 | - list 29 | - watch 30 | - create 31 | - update 32 | - patch 33 | - delete 34 | - apiGroups: 35 | - "" 36 | resources: 37 | - events 38 | verbs: 39 | - create 40 | - patch 41 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: lastpass-operator 6 | app.kubernetes.io/managed-by: kustomize 7 | name: leader-election-rolebinding 8 | roleRef: 9 | apiGroup: rbac.authorization.k8s.io 10 | kind: Role 11 | name: leader-election-role 12 | subjects: 13 | - kind: ServiceAccount 14 | name: controller-manager 15 | namespace: system 16 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: manager-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - secrets 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - edgelevel.com 21 | resources: 22 | - lastpasses 23 | verbs: 24 | - create 25 | - delete 26 | - get 27 | - list 28 | - patch 29 | - update 30 | - watch 31 | - apiGroups: 32 | - edgelevel.com 33 | resources: 34 | - lastpasses/status 35 | verbs: 36 | - get 37 | - patch 38 | - update 39 | - apiGroups: 40 | - edgelevel.com 41 | resources: 42 | - lastpassgroups 43 | verbs: 44 | - create 45 | - delete 46 | - get 47 | - list 48 | - patch 49 | - update 50 | - watch 51 | - apiGroups: 52 | - edgelevel.com 53 | resources: 54 | - lastpassgroups/finalizers 55 | verbs: 56 | - update 57 | - apiGroups: 58 | - edgelevel.com 59 | resources: 60 | - lastpassgroups/status 61 | verbs: 62 | - get 63 | - patch 64 | - update 65 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: lastpass-operator 6 | app.kubernetes.io/managed-by: kustomize 7 | name: manager-rolebinding 8 | roleRef: 9 | apiGroup: rbac.authorization.k8s.io 10 | kind: ClusterRole 11 | name: manager-role 12 | subjects: 13 | - kind: ServiceAccount 14 | name: controller-manager 15 | namespace: system 16 | -------------------------------------------------------------------------------- /config/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: lastpass-operator 6 | app.kubernetes.io/managed-by: kustomize 7 | name: controller-manager 8 | namespace: system 9 | -------------------------------------------------------------------------------- /config/samples/kustomization.yaml: -------------------------------------------------------------------------------- 1 | ## Append samples of your project ## 2 | resources: 3 | - v1alpha1_lastpass.yaml 4 | - v1alpha1_lastpassgroup.yaml 5 | # - _v1alpha1_lastpassgroup.yaml 6 | #+kubebuilder:scaffold:manifestskustomizesamples 7 | -------------------------------------------------------------------------------- /config/samples/v1alpha1_lastpass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: edgelevel.com/v1alpha1 2 | kind: LastPass 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: lastpass-operator 6 | app.kubernetes.io/managed-by: kustomize 7 | name: lastpass-sample 8 | spec: 9 | # required 10 | secretRef: 11 | # optional (default "") 12 | group: SampleGroup 13 | # required 14 | name: Saample 15 | # optional (default false) 16 | withUsername: true 17 | # optional (default false) 18 | withPassword: true 19 | # optional (default false) 20 | withUrl: true 21 | # optional (default false) 22 | withNote: true 23 | # optional 24 | syncPolicy: 25 | # required (default false) 26 | enabled: true 27 | # required (in seconds) 28 | refresh: 60 29 | -------------------------------------------------------------------------------- /config/samples/v1alpha1_lastpassgroup.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: edgelevel.com/v1alpha1 2 | kind: LastPassGroup 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: lastpass-operator 6 | app.kubernetes.io/managed-by: kustomize 7 | name: lastpassgroup-sample 8 | spec: 9 | # required 10 | groupRef: 11 | # optional (default "") 12 | group: SampleGroup 13 | # optional (default false) 14 | withUsername: true 15 | # optional (default false) 16 | withPassword: true 17 | # optional (default false) 18 | withUrl: true 19 | # optional (default false) 20 | withNote: true 21 | # optional 22 | syncPolicy: 23 | # required (default false) 24 | enabled: true 25 | # required (in seconds) 26 | refresh: 60 27 | -------------------------------------------------------------------------------- /config/scorecard/bases/config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: scorecard.operatorframework.io/v1alpha3 2 | kind: Configuration 3 | metadata: 4 | name: config 5 | stages: 6 | - parallel: true 7 | tests: [] 8 | -------------------------------------------------------------------------------- /config/scorecard/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - bases/config.yaml 3 | patchesJson6902: 4 | - path: patches/basic.config.yaml 5 | target: 6 | group: scorecard.operatorframework.io 7 | version: v1alpha3 8 | kind: Configuration 9 | name: config 10 | - path: patches/olm.config.yaml 11 | target: 12 | group: scorecard.operatorframework.io 13 | version: v1alpha3 14 | kind: Configuration 15 | name: config 16 | #+kubebuilder:scaffold:patchesJson6902 17 | -------------------------------------------------------------------------------- /config/scorecard/patches/basic.config.yaml: -------------------------------------------------------------------------------- 1 | - op: add 2 | path: /stages/0/tests/- 3 | value: 4 | entrypoint: 5 | - scorecard-test 6 | - basic-check-spec 7 | image: quay.io/operator-framework/scorecard-test:v1.36.0 8 | labels: 9 | suite: basic 10 | test: basic-check-spec-test 11 | -------------------------------------------------------------------------------- /config/scorecard/patches/olm.config.yaml: -------------------------------------------------------------------------------- 1 | - op: add 2 | path: /stages/0/tests/- 3 | value: 4 | entrypoint: 5 | - scorecard-test 6 | - olm-bundle-validation 7 | image: quay.io/operator-framework/scorecard-test:v1.36.0 8 | labels: 9 | suite: olm 10 | test: olm-bundle-validation-test 11 | - op: add 12 | path: /stages/0/tests/- 13 | value: 14 | entrypoint: 15 | - scorecard-test 16 | - olm-crds-have-validation 17 | image: quay.io/operator-framework/scorecard-test:v1.36.0 18 | labels: 19 | suite: olm 20 | test: olm-crds-have-validation-test 21 | - op: add 22 | path: /stages/0/tests/- 23 | value: 24 | entrypoint: 25 | - scorecard-test 26 | - olm-crds-have-resources 27 | image: quay.io/operator-framework/scorecard-test:v1.36.0 28 | labels: 29 | suite: olm 30 | test: olm-crds-have-resources-test 31 | - op: add 32 | path: /stages/0/tests/- 33 | value: 34 | entrypoint: 35 | - scorecard-test 36 | - olm-spec-descriptors 37 | image: quay.io/operator-framework/scorecard-test:v1.36.0 38 | labels: 39 | suite: olm 40 | test: olm-spec-descriptors-test 41 | - op: add 42 | path: /stages/0/tests/- 43 | value: 44 | entrypoint: 45 | - scorecard-test 46 | - olm-status-descriptors 47 | image: quay.io/operator-framework/scorecard-test:v1.36.0 48 | labels: 49 | suite: olm 50 | test: olm-status-descriptors-test 51 | -------------------------------------------------------------------------------- /docs/dev.txt: -------------------------------------------------------------------------------- 1 | https://github.com/entretechno/lastpass-api 2 | https://github.com/konomae/lastpass-python 3 | 4 | https://godaddy.github.io/2019/04/16/kubernetes-external-secrets 5 | https://external-secrets.io 6 | https://github.com/mumoshu/aws-secret-operator 7 | https://github.com/acamillo/aws-secret-operator 8 | https://github.com/cmattoon/aws-ssm 9 | https://github.com/shyiko/kubesec 10 | https://github.com/Boostport/kubernetes-vault 11 | https://github.com/contentful-labs/kube-secret-syncer 12 | https://itnext.io/argocd-secret-management-with-argocd-vault-plugin-539f104aff05 13 | https://developer.1password.com/docs/connect/helm 14 | -------------------------------------------------------------------------------- /docs/golang.md: -------------------------------------------------------------------------------- 1 | ## golang 2 | 3 | * [go](https://golang.org/doc) documentation 4 | * [Convert JSON into a Go type definition](https://mholt.github.io/json-to-go/) 5 | 6 | ```bash 7 | # download source 8 | mkdir -p $GOPATH/src/github.com/edgelevel && cd $_ 9 | git clone git@github.com:edgelevel/lastpass-operator.git 10 | 11 | # add dependencies 12 | go get github.com/USER/DEP1 github.com/USER/DEP2 13 | # example 14 | go get github.com/spf13/cobra 15 | go get github.com/codeskyblue/go-sh 16 | 17 | # verify and update all dependencies 18 | go get -u 19 | go mod tidy 20 | 21 | # init cli 22 | cobra init . --pkg-name lastpass-operator 23 | 24 | # run 25 | go run main.go 26 | 27 | # compile 28 | go build $GOPATH/src/github.com/edgelevel/lastpass-operator 29 | 30 | # compile and build executable 31 | go install $GOPATH/src/github.com/edgelevel/lastpass-operator 32 | 33 | # test 34 | go test $GOPATH/src/github.com/edgelevel/lastpass-operator 35 | ``` 36 | -------------------------------------------------------------------------------- /docs/img/lastpass-operator.drawio: -------------------------------------------------------------------------------- 1 | 7VpJc+I4FP41XKaqU17YcgRCOqkiwOD0NJnLlLCFrWlhEVkG079+JFvGi8zSKRI8k8nB0fv8tL1dMg1zsIq+UrD2nogDccPQnKhh3jUMo901+FMAuwRo6bcJ4FLkJJCeARb6CSWoSTREDgwKjIwQzNC6CNrE96HNChiglGyLbEuCi7OugQsVwLIBVtHvyGFegnaNToY/QOR66cx6W+5vBVJmuZPAAw7Z5iBz2DAHlBCWtFbRAGIhu1QuSb/7A2/3C6PQZ+d0+GP78PtsviWuH73aD+PZg/Pn5ks6TMB26Y6hwwUgSUKZR1ziAzzM0D4loe9AMazGqYxnRMiagzoH/4aM7aQ2QcgIhzy2wvItjBCbi+43LUm9yMFE+y7KE7uU8BndzfPESzaCILNuMZX2S/YnNnVQbqkMSEhteERYqf0B6kJ2hM/ca5d7BSQryNfD+1GIAUOb4jqAtE93z5epkDekFn9Fo9LhNgCHcqrH8eNzw2hjvub+gvKWK1qz4WAyHjyOhuqr0WQyVeyCwYgV1UhhgH6CRcwgRL0myGfxflr9RuuOIwAj1+eAzWUNKQc2kDLEvasnX6yQ48Q2hcEC4j6wf7ixdQ0IJjSe11zGf3tNihFgVOX7ciWZx+V1fMT6VU3J4b9oN6ZumslgMn6lRnC2MuXoUyGbHAtZLgNuRGVt7xfxdgPQFf1bz71ZhQGono8xD6tCG1sPMWitQewOWx7Yi4oHwTqJtUsUiThwWDWKmx0Utt7WCoI29VZCb7Owm7J4uYibYpd3JEWOw3mVH9VLjM1WzcTYuUqC2SeLLD/sU8zHJAvzzGRhXDNZdBUb51YNVsJO/UUg/o0nZeQMD/gPporOqUxhdJrGRTOFHNos+nO7WRzgHfOIUZ/SMH1zqjSsvbfrB6L3x7i7bir+/jK0PoP76gfCbL7S02tf2qnqewKBEDeXDIUOFwECOBA2HaGAi+P+jFhNPbJahMHpauUCxYnRLQazfc2XK05uK4qT2/cqTpqKQEc963nas6y/vlnD2bj3VHU6SlnE4/tkdvemjBgwSn7A1Pp94gvfWCKMS9D5jlWlv2I8voAKzbZ5UoV6s6Pq0HwvHerNa+aptJ27jKj1Fca5dxhXLUvTM8v/denpxNY8ldi0ZrdV98SmX+eU+O+sNc/2Yb19VSdWD5efpdg8fVisvUOqt4iPorDkofU1FF8s+ngNgiAj1egLNgDhRHh1r0NN7cp1aOKpBXH/xskRcZEvPJ0Igpf600TmJVkK1nvIbK/ApQ3CgJEVb8ygjCmVPWfwNYQBU84QCyGwr7PJtynfUnUlLNdIQnZgUUQcTSBQVjaLZc0EHIAVVCYSSwFM4PEI4dpJqADyF+yACNCSP6ydb08JRrZYPYrPQr4wQqdhDGKPW/LwIuZlHiwuqnSC4gYmhpgnsxLfqZhVNeJCSjp1/VuOYpewbM24aRVse38hnLNtDqrGrb9bhW603pDda57Pqw5Wl8nu7Y/K7nHXHqVgl2OQyfbwdaRRNK+2VvrqXOJvlkLtr/I3zeP85fWU+Hkj2eFFs6OhhutP88m0dTT+8ONGSSHpd7Rr1DqczH5fkbBnP1Ixh/8A -------------------------------------------------------------------------------- /docs/img/reconcile-loop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edgelevel/lastpass-operator/935b45330779efccae8561d8e7d1343944de7775/docs/img/reconcile-loop.png -------------------------------------------------------------------------------- /docs/lastpass.md: -------------------------------------------------------------------------------- 1 | ## LastPass 2 | 3 | * [Website](https://www.lastpass.com) 4 | * [lastpass-cli](https://github.com/lastpass/lastpass-cli) 5 | * `lpass` [examples](../example/lpass-examples.txt) 6 | 7 | CLI 8 | ```bash 9 | # login 10 | echo | LPASS_DISABLE_PINENTRY=1 lpass login --trust 11 | 12 | # list 13 | lpass ls 14 | 15 | # retrieve passwords 16 | lpass show / --json --expand-multi 17 | 18 | # logout 19 | lpass logout --force 20 | ``` 21 | 22 | Docker Alpine (20.1MB) 23 | ```bash 24 | # temporary base container 25 | docker run --rm -it alpine /bin/sh 26 | 27 | # build image 28 | docker build -t edgelevel/lastpass-cli -f example/lastpass-alpine . 29 | 30 | # temporary container 31 | docker run --rm -it edgelevel/lastpass-cli /bin/sh 32 | echo | LPASS_DISABLE_PINENTRY=1 lpass login --trust 33 | echo | lpass show / --json --expand-multi 34 | ``` 35 | 36 | Docker Ubuntu (192MB) 37 | ```bash 38 | # build image 39 | docker build -t edgelevel/lastpass-cli -f example/lastpass-ubuntu . 40 | 41 | # temporary container 42 | docker run --rm --name lastpass-cli edgelevel/lastpass-cli 43 | 44 | # access container 45 | docker exec -it lastpass-cli bash 46 | lpass --version 47 | 48 | # execute command inline 49 | docker run --rm -it edgelevel/lastpass-cli lpass --version 50 | ``` 51 | -------------------------------------------------------------------------------- /docs/operator.md: -------------------------------------------------------------------------------- 1 | ## operator-sdk 2 | 3 | * [operator-sdk](https://github.com/operator-framework/operator-sdk/blob/master/doc/user-guide.md) 4 | * [Controller Runtime Client API](https://github.com/operator-framework/operator-sdk/blob/master/doc/user/client.md) 5 | * [A complete guide to Kubernetes Operator SDK](https://banzaicloud.com/blog/operator-sdk) 6 | * [OperatorHub](https://operatorhub.io) 7 | * [Extend the Kubernetes API with CustomResourceDefinitions](https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions) 8 | * [Groups and Versions and Kinds, oh my!](https://book.kubebuilder.io/cronjob-tutorial/gvks.html) 9 | * [Level Triggering and Reconciliation in Kubernetes](https://hackernoon.com/level-triggering-and-reconciliation-in-kubernetes-1f17fe30333d) 10 | * [A deep dive into Kubernetes controllers](https://engineering.bitnami.com/articles/a-deep-dive-into-kubernetes-controllers.html) 11 | 12 | Setup go [workspace](https://golang.org/doc/code.html#Workspaces) 13 | ```bash 14 | # add to .bashrc or .bash_profile 15 | export GOPATH=$HOME/go 16 | export PATH=$PATH:$(go env GOPATH)/bin 17 | ``` 18 | 19 | Initialize project 20 | ```bash 21 | # create project 22 | mkdir -p $GOPATH/src/github.com/edgelevel && cd $_ 23 | operator-sdk new lastpass-operator --dep-manager=dep 24 | 25 | # add crd 26 | operator-sdk add api --api-version=edgelevel.com/v1alpha1 --kind=LastPass 27 | operator-sdk generate k8s 28 | 29 | # add controller 30 | operator-sdk add controller --api-version=edgelevel.com/v1alpha1 --kind=LastPass 31 | ``` 32 | -------------------------------------------------------------------------------- /docs/setup.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | * [docker](https://docs.docker.com/install) 4 | * [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl) 5 | * [helm](https://helm.sh/docs/using_helm/#installing-helm) 6 | * [go](https://golang.org/doc) 7 | * [operator-sdk](https://github.com/operator-framework/operator-sdk/blob/master/doc/user/install-operator-sdk.md) 8 | * [minikube](https://github.com/kubernetes/minikube) 9 | 10 | ## Ubuntu: 11 | 12 | Install docker using the instructions below from the [docker website](https://docs.docker.com/engine/install/ubuntu/#installation-methods) 13 | ```bash 14 | # Add Docker's official GPG key: 15 | sudo apt-get update 16 | sudo apt-get install ca-certificates curl 17 | sudo install -m 0755 -d /etc/apt/keyrings 18 | sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc 19 | sudo chmod a+r /etc/apt/keyrings/docker.asc 20 | 21 | # Add the repository to Apt sources: 22 | echo \ 23 | "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ 24 | $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ 25 | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null 26 | sudo apt-get update 27 | 28 | sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin 29 | ``` 30 | 31 | ### kubectl: 32 | 33 | `sudo snap install kubectl --classic` 34 | 35 | ### helm: 36 | 37 | `sudo snap install helm --classic` 38 | 39 | ### go: 40 | 41 | `sudo snap install go --classic` 42 | 43 | 44 | ### Operator-SDK: 45 | ```bash 46 | # operator-sdk (from source) 47 | go get -d github.com/operator-framework/operator-sdk 48 | cd $GOPATH/src/github.com/operator-framework/operator-sdk 49 | git checkout master 50 | make dep 51 | make install 52 | ``` 53 | Alternatively, you can use the operator-sdk build dependency that is in the `bin` directory. This operator-sdk binary can be downloaded using: 54 | 55 | `make operator-sdk` 56 | 57 | ### minikube 58 | ```bash 59 | curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 60 | sudo install minikube-linux-amd64 /usr/local/bin/minikube && rm minikube-linux-amd64 61 | ``` 62 | 63 | ## macOS 64 | ```bash 65 | # docker 66 | # download from https://hub.docker.com/editions/community/docker-ce-desktop-mac 67 | 68 | # kubectl 69 | brew install kubernetes-cli 70 | 71 | # helm 72 | brew install kubernetes-helm 73 | 74 | # go 75 | brew install go 76 | 77 | # operator-sdk 78 | brew install operator-sdk 79 | 80 | # minikube 81 | brew cask install minikube 82 | ``` 83 | -------------------------------------------------------------------------------- /example/edgelevel_v1alpha1_lastpass_cr.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: edgelevel.com/v1alpha1 2 | kind: LastPass 3 | metadata: 4 | name: example-lastpass 5 | spec: 6 | # required 7 | secretRef: 8 | # optional (default "") 9 | group: example 10 | # required 11 | name: my-secret 12 | # optional (default false) 13 | withUsername: true 14 | # optional (default false) 15 | withPassword: true 16 | # optional (default false) 17 | withUrl: true 18 | # optional (default false) 19 | withNote: true 20 | # optional 21 | syncPolicy: 22 | # required (default false) 23 | enabled: true 24 | # required (in seconds) 25 | refresh: 10 26 | -------------------------------------------------------------------------------- /example/edgelevel_v1alpha1_lastpassgroup_cr.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: edgelevel.com/v1alpha1 2 | kind: LastPassGroup 3 | metadata: 4 | name: example-lastpassgroup 5 | spec: 6 | # required 7 | secretRef: 8 | # optional (default "") 9 | group: example 10 | # optional (default false) 11 | withUsername: true 12 | # optional (default false) 13 | withPassword: true 14 | # optional (default false) 15 | withUrl: true 16 | # optional (default false) 17 | withNote: true 18 | # optional 19 | syncPolicy: 20 | # required (default false) 21 | enabled: true 22 | # required (in seconds) 23 | refresh: 10 24 | -------------------------------------------------------------------------------- /example/lastpass-alpine: -------------------------------------------------------------------------------- 1 | FROM alpine:latest AS builder 2 | 3 | ENV LASTPASS_VERSION=1.5.0 4 | 5 | RUN apk add --update --no-cache \ 6 | bash-completion \ 7 | cmake \ 8 | curl-dev \ 9 | openssl-dev \ 10 | libxml2 \ 11 | libxml2-dev \ 12 | libssl3 \ 13 | pkgconf \ 14 | ca-certificates \ 15 | xclip \ 16 | g++ \ 17 | make \ 18 | wget 19 | 20 | RUN wget -O lastpass-cli.tar.gz https://github.com/lastpass/lastpass-cli/releases/download/v${LASTPASS_VERSION}/lastpass-cli-${LASTPASS_VERSION}.tar.gz; \ 21 | tar -xf lastpass-cli.tar.gz 22 | 23 | WORKDIR /lastpass-cli-${LASTPASS_VERSION} 24 | 25 | RUN make && make install 26 | 27 | RUN mkdir -p /usr/lib/lastpass-cli \ 28 | && ldd /usr/bin/lpass | grep '=>' | awk '{ print $3 }' | xargs cp -t /usr/lib/lastpass-cli 29 | 30 | 31 | 32 | FROM alpine:latest 33 | 34 | COPY --from=builder /usr/bin/lpass /usr/bin/which /usr/bin/ 35 | COPY --from=builder /usr/lib/lastpass-cli /lib -------------------------------------------------------------------------------- /example/lastpass-ubuntu: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 AS builder 2 | 3 | ENV LASTPASS_VERSION=1.5.0 4 | 5 | # https://github.com/lastpass/lastpass-cli?tab=readme-ov-file#building 6 | RUN apt update && apt --no-install-recommends -yqq install \ 7 | bash-completion \ 8 | build-essential \ 9 | cmake \ 10 | libcurl4 \ 11 | libcurl4-openssl-dev \ 12 | libssl-dev \ 13 | libxml2 \ 14 | libxml2-dev \ 15 | libssl3 \ 16 | pkg-config \ 17 | ca-certificates \ 18 | xclip \ 19 | wget 20 | 21 | RUN wget -O lastpass-cli.tar.gz https://github.com/lastpass/lastpass-cli/releases/download/v${LASTPASS_VERSION}/lastpass-cli-${LASTPASS_VERSION}.tar.gz; \ 22 | tar -xf lastpass-cli.tar.gz 23 | 24 | WORKDIR /lastpass-cli-${LASTPASS_VERSION} 25 | 26 | RUN make && make install 27 | 28 | RUN mkdir -p /usr/lib/lastpass-cli \ 29 | && ldd /usr/bin/lpass | grep '=>' | awk '{ print $3 }' | xargs cp -t /usr/lib/lastpass-cli 30 | 31 | FROM ubuntu:22.04 32 | 33 | RUN apt update && apt --no-install-recommends -yqq install ca-certificates 34 | 35 | COPY --from=builder /usr/bin/lpass /usr/bin/which /usr/bin/ 36 | COPY --from=builder /usr/lib/lastpass-cli /lib -------------------------------------------------------------------------------- /example/lpass-examples.txt: -------------------------------------------------------------------------------- 1 | $ lpass show example/database 2 | example/database [id: 9039358816050440021] 3 | Username: myUsername 4 | Password: myPassword 5 | Alias: myAlias 6 | SID: mySid 7 | Database: AAA 8 | Port: 8080 9 | Hostname: myHostname 10 | Type: myType 11 | Language: en-GB 12 | NoteType: Database 13 | Notes: myNotes 14 | 15 | $ lpass show example/database --json 16 | [ 17 | { 18 | "id": "9039358816050440021", 19 | "name": "database", 20 | "fullname": "example/database", 21 | "username": "", 22 | "password": "", 23 | "last_modified_gmt": "1562254850", 24 | "last_touch": "0", 25 | "group": "example", 26 | "url": "http://sn", 27 | "note": "NoteType:Database\nLanguage:en-GB\nType:myType\nHostname:myHostname\nPort:8080\nDatabase:AAA\nUsername:myUsername\nPassword:myPassword\nSID:mySid\nAlias:myAlias\nNotes:myNotes" 28 | } 29 | ] 30 | 31 | --- 32 | 33 | $ lpass show example/example-pwd --json 34 | [ 35 | { 36 | "id": "8281904599677253490", 37 | "name": "example-pwd", 38 | "fullname": "example/example-pwd", 39 | "username": "my-username", 40 | "password": "my-password", 41 | "last_modified_gmt": "1561684727", 42 | "last_touch": "1562236360", 43 | "group": "example", 44 | "url": "http://", 45 | "note": "" 46 | } 47 | ] 48 | 49 | $ lpass show example/example-pwd 50 | example/example-pwd [id: 8281904599677253490] 51 | Username: my-username 52 | Password: my-password 53 | 54 | --- 55 | 56 | $ lpass show example/secure-note-env --json 57 | [ 58 | { 59 | "id": "8488491124605622019", 60 | "name": "secure-note-env", 61 | "fullname": "example/secure-note-env", 62 | "username": "", 63 | "password": "", 64 | "last_modified_gmt": "1562254735", 65 | "last_touch": "0", 66 | "group": "example", 67 | "url": "http://sn", 68 | "note": "abc123" 69 | } 70 | ] 71 | 72 | $ lpass show example/secure-note-env 73 | example/secure-note-env [id: 8488491124605622019] 74 | URL: http://sn 75 | Notes: abc123 76 | 77 | --- 78 | 79 | $ lpass show example/secure-note-json --json 80 | [ 81 | { 82 | "id": "5432661329597563641", 83 | "name": "secure-note-json", 84 | "fullname": "example/secure-note-json", 85 | "username": "", 86 | "password": "", 87 | "last_modified_gmt": "1562254769", 88 | "last_touch": "0", 89 | "group": "example", 90 | "url": "http://sn", 91 | "note": "{\"myKey\":\"myValue\"}" 92 | } 93 | ] 94 | 95 | $ lpass show example/secure-note-json 96 | example/secure-note-json [id: 5432661329597563641] 97 | URL: http://sn 98 | Notes: {"myKey":"myValue"} 99 | 100 | --- 101 | 102 | $ lpass show example/example-pwd --json --expand-multi 103 | [ 104 | { 105 | "id": "8281904599677253490", 106 | "name": "example-pwd", 107 | "fullname": "example/example-pwd", 108 | "username": "my-username", 109 | "password": "my-password", 110 | "last_modified_gmt": "1561684727", 111 | "last_touch": "1562237088", 112 | "group": "example", 113 | "url": "http://", 114 | "note": "" 115 | }, 116 | { 117 | "id": "363075885435852539", 118 | "name": "example-pwd", 119 | "fullname": "example/example-pwd", 120 | "username": "", 121 | "password": "", 122 | "last_modified_gmt": "1562264157", 123 | "last_touch": "0", 124 | "group": "example", 125 | "url": "http://", 126 | "note": "" 127 | } 128 | ] 129 | -------------------------------------------------------------------------------- /example/metrics.txt: -------------------------------------------------------------------------------- 1 | # HELP controller_runtime_reconcile_time_seconds Length of time per reconciliation per controller 2 | # TYPE controller_runtime_reconcile_time_seconds histogram 3 | controller_runtime_reconcile_time_seconds_bucket{controller="lastpass-controller",le="0.005"} 0 4 | controller_runtime_reconcile_time_seconds_bucket{controller="lastpass-controller",le="0.01"} 0 5 | controller_runtime_reconcile_time_seconds_bucket{controller="lastpass-controller",le="0.025"} 0 6 | controller_runtime_reconcile_time_seconds_bucket{controller="lastpass-controller",le="0.05"} 0 7 | controller_runtime_reconcile_time_seconds_bucket{controller="lastpass-controller",le="0.1"} 0 8 | controller_runtime_reconcile_time_seconds_bucket{controller="lastpass-controller",le="0.25"} 0 9 | controller_runtime_reconcile_time_seconds_bucket{controller="lastpass-controller",le="0.5"} 0 10 | controller_runtime_reconcile_time_seconds_bucket{controller="lastpass-controller",le="1"} 0 11 | controller_runtime_reconcile_time_seconds_bucket{controller="lastpass-controller",le="2.5"} 0 12 | controller_runtime_reconcile_time_seconds_bucket{controller="lastpass-controller",le="5"} 1 13 | controller_runtime_reconcile_time_seconds_bucket{controller="lastpass-controller",le="10"} 1 14 | controller_runtime_reconcile_time_seconds_bucket{controller="lastpass-controller",le="+Inf"} 2 15 | controller_runtime_reconcile_time_seconds_sum{controller="lastpass-controller"} 466.682450088 16 | controller_runtime_reconcile_time_seconds_count{controller="lastpass-controller"} 2 17 | # HELP controller_runtime_reconcile_total Total number of reconciliations per controller 18 | # TYPE controller_runtime_reconcile_total counter 19 | controller_runtime_reconcile_total{controller="lastpass-controller",result="success"} 2 20 | # HELP go_gc_duration_seconds A summary of the GC invocation durations. 21 | # TYPE go_gc_duration_seconds summary 22 | go_gc_duration_seconds{quantile="0"} 8.245e-06 23 | go_gc_duration_seconds{quantile="0.25"} 7.4483e-05 24 | go_gc_duration_seconds{quantile="0.5"} 0.000180311 25 | go_gc_duration_seconds{quantile="0.75"} 0.000605724 26 | go_gc_duration_seconds{quantile="1"} 0.001910786 27 | go_gc_duration_seconds_sum 0.005951505 28 | go_gc_duration_seconds_count 15 29 | # HELP go_goroutines Number of goroutines that currently exist. 30 | # TYPE go_goroutines gauge 31 | go_goroutines 42 32 | # HELP go_info Information about the Go environment. 33 | # TYPE go_info gauge 34 | go_info{version="go1.12.6"} 1 35 | # HELP go_memstats_alloc_bytes Number of bytes allocated and still in use. 36 | # TYPE go_memstats_alloc_bytes gauge 37 | go_memstats_alloc_bytes 5.494688e+06 38 | # HELP go_memstats_alloc_bytes_total Total number of bytes allocated, even if freed. 39 | # TYPE go_memstats_alloc_bytes_total counter 40 | go_memstats_alloc_bytes_total 1.7851168e+07 41 | # HELP go_memstats_buck_hash_sys_bytes Number of bytes used by the profiling bucket hash table. 42 | # TYPE go_memstats_buck_hash_sys_bytes gauge 43 | go_memstats_buck_hash_sys_bytes 1.451357e+06 44 | # HELP go_memstats_frees_total Total number of frees. 45 | # TYPE go_memstats_frees_total counter 46 | go_memstats_frees_total 122012 47 | # HELP go_memstats_gc_cpu_fraction The fraction of this program's available CPU time used by the GC since the program started. 48 | # TYPE go_memstats_gc_cpu_fraction gauge 49 | go_memstats_gc_cpu_fraction 1.1518418534427328e-05 50 | # HELP go_memstats_gc_sys_bytes Number of bytes used for garbage collection system metadata. 51 | # TYPE go_memstats_gc_sys_bytes gauge 52 | go_memstats_gc_sys_bytes 2.390016e+06 53 | # HELP go_memstats_heap_alloc_bytes Number of heap bytes allocated and still in use. 54 | # TYPE go_memstats_heap_alloc_bytes gauge 55 | go_memstats_heap_alloc_bytes 5.494688e+06 56 | # HELP go_memstats_heap_idle_bytes Number of heap bytes waiting to be used. 57 | # TYPE go_memstats_heap_idle_bytes gauge 58 | go_memstats_heap_idle_bytes 5.8236928e+07 59 | # HELP go_memstats_heap_inuse_bytes Number of heap bytes that are in use. 60 | # TYPE go_memstats_heap_inuse_bytes gauge 61 | go_memstats_heap_inuse_bytes 8.216576e+06 62 | # HELP go_memstats_heap_objects Number of allocated objects. 63 | # TYPE go_memstats_heap_objects gauge 64 | go_memstats_heap_objects 25201 65 | # HELP go_memstats_heap_released_bytes Number of heap bytes released to OS. 66 | # TYPE go_memstats_heap_released_bytes gauge 67 | go_memstats_heap_released_bytes 5.820416e+07 68 | # HELP go_memstats_heap_sys_bytes Number of heap bytes obtained from system. 69 | # TYPE go_memstats_heap_sys_bytes gauge 70 | go_memstats_heap_sys_bytes 6.6453504e+07 71 | # HELP go_memstats_last_gc_time_seconds Number of seconds since 1970 of last garbage collection. 72 | # TYPE go_memstats_last_gc_time_seconds gauge 73 | go_memstats_last_gc_time_seconds 1.562771162351262e+09 74 | # HELP go_memstats_lookups_total Total number of pointer lookups. 75 | # TYPE go_memstats_lookups_total counter 76 | go_memstats_lookups_total 0 77 | # HELP go_memstats_mallocs_total Total number of mallocs. 78 | # TYPE go_memstats_mallocs_total counter 79 | go_memstats_mallocs_total 147213 80 | # HELP go_memstats_mcache_inuse_bytes Number of bytes in use by mcache structures. 81 | # TYPE go_memstats_mcache_inuse_bytes gauge 82 | go_memstats_mcache_inuse_bytes 3472 83 | # HELP go_memstats_mcache_sys_bytes Number of bytes used for mcache structures obtained from system. 84 | # TYPE go_memstats_mcache_sys_bytes gauge 85 | go_memstats_mcache_sys_bytes 16384 86 | # HELP go_memstats_mspan_inuse_bytes Number of bytes in use by mspan structures. 87 | # TYPE go_memstats_mspan_inuse_bytes gauge 88 | go_memstats_mspan_inuse_bytes 103824 89 | # HELP go_memstats_mspan_sys_bytes Number of bytes used for mspan structures obtained from system. 90 | # TYPE go_memstats_mspan_sys_bytes gauge 91 | go_memstats_mspan_sys_bytes 131072 92 | # HELP go_memstats_next_gc_bytes Number of heap bytes when next garbage collection will take place. 93 | # TYPE go_memstats_next_gc_bytes gauge 94 | go_memstats_next_gc_bytes 1.0702704e+07 95 | # HELP go_memstats_other_sys_bytes Number of bytes used for other system allocations. 96 | # TYPE go_memstats_other_sys_bytes gauge 97 | go_memstats_other_sys_bytes 664475 98 | # HELP go_memstats_stack_inuse_bytes Number of bytes in use by the stack allocator. 99 | # TYPE go_memstats_stack_inuse_bytes gauge 100 | go_memstats_stack_inuse_bytes 655360 101 | # HELP go_memstats_stack_sys_bytes Number of bytes obtained from system for stack allocator. 102 | # TYPE go_memstats_stack_sys_bytes gauge 103 | go_memstats_stack_sys_bytes 655360 104 | # HELP go_memstats_sys_bytes Number of bytes obtained from system. 105 | # TYPE go_memstats_sys_bytes gauge 106 | go_memstats_sys_bytes 7.1762168e+07 107 | # HELP go_threads Number of OS threads created. 108 | # TYPE go_threads gauge 109 | go_threads 10 110 | # HELP process_cpu_seconds_total Total user and system CPU time spent in seconds. 111 | # TYPE process_cpu_seconds_total counter 112 | process_cpu_seconds_total 3.5 113 | # HELP process_max_fds Maximum number of open file descriptors. 114 | # TYPE process_max_fds gauge 115 | process_max_fds 1.048576e+06 116 | # HELP process_open_fds Number of open file descriptors. 117 | # TYPE process_open_fds gauge 118 | process_open_fds 8 119 | # HELP process_resident_memory_bytes Resident memory size in bytes. 120 | # TYPE process_resident_memory_bytes gauge 121 | process_resident_memory_bytes 3.2333824e+07 122 | # HELP process_start_time_seconds Start time of the process since unix epoch in seconds. 123 | # TYPE process_start_time_seconds gauge 124 | process_start_time_seconds 1.56277008121e+09 125 | # HELP process_virtual_memory_bytes Virtual memory size in bytes. 126 | # TYPE process_virtual_memory_bytes gauge 127 | process_virtual_memory_bytes 1.40562432e+08 128 | # HELP process_virtual_memory_max_bytes Maximum amount of virtual memory available in bytes. 129 | # TYPE process_virtual_memory_max_bytes gauge 130 | process_virtual_memory_max_bytes -1 131 | # HELP reflector_items_per_list How many items an API list returns to the reflectors 132 | # TYPE reflector_items_per_list summary 133 | reflector_items_per_list{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5041",quantile="0.5"} NaN 134 | reflector_items_per_list{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5041",quantile="0.9"} NaN 135 | reflector_items_per_list{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5041",quantile="0.99"} NaN 136 | reflector_items_per_list_sum{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5041"} 0 137 | reflector_items_per_list_count{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5041"} 1 138 | reflector_items_per_list{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5042",quantile="0.5"} NaN 139 | reflector_items_per_list{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5042",quantile="0.9"} NaN 140 | reflector_items_per_list{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5042",quantile="0.99"} NaN 141 | reflector_items_per_list_sum{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5042"} 38 142 | reflector_items_per_list_count{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5042"} 1 143 | # HELP reflector_items_per_watch How many items an API watch returns to the reflectors 144 | # TYPE reflector_items_per_watch summary 145 | reflector_items_per_watch{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5041",quantile="0.5"} 0 146 | reflector_items_per_watch{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5041",quantile="0.9"} 0 147 | reflector_items_per_watch{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5041",quantile="0.99"} 0 148 | reflector_items_per_watch_sum{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5041"} 1 149 | reflector_items_per_watch_count{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5041"} 2 150 | reflector_items_per_watch{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5042",quantile="0.5"} 0 151 | reflector_items_per_watch{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5042",quantile="0.9"} 0 152 | reflector_items_per_watch{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5042",quantile="0.99"} 0 153 | reflector_items_per_watch_sum{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5042"} 1 154 | reflector_items_per_watch_count{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5042"} 2 155 | # HELP reflector_last_resource_version Last resource version seen for the reflectors 156 | # TYPE reflector_last_resource_version gauge 157 | reflector_last_resource_version{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5041"} 1331 158 | reflector_last_resource_version{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5042"} 1334 159 | # HELP reflector_list_duration_seconds How long an API list takes to return and decode for the reflectors 160 | # TYPE reflector_list_duration_seconds summary 161 | reflector_list_duration_seconds{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5041",quantile="0.5"} NaN 162 | reflector_list_duration_seconds{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5041",quantile="0.9"} NaN 163 | reflector_list_duration_seconds{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5041",quantile="0.99"} NaN 164 | reflector_list_duration_seconds_sum{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5041"} 0.025881228 165 | reflector_list_duration_seconds_count{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5041"} 1 166 | reflector_list_duration_seconds{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5042",quantile="0.5"} NaN 167 | reflector_list_duration_seconds{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5042",quantile="0.9"} NaN 168 | reflector_list_duration_seconds{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5042",quantile="0.99"} NaN 169 | reflector_list_duration_seconds_sum{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5042"} 0.006435053 170 | reflector_list_duration_seconds_count{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5042"} 1 171 | # HELP reflector_lists_total Total number of API lists done by the reflectors 172 | # TYPE reflector_lists_total counter 173 | reflector_lists_total{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5041"} 1 174 | reflector_lists_total{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5042"} 1 175 | # HELP reflector_short_watches_total Total number of short API watches done by the reflectors 176 | # TYPE reflector_short_watches_total counter 177 | reflector_short_watches_total{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5041"} 0 178 | reflector_short_watches_total{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5042"} 0 179 | # HELP reflector_watch_duration_seconds How long an API watch takes to return and decode for the reflectors 180 | # TYPE reflector_watch_duration_seconds summary 181 | reflector_watch_duration_seconds{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5041",quantile="0.5"} 431.002185165 182 | reflector_watch_duration_seconds{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5041",quantile="0.9"} 431.002185165 183 | reflector_watch_duration_seconds{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5041",quantile="0.99"} 431.002185165 184 | reflector_watch_duration_seconds_sum{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5041"} 1013.003227569 185 | reflector_watch_duration_seconds_count{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5041"} 2 186 | reflector_watch_duration_seconds{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5042",quantile="0.5"} 499.000859737 187 | reflector_watch_duration_seconds{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5042",quantile="0.9"} 499.000859737 188 | reflector_watch_duration_seconds{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5042",quantile="0.99"} 499.000859737 189 | reflector_watch_duration_seconds_sum{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5042"} 980.000782503 190 | reflector_watch_duration_seconds_count{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5042"} 2 191 | # HELP reflector_watches_total Total number of API watches done by the reflectors 192 | # TYPE reflector_watches_total counter 193 | reflector_watches_total{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5041"} 3 194 | reflector_watches_total{name="reflector_sigs_k8s_io_controller_runtime_pkg_cache_internal_informers_map_go_126_5042"} 3 195 | # HELP rest_client_request_latency_seconds Request latency in seconds. Broken down by verb and URL. 196 | # TYPE rest_client_request_latency_seconds histogram 197 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/%7Bprefix%7D",verb="GET",le="0.001"} 96 198 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/%7Bprefix%7D",verb="GET",le="0.002"} 103 199 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/%7Bprefix%7D",verb="GET",le="0.004"} 105 200 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/%7Bprefix%7D",verb="GET",le="0.008"} 105 201 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/%7Bprefix%7D",verb="GET",le="0.016"} 105 202 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/%7Bprefix%7D",verb="GET",le="0.032"} 106 203 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/%7Bprefix%7D",verb="GET",le="0.064"} 106 204 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/%7Bprefix%7D",verb="GET",le="0.128"} 106 205 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/%7Bprefix%7D",verb="GET",le="0.256"} 106 206 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/%7Bprefix%7D",verb="GET",le="0.512"} 106 207 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/%7Bprefix%7D",verb="GET",le="+Inf"} 106 208 | rest_client_request_latency_seconds_sum{url="https://10.96.0.1:443/%7Bprefix%7D",verb="GET"} 0.095270261 209 | rest_client_request_latency_seconds_count{url="https://10.96.0.1:443/%7Bprefix%7D",verb="GET"} 106 210 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/%7Bprefix%7D",verb="POST",le="0.001"} 0 211 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/%7Bprefix%7D",verb="POST",le="0.002"} 0 212 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/%7Bprefix%7D",verb="POST",le="0.004"} 1 213 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/%7Bprefix%7D",verb="POST",le="0.008"} 3 214 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/%7Bprefix%7D",verb="POST",le="0.016"} 3 215 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/%7Bprefix%7D",verb="POST",le="0.032"} 3 216 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/%7Bprefix%7D",verb="POST",le="0.064"} 3 217 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/%7Bprefix%7D",verb="POST",le="0.128"} 3 218 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/%7Bprefix%7D",verb="POST",le="0.256"} 3 219 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/%7Bprefix%7D",verb="POST",le="0.512"} 3 220 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/%7Bprefix%7D",verb="POST",le="+Inf"} 3 221 | rest_client_request_latency_seconds_sum{url="https://10.96.0.1:443/%7Bprefix%7D",verb="POST"} 0.014804408 222 | rest_client_request_latency_seconds_count{url="https://10.96.0.1:443/%7Bprefix%7D",verb="POST"} 3 223 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/api?timeout=32s",verb="GET",le="0.001"} 1 224 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/api?timeout=32s",verb="GET",le="0.002"} 2 225 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/api?timeout=32s",verb="GET",le="0.004"} 2 226 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/api?timeout=32s",verb="GET",le="0.008"} 3 227 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/api?timeout=32s",verb="GET",le="0.016"} 3 228 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/api?timeout=32s",verb="GET",le="0.032"} 3 229 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/api?timeout=32s",verb="GET",le="0.064"} 3 230 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/api?timeout=32s",verb="GET",le="0.128"} 3 231 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/api?timeout=32s",verb="GET",le="0.256"} 3 232 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/api?timeout=32s",verb="GET",le="0.512"} 3 233 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/api?timeout=32s",verb="GET",le="+Inf"} 3 234 | rest_client_request_latency_seconds_sum{url="https://10.96.0.1:443/api?timeout=32s",verb="GET"} 0.007466387 235 | rest_client_request_latency_seconds_count{url="https://10.96.0.1:443/api?timeout=32s",verb="GET"} 3 236 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/apis?timeout=32s",verb="GET",le="0.001"} 2 237 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/apis?timeout=32s",verb="GET",le="0.002"} 3 238 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/apis?timeout=32s",verb="GET",le="0.004"} 3 239 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/apis?timeout=32s",verb="GET",le="0.008"} 3 240 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/apis?timeout=32s",verb="GET",le="0.016"} 3 241 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/apis?timeout=32s",verb="GET",le="0.032"} 3 242 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/apis?timeout=32s",verb="GET",le="0.064"} 3 243 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/apis?timeout=32s",verb="GET",le="0.128"} 3 244 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/apis?timeout=32s",verb="GET",le="0.256"} 3 245 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/apis?timeout=32s",verb="GET",le="0.512"} 3 246 | rest_client_request_latency_seconds_bucket{url="https://10.96.0.1:443/apis?timeout=32s",verb="GET",le="+Inf"} 3 247 | rest_client_request_latency_seconds_sum{url="https://10.96.0.1:443/apis?timeout=32s",verb="GET"} 0.002392135 248 | rest_client_request_latency_seconds_count{url="https://10.96.0.1:443/apis?timeout=32s",verb="GET"} 3 249 | # HELP rest_client_requests_total Number of HTTP requests, partitioned by status code, method, and host. 250 | # TYPE rest_client_requests_total counter 251 | rest_client_requests_total{code="200",host="10.96.0.1:443",method="GET"} 117 252 | rest_client_requests_total{code="201",host="10.96.0.1:443",method="POST"} 3 253 | rest_client_requests_total{code="404",host="10.96.0.1:443",method="GET"} 1 254 | # HELP workqueue_adds_total Total number of adds handled by workqueue 255 | # TYPE workqueue_adds_total counter 256 | workqueue_adds_total{name="lastpass-controller"} 2 257 | # HELP workqueue_depth Current depth of workqueue 258 | # TYPE workqueue_depth gauge 259 | workqueue_depth{name="lastpass-controller"} 0 260 | # HELP workqueue_longest_running_processor_microseconds How many microseconds has the longest running processor for workqueue been running. 261 | # TYPE workqueue_longest_running_processor_microseconds gauge 262 | workqueue_longest_running_processor_microseconds{name="lastpass-controller"} 0 263 | # HELP workqueue_queue_latency_seconds How long in seconds an item stays in workqueue before being requested. 264 | # TYPE workqueue_queue_latency_seconds histogram 265 | workqueue_queue_latency_seconds_bucket{name="lastpass-controller",le="1e-08"} 0 266 | workqueue_queue_latency_seconds_bucket{name="lastpass-controller",le="1e-07"} 0 267 | workqueue_queue_latency_seconds_bucket{name="lastpass-controller",le="1e-06"} 0 268 | workqueue_queue_latency_seconds_bucket{name="lastpass-controller",le="9.999999999999999e-06"} 0 269 | workqueue_queue_latency_seconds_bucket{name="lastpass-controller",le="9.999999999999999e-05"} 0 270 | workqueue_queue_latency_seconds_bucket{name="lastpass-controller",le="0.001"} 0 271 | workqueue_queue_latency_seconds_bucket{name="lastpass-controller",le="0.01"} 0 272 | workqueue_queue_latency_seconds_bucket{name="lastpass-controller",le="0.1"} 0 273 | workqueue_queue_latency_seconds_bucket{name="lastpass-controller",le="1"} 0 274 | workqueue_queue_latency_seconds_bucket{name="lastpass-controller",le="10"} 1 275 | workqueue_queue_latency_seconds_bucket{name="lastpass-controller",le="+Inf"} 2 276 | workqueue_queue_latency_seconds_sum{name="lastpass-controller"} 309 277 | workqueue_queue_latency_seconds_count{name="lastpass-controller"} 2 278 | # HELP workqueue_retries_total Total number of retries handled by workqueue 279 | # TYPE workqueue_retries_total counter 280 | workqueue_retries_total{name="lastpass-controller"} 0 281 | # HELP workqueue_unfinished_work_seconds How many seconds of work has done that is in progress and hasn't been observed by work_duration. Large values indicate stuck threads. One can deduce the number of stuck threads by observing the rate at which this increases. 282 | # TYPE workqueue_unfinished_work_seconds gauge 283 | workqueue_unfinished_work_seconds{name="lastpass-controller"} 0 284 | # HELP workqueue_work_duration_seconds How long in seconds processing an item from workqueue takes. 285 | # TYPE workqueue_work_duration_seconds histogram 286 | workqueue_work_duration_seconds_bucket{name="lastpass-controller",le="1e-08"} 0 287 | workqueue_work_duration_seconds_bucket{name="lastpass-controller",le="1e-07"} 0 288 | workqueue_work_duration_seconds_bucket{name="lastpass-controller",le="1e-06"} 0 289 | workqueue_work_duration_seconds_bucket{name="lastpass-controller",le="9.999999999999999e-06"} 0 290 | workqueue_work_duration_seconds_bucket{name="lastpass-controller",le="9.999999999999999e-05"} 0 291 | workqueue_work_duration_seconds_bucket{name="lastpass-controller",le="0.001"} 0 292 | workqueue_work_duration_seconds_bucket{name="lastpass-controller",le="0.01"} 0 293 | workqueue_work_duration_seconds_bucket{name="lastpass-controller",le="0.1"} 0 294 | workqueue_work_duration_seconds_bucket{name="lastpass-controller",le="1"} 0 295 | workqueue_work_duration_seconds_bucket{name="lastpass-controller",le="10"} 0 296 | workqueue_work_duration_seconds_bucket{name="lastpass-controller",le="+Inf"} 2 297 | workqueue_work_duration_seconds_sum{name="lastpass-controller"} 4.955925e+06 298 | workqueue_work_duration_seconds_count{name="lastpass-controller"} 2 299 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/edgelevel/lastpass-operator 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/codeskyblue/go-sh v0.0.0-20200712050446-30169cf553fe 7 | github.com/go-logr/logr v1.4.2 8 | github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 9 | github.com/onsi/ginkgo/v2 v2.20.1 10 | github.com/onsi/gomega v1.34.1 11 | github.com/rs/zerolog v1.33.0 12 | k8s.io/api v0.29.7 13 | k8s.io/apimachinery v0.29.7 14 | k8s.io/client-go v0.29.7 15 | sigs.k8s.io/controller-runtime v0.17.5 16 | ) 17 | 18 | require ( 19 | github.com/beorn7/perks v1.0.1 // indirect 20 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 21 | github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 // indirect 22 | github.com/davecgh/go-spew v1.1.1 // indirect 23 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 24 | github.com/evanphx/json-patch/v5 v5.8.0 // indirect 25 | github.com/fsnotify/fsnotify v1.7.0 // indirect 26 | github.com/go-logr/zapr v1.3.0 // indirect 27 | github.com/go-openapi/jsonpointer v0.19.6 // indirect 28 | github.com/go-openapi/jsonreference v0.20.2 // indirect 29 | github.com/go-openapi/swag v0.22.3 // indirect 30 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 31 | github.com/gogo/protobuf v1.3.2 // indirect 32 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 33 | github.com/golang/protobuf v1.5.4 // indirect 34 | github.com/google/gnostic-models v0.6.8 // indirect 35 | github.com/google/go-cmp v0.6.0 // indirect 36 | github.com/google/gofuzz v1.2.0 // indirect 37 | github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect 38 | github.com/google/uuid v1.3.0 // indirect 39 | github.com/imdario/mergo v0.3.12 // indirect 40 | github.com/josharian/intern v1.0.0 // indirect 41 | github.com/json-iterator/go v1.1.12 // indirect 42 | github.com/mailru/easyjson v0.7.7 // indirect 43 | github.com/mattn/go-colorable v0.1.13 // indirect 44 | github.com/mattn/go-isatty v0.0.19 // indirect 45 | github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect 46 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 47 | github.com/modern-go/reflect2 v1.0.2 // indirect 48 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 49 | github.com/pkg/errors v0.9.1 // indirect 50 | github.com/prometheus/client_golang v1.18.0 // indirect 51 | github.com/prometheus/client_model v0.5.0 // indirect 52 | github.com/prometheus/common v0.45.0 // indirect 53 | github.com/prometheus/procfs v0.12.0 // indirect 54 | github.com/spf13/pflag v1.0.5 // indirect 55 | go.uber.org/multierr v1.11.0 // indirect 56 | go.uber.org/zap v1.26.0 // indirect 57 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect 58 | golang.org/x/net v0.28.0 // indirect 59 | golang.org/x/oauth2 v0.12.0 // indirect 60 | golang.org/x/sys v0.23.0 // indirect 61 | golang.org/x/term v0.23.0 // indirect 62 | golang.org/x/text v0.17.0 // indirect 63 | golang.org/x/time v0.3.0 // indirect 64 | golang.org/x/tools v0.24.0 // indirect 65 | gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect 66 | google.golang.org/appengine v1.6.7 // indirect 67 | google.golang.org/protobuf v1.34.1 // indirect 68 | gopkg.in/inf.v0 v0.9.1 // indirect 69 | gopkg.in/yaml.v2 v2.4.0 // indirect 70 | gopkg.in/yaml.v3 v3.0.1 // indirect 71 | k8s.io/apiextensions-apiserver v0.29.2 // indirect 72 | k8s.io/component-base v0.29.2 // indirect 73 | k8s.io/klog/v2 v2.110.1 // indirect 74 | k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect 75 | k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect 76 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 77 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect 78 | sigs.k8s.io/yaml v1.4.0 // indirect 79 | ) 80 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 2 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 3 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 4 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 5 | github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 h1:sDMmm+q/3+BukdIpxwO365v/Rbspp2Nt5XntgQRXq8Q= 6 | github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= 7 | github.com/codeskyblue/go-sh v0.0.0-20200712050446-30169cf553fe h1:69JI97HlzP+PH5Mi1thcGlDoBr6PS2Oe+l3mNmAkbs4= 8 | github.com/codeskyblue/go-sh v0.0.0-20200712050446-30169cf553fe/go.mod h1:VQx0hjo2oUeQkQUET7wRwradO6f+fN5jzXgB/zROxxE= 9 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 10 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 11 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 13 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= 15 | github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 16 | github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= 17 | github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 18 | github.com/evanphx/json-patch/v5 v5.8.0 h1:lRj6N9Nci7MvzrXuX6HFzU8XjmhPiXPlsKEy1u0KQro= 19 | github.com/evanphx/json-patch/v5 v5.8.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= 20 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 21 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 22 | github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 23 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 24 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 25 | github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= 26 | github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= 27 | github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= 28 | github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 29 | github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= 30 | github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= 31 | github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= 32 | github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 33 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 34 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 35 | github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 h1:FWNFq4fM1wPfcK40yHE5UO3RUdSNPaBC+j3PokzA6OQ= 36 | github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI= 37 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 38 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 39 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 40 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 41 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 42 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 43 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 44 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 45 | github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= 46 | github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= 47 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 48 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 49 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 50 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 51 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 52 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 53 | github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k= 54 | github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= 55 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 56 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 57 | github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= 58 | github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= 59 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 60 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 61 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 62 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 63 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 64 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 65 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 66 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 67 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 68 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 69 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 70 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 71 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 72 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 73 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 74 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 75 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 76 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 77 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 78 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 79 | github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= 80 | github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= 81 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 82 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 83 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 84 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 85 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 86 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 87 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 88 | github.com/onsi/ginkgo/v2 v2.20.1 h1:YlVIbqct+ZmnEph770q9Q7NVAz4wwIiVNahee6JyUzo= 89 | github.com/onsi/ginkgo/v2 v2.20.1/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI= 90 | github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= 91 | github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= 92 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 93 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 94 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 95 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 96 | github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= 97 | github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= 98 | github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= 99 | github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= 100 | github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= 101 | github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= 102 | github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= 103 | github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= 104 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 105 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 106 | github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 107 | github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= 108 | github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= 109 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 110 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 111 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 112 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 113 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 114 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 115 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 116 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 117 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 118 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 119 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 120 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 121 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 122 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 123 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 124 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 125 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 126 | go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= 127 | go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= 128 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 129 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 130 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 131 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= 132 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= 133 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 134 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 135 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 136 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 137 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 138 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 139 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 140 | golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= 141 | golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= 142 | golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4= 143 | golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4= 144 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 145 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 146 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 147 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 148 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 149 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 150 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 151 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 152 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 153 | golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= 154 | golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 155 | golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= 156 | golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= 157 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 158 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 159 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 160 | golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= 161 | golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 162 | golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= 163 | golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 164 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 165 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 166 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 167 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 168 | golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= 169 | golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= 170 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 171 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 172 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 173 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 174 | gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= 175 | gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= 176 | google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= 177 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 178 | google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= 179 | google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 180 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 181 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 182 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 183 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 184 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 185 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 186 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 187 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 188 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 189 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 190 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 191 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 192 | k8s.io/api v0.29.7 h1:Q2/thp7YYESgy0MGzxT9RvA/6doLJHBXSFH8GGLxSbc= 193 | k8s.io/api v0.29.7/go.mod h1:mPimdbyuIjwoLtBEVIGVUYb4BKOE+44XHt/n4IqKsLA= 194 | k8s.io/apiextensions-apiserver v0.29.2 h1:UK3xB5lOWSnhaCk0RFZ0LUacPZz9RY4wi/yt2Iu+btg= 195 | k8s.io/apiextensions-apiserver v0.29.2/go.mod h1:aLfYjpA5p3OwtqNXQFkhJ56TB+spV8Gc4wfMhUA3/b8= 196 | k8s.io/apimachinery v0.29.7 h1:ICXzya58Q7hyEEfnTrbmdfX1n1schSepX2KUfC2/ykc= 197 | k8s.io/apimachinery v0.29.7/go.mod h1:i3FJVwhvSp/6n8Fl4K97PJEP8C+MM+aoDq4+ZJBf70Y= 198 | k8s.io/client-go v0.29.7 h1:vTtiFrGBKlcBhxaeZC4eDrqui1e108nsTyue/KU63IY= 199 | k8s.io/client-go v0.29.7/go.mod h1:69BvVqdRozgR/9TP45u/oO0tfrdbP+I8RqrcCJQshzg= 200 | k8s.io/component-base v0.29.2 h1:lpiLyuvPA9yV1aQwGLENYyK7n/8t6l3nn3zAtFTJYe8= 201 | k8s.io/component-base v0.29.2/go.mod h1:BfB3SLrefbZXiBfbM+2H1dlat21Uewg/5qtKOl8degM= 202 | k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= 203 | k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= 204 | k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= 205 | k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= 206 | k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= 207 | k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 208 | sigs.k8s.io/controller-runtime v0.17.5 h1:1FI9Lm7NiOOmBsgTV36/s2XrEFXnO2C4sbg/Zme72Rw= 209 | sigs.k8s.io/controller-runtime v0.17.5/go.mod h1:N0jpP5Lo7lMTF9aL56Z/B2oWBJjey6StQM0jRbKQXtY= 210 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= 211 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= 212 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= 213 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= 214 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 215 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 216 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edgelevel/lastpass-operator/935b45330779efccae8561d8e7d1343944de7775/hack/boilerplate.go.txt -------------------------------------------------------------------------------- /internal/controller/lastpass_controller.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "text/template" 7 | "time" 8 | 9 | edgelevelv1alpha1 "github.com/edgelevel/lastpass-operator/api/v1alpha1" 10 | "github.com/edgelevel/lastpass-operator/pkg/lastpass" 11 | "github.com/edgelevel/lastpass-operator/pkg/utils" 12 | "github.com/go-logr/logr" 13 | corev1 "k8s.io/api/core/v1" 14 | "k8s.io/apimachinery/pkg/api/errors" 15 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 16 | "k8s.io/apimachinery/pkg/runtime" 17 | "k8s.io/apimachinery/pkg/types" 18 | ctrl "sigs.k8s.io/controller-runtime" 19 | "sigs.k8s.io/controller-runtime/pkg/client" 20 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 21 | logf "sigs.k8s.io/controller-runtime/pkg/log" 22 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 23 | ) 24 | 25 | // LastPassReconciler reconciles a LastPass object 26 | type LastPassReconciler struct { 27 | client.Client 28 | Log logr.Logger 29 | Scheme *runtime.Scheme 30 | SecretNameTemplate *template.Template 31 | } 32 | 33 | //+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete 34 | //+kubebuilder:rbac:groups=edgelevel.com,resources=lastpasses,verbs=get;list;watch;create;update;patch;delete 35 | //+kubebuilder:rbac:groups=edgelevel.com,resources=lastpasses/status,verbs=get;update;patch 36 | 37 | // Reconcile is part of the main kubernetes reconciliation loop which aims to 38 | // move the current state of the cluster closer to the desired state. 39 | // TODO(user): Modify the Reconcile function to compare the state specified by 40 | // the LastPass object against the actual cluster state, and then 41 | // perform operations to make the cluster state reflect the state specified by 42 | // the user. 43 | // 44 | // For more details, check Reconcile and its Result here: 45 | // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.6.4/pkg/reconcile 46 | func (r *LastPassReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 47 | var log = logf.Log.WithName("controller_lastpass") 48 | reqLogger := log.WithValues("Request.Namespace", req.Namespace, "Request.Name", req.Name) 49 | reqLogger.Info("Reconciling LastPass") 50 | 51 | // Check that the environment variables are defined or exit. See also lastpass-master-secret 52 | lastPassUsername := utils.GetEnvOrDie("LASTPASS_USERNAME") 53 | lastPassPassword := utils.GetEnvOrDie("LASTPASS_PASSWORD") 54 | 55 | // Check that "lpass" binary is available or exit 56 | lastpass.VerifyCliExistsOrDie() 57 | 58 | // Login to LastPass 59 | if err := lastpass.Login(lastPassUsername, lastPassPassword); err != nil { 60 | // Attempt login again, sometimes it fails even if the credentials are valid - requeue the request. 61 | return reconcile.Result{}, err 62 | } 63 | 64 | // Fetch the LastPass instance 65 | instance := &edgelevelv1alpha1.LastPass{} 66 | err := r.Client.Get(context.TODO(), req.NamespacedName, instance) 67 | if err != nil { 68 | if errors.IsNotFound(err) { 69 | // Request object not found, could have been deleted after reconcile request. 70 | // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. 71 | // Return and don't requeue 72 | return reconcile.Result{}, nil 73 | } 74 | // Error reading the object - requeue the request. 75 | return reconcile.Result{}, err 76 | } 77 | 78 | // Request LastPass secrets 79 | lastPassSecrets, err := lastpass.RequestSecrets(instance.Spec.SecretRef.Group, instance.Spec.SecretRef.Name) 80 | // Logout 81 | // lastpass.Logout() 82 | if err != nil { 83 | // Error parsing the response - requeue the request. 84 | return reconcile.Result{}, err 85 | } 86 | 87 | for index := range lastPassSecrets { 88 | 89 | // Define a new Secret object 90 | desired := r.newSecretForCR(instance, lastPassSecrets[index]) 91 | 92 | reqLogger.Info("Verify LastPassSecret", "Secret.Namespace", desired.Namespace, "Secret.Name", desired.Name) 93 | 94 | // Set LastPassSecret instance as the owner and controller 95 | if err := controllerutil.SetControllerReference(instance, desired, r.Scheme); err != nil { 96 | reqLogger.Error(err, "Failed to set LastPassSecret instance as the owner and controller") 97 | return reconcile.Result{}, err 98 | } 99 | 100 | // Check if this Secret already exists 101 | current := &corev1.Secret{} 102 | err = r.Client.Get(context.TODO(), types.NamespacedName{Name: desired.Name, Namespace: desired.Namespace}, current) 103 | if err != nil && errors.IsNotFound(err) { 104 | reqLogger.Info("Creating Secret", "Secret.Namespace", desired.Namespace, "Secret.Name", desired.Name) 105 | err = r.Client.Create(context.TODO(), desired) 106 | if err != nil { 107 | reqLogger.Error(err, "Failed to create Secret", "Secret.Namespace", desired.Namespace, "Secret.Name", desired.Name) 108 | return reconcile.Result{}, err 109 | } 110 | // Secret created successfully - don't requeue 111 | continue 112 | } else if err != nil { 113 | reqLogger.Error(err, "Failed to get Secret", "Secret.Namespace", desired.Namespace, "Secret.Name", desired.Name) 114 | return reconcile.Result{}, err 115 | } 116 | 117 | // Check if this Secret is changed 118 | if current.Annotations["lastModifiedGmt"] != desired.Annotations["lastModifiedGmt"] || current.Annotations["lastTouch"] != desired.Annotations["lastTouch"] { 119 | reqLogger.Info("Updating Secret", 120 | "Secret.Namespace", desired.Namespace, 121 | "Secret.Name", desired.Name, 122 | "Current:LastModifiedGmt", current.Annotations["lastModifiedGmt"], 123 | "Desired:LastModifiedGmt", desired.Annotations["lastModifiedGmt"], 124 | "Current:LastTouch", current.Annotations["lastTouch"], 125 | "Desired:LastTouch", desired.Annotations["lastTouch"]) 126 | err = r.Client.Update(context.TODO(), desired) 127 | if err != nil { 128 | reqLogger.Error(err, "Failed to update Secret", "Secret.Namespace", desired.Namespace, "Secret.Name", desired.Name) 129 | return reconcile.Result{}, err 130 | } 131 | // Secret updated successfully - don't requeue 132 | continue 133 | } 134 | 135 | reqLogger.Info("Skip reconcile: Secret already exists and is up to date", "Secret.Namespace", current.Namespace, "Secret.Name", current.Name) 136 | } 137 | 138 | // Periodically reconcile the Custom Resource 139 | if instance.Spec.SyncPolicy.Enabled { 140 | return reconcile.Result{RequeueAfter: time.Second * instance.Spec.SyncPolicy.Refresh}, nil 141 | } 142 | 143 | // Reconcile only if something happens inside the cluster: ignore if the Secret changes externally 144 | return reconcile.Result{}, nil 145 | } 146 | 147 | // SetupWithManager sets up the controller with the Manager. 148 | func (r *LastPassReconciler) SetupWithManager(mgr ctrl.Manager) error { 149 | return ctrl.NewControllerManagedBy(mgr). 150 | For(&edgelevelv1alpha1.LastPass{}). 151 | Complete(r) 152 | } 153 | 154 | // newSecretForCR creates a new secret 155 | func (r *LastPassReconciler) newSecretForCR(cr *edgelevelv1alpha1.LastPass, secret lastpass.LastPassSecret) *corev1.Secret { 156 | labels := map[string]string{ 157 | "app": "lastpass-operator", 158 | } 159 | annotations := map[string]string{ 160 | "id": secret.ID, 161 | "group": secret.Group, 162 | "name": secret.Name, 163 | "fullname": secret.Fullname, 164 | "lastModifiedGmt": secret.LastModifiedGmt, 165 | "lastTouch": secret.LastTouch, 166 | } 167 | 168 | data := map[string]string{} 169 | if cr.Spec.SecretRef.WithUsername { 170 | data["USERNAME"] = secret.Username 171 | } 172 | if cr.Spec.SecretRef.WithPassword { 173 | data["PASSWORD"] = secret.Password 174 | } 175 | if cr.Spec.SecretRef.WithUrl { 176 | data["URL"] = secret.URL 177 | } 178 | if cr.Spec.SecretRef.WithNote { 179 | data["NOTE"] = secret.Note 180 | } 181 | 182 | var secretName bytes.Buffer 183 | r.SecretNameTemplate.Execute(&secretName, struct { 184 | LastPass *edgelevelv1alpha1.LastPass 185 | LastPassSecret lastpass.LastPassSecret 186 | }{cr, secret}) 187 | 188 | return &corev1.Secret{ 189 | ObjectMeta: metav1.ObjectMeta{ 190 | Name: secretName.String(), 191 | Namespace: cr.Namespace, 192 | Labels: labels, 193 | Annotations: annotations, 194 | }, 195 | StringData: data, 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /internal/controller/lastpass_controller_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controller 18 | 19 | import ( 20 | . "github.com/onsi/ginkgo/v2" 21 | ) 22 | 23 | var _ = Describe("LastPass Controller", func() { 24 | 25 | // Context("When reconciling a resource", func() { 26 | // const resourceName = "test-resource" 27 | 28 | // ctx := context.Background() 29 | 30 | // typeNamespacedName := types.NamespacedName{ 31 | // Name: resourceName, 32 | // Namespace: "default", // TODO(user):Modify as needed 33 | // } 34 | // lastpass := &edgelevelcomv1alpha1.LastPass{} 35 | 36 | // BeforeEach(func() { 37 | // By("creating the custom resource for the Kind LastPass") 38 | // err := k8sClient.Get(ctx, typeNamespacedName, lastpass) 39 | // if err != nil && errors.IsNotFound(err) { 40 | // resource := &edgelevelcomv1alpha1.LastPass{ 41 | // ObjectMeta: metav1.ObjectMeta{ 42 | // Name: resourceName, 43 | // Namespace: "default", 44 | // }, 45 | // // TODO(user): Specify other spec details if needed. 46 | // } 47 | // Expect(k8sClient.Create(ctx, resource)).To(Succeed()) 48 | // } 49 | // }) 50 | 51 | // AfterEach(func() { 52 | // // TODO(user): Cleanup logic after each test, like removing the resource instance. 53 | // resource := &edgelevelcomv1alpha1.LastPass{} 54 | // err := k8sClient.Get(ctx, typeNamespacedName, resource) 55 | // Expect(err).NotTo(HaveOccurred()) 56 | 57 | // By("Cleanup the specific resource instance LastPass") 58 | // Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) 59 | // }) 60 | // It("should successfully reconcile the resource", func() { 61 | // By("Reconciling the created resource") 62 | // controllerReconciler := &LastPassReconciler{ 63 | // Client: k8sClient, 64 | // Scheme: k8sClient.Scheme(), 65 | // } 66 | 67 | // _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ 68 | // NamespacedName: typeNamespacedName, 69 | // }) 70 | // Expect(err).NotTo(HaveOccurred()) 71 | // // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. 72 | // // Example: If you expect a certain status condition after reconciliation, verify it here. 73 | // }) 74 | // }) 75 | }) 76 | -------------------------------------------------------------------------------- /internal/controller/lastpassgroup_controller.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "slices" 7 | "text/template" 8 | "time" 9 | 10 | corev1 "k8s.io/api/core/v1" 11 | 12 | "k8s.io/apimachinery/pkg/api/errors" 13 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | "k8s.io/apimachinery/pkg/runtime" 15 | "k8s.io/apimachinery/pkg/types" 16 | ctrl "sigs.k8s.io/controller-runtime" 17 | "sigs.k8s.io/controller-runtime/pkg/client" 18 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 19 | logf "sigs.k8s.io/controller-runtime/pkg/log" 20 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 21 | 22 | edgelevelv1alpha1 "github.com/edgelevel/lastpass-operator/api/v1alpha1" 23 | "github.com/edgelevel/lastpass-operator/pkg/lastpass" 24 | "github.com/edgelevel/lastpass-operator/pkg/utils" 25 | "github.com/go-logr/logr" 26 | ) 27 | 28 | var ( 29 | secretOwnerKey = ".metadata.controller" 30 | apiGVStr = edgelevelv1alpha1.GroupVersion.String() 31 | ) 32 | 33 | // LastPassGroupReconciler reconciles a LastPassGroup object 34 | type LastPassGroupReconciler struct { 35 | client.Client 36 | Log logr.Logger 37 | Scheme *runtime.Scheme 38 | SecretNameTemplate *template.Template 39 | } 40 | 41 | //+kubebuilder:rbac:groups=edgelevel.com,resources=lastpassgroups,verbs=get;list;watch;create;update;patch;delete 42 | //+kubebuilder:rbac:groups=edgelevel.com,resources=lastpassgroups/status,verbs=get;update;patch 43 | //+kubebuilder:rbac:groups=edgelevel.com,resources=lastpassgroups/finalizers,verbs=update 44 | 45 | // Reconcile is part of the main kubernetes reconciliation loop which aims to 46 | // move the current state of the cluster closer to the desired state. 47 | // TODO(user): Modify the Reconcile function to compare the state specified by 48 | // the LastPassGroup object against the actual cluster state, and then 49 | // perform operations to make the cluster state reflect the state specified by 50 | // the user. 51 | // 52 | // For more details, check Reconcile and its Result here: 53 | // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.17.3/pkg/reconcile 54 | func (r *LastPassGroupReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 55 | var log = logf.Log.WithName("controller_lastpassgroup") 56 | reqLogger := log.WithValues("Request.Namespace", req.Namespace, "Request.Name", req.Name) 57 | reqLogger.Info("Reconciling LastPass") 58 | 59 | lastPassUsername := utils.GetEnvOrDie("LASTPASS_USERNAME") 60 | lastPassPassword := utils.GetEnvOrDie("LASTPASS_PASSWORD") 61 | 62 | // Check that "lpass" binary is available or exit 63 | lastpass.VerifyCliExistsOrDie() 64 | 65 | // Login to LastPass 66 | if err := lastpass.Login(lastPassUsername, lastPassPassword); err != nil { 67 | // Attempt login again, sometimes it fails even if the credentials are valid - requeue the request. 68 | return reconcile.Result{}, err 69 | } 70 | 71 | instance := &edgelevelv1alpha1.LastPassGroup{} 72 | err := r.Client.Get(context.TODO(), req.NamespacedName, instance) 73 | if err != nil { 74 | if errors.IsNotFound(err) { 75 | // Request object not found, could have been deleted after reconcile request. 76 | // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. 77 | // Return and don't requeue 78 | return reconcile.Result{}, nil 79 | } 80 | // Error reading the object - requeue the request. 81 | return reconcile.Result{}, err 82 | } 83 | 84 | lastPassSecrets, err := lastpass.RequestSecretsGroup(instance.Spec.GroupRef.Group) 85 | if err != nil { 86 | // Error parsing the response - requeue the request. 87 | return reconcile.Result{}, err 88 | } 89 | 90 | desiredSecrets := r.newGroupSecretsForCR(instance, lastPassSecrets) 91 | 92 | existingSecretslist := &corev1.SecretList{} 93 | r.Client.List(context.TODO(), existingSecretslist, client.InNamespace(instance.Namespace), client.MatchingFields{ 94 | secretOwnerKey: req.Name, 95 | }) 96 | 97 | deletedSecrets := []corev1.Secret{} 98 | for _, sec := range existingSecretslist.Items { 99 | contains := slices.ContainsFunc(desiredSecrets, func(s *corev1.Secret) bool { return s.Name == sec.Name }) 100 | if !contains { 101 | deletedSecrets = append(deletedSecrets, sec) 102 | } 103 | } 104 | 105 | for _, del := range deletedSecrets { 106 | reqLogger.Info("Deleting Secret", "Secret.Namespace", del.Namespace, "Secret.Name", del.Name) 107 | 108 | if err := r.Client.Delete(context.TODO(), &del); err != nil { 109 | reqLogger.Error(err, "Failed to delete Secret", "Secret.Namespace", del.Namespace, "Secret.Name", del.Name) 110 | return reconcile.Result{}, err 111 | } 112 | } 113 | 114 | for _, desired := range desiredSecrets { 115 | reqLogger.Info("Verify LastPassSecret", "Secret.Namespace", desired.Namespace, "Secret.Name", desired.Name) 116 | 117 | // Set LastPassSecret instance as the owner and controller 118 | if err := controllerutil.SetControllerReference(instance, desired, r.Scheme); err != nil { 119 | reqLogger.Error(err, "Failed to set LastPassSecret instance as the owner and controller") 120 | return reconcile.Result{}, err 121 | } 122 | 123 | // Check if this Secret already exists 124 | current := &corev1.Secret{} 125 | err = r.Client.Get(context.TODO(), types.NamespacedName{Name: desired.Name, Namespace: desired.Namespace}, current) 126 | if err != nil && errors.IsNotFound(err) { 127 | reqLogger.Info("Creating Secret", "Secret.Namespace", desired.Namespace, "Secret.Name", desired.Name) 128 | err = r.Client.Create(context.TODO(), desired) 129 | if err != nil { 130 | reqLogger.Error(err, "Failed to create Secret", "Secret.Namespace", desired.Namespace, "Secret.Name", desired.Name) 131 | return reconcile.Result{}, err 132 | } 133 | // Secret created successfully - don't requeue 134 | continue 135 | } else if err != nil { 136 | reqLogger.Error(err, "Failed to get Secret", "Secret.Namespace", desired.Namespace, "Secret.Name", desired.Name) 137 | return reconcile.Result{}, err 138 | } 139 | 140 | // Check if this Secret is changed 141 | if current.Annotations["lastModifiedGmt"] != desired.Annotations["lastModifiedGmt"] || current.Annotations["lastTouch"] != desired.Annotations["lastTouch"] { 142 | reqLogger.Info("Updating Secret", 143 | "Secret.Namespace", desired.Namespace, 144 | "Secret.Name", desired.Name, 145 | "Current:LastModifiedGmt", current.Annotations["lastModifiedGmt"], 146 | "Desired:LastModifiedGmt", desired.Annotations["lastModifiedGmt"], 147 | "Current:LastTouch", current.Annotations["lastTouch"], 148 | "Desired:LastTouch", desired.Annotations["lastTouch"]) 149 | err = r.Client.Update(context.TODO(), desired) 150 | if err != nil { 151 | reqLogger.Error(err, "Failed to update Secret", "Secret.Namespace", desired.Namespace, "Secret.Name", desired.Name) 152 | return reconcile.Result{}, err 153 | } 154 | // Secret updated successfully - don't requeue 155 | continue 156 | } 157 | 158 | reqLogger.Info("Skip reconcile: Secret already exists and is up to date", "Secret.Namespace", current.Namespace, "Secret.Name", current.Name) 159 | } 160 | 161 | if instance.Spec.SyncPolicy.Enabled { 162 | return reconcile.Result{RequeueAfter: time.Second * instance.Spec.SyncPolicy.Refresh}, nil 163 | } 164 | 165 | return reconcile.Result{}, nil 166 | } 167 | 168 | // SetupWithManager sets up the controller with the Manager. 169 | func (r *LastPassGroupReconciler) SetupWithManager(mgr ctrl.Manager) error { 170 | 171 | if err := mgr.GetFieldIndexer().IndexField(context.Background(), &corev1.Secret{}, secretOwnerKey, func(rawObj client.Object) []string { 172 | owner := metav1.GetControllerOf(rawObj.(*corev1.Secret)) 173 | if owner == nil || owner.APIVersion != apiGVStr || owner.Kind != "LastPassGroup" { 174 | return nil 175 | } 176 | 177 | return []string{owner.Name} 178 | }); err != nil { 179 | return err 180 | } 181 | 182 | return ctrl.NewControllerManagedBy(mgr). 183 | For(&edgelevelv1alpha1.LastPassGroup{}). 184 | Complete(r) 185 | } 186 | 187 | // newSecretForCR creates a new secret 188 | func (r *LastPassGroupReconciler) newGroupSecretsForCR(cr *edgelevelv1alpha1.LastPassGroup, secrets []lastpass.LastPassSecret) []*corev1.Secret { 189 | labels := map[string]string{ 190 | "app": "lastpass-operator", 191 | } 192 | 193 | desiredSecrets := []*corev1.Secret{} 194 | for _, secret := range secrets { 195 | annotations := map[string]string{ 196 | "id": secret.ID, 197 | "group": secret.Group, 198 | "name": secret.Name, 199 | "fullname": secret.Fullname, 200 | "lastModifiedGmt": secret.LastModifiedGmt, 201 | "lastTouch": secret.LastTouch, 202 | } 203 | 204 | data := map[string]string{} 205 | if cr.Spec.GroupRef.WithUsername { 206 | data["USERNAME"] = secret.Username 207 | } 208 | if cr.Spec.GroupRef.WithPassword { 209 | data["PASSWORD"] = secret.Password 210 | } 211 | if cr.Spec.GroupRef.WithUrl { 212 | data["URL"] = secret.URL 213 | } 214 | if cr.Spec.GroupRef.WithNote { 215 | data["NOTE"] = secret.Note 216 | } 217 | 218 | var secretName bytes.Buffer 219 | r.SecretNameTemplate.Execute(&secretName, struct { 220 | LastPass *edgelevelv1alpha1.LastPassGroup 221 | LastPassSecret lastpass.LastPassSecret 222 | }{cr, secret}) 223 | 224 | desiredSecrets = append(desiredSecrets, &corev1.Secret{ 225 | ObjectMeta: metav1.ObjectMeta{ 226 | Name: secretName.String(), 227 | Namespace: cr.Namespace, 228 | Labels: labels, 229 | Annotations: annotations, 230 | }, 231 | StringData: data, 232 | }) 233 | } 234 | 235 | return desiredSecrets 236 | } 237 | -------------------------------------------------------------------------------- /internal/controller/lastpassgroup_controller_test.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | "k8s.io/apimachinery/pkg/api/errors" 9 | "k8s.io/apimachinery/pkg/types" 10 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 11 | 12 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | 14 | edgelevelcomv1alpha1 "github.com/edgelevel/lastpass-operator/api/v1alpha1" 15 | ) 16 | 17 | var _ = Describe("LastPassGroup Controller", func() { 18 | Context("When reconciling a resource", func() { 19 | const resourceName = "test-resource" 20 | 21 | ctx := context.Background() 22 | 23 | typeNamespacedName := types.NamespacedName{ 24 | Name: resourceName, 25 | Namespace: "default", // TODO(user):Modify as needed 26 | } 27 | lastpassgroup := &edgelevelcomv1alpha1.LastPassGroup{} 28 | 29 | BeforeEach(func() { 30 | By("creating the custom resource for the Kind LastPassGroup") 31 | err := k8sClient.Get(ctx, typeNamespacedName, lastpassgroup) 32 | if err != nil && errors.IsNotFound(err) { 33 | resource := &edgelevelcomv1alpha1.LastPassGroup{ 34 | ObjectMeta: metav1.ObjectMeta{ 35 | Name: resourceName, 36 | Namespace: "default", 37 | }, 38 | // TODO(user): Specify other spec details if needed. 39 | } 40 | Expect(k8sClient.Create(ctx, resource)).To(Succeed()) 41 | } 42 | }) 43 | 44 | AfterEach(func() { 45 | // TODO(user): Cleanup logic after each test, like removing the resource instance. 46 | resource := &edgelevelcomv1alpha1.LastPassGroup{} 47 | err := k8sClient.Get(ctx, typeNamespacedName, resource) 48 | Expect(err).NotTo(HaveOccurred()) 49 | 50 | By("Cleanup the specific resource instance LastPassGroup") 51 | Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) 52 | }) 53 | It("should successfully reconcile the resource", func() { 54 | By("Reconciling the created resource") 55 | controllerReconciler := &LastPassGroupReconciler{ 56 | Client: k8sClient, 57 | Scheme: k8sClient.Scheme(), 58 | } 59 | 60 | _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ 61 | NamespacedName: typeNamespacedName, 62 | }) 63 | Expect(err).NotTo(HaveOccurred()) 64 | // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. 65 | // Example: If you expect a certain status condition after reconciliation, verify it here. 66 | }) 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /internal/controller/suite_test.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | "k8s.io/client-go/rest" 9 | "sigs.k8s.io/controller-runtime/pkg/client" 10 | "sigs.k8s.io/controller-runtime/pkg/envtest" 11 | // +kubebuilder:scaffold:imports 12 | ) 13 | 14 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to 15 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 16 | 17 | var cfg *rest.Config 18 | var k8sClient client.Client 19 | var testEnv *envtest.Environment 20 | 21 | func TestAPIs(t *testing.T) { 22 | RegisterFailHandler(Fail) 23 | 24 | RunSpecs(t, "Controller Suite") 25 | } 26 | 27 | // var _ = BeforeSuite(func(done Done) { 28 | // logf.SetLogger(zap.LoggerTo(GinkgoWriter, true)) 29 | // By("bootstrapping test environment") 30 | // testEnv = &envtest.Environment{ 31 | // CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, 32 | // } 33 | 34 | // var err error 35 | // cfg, err = testEnv.Start() 36 | // Expect(err).ToNot(HaveOccurred()) 37 | // Expect(cfg).ToNot(BeNil()) 38 | 39 | // err = edgelevelcomv1alpha1.AddToScheme(scheme.Scheme) 40 | // Expect(err).NotTo(HaveOccurred()) 41 | 42 | // err = edgelevelcomv1alpha1.AddToScheme(scheme.Scheme) 43 | // Expect(err).NotTo(HaveOccurred()) 44 | 45 | // err = edgelevelcomv1alpha1.AddToScheme(scheme.Scheme) 46 | // Expect(err).NotTo(HaveOccurred()) 47 | 48 | // err = edgelevelcomv1alpha1.AddToScheme(scheme.Scheme) 49 | // Expect(err).NotTo(HaveOccurred()) 50 | 51 | // err = edgelevelcomv1alpha1.AddToScheme(scheme.Scheme) 52 | // Expect(err).NotTo(HaveOccurred()) 53 | 54 | // // +kubebuilder:scaffold:scheme 55 | 56 | // k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) 57 | // Expect(err).ToNot(HaveOccurred()) 58 | // Expect(k8sClient).ToNot(BeNil()) 59 | 60 | // close(done) 61 | // }, 60) 62 | 63 | // var _ = AfterSuite(func() { 64 | // By("tearing down the test environment") 65 | // err := testEnv.Stop() 66 | // Expect(err).ToNot(HaveOccurred()) 67 | // }) 68 | -------------------------------------------------------------------------------- /pkg/lastpass/cli.go: -------------------------------------------------------------------------------- 1 | package lastpass 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | 9 | "github.com/codeskyblue/go-sh" 10 | "github.com/gocarina/gocsv" 11 | ) 12 | 13 | // LastPassSecret represents a LastPass secret 14 | // For more examples see example/lpass-examples.txt 15 | // https://mholt.github.io/json-to-go/ 16 | type LastPassSecret struct { 17 | ID string `json:"id" csv:"id"` 18 | Name string `json:"name" csv:"name"` 19 | Fullname string `json:"fullname" csv:"fullname"` 20 | Username string `json:"username" csv:"username"` 21 | Password string `json:"password" csv:"password"` 22 | LastModifiedGmt string `json:"last_modified_gmt" csv:"last_modified_gmt"` 23 | LastTouch string `json:"last_touch" csv:"last_touch"` 24 | Group string `json:"group" csv:"group"` 25 | URL string `json:"url" csv:"url"` 26 | Note string `json:"note" csv:"extra"` 27 | } 28 | 29 | // VerifyCliExistsOrDie verifies that lastpass-cli is properly installed 30 | func VerifyCliExistsOrDie() { 31 | out, err := sh.Command("which", "lpass").Output() 32 | if err != nil || "" == string(out) { 33 | panic(fmt.Sprintf("lpass binary not found: [%s]", err)) 34 | } 35 | log.Printf("lpass binary found") 36 | } 37 | 38 | // Login using lastpass-cli 39 | func Login(username string, password string) error { 40 | _, err := sh.Command("lpass", "status").Output() 41 | log.Printf("Checking if already logged in") 42 | if err != nil { 43 | log.Printf("Doing login") 44 | out, err := sh.NewSession().SetEnv("LPASS_DISABLE_PINENTRY", "1").Command("echo", password).Command("lpass", "login", "--trust", username).Output() 45 | if err != nil || "" == string(out) { 46 | // sometimes returns error: "Error: HTTP response code said error" even if the credentials are valid 47 | return fmt.Errorf("verify credentials, unable to login: %s", err) 48 | } 49 | } 50 | log.Printf("Succesfully logged in") 51 | return nil 52 | } 53 | 54 | // Logout using lastpass-cli 55 | func Logout() { 56 | // lpass logout --force 57 | _, err := sh.Command("lpass", "logout", "--force").Output() 58 | if err != nil { 59 | log.Printf("Ignore error while logging out: %s", err) 60 | } 61 | log.Printf("Succesfully logged out") 62 | } 63 | 64 | // RequestSecrets returns one or more secrets using lastpass-cli 65 | func RequestSecrets(group string, name string) ([]LastPassSecret, error) { 66 | 67 | fullName := buildFullName(group, name) 68 | secrets := []LastPassSecret{} 69 | 70 | log.Printf("Request secrets: [%s]", fullName) 71 | 72 | // lpass show / --json --expand-multi 73 | out, err := sh.Command("lpass", "show", fullName, "--json", "--expand-multi").Output() 74 | if err != nil { 75 | return secrets, fmt.Errorf("invalid secrets: [%s] - %s", fullName, err) 76 | } 77 | 78 | // uncomment for debug 79 | //log.Printf("Secret response: %s", out) 80 | 81 | // decode JSON structure into Go structure 82 | jsonErr := json.Unmarshal([]byte(out), &secrets) 83 | if jsonErr != nil { 84 | return secrets, fmt.Errorf("invalid JSON: [%s] - %s", fullName, err) 85 | } 86 | 87 | log.Printf("Found [%d] secrets", len(secrets)) 88 | 89 | return secrets, nil 90 | } 91 | 92 | func RequestSecretsGroup(group string) ([]LastPassSecret, error) { 93 | secrets := []LastPassSecret{} 94 | 95 | log.Printf("Request Secrets Group: [%s]", group) 96 | 97 | // Export is not dumping all the fields, so we must explicitly request the desired fields. 98 | fields := "--fields=id,name,fullname,username,password,last_modified_gmt,last_touch,group,url,extra" 99 | 100 | out, err := sh.Command("lpass", "export", fields).Output() 101 | if err != nil { 102 | return secrets, fmt.Errorf("invalid Secrets Group: [%s]", group) 103 | } 104 | 105 | if err := gocsv.UnmarshalBytes(out, &secrets); err != nil { 106 | return secrets, fmt.Errorf("error unmarshaling secrets %s", err) 107 | } 108 | 109 | filteredSecrets := []LastPassSecret{} 110 | for _, secret := range secrets { 111 | if secret.Group == group { 112 | filteredSecrets = append(filteredSecrets, secret) 113 | } 114 | } 115 | 116 | return filteredSecrets, nil 117 | } 118 | 119 | // returns / or 120 | func buildFullName(group string, name string) string { 121 | var b bytes.Buffer 122 | if group != "" { 123 | b.WriteString(group) 124 | b.WriteString("/") 125 | b.WriteString(name) 126 | } else { 127 | b.WriteString(name) 128 | } 129 | return b.String() 130 | } 131 | -------------------------------------------------------------------------------- /pkg/utils/os.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | ) 8 | 9 | // GetEnvOrDie retrieve an environment variable or exit 10 | func GetEnvOrDie(key string) string { 11 | if value, ok := os.LookupEnv(key); ok { 12 | log.Printf("Found environment variable: [%s]", key) 13 | return value 14 | } 15 | panic(fmt.Sprintf("No environment variable found: [%s]", key)) 16 | } 17 | -------------------------------------------------------------------------------- /test/e2e/e2e_suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package e2e 18 | 19 | import ( 20 | "fmt" 21 | "testing" 22 | 23 | . "github.com/onsi/ginkgo/v2" 24 | . "github.com/onsi/gomega" 25 | ) 26 | 27 | // Run e2e tests using the Ginkgo runner. 28 | func TestE2E(t *testing.T) { 29 | RegisterFailHandler(Fail) 30 | fmt.Fprintf(GinkgoWriter, "Starting lastpass-operator suite\n") 31 | RunSpecs(t, "e2e suite") 32 | } 33 | -------------------------------------------------------------------------------- /test/e2e/e2e_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package e2e 18 | 19 | import ( 20 | "fmt" 21 | "os/exec" 22 | "time" 23 | 24 | . "github.com/onsi/ginkgo/v2" 25 | . "github.com/onsi/gomega" 26 | 27 | "github.com/edgelevel/lastpass-operator/test/utils" 28 | ) 29 | 30 | const namespace = "lastpass-operator-system" 31 | 32 | var _ = Describe("controller", Ordered, func() { 33 | BeforeAll(func() { 34 | // By("installing prometheus operator") 35 | // Expect(utils.InstallPrometheusOperator()).To(Succeed()) 36 | 37 | // By("installing the cert-manager") 38 | // Expect(utils.InstallCertManager()).To(Succeed()) 39 | 40 | By("creating manager namespace") 41 | cmd := exec.Command("kubectl", "create", "ns", namespace) 42 | _, _ = utils.Run(cmd) 43 | }) 44 | 45 | AfterAll(func() { 46 | // By("uninstalling the Prometheus manager bundle") 47 | // utils.UninstallPrometheusOperator() 48 | 49 | // By("uninstalling the cert-manager bundle") 50 | // utils.UninstallCertManager() 51 | 52 | By("removing manager namespace") 53 | cmd := exec.Command("kubectl", "delete", "ns", namespace) 54 | _, _ = utils.Run(cmd) 55 | }) 56 | 57 | Context("Operator", func() { 58 | It("should run successfully", func() { 59 | var controllerPodName string 60 | var err error 61 | 62 | // projectimage stores the name of the image used in the example 63 | var projectimage = "edgelevel.com/lastpass-operator:v0.0.1" 64 | 65 | By("building the manager(Operator) image") 66 | cmd := exec.Command("make", "docker-build", fmt.Sprintf("IMG=%s", projectimage)) 67 | _, err = utils.Run(cmd) 68 | ExpectWithOffset(1, err).NotTo(HaveOccurred()) 69 | 70 | By("loading the the manager(Operator) image on Kind") 71 | err = utils.LoadImageToKindClusterWithName(projectimage) 72 | ExpectWithOffset(1, err).NotTo(HaveOccurred()) 73 | 74 | By("installing CRDs") 75 | cmd = exec.Command("make", "install") 76 | _, err = utils.Run(cmd) 77 | ExpectWithOffset(1, err).NotTo(HaveOccurred()) 78 | 79 | By("deploying the controller-manager") 80 | cmd = exec.Command("make", "deploy", fmt.Sprintf("IMG=%s", projectimage)) 81 | _, err = utils.Run(cmd) 82 | ExpectWithOffset(1, err).NotTo(HaveOccurred()) 83 | 84 | By("validating that the controller-manager pod is running as expected") 85 | verifyControllerUp := func() error { 86 | // Get pod name 87 | 88 | cmd = exec.Command("kubectl", "get", 89 | "pods", "-l", "control-plane=controller-manager", 90 | "-o", "go-template={{ range .items }}"+ 91 | "{{ if not .metadata.deletionTimestamp }}"+ 92 | "{{ .metadata.name }}"+ 93 | "{{ \"\\n\" }}{{ end }}{{ end }}", 94 | "-n", namespace, 95 | ) 96 | 97 | podOutput, err := utils.Run(cmd) 98 | ExpectWithOffset(2, err).NotTo(HaveOccurred()) 99 | podNames := utils.GetNonEmptyLines(string(podOutput)) 100 | if len(podNames) != 1 { 101 | return fmt.Errorf("expect 1 controller pods running, but got %d", len(podNames)) 102 | } 103 | controllerPodName = podNames[0] 104 | ExpectWithOffset(2, controllerPodName).Should(ContainSubstring("controller-manager")) 105 | 106 | // Validate pod status 107 | cmd = exec.Command("kubectl", "get", 108 | "pods", controllerPodName, "-o", "jsonpath={.status.phase}", 109 | "-n", namespace, 110 | ) 111 | status, err := utils.Run(cmd) 112 | ExpectWithOffset(2, err).NotTo(HaveOccurred()) 113 | if string(status) != "Running" { 114 | return fmt.Errorf("controller pod in %s status", status) 115 | } 116 | return nil 117 | } 118 | EventuallyWithOffset(1, verifyControllerUp, time.Minute, time.Second).Should(Succeed()) 119 | 120 | }) 121 | }) 122 | }) 123 | -------------------------------------------------------------------------------- /test/utils/utils.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package utils 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "os/exec" 23 | "strings" 24 | 25 | . "github.com/onsi/ginkgo/v2" //nolint:golint,revive 26 | ) 27 | 28 | const ( 29 | prometheusOperatorVersion = "v0.72.0" 30 | prometheusOperatorURL = "https://github.com/prometheus-operator/prometheus-operator/" + 31 | "releases/download/%s/bundle.yaml" 32 | 33 | certmanagerVersion = "v1.14.4" 34 | certmanagerURLTmpl = "https://github.com/jetstack/cert-manager/releases/download/%s/cert-manager.yaml" 35 | ) 36 | 37 | func warnError(err error) { 38 | fmt.Fprintf(GinkgoWriter, "warning: %v\n", err) 39 | } 40 | 41 | // InstallPrometheusOperator installs the prometheus Operator to be used to export the enabled metrics. 42 | func InstallPrometheusOperator() error { 43 | url := fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion) 44 | cmd := exec.Command("kubectl", "create", "-f", url) 45 | _, err := Run(cmd) 46 | return err 47 | } 48 | 49 | // Run executes the provided command within this context 50 | func Run(cmd *exec.Cmd) ([]byte, error) { 51 | dir, _ := GetProjectDir() 52 | cmd.Dir = dir 53 | 54 | if err := os.Chdir(cmd.Dir); err != nil { 55 | fmt.Fprintf(GinkgoWriter, "chdir dir: %s\n", err) 56 | } 57 | 58 | cmd.Env = append(os.Environ(), "GO111MODULE=on") 59 | command := strings.Join(cmd.Args, " ") 60 | fmt.Fprintf(GinkgoWriter, "running: %s\n", command) 61 | output, err := cmd.CombinedOutput() 62 | if err != nil { 63 | return output, fmt.Errorf("%s failed with error: (%v) %s", command, err, string(output)) 64 | } 65 | 66 | return output, nil 67 | } 68 | 69 | // UninstallPrometheusOperator uninstalls the prometheus 70 | func UninstallPrometheusOperator() { 71 | url := fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion) 72 | cmd := exec.Command("kubectl", "delete", "-f", url) 73 | if _, err := Run(cmd); err != nil { 74 | warnError(err) 75 | } 76 | } 77 | 78 | // UninstallCertManager uninstalls the cert manager 79 | func UninstallCertManager() { 80 | url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion) 81 | cmd := exec.Command("kubectl", "delete", "-f", url) 82 | if _, err := Run(cmd); err != nil { 83 | warnError(err) 84 | } 85 | } 86 | 87 | // InstallCertManager installs the cert manager bundle. 88 | func InstallCertManager() error { 89 | url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion) 90 | cmd := exec.Command("kubectl", "apply", "-f", url) 91 | if _, err := Run(cmd); err != nil { 92 | return err 93 | } 94 | // Wait for cert-manager-webhook to be ready, which can take time if cert-manager 95 | // was re-installed after uninstalling on a cluster. 96 | cmd = exec.Command("kubectl", "wait", "deployment.apps/cert-manager-webhook", 97 | "--for", "condition=Available", 98 | "--namespace", "cert-manager", 99 | "--timeout", "5m", 100 | ) 101 | 102 | _, err := Run(cmd) 103 | return err 104 | } 105 | 106 | // LoadImageToKindCluster loads a local docker image to the kind cluster 107 | func LoadImageToKindClusterWithName(name string) error { 108 | cluster := "kind" 109 | if v, ok := os.LookupEnv("KIND_CLUSTER"); ok { 110 | cluster = v 111 | } 112 | kindOptions := []string{"load", "docker-image", name, "--name", cluster} 113 | cmd := exec.Command("kind", kindOptions...) 114 | _, err := Run(cmd) 115 | return err 116 | } 117 | 118 | // GetNonEmptyLines converts given command output string into individual objects 119 | // according to line breakers, and ignores the empty elements in it. 120 | func GetNonEmptyLines(output string) []string { 121 | var res []string 122 | elements := strings.Split(output, "\n") 123 | for _, element := range elements { 124 | if element != "" { 125 | res = append(res, element) 126 | } 127 | } 128 | 129 | return res 130 | } 131 | 132 | // GetProjectDir will return the directory where the project is 133 | func GetProjectDir() (string, error) { 134 | wd, err := os.Getwd() 135 | if err != nil { 136 | return wd, err 137 | } 138 | wd = strings.Replace(wd, "/test/e2e", "", -1) 139 | return wd, nil 140 | } 141 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | var ( 4 | Version = "1.0.1" 5 | ) 6 | --------------------------------------------------------------------------------