mirror of
https://github.com/1Password/onepassword-operator.git
synced 2025-10-22 07:28:06 +00:00
Compare commits
86 Commits
f6b267726d
...
main
Author | SHA1 | Date | |
---|---|---|---|
![]() |
5ccb0d88bb | ||
![]() |
3f52bb2840 | ||
![]() |
7650aef60a | ||
![]() |
63e3f29be9 | ||
![]() |
49bc9cb329 | ||
![]() |
a5e4a352e9 | ||
![]() |
edde903759 | ||
![]() |
03b093ac17 | ||
![]() |
79ee171b7f | ||
![]() |
c9a8cc6fb8 | ||
![]() |
a390354100 | ||
![]() |
de62e07bcf | ||
![]() |
3ebc536dd7 | ||
![]() |
6769e25a98 | ||
![]() |
d8734c9ae3 | ||
![]() |
460742869b | ||
![]() |
35e476230c | ||
![]() |
0f56cab693 | ||
![]() |
a1ab24f244 | ||
![]() |
13e4b16846 | ||
![]() |
3a9691576a | ||
![]() |
94602ddd72 | ||
![]() |
292c6f0e93 | ||
![]() |
0f1293ca95 | ||
![]() |
706ebdd8b8 | ||
![]() |
bd963bcd1d | ||
![]() |
bf6cac81cb | ||
![]() |
9c4849ec2e | ||
![]() |
c2788770fd | ||
![]() |
6baef1b9cf | ||
![]() |
7e08158d2f | ||
![]() |
976909c438 | ||
![]() |
e61ba49018 | ||
![]() |
6492b3cf34 | ||
![]() |
9d08bcc864 | ||
![]() |
f7f5462133 | ||
![]() |
128954cd80 | ||
![]() |
a1cbd40f9e | ||
![]() |
d75a33d524 | ||
![]() |
b1b6c97a88 | ||
![]() |
0c3caf88b6 | ||
![]() |
24edff22d4 | ||
![]() |
8c893270f4 | ||
![]() |
d5f1044571 | ||
![]() |
b40f27b052 | ||
![]() |
cd03a651ad | ||
![]() |
9aac824066 | ||
![]() |
05ad484bd6 | ||
![]() |
71b29d5fe6 | ||
![]() |
c082f9562e | ||
![]() |
57478247cf | ||
![]() |
4836140f66 | ||
![]() |
2b36f16940 | ||
![]() |
bb97134e10 | ||
![]() |
904d269e7b | ||
![]() |
cf9b267eaf | ||
![]() |
4d64beab86 | ||
![]() |
ca051a08cf | ||
![]() |
22a7c8f586 | ||
![]() |
2003d13788 | ||
![]() |
7187f41ef1 | ||
![]() |
d0b11c70f0 | ||
![]() |
9825cb57c9 | ||
![]() |
6bb6088353 | ||
![]() |
5a56fd3330 | ||
![]() |
dcd7eefac0 | ||
![]() |
29b7ed7899 | ||
![]() |
331e8d7bfb | ||
![]() |
c144bd3d01 | ||
![]() |
299689fe13 | ||
![]() |
882d8e951d | ||
![]() |
7885ba649b | ||
![]() |
600adf2670 | ||
![]() |
88b2dfbf67 | ||
![]() |
e167db2357 | ||
![]() |
91a9bb6d63 | ||
![]() |
116c8c92a7 | ||
![]() |
4307e9d713 | ||
![]() |
1759055edd | ||
![]() |
c1e9934088 | ||
![]() |
19b629f2ee | ||
![]() |
174f952691 | ||
![]() |
f8704223c8 | ||
![]() |
5630d788a2 | ||
![]() |
d504e5ef35 | ||
![]() |
7d2596a4aa |
17
.github/pull_request_template.md
vendored
Normal file
17
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
### ✨ Summary
|
||||
<!-- What does this change do? -->
|
||||
|
||||
<!-- What issue does it resolve? -->
|
||||
### 🔗 Resolves:
|
||||
|
||||
### ✅ Checklist
|
||||
- [ ] 🖊️ Commits are signed
|
||||
- [ ] 🧪 Tests added/updated: _(See the [Testing Guide](docs/testing.md) for when to use each type and how to run them)_
|
||||
- [ ] 🔹 Unit
|
||||
- [ ] 🔸 Integration
|
||||
- [ ] 🌐 E2E (Connect)
|
||||
- [ ] 🔑 E2E (Service Account)
|
||||
- [ ] 📚 Docs updated (if behavior changed)
|
||||
|
||||
### 🕵️ Review Notes & ⚠️ Risks
|
||||
<!-- Notes for reviewers, flags, feature gates, rollout considerations, etc. -->
|
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -11,10 +11,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone the code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
|
52
.github/workflows/e2e-tests.yml
vendored
Normal file
52
.github/workflows/e2e-tests.yml
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
name: E2E Tests
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
secrets:
|
||||
OP_CONNECT_CREDENTIALS:
|
||||
description: '1Password Connect credentials'
|
||||
required: true
|
||||
OP_CONNECT_TOKEN:
|
||||
description: '1Password Connect token'
|
||||
required: true
|
||||
OP_SERVICE_ACCOUNT_TOKEN:
|
||||
description: '1Password service account token'
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
e2e-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
- name: Install dependencies
|
||||
run: go mod tidy
|
||||
|
||||
- name: Create kind cluster
|
||||
uses: helm/kind-action@v1
|
||||
with:
|
||||
cluster_name: onepassword-operator-test-e2e
|
||||
|
||||
# install cli to interact with item in 1Password to update/read using `testhelper/op` package
|
||||
- name: Install 1Password CLI
|
||||
uses: 1password/install-cli-action@v2
|
||||
with:
|
||||
version: 2.32.0
|
||||
|
||||
- name: Create '1password-credentials.json' file
|
||||
env:
|
||||
OP_CONNECT_CREDENTIALS: ${{ secrets.OP_CONNECT_CREDENTIALS }}
|
||||
run: |
|
||||
echo "$OP_CONNECT_CREDENTIALS" > 1password-credentials.json
|
||||
|
||||
- name: Run E2E tests
|
||||
run: make test-e2e
|
||||
env:
|
||||
OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN }}
|
||||
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
|
4
.github/workflows/lint.yml
vendored
4
.github/workflows/lint.yml
vendored
@@ -11,10 +11,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone the code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
|
25
.github/workflows/ok-to-test.yml
vendored
Normal file
25
.github/workflows/ok-to-test.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# Write comments "/ok-to-test <hash>" on a pull request. This will emit a repository_dispatch event.
|
||||
name: Ok To Test
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
ok-to-test:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write # For adding reactions to the pull request comments
|
||||
contents: write # For executing the repository_dispatch event
|
||||
# Only run for PRs, not issue comments
|
||||
if: ${{ github.event.issue.pull_request }}
|
||||
steps:
|
||||
- name: Slash Command Dispatch
|
||||
uses: volodymyrZotov/slash-command-dispatch@7c1b623a2b0eba93f684c34f689a441f0be84cf1 # TODO: use peter-evans/slash-command-dispatch when fix for team permissions is released https://github.com/peter-evans/slash-command-dispatch/pull/424
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
reaction-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-type: pull-request
|
||||
commands: ok-to-test
|
||||
# The repository permission level required by the user to dispatch commands. Only allows 1Password collaborators to run this.
|
||||
permission: write
|
2
.github/workflows/release-pr.yml
vendored
2
.github/workflows/release-pr.yml
vendored
@@ -43,7 +43,7 @@ jobs:
|
||||
name: Create Release Pull Request
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Parse release version
|
||||
id: get_version
|
||||
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
DOCKER_CLI_EXPERIMENTAL: "enabled"
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
56
.github/workflows/test-e2e-fork.yml
vendored
Normal file
56
.github/workflows/test-e2e-fork.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
name: E2E tests [fork]
|
||||
|
||||
on:
|
||||
repository_dispatch:
|
||||
types: [ ok-to-test-command ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
checks: write
|
||||
|
||||
concurrency:
|
||||
group: e2e-fork-${{ github.event.client_payload.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true # cancel previous job runs for the same branch
|
||||
|
||||
jobs:
|
||||
e2e-tests:
|
||||
uses: ./.github/workflows/e2e-tests.yml
|
||||
if: |
|
||||
github.event_name == 'repository_dispatch' &&
|
||||
github.event.client_payload.slash_command.args.named.sha != '' &&
|
||||
contains(
|
||||
github.event.client_payload.pull_request.head.sha,
|
||||
github.event.client_payload.slash_command.args.named.sha
|
||||
)
|
||||
secrets:
|
||||
OP_CONNECT_CREDENTIALS: ${{ secrets.OP_CONNECT_CREDENTIALS }}
|
||||
OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN }}
|
||||
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
|
||||
|
||||
update-check-status:
|
||||
needs: e2e-tests
|
||||
runs-on: ubuntu-latest
|
||||
if: always() && github.event_name == 'repository_dispatch'
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
env:
|
||||
ref: ${{ github.event.client_payload.pull_request.head.sha }}
|
||||
conclusion: ${{ needs.e2e-tests.result }}
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const { data: checks } = await github.rest.checks.listForRef({
|
||||
...context.repo,
|
||||
ref: process.env.ref
|
||||
});
|
||||
|
||||
const check = checks.check_runs.filter(c => c.name === 'e2e-test');
|
||||
|
||||
const { data: result } = await github.rest.checks.update({
|
||||
...context.repo,
|
||||
check_run_id: check[0].id,
|
||||
status: 'completed',
|
||||
conclusion: process.env.conclusion
|
||||
});
|
||||
|
||||
return result;
|
43
.github/workflows/test-e2e.yml
vendored
Normal file
43
.github/workflows/test-e2e.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
name: E2E Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
branches: ['**'] # run for PRs targeting any branch (main and others)
|
||||
paths-ignore: &ignore_paths
|
||||
- 'docs/**'
|
||||
- '*.md'
|
||||
- '.golangci.yml'
|
||||
- '.gitignore'
|
||||
- '.dockerignore'
|
||||
- 'LICENSE'
|
||||
push:
|
||||
branches: [main]
|
||||
paths-ignore: *ignore_paths
|
||||
|
||||
concurrency:
|
||||
group: e2e-${{ github.event.pull_request.head.ref }}
|
||||
cancel-in-progress: true # cancel previous job runs for the same branch
|
||||
|
||||
jobs:
|
||||
check-external-pr:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request'
|
||||
steps:
|
||||
- name: Check if PR is from external contributor
|
||||
run: |
|
||||
if [ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]; then
|
||||
echo "❌ External PR detected. This workflow requires approval from a maintainer."
|
||||
echo "Please ask a maintainer to run '/ok-to-test' command to trigger the fork workflow."
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Internal PR detected. Proceeding with tests."
|
||||
|
||||
e2e-test:
|
||||
needs: check-external-pr
|
||||
if: always() && (needs.check-external-pr.result == 'success' || github.event_name != 'pull_request')
|
||||
uses: ./.github/workflows/e2e-tests.yml
|
||||
secrets:
|
||||
OP_CONNECT_CREDENTIALS: ${{ secrets.OP_CONNECT_CREDENTIALS }}
|
||||
OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN }}
|
||||
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
|
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -11,10 +11,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone the code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -25,3 +25,6 @@ go.work
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
**/1password-credentials.json
|
||||
**/op-session
|
||||
|
@@ -4,7 +4,17 @@ Thank you for your interest in contributing to the 1Password Kubernetes Operator
|
||||
|
||||
## Testing
|
||||
|
||||
- For functional testing, run the local version of the operator. From the project root:
|
||||
All contributions must include tests where applicable.
|
||||
|
||||
- **Unit tests** for pure Go logic.
|
||||
- **Integration tests** for controller/reconciler logic using envtest.
|
||||
- **E2E tests** for full cluster behavior with kind.
|
||||
|
||||
👉 See the [Testing Guide](docs/testing.md) for details on when to use each, how to run them locally, and how they are run in CI.
|
||||
|
||||
----
|
||||
|
||||
For functional testing, run the local version of the operator. From the project root:
|
||||
|
||||
```sh
|
||||
# Go to the K8s environment (e.g. minikube)
|
||||
@@ -24,6 +34,8 @@ Thank you for your interest in contributing to the 1Password Kubernetes Operator
|
||||
1. Rebuild the Docker image by running `make docker-build`
|
||||
2. Restart deployment `make restart`
|
||||
|
||||
----
|
||||
|
||||
- For testing the changes made to the `OnePasswordItem` Custom Resource Definition (CRD), you need to re-generate the object:
|
||||
```sh
|
||||
make manifests
|
||||
|
@@ -8,6 +8,9 @@ WORKDIR /workspace
|
||||
COPY go.mod go.mod
|
||||
COPY go.sum go.sum
|
||||
|
||||
# Copy the testhelper module (needed for replace directive)
|
||||
COPY pkg/testhelper/ pkg/testhelper/
|
||||
|
||||
# Download dependencies
|
||||
RUN go mod download
|
||||
|
||||
|
2
Makefile
2
Makefile
@@ -117,7 +117,7 @@ vet: ## Run go vet against code.
|
||||
|
||||
.PHONY: test
|
||||
test: manifests generate fmt vet setup-envtest ## Run tests.
|
||||
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test ./... -coverprofile cover.out
|
||||
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test $(shell go list ./... | grep -v /test/e2e) -coverprofile cover.out
|
||||
|
||||
# TODO(user): To use a different vendor for e2e tests, modify the setup under 'tests/e2e'.
|
||||
# The default setup assumes Kind is pre-installed and builds/loads the Manager Docker image locally.
|
||||
|
@@ -14,6 +14,8 @@ spec:
|
||||
spec:
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
fsGroup: 999
|
||||
fsGroupChangePolicy: OnRootMismatch
|
||||
volumes:
|
||||
- name: shared-data
|
||||
emptyDir: {}
|
||||
@@ -31,10 +33,20 @@ spec:
|
||||
volumeMounts:
|
||||
- mountPath: /home/opuser/.op/data
|
||||
name: shared-data
|
||||
securityContext:
|
||||
runAsUser: 0
|
||||
runAsNonRoot: false
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop: [ "ALL" ]
|
||||
add: ["CHOWN", "FOWNER"]
|
||||
containers:
|
||||
- name: connect-api
|
||||
image: 1password/connect-api:latest
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 999
|
||||
runAsGroup: 999
|
||||
allowPrivilegeEscalation: false
|
||||
resources:
|
||||
limits:
|
||||
@@ -55,6 +67,9 @@ spec:
|
||||
- name: connect-sync
|
||||
image: 1password/connect-sync:latest
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 999
|
||||
runAsGroup: 999
|
||||
allowPrivilegeEscalation: false
|
||||
resources:
|
||||
limits:
|
||||
|
20
docs/testing.md
Normal file
20
docs/testing.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Testing
|
||||
|
||||
## Unit & Integration tests
|
||||
**When**: Unit (pure Go) and integration (controller-runtime envtest).
|
||||
**Where**: `internal/...`, `pkg/...`
|
||||
**Add files in**: `*_test.go` next to the code.
|
||||
**Run**: `make test`
|
||||
|
||||
## E2E tests (kind)
|
||||
**When**: Full cluster behavior (CRDs, operator image, Connect/SA flows).
|
||||
**Where**: `test/e2e/...`
|
||||
**Add files in**: `*_test.go` next to the code.
|
||||
**Framework**: Ginkgo + `pkg/testhelper`.
|
||||
|
||||
**Local prep**:
|
||||
1. [Install `kind`](https://kind.sigs.k8s.io/docs/user/quick-start/#installing-with-a-package-manager) to spin up local Kubernetes cluster.
|
||||
2. `export OP_CONNECT_TOKEN=<token>`
|
||||
3. `export OP_SERVICE_ACCOUNT_TOKEN=<token>`
|
||||
4. Put `1password-credentials.json` into project root.
|
||||
5. `make test-e2e`
|
4
go.mod
4
go.mod
@@ -4,8 +4,12 @@ go 1.24.0
|
||||
|
||||
toolchain go1.24.5
|
||||
|
||||
// In main go.mod, add this replace directive:
|
||||
replace github.com/1Password/onepassword-operator/pkg/testhelper => ./pkg/testhelper
|
||||
|
||||
require (
|
||||
github.com/1Password/connect-sdk-go v1.5.3
|
||||
github.com/1Password/onepassword-operator/pkg/testhelper v0.0.0-00010101000000-000000000000
|
||||
github.com/1password/onepassword-sdk-go v0.3.1
|
||||
github.com/go-logr/logr v1.4.2
|
||||
github.com/onsi/ginkgo/v2 v2.22.0
|
||||
|
115
pkg/testhelper/README.md
Normal file
115
pkg/testhelper/README.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# OnePassword Operator Test Helper
|
||||
|
||||
This is a standalone Go module that provides testing utilities for Kubernetes operators and webhooks. It's specifically designed for testing 1Password Kubernetes operator and secrets injector, but it can be used for any Kubernetes operator or webhook testing.
|
||||
|
||||
## Installation
|
||||
|
||||
To use this module in your project, add it as a dependency:
|
||||
|
||||
```bash
|
||||
go get github.com/1Password/onepassword-operator/pkg/testhelper@<commit-hash>
|
||||
```
|
||||
|
||||
### Basic Setup
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/1Password/onepassword-operator/pkg/testhelper/kube"
|
||||
"github.com/1Password/onepassword-operator/pkg/testhelper/defaults"
|
||||
)
|
||||
|
||||
// Create a kube client for testing
|
||||
kubeClient := kube.NewKubeClient(&kube.Config{
|
||||
Namespace: "default",
|
||||
ManifestsDir: "manifests",
|
||||
TestConfig: &kube.TestConfig{
|
||||
Timeout: defaults.E2ETimeout,
|
||||
Interval: defaults.E2EInterval,
|
||||
},
|
||||
CRDs: []string{
|
||||
"path/to/your/crd.yaml",
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Working with Secrets
|
||||
|
||||
```go
|
||||
// Create k8s secret from environment variable
|
||||
k8sSecret := kubeClient.Secret("my-secret")
|
||||
k8sSecret.CreateFromEnvVar(ctx, "MY_ENV_VAR")
|
||||
|
||||
// Create a secret from file
|
||||
data := []byte("secret content")
|
||||
k8sSecret.CreateFromFile(ctx, "filename", data)
|
||||
|
||||
// Check if secret exists
|
||||
k8sSecret.CheckIfExists(ctx)
|
||||
|
||||
// Get secret
|
||||
secretObj := k8sSecret.Get(ctx)
|
||||
```
|
||||
|
||||
### Working with Deployments
|
||||
|
||||
```go
|
||||
deployment := kubeClient.Deployment("my-deployment")
|
||||
|
||||
// Read environment variable from deployment
|
||||
envVar := deployment.ReadEnvVar(ctx, "MY_ENV_VAR")
|
||||
|
||||
// Patch environment variables
|
||||
deployment.PatchEnvVars(ctx,
|
||||
[]corev1.EnvVar{
|
||||
{Name: "NEW_VAR", Value: "new_value"},
|
||||
},
|
||||
[]string{"OLD_VAR"}, // variables to remove
|
||||
)
|
||||
|
||||
// Wait for deployment rollout
|
||||
deployment.WaitDeploymentRolledOut(ctx)
|
||||
```
|
||||
|
||||
### Working with Pods
|
||||
|
||||
```go
|
||||
pod := kubeClient.Pod(map[string]string{"app": "my-app"})
|
||||
pod.WaitingForRunningPod(ctx)
|
||||
```
|
||||
|
||||
### Working with Namespaces
|
||||
|
||||
```go
|
||||
namespace := kubeClient.Namespace("my-namespace")
|
||||
namespace.LabelNamespace(ctx, map[string]string{
|
||||
"environment": "test",
|
||||
})
|
||||
```
|
||||
|
||||
### System Utilities
|
||||
|
||||
```go
|
||||
import "github.com/1Password/onepassword-operator/pkg/testhelper/system"
|
||||
|
||||
// Run shell commands
|
||||
output, err := system.Run("kubectl", "get", "pods")
|
||||
|
||||
// Get project root directory
|
||||
rootDir, err := system.GetProjectRoot()
|
||||
|
||||
// Replace files
|
||||
err := system.ReplaceFile("source.yaml", "dest.yaml")
|
||||
```
|
||||
|
||||
### Kind Integration
|
||||
|
||||
```go
|
||||
import "github.com/1Password/onepassword-operator/pkg/testhelper/kind"
|
||||
|
||||
// Load Docker image to Kind cluster
|
||||
kind.LoadImageToKind("my-image:latest")
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see the main project LICENSE file for details.
|
8
pkg/testhelper/defaults/defaults.go
Normal file
8
pkg/testhelper/defaults/defaults.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package defaults
|
||||
|
||||
import "time"
|
||||
|
||||
const (
|
||||
E2EInterval = 1 * time.Second
|
||||
E2ETimeout = 1 * time.Minute
|
||||
)
|
59
pkg/testhelper/go.mod
Normal file
59
pkg/testhelper/go.mod
Normal file
@@ -0,0 +1,59 @@
|
||||
module github.com/1Password/onepassword-operator/pkg/testhelper
|
||||
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.5
|
||||
|
||||
require (
|
||||
github.com/onsi/ginkgo/v2 v2.22.0
|
||||
github.com/onsi/gomega v1.36.1
|
||||
k8s.io/api v0.33.0
|
||||
k8s.io/apiextensions-apiserver v0.33.0
|
||||
k8s.io/apimachinery v0.33.0
|
||||
k8s.io/client-go v0.33.0
|
||||
sigs.k8s.io/controller-runtime v0.21.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.12.0 // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/gnostic-models v0.6.9 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
golang.org/x/net v0.41.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/term v0.32.0 // indirect
|
||||
golang.org/x/text v0.26.0 // indirect
|
||||
golang.org/x/time v0.9.0 // indirect
|
||||
golang.org/x/tools v0.33.0 // indirect
|
||||
google.golang.org/protobuf v1.36.5 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect
|
||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||
)
|
156
pkg/testhelper/go.sum
Normal file
156
pkg/testhelper/go.sum
Normal file
@@ -0,0 +1,156 @@
|
||||
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
||||
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/emicklei/go-restful/v3 v3.12.0 h1:y2DdzBAURM29NFF94q6RaY4vjIH1rtwDapwQtU84iWk=
|
||||
github.com/emicklei/go-restful/v3 v3.12.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
|
||||
github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
|
||||
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
|
||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
|
||||
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw=
|
||||
github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg=
|
||||
github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
|
||||
github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw=
|
||||
github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
k8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU=
|
||||
k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM=
|
||||
k8s.io/apiextensions-apiserver v0.33.0 h1:d2qpYL7Mngbsc1taA4IjJPRJ9ilnsXIrndH+r9IimOs=
|
||||
k8s.io/apiextensions-apiserver v0.33.0/go.mod h1:VeJ8u9dEEN+tbETo+lFkwaaZPg6uFKLGj5vyNEwwSzc=
|
||||
k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ=
|
||||
k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
|
||||
k8s.io/client-go v0.33.0 h1:UASR0sAYVUzs2kYuKn/ZakZlcs2bEHaizrrHUZg0G98=
|
||||
k8s.io/client-go v0.33.0/go.mod h1:kGkd+l/gNGg8GYWAPr0xF1rRKvVWvzh9vmZAMXtaKOg=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4=
|
||||
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8=
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8=
|
||||
sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM=
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
|
||||
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
|
||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
|
23
pkg/testhelper/kind/kind.go
Normal file
23
pkg/testhelper/kind/kind.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package kind
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
//nolint:staticcheck // ST1001
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
//nolint:staticcheck // ST1001
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/1Password/onepassword-operator/pkg/testhelper/system"
|
||||
)
|
||||
|
||||
// LoadImageToKind loads a local docker image to the Kind cluster
|
||||
func LoadImageToKind(imageName string) {
|
||||
By("Loading the operator image on Kind")
|
||||
clusterName := "kind"
|
||||
if value, ok := os.LookupEnv("KIND_CLUSTER"); ok {
|
||||
clusterName = value
|
||||
}
|
||||
_, err := system.Run("kind", "load", "docker-image", imageName, "--name", clusterName)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
}
|
125
pkg/testhelper/kube/deployment.go
Normal file
125
pkg/testhelper/kube/deployment.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package kube
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
//nolint:staticcheck // ST1001
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
//nolint:staticcheck // ST1001
|
||||
. "github.com/onsi/gomega"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
type Deployment struct {
|
||||
client client.Client
|
||||
config *Config
|
||||
name string
|
||||
}
|
||||
|
||||
func (d *Deployment) Get(ctx context.Context) *appsv1.Deployment {
|
||||
// Derive a short-lived context so this API call won't hang indefinitely.
|
||||
c, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
deployment := &appsv1.Deployment{}
|
||||
err := d.client.Get(c, client.ObjectKey{Name: d.name, Namespace: d.config.Namespace}, deployment)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
return deployment
|
||||
}
|
||||
|
||||
func (d *Deployment) ReadEnvVar(ctx context.Context, envVarName string) string {
|
||||
By("Reading " + envVarName + " value from deployment/" + d.name)
|
||||
deployment := d.Get(ctx)
|
||||
|
||||
// Search env across all containers
|
||||
found := ""
|
||||
for _, container := range deployment.Spec.Template.Spec.Containers {
|
||||
for _, env := range container.Env {
|
||||
if env.Name == envVarName && env.Value != "" {
|
||||
found = env.Value
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Expect(found).NotTo(BeEmpty())
|
||||
return found
|
||||
}
|
||||
|
||||
func (d *Deployment) PatchEnvVars(ctx context.Context, upsert []corev1.EnvVar, remove []string) {
|
||||
By("Patching env variables for deployment/" + d.name)
|
||||
deployment := d.Get(ctx)
|
||||
deploymentCopy := deployment.DeepCopy()
|
||||
container := &deployment.Spec.Template.Spec.Containers[0]
|
||||
|
||||
// Build removal set for quick lookup
|
||||
toRemove := make(map[string]struct{}, len(remove))
|
||||
for _, n := range remove {
|
||||
toRemove[n] = struct{}{}
|
||||
}
|
||||
|
||||
// Build upsert map for quick lookup
|
||||
upserts := make(map[string]corev1.EnvVar, len(upsert))
|
||||
for _, e := range upsert {
|
||||
upserts[e.Name] = e
|
||||
}
|
||||
|
||||
// Filter existing envs: keep if not in remove and not being upserted
|
||||
filtered := make([]corev1.EnvVar, 0, len(container.Env))
|
||||
for _, e := range container.Env {
|
||||
if _, ok := toRemove[e.Name]; ok {
|
||||
continue
|
||||
}
|
||||
if newE, ok := upserts[e.Name]; ok {
|
||||
filtered = append(filtered, newE) // replace existing
|
||||
delete(upserts, e.Name) // delete from map to not use once again
|
||||
} else {
|
||||
filtered = append(filtered, e)
|
||||
}
|
||||
}
|
||||
|
||||
// Append any new envs that weren’t already in the container
|
||||
for _, e := range upserts {
|
||||
filtered = append(filtered, e)
|
||||
}
|
||||
|
||||
container.Env = filtered
|
||||
|
||||
// Derive a short-lived context so this API call won't hang indefinitely.
|
||||
c, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err := d.client.Patch(c, deployment, client.MergeFrom(deploymentCopy))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// wait for new deployment to roll out
|
||||
d.WaitDeploymentRolledOut(ctx)
|
||||
}
|
||||
|
||||
// WaitDeploymentRolledOut waits for deployment to finish a rollout.
|
||||
func (d *Deployment) WaitDeploymentRolledOut(ctx context.Context) {
|
||||
By("Waiting for deployment/" + d.name + " to roll out")
|
||||
|
||||
deployment := d.Get(ctx)
|
||||
targetGen := deployment.Generation
|
||||
|
||||
Eventually(func(g Gomega) error {
|
||||
newDeployment := d.Get(ctx)
|
||||
g.Expect(newDeployment.Status.ObservedGeneration).To(BeNumerically(">=", targetGen))
|
||||
|
||||
desired := int32(1)
|
||||
if newDeployment.Spec.Replicas != nil {
|
||||
desired = *newDeployment.Spec.Replicas
|
||||
}
|
||||
|
||||
g.Expect(newDeployment.Status.UpdatedReplicas).To(Equal(desired))
|
||||
g.Expect(newDeployment.Status.AvailableReplicas).To(Equal(desired))
|
||||
g.Expect(newDeployment.Status.Replicas).To(Equal(desired))
|
||||
|
||||
return nil
|
||||
}, d.config.TestConfig.Timeout, d.config.TestConfig.Interval).Should(Succeed())
|
||||
}
|
240
pkg/testhelper/kube/kube.go
Normal file
240
pkg/testhelper/kube/kube.go
Normal file
@@ -0,0 +1,240 @@
|
||||
package kube
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
//nolint:staticcheck // ST1001
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
//nolint:staticcheck // ST1001
|
||||
. "github.com/onsi/gomega"
|
||||
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
apix "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/yaml"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
|
||||
|
||||
"github.com/1Password/onepassword-operator/pkg/testhelper/defaults"
|
||||
)
|
||||
|
||||
type TestConfig struct {
|
||||
Timeout time.Duration
|
||||
Interval time.Duration
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Namespace string
|
||||
ManifestsDir string
|
||||
TestConfig *TestConfig
|
||||
CRDs []string
|
||||
}
|
||||
|
||||
type Kube struct {
|
||||
Config *Config
|
||||
Client client.Client
|
||||
Clientset kubernetes.Interface
|
||||
Mapper meta.RESTMapper
|
||||
}
|
||||
|
||||
func NewKubeClient(config *Config) *Kube {
|
||||
By("Creating a kubernetes client")
|
||||
kubeconfig := os.Getenv("KUBECONFIG")
|
||||
if kubeconfig == "" {
|
||||
home, _ := os.UserHomeDir()
|
||||
kubeconfig = filepath.Join(home, ".kube", "config")
|
||||
}
|
||||
restConfig, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Install CRDs first (so discovery sees them)
|
||||
installCRDs(context.Background(), restConfig, config.CRDs)
|
||||
|
||||
// Build an http.Client from restConfig
|
||||
httpClient, err := rest.HTTPClientFor(restConfig)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Create a Dynamic RESTMapper that uses restConfig
|
||||
rm, err := apiutil.NewDynamicRESTMapper(restConfig, httpClient)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
scheme := runtime.NewScheme()
|
||||
utilruntime.Must(corev1.AddToScheme(scheme))
|
||||
utilruntime.Must(appsv1.AddToScheme(scheme))
|
||||
utilruntime.Must(admissionregistrationv1.AddToScheme(scheme))
|
||||
|
||||
kubernetesClient, err := client.New(restConfig, client.Options{
|
||||
Scheme: scheme,
|
||||
Mapper: rm,
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Create Kubernetes clientset for logs and other operations
|
||||
clientset, err := kubernetes.NewForConfig(restConfig)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// update the current context’s namespace in kubeconfig
|
||||
pathOpts := clientcmd.NewDefaultPathOptions()
|
||||
cfg, err := pathOpts.GetStartingConfig()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
currentContext := cfg.CurrentContext
|
||||
Expect(currentContext).NotTo(BeEmpty(), "no current kube context is set in kubeconfig")
|
||||
|
||||
ctx, ok := cfg.Contexts[currentContext]
|
||||
Expect(ok).To(BeTrue(), fmt.Sprintf("current context %q not found in kubeconfig", currentContext))
|
||||
|
||||
ctx.Namespace = config.Namespace
|
||||
err = clientcmd.ModifyConfig(pathOpts, *cfg, true)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
return &Kube{
|
||||
Config: config,
|
||||
Client: kubernetesClient,
|
||||
Clientset: clientset,
|
||||
Mapper: rm,
|
||||
}
|
||||
}
|
||||
|
||||
func (k *Kube) Secret(name string) *Secret {
|
||||
return &Secret{
|
||||
client: k.Client,
|
||||
config: k.Config,
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
|
||||
func (k *Kube) Deployment(name string) *Deployment {
|
||||
return &Deployment{
|
||||
client: k.Client,
|
||||
config: k.Config,
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
|
||||
func (k *Kube) Pod(selector map[string]string) *Pod {
|
||||
return &Pod{
|
||||
client: k.Client,
|
||||
clientset: k.Clientset,
|
||||
config: k.Config,
|
||||
selector: selector,
|
||||
}
|
||||
}
|
||||
|
||||
func (k *Kube) Namespace(name string) *Namespace {
|
||||
return &Namespace{
|
||||
client: k.Client,
|
||||
config: k.Config,
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
|
||||
func (k *Kube) Webhook(name string) *Webhook {
|
||||
return &Webhook{
|
||||
client: k.Client,
|
||||
config: k.Config,
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
|
||||
// Apply applies a Kubernetes manifest file using server-side apply.
|
||||
func (k *Kube) Apply(ctx context.Context, fileName string) {
|
||||
By("Applying " + fileName)
|
||||
|
||||
// Derive a short-lived context so this API call won't hang indefinitely.
|
||||
c, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
data, err := os.ReadFile(k.Config.ManifestsDir + "/" + fileName)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Decode YAML -> JSON -> unstructured.Unstructured
|
||||
jsonBytes, err := yaml.ToJSON(data)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
var obj unstructured.Unstructured
|
||||
Expect(obj.UnmarshalJSON(jsonBytes)).To(Succeed())
|
||||
|
||||
// Default namespace for namespaced resources if not set in YAML
|
||||
if obj.GetNamespace() == "" && k.Config.Namespace != "" {
|
||||
gvk := obj.GroupVersionKind()
|
||||
mapping, mapErr := k.Mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
|
||||
if mapErr == nil && mapping.Scope.Name() == meta.RESTScopeNameNamespace {
|
||||
obj.SetNamespace(k.Config.Namespace)
|
||||
}
|
||||
}
|
||||
|
||||
// Server-Side Apply (create or update)
|
||||
patchOpts := []client.PatchOption{
|
||||
client.FieldOwner("onepassword-e2e"),
|
||||
client.ForceOwnership, // to force-take conflicting fields
|
||||
}
|
||||
Expect(k.Client.Patch(c, &obj, client.Apply, patchOpts...)).To(Succeed())
|
||||
}
|
||||
|
||||
func installCRDs(ctx context.Context, restConfig *rest.Config, crdFiles []string) {
|
||||
apixClient, err := apix.NewForConfig(restConfig)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
for _, f := range crdFiles {
|
||||
By("Installing CRD " + f)
|
||||
b, err := os.ReadFile(filepath.Clean(f))
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
var crd apiextv1.CustomResourceDefinition
|
||||
err = yaml.Unmarshal(b, &crd)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Create or Update
|
||||
_, err = apixClient.ApiextensionsV1().CustomResourceDefinitions().Create(ctx, &crd, metav1.CreateOptions{})
|
||||
if apierrors.IsAlreadyExists(err) {
|
||||
existing, getErr := apixClient.ApiextensionsV1().CustomResourceDefinitions().Get(ctx, crd.Name, metav1.GetOptions{})
|
||||
Expect(getErr).NotTo(HaveOccurred())
|
||||
|
||||
crd.ResourceVersion = existing.ResourceVersion
|
||||
_, err = apixClient.ApiextensionsV1().CustomResourceDefinitions().Update(ctx, &crd, metav1.UpdateOptions{})
|
||||
}
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
waitCRDEstablished(ctx, apixClient, crd.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// waitCRDEstablished Wait until the CRD reaches Established=True, retrying until the suite timeout.
|
||||
func waitCRDEstablished(ctx context.Context, apixClient *apix.Clientset, name string) {
|
||||
By("Waiting for CRD " + name + " to be Established")
|
||||
|
||||
Eventually(func(g Gomega) {
|
||||
// Short per-attempt timeout so a single Get can't hang the whole Eventually loop.
|
||||
attemptCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
crd, err := apixClient.ApiextensionsV1().
|
||||
CustomResourceDefinitions().
|
||||
Get(attemptCtx, name, metav1.GetOptions{})
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
established := false
|
||||
for _, c := range crd.Status.Conditions {
|
||||
if c.Type == apiextv1.Established && c.Status == apiextv1.ConditionTrue {
|
||||
established = true
|
||||
break
|
||||
}
|
||||
}
|
||||
g.Expect(established).To(BeTrue(), "CRD %q is not Established yet", name)
|
||||
}, defaults.E2ETimeout, defaults.E2EInterval).Should(Succeed())
|
||||
}
|
41
pkg/testhelper/kube/namespace.go
Normal file
41
pkg/testhelper/kube/namespace.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package kube
|
||||
|
||||
import (
|
||||
"context"
|
||||
//nolint:staticcheck // ST1001
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
//nolint:staticcheck // ST1001
|
||||
. "github.com/onsi/gomega"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
type Namespace struct {
|
||||
client client.Client
|
||||
config *Config
|
||||
name string
|
||||
}
|
||||
|
||||
// LabelNamespace applies the given labels to the specified namespace
|
||||
func (n *Namespace) LabelNamespace(ctx context.Context, labelsMap map[string]string) {
|
||||
if len(labelsMap) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
By("Setting labelsMap " + labels.Set(labelsMap).String() + " to namespace/" + n.name)
|
||||
ns := &corev1.Namespace{}
|
||||
err := n.client.Get(ctx, client.ObjectKey{Name: n.name}, ns)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
if ns.Labels == nil {
|
||||
ns.Labels = map[string]string{}
|
||||
}
|
||||
|
||||
for k, v := range labelsMap {
|
||||
ns.Labels[k] = v
|
||||
}
|
||||
|
||||
err = n.client.Update(ctx, ns)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
}
|
148
pkg/testhelper/kube/pod.go
Normal file
148
pkg/testhelper/kube/pod.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package kube
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
//nolint:staticcheck // ST1001
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
//nolint:staticcheck // ST1001
|
||||
. "github.com/onsi/gomega"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
type Pod struct {
|
||||
client client.Client
|
||||
clientset kubernetes.Interface
|
||||
config *Config
|
||||
selector map[string]string
|
||||
}
|
||||
|
||||
func (p *Pod) WaitingForRunningPod(ctx context.Context) {
|
||||
By("Waiting for the pod " + labels.Set(p.selector).String() + " to be 'Running'")
|
||||
|
||||
Eventually(func(g Gomega) {
|
||||
// short per-attempt timeout to avoid hanging calls while Eventually polls
|
||||
attemptCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var pods corev1.PodList
|
||||
listOpts := []client.ListOption{
|
||||
client.InNamespace(p.config.Namespace),
|
||||
client.MatchingLabels(p.selector),
|
||||
}
|
||||
g.Expect(p.client.List(attemptCtx, &pods, listOpts...)).To(Succeed())
|
||||
g.Expect(pods.Items).NotTo(BeEmpty(), "no pods found with selector %q", labels.Set(p.selector).String())
|
||||
|
||||
foundRunning := false
|
||||
for _, p := range pods.Items {
|
||||
if p.Status.Phase == corev1.PodRunning {
|
||||
foundRunning = true
|
||||
break
|
||||
}
|
||||
}
|
||||
g.Expect(foundRunning).To(BeTrue(), "pod not Running yet")
|
||||
}, p.config.TestConfig.Timeout, p.config.TestConfig.Interval).Should(Succeed())
|
||||
}
|
||||
|
||||
func (p *Pod) GetPodLogs(ctx context.Context) string {
|
||||
// First find the pod by label selector
|
||||
var pods corev1.PodList
|
||||
listOpts := []client.ListOption{
|
||||
client.InNamespace(p.config.Namespace),
|
||||
client.MatchingLabels(p.selector),
|
||||
}
|
||||
err := p.client.List(ctx, &pods, listOpts...)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(pods.Items).NotTo(BeEmpty(), "no pods found with selector %q", labels.Set(p.selector).String())
|
||||
|
||||
// Find a running pod to get logs from
|
||||
var pod *corev1.Pod
|
||||
for i := range pods.Items {
|
||||
if pods.Items[i].Status.Phase == corev1.PodRunning {
|
||||
pod = &pods.Items[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
Expect(pod).NotTo(BeNil(), "no running pod found with selector %q", labels.Set(p.selector).String())
|
||||
|
||||
podName := pod.Name
|
||||
// Get logs using the Kubernetes clientset
|
||||
req := p.clientset.CoreV1().Pods(p.config.Namespace).GetLogs(podName, &corev1.PodLogOptions{})
|
||||
stream, err := req.Stream(context.TODO())
|
||||
Expect(err).NotTo(HaveOccurred(), "failed to stream logs for pod %s", podName)
|
||||
defer stream.Close()
|
||||
|
||||
// Read all logs from the stream
|
||||
logs, err := io.ReadAll(stream)
|
||||
Expect(err).NotTo(HaveOccurred(), "failed to read logs for pod %s", podName)
|
||||
|
||||
return string(logs)
|
||||
}
|
||||
|
||||
func (p *Pod) VerifyWebhookInjection(ctx context.Context) {
|
||||
By("Verifying webhook injection for pod with selector " + labels.Set(p.selector).String())
|
||||
|
||||
Eventually(func(g Gomega) {
|
||||
// short per-attempt timeout to avoid hanging calls while Eventually polls
|
||||
attemptCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// First find the pod by label selector
|
||||
var pods corev1.PodList
|
||||
listOpts := []client.ListOption{
|
||||
client.InNamespace(p.config.Namespace),
|
||||
client.MatchingLabels(p.selector),
|
||||
}
|
||||
g.Expect(p.client.List(attemptCtx, &pods, listOpts...)).To(Succeed())
|
||||
g.Expect(pods.Items).NotTo(BeEmpty(), "no pods found with selector %q", labels.Set(p.selector).String())
|
||||
|
||||
// Find a running pod to verify webhook injection
|
||||
var pod *corev1.Pod
|
||||
for i := range pods.Items {
|
||||
if pods.Items[i].Status.Phase == corev1.PodRunning {
|
||||
pod = &pods.Items[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
g.Expect(pod).NotTo(BeNil(), "no running pod found with selector %q", labels.Set(p.selector).String())
|
||||
|
||||
// Check injection status annotation
|
||||
g.Expect(pod.Annotations).To(HaveKey("operator.1password.io/status"))
|
||||
g.Expect(pod.Annotations["operator.1password.io/status"]).To(Equal("injected"))
|
||||
|
||||
// Check command was modified to use op run
|
||||
if len(pod.Spec.Containers) > 0 {
|
||||
container := pod.Spec.Containers[0]
|
||||
g.Expect(container.Command).To(HaveLen(4))
|
||||
g.Expect(container.Command[0]).To(Equal("/op/bin/op"))
|
||||
g.Expect(container.Command[1]).To(Equal("run"))
|
||||
g.Expect(container.Command[2]).To(Equal("--"))
|
||||
}
|
||||
|
||||
// Check init container was added
|
||||
g.Expect(pod.Spec.InitContainers).To(HaveLen(1))
|
||||
g.Expect(pod.Spec.InitContainers[0].Name).To(Equal("copy-op-bin"))
|
||||
|
||||
// Check volume mount was added
|
||||
g.Expect(pod.Spec.Containers[0].VolumeMounts).To(ContainElement(HaveField("Name", "op-bin")))
|
||||
}, p.config.TestConfig.Timeout, p.config.TestConfig.Interval).Should(Succeed())
|
||||
}
|
||||
|
||||
func (p *Pod) VerifySecretsInjected(ctx context.Context) {
|
||||
By("Verifying secrets are injected and concealed in pod with selector " + labels.Set(p.selector).String())
|
||||
|
||||
Eventually(func(g Gomega) {
|
||||
// short per-attempt timeout to avoid hanging calls while Eventually polls
|
||||
attemptCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
logs := p.GetPodLogs(attemptCtx)
|
||||
// Check that secrets are concealed in the application logs
|
||||
g.Expect(logs).To(ContainSubstring("SECRET: '<concealed by 1Password>'"))
|
||||
}, p.config.TestConfig.Timeout, p.config.TestConfig.Interval).Should(Succeed())
|
||||
}
|
141
pkg/testhelper/kube/secret.go
Normal file
141
pkg/testhelper/kube/secret.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package kube
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
//nolint:staticcheck // ST1001
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
//nolint:staticcheck // ST1001
|
||||
. "github.com/onsi/gomega"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/1Password/onepassword-operator/pkg/testhelper/system"
|
||||
)
|
||||
|
||||
type Secret struct {
|
||||
client client.Client
|
||||
config *Config
|
||||
name string
|
||||
}
|
||||
|
||||
// CreateFromEnvVar creates a kubernetes secret from an environment variable
|
||||
func (s *Secret) CreateFromEnvVar(ctx context.Context, envVar string) *corev1.Secret {
|
||||
By("Creating '" + s.name + "' secret from environment variable")
|
||||
|
||||
// Derive a short-lived context so this API call won't hang indefinitely.
|
||||
c, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
value, ok := os.LookupEnv(envVar)
|
||||
Expect(ok).To(BeTrue())
|
||||
Expect(value).NotTo(BeEmpty())
|
||||
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: s.name,
|
||||
Namespace: s.config.Namespace,
|
||||
},
|
||||
StringData: map[string]string{
|
||||
"token": value,
|
||||
},
|
||||
}
|
||||
|
||||
err := s.client.Create(c, secret)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
return secret
|
||||
}
|
||||
|
||||
// CreateFromFile creates a kubernetes secret from a file
|
||||
func (s *Secret) CreateFromFile(ctx context.Context, fileName string, content []byte) *corev1.Secret {
|
||||
By("Creating '" + s.name + "' secret from file " + fileName)
|
||||
|
||||
// Derive a short-lived context so this API call won't hang indefinitely.
|
||||
c, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: s.name,
|
||||
Namespace: s.config.Namespace,
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
filepath.Base(fileName): content,
|
||||
},
|
||||
}
|
||||
|
||||
err := s.client.Create(c, secret)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
return secret
|
||||
}
|
||||
|
||||
// CreateOpCredentials creates a kubernetes secret from 1password-credentials.json file in the project root
|
||||
// encodes it in base64 and saves it to op-session file
|
||||
func (s *Secret) CreateOpCredentials(ctx context.Context) *corev1.Secret {
|
||||
rootDir, err := system.GetProjectRoot()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
credentialsFilePath := filepath.Join(rootDir, "1password-credentials.json")
|
||||
data, err := os.ReadFile(credentialsFilePath)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
encoded := base64.RawURLEncoding.EncodeToString(data)
|
||||
|
||||
return s.CreateFromFile(ctx, "op-session", []byte(encoded))
|
||||
}
|
||||
|
||||
// Get retrieves a kubernetes secret
|
||||
func (s *Secret) Get(ctx context.Context) *corev1.Secret {
|
||||
By("Getting '" + s.name + "' secret")
|
||||
|
||||
// Derive a short-lived context so this API call won't hang indefinitely.
|
||||
c, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
secret := &corev1.Secret{}
|
||||
err := s.client.Get(c, client.ObjectKey{Name: s.name, Namespace: s.config.Namespace}, secret)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
return secret
|
||||
}
|
||||
|
||||
// Delete deletes a kubernetes secret
|
||||
func (s *Secret) Delete(ctx context.Context) {
|
||||
By("Deleting '" + s.name + "' secret")
|
||||
|
||||
// Derive a short-lived context so this API call won't hang indefinitely.
|
||||
c, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: s.name,
|
||||
Namespace: s.config.Namespace,
|
||||
},
|
||||
}
|
||||
err := s.client.Delete(c, secret)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
}
|
||||
|
||||
// CheckIfExists repeatedly attempts to retrieve the given Secret
|
||||
// from the cluster until it is found or the test's timeout expires.
|
||||
func (s *Secret) CheckIfExists(ctx context.Context) {
|
||||
By("Checking '" + s.name + "' secret")
|
||||
|
||||
Eventually(func(g Gomega) {
|
||||
// Derive a short-lived context so this API call won't hang indefinitely.
|
||||
attemptCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
secret := &corev1.Secret{}
|
||||
err := s.client.Get(attemptCtx, client.ObjectKey{Name: s.name, Namespace: s.config.Namespace}, secret)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
}, s.config.TestConfig.Timeout, s.config.TestConfig.Interval).Should(Succeed())
|
||||
}
|
33
pkg/testhelper/kube/webhook.go
Normal file
33
pkg/testhelper/kube/webhook.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package kube
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
//nolint:staticcheck // ST1001
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
//nolint:staticcheck // ST1001
|
||||
. "github.com/onsi/gomega"
|
||||
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
type Webhook struct {
|
||||
client client.Client
|
||||
config *Config
|
||||
name string
|
||||
}
|
||||
|
||||
func (w *Webhook) WaitForWebhookToBeRegistered(ctx context.Context) {
|
||||
By("Waiting for webhook " + w.name + " to be registered")
|
||||
|
||||
Eventually(func(g Gomega) {
|
||||
// short per-attempt timeout to avoid hanging calls while Eventually polls
|
||||
attemptCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
webhookConfig := &admissionregistrationv1.MutatingWebhookConfiguration{}
|
||||
err := w.client.Get(attemptCtx, client.ObjectKey{Name: w.name}, webhookConfig)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
}, w.config.TestConfig.Timeout, w.config.TestConfig.Interval).Should(Succeed())
|
||||
}
|
32
pkg/testhelper/op/op.go
Normal file
32
pkg/testhelper/op/op.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package op
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/1Password/onepassword-operator/pkg/testhelper/system"
|
||||
)
|
||||
|
||||
type Field string
|
||||
|
||||
const (
|
||||
FieldUsername = "username"
|
||||
FieldPassword = "password"
|
||||
)
|
||||
|
||||
// UpdateItemPassword updates the password of an item in 1Password
|
||||
func UpdateItemPassword(item string) error {
|
||||
_, err := system.Run("op", "item", "edit", item, "--generate-password=letters,digits,symbols,32")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadItemField reads the password of an item in 1Password
|
||||
func ReadItemField(item, vault string, field Field) (string, error) {
|
||||
output, err := system.Run("op", "read", fmt.Sprintf("op://%s/%s/%s", vault, item, field))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return output, nil
|
||||
}
|
94
pkg/testhelper/system/system.go
Normal file
94
pkg/testhelper/system/system.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Run executes the provided command within this context
|
||||
func Run(name string, args ...string) (string, error) {
|
||||
cmd := exec.Command(name, args...)
|
||||
|
||||
rootDir, err := GetProjectRoot()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Command will run from project root
|
||||
cmd.Dir = rootDir
|
||||
|
||||
command := strings.Join(cmd.Args, " ")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return string(output), fmt.Errorf("%s failed with error: (%v) %s", command, err, string(output))
|
||||
}
|
||||
|
||||
return string(output), nil
|
||||
}
|
||||
|
||||
func GetProjectRoot() (string, error) {
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for {
|
||||
// check if go.mod exists in current dir
|
||||
modFile := filepath.Join(dir, "go.mod")
|
||||
if _, err := os.Stat(modFile); err == nil {
|
||||
return dir, nil
|
||||
}
|
||||
|
||||
// move one level up
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
// reached filesystem root
|
||||
return "", fmt.Errorf("project root not found (no go.mod)")
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
}
|
||||
|
||||
func ReplaceFile(src, dst string) error {
|
||||
rootDir, err := GetProjectRoot()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Open the source file
|
||||
sourceFile, err := os.Open(filepath.Join(rootDir, src))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func(sourceFile *os.File) {
|
||||
cerr := sourceFile.Close()
|
||||
if err != nil {
|
||||
err = errors.Join(err, cerr)
|
||||
}
|
||||
}(sourceFile)
|
||||
|
||||
// Create (or overwrite) the destination file
|
||||
destFile, err := os.Create(filepath.Join(rootDir, dst))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func(destFile *os.File) {
|
||||
cerr := destFile.Close()
|
||||
if err != nil {
|
||||
err = errors.Join(err, cerr)
|
||||
}
|
||||
}(destFile)
|
||||
|
||||
// Copy contents
|
||||
if _, err = io.Copy(destFile, sourceFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Ensure data is written to disk
|
||||
return destFile.Sync()
|
||||
}
|
14
test/e2e/e2e_suite_test.go
Normal file
14
test/e2e/e2e_suite_test.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
// Run e2e tests using the Ginkgo runner.
|
||||
func TestE2E(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "onepassword-operator e2e suite")
|
||||
}
|
233
test/e2e/e2e_test.go
Normal file
233
test/e2e/e2e_test.go
Normal file
@@ -0,0 +1,233 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
//nolint:staticcheck // ST1001
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
//nolint:staticcheck // ST1001
|
||||
. "github.com/onsi/gomega"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
||||
"github.com/1Password/onepassword-operator/pkg/testhelper/defaults"
|
||||
"github.com/1Password/onepassword-operator/pkg/testhelper/kind"
|
||||
"github.com/1Password/onepassword-operator/pkg/testhelper/kube"
|
||||
"github.com/1Password/onepassword-operator/pkg/testhelper/op"
|
||||
"github.com/1Password/onepassword-operator/pkg/testhelper/system"
|
||||
)
|
||||
|
||||
const (
|
||||
operatorImageName = "1password/onepassword-operator:latest"
|
||||
vaultName = "operator-acceptance-tests"
|
||||
)
|
||||
|
||||
var kubeClient *kube.Kube
|
||||
|
||||
var _ = Describe("Onepassword Operator e2e", Ordered, func() {
|
||||
ctx := context.Background()
|
||||
|
||||
BeforeAll(func() {
|
||||
rootDir, err := system.GetProjectRoot()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
kubeClient = kube.NewKubeClient(&kube.Config{
|
||||
Namespace: "default",
|
||||
ManifestsDir: filepath.Join("manifests"),
|
||||
TestConfig: &kube.TestConfig{
|
||||
Timeout: defaults.E2ETimeout,
|
||||
Interval: defaults.E2EInterval,
|
||||
},
|
||||
CRDs: []string{
|
||||
filepath.Join(rootDir, "config", "crd", "bases", "onepassword.com_onepassworditems.yaml"),
|
||||
},
|
||||
})
|
||||
|
||||
By("Building the Operator image")
|
||||
_, err = system.Run("make", "docker-build")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
kind.LoadImageToKind(operatorImageName)
|
||||
|
||||
kubeClient.Secret("op-credentials").CreateOpCredentials(ctx)
|
||||
kubeClient.Secret("op-credentials").CheckIfExists(ctx)
|
||||
|
||||
kubeClient.Secret("onepassword-token").CreateFromEnvVar(ctx, "OP_CONNECT_TOKEN")
|
||||
kubeClient.Secret("onepassword-token").CheckIfExists(ctx)
|
||||
|
||||
kubeClient.Secret("onepassword-service-account-token").CreateFromEnvVar(ctx, "OP_SERVICE_ACCOUNT_TOKEN")
|
||||
kubeClient.Secret("onepassword-service-account-token").CheckIfExists(ctx)
|
||||
|
||||
By("Replace manager.yaml")
|
||||
err = system.ReplaceFile("test/e2e/manifests/manager.yaml", "config/manager/manager.yaml")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
_, err = system.Run("make", "deploy")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
kubeClient.Pod(map[string]string{"name": "onepassword-connect-operator"}).WaitingForRunningPod(ctx)
|
||||
})
|
||||
|
||||
Context("Use the operator with Service Account", func() {
|
||||
runCommonTestCases(ctx)
|
||||
})
|
||||
|
||||
Context("Use the operator with Connect", func() {
|
||||
BeforeAll(func() {
|
||||
kubeClient.Deployment("onepassword-connect-operator").PatchEnvVars(ctx, []corev1.EnvVar{
|
||||
{Name: "MANAGE_CONNECT", Value: "true"},
|
||||
{Name: "OP_CONNECT_HOST", Value: "http://onepassword-connect:8080"},
|
||||
{
|
||||
Name: "OP_CONNECT_TOKEN",
|
||||
ValueFrom: &corev1.EnvVarSource{
|
||||
SecretKeyRef: &corev1.SecretKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: "onepassword-token",
|
||||
},
|
||||
Key: "token",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, []string{"OP_SERVICE_ACCOUNT_TOKEN"})
|
||||
|
||||
kubeClient.Secret("login").Delete(ctx) // remove secret crated in previous test
|
||||
kubeClient.Secret("secret-ignored").Delete(ctx) // remove secret crated in previous test
|
||||
kubeClient.Secret("secret-for-update").Delete(ctx) // remove secret crated in previous test
|
||||
|
||||
kubeClient.Pod(map[string]string{"app": "onepassword-connect"}).WaitingForRunningPod(ctx)
|
||||
})
|
||||
|
||||
runCommonTestCases(ctx)
|
||||
})
|
||||
})
|
||||
|
||||
// runCommonTestCases contains test cases that are common to both Connect and Service Account authentication methods.
|
||||
func runCommonTestCases(ctx context.Context) {
|
||||
It("Should create kubernetes secret from manifest file", func() {
|
||||
By("Creating secret `login` from 1Password item")
|
||||
kubeClient.Apply(ctx, "secret.yaml")
|
||||
kubeClient.Secret("login").CheckIfExists(ctx)
|
||||
})
|
||||
|
||||
It("Kubernetes secret is updated after POOLING_INTERVAL, when updating item in 1Password", func() {
|
||||
itemName := "secret-for-update"
|
||||
secretName := itemName
|
||||
|
||||
By("Creating secret `" + secretName + "` from 1Password item")
|
||||
kubeClient.Apply(ctx, secretName+".yaml")
|
||||
kubeClient.Secret(secretName).CheckIfExists(ctx)
|
||||
|
||||
By("Reading old password")
|
||||
secret := kubeClient.Secret(secretName).Get(ctx)
|
||||
oldPassword, ok := secret.Data["password"]
|
||||
Expect(ok).To(BeTrue())
|
||||
|
||||
By("Updating `" + secretName + "` 1Password item")
|
||||
err := op.UpdateItemPassword(itemName)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// checking that password was updated
|
||||
Eventually(func(g Gomega) {
|
||||
// Derive a short-lived context so this API call won't hang indefinitely.
|
||||
attemptCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
secret = kubeClient.Secret(secretName).Get(attemptCtx)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
newPassword, ok := secret.Data["password"]
|
||||
g.Expect(ok).To(BeTrue())
|
||||
g.Expect(newPassword).NotTo(Equal(oldPassword))
|
||||
}, defaults.E2ETimeout, defaults.E2EInterval).Should(Succeed())
|
||||
})
|
||||
|
||||
It("1Password item with `ignore-secret` tag doesn't pull updates to kubernetes secret", func() {
|
||||
itemName := "secret-ignored"
|
||||
secretName := itemName
|
||||
|
||||
By("Creating secret `" + secretName + "` from 1Password item")
|
||||
kubeClient.Apply(ctx, secretName+".yaml")
|
||||
kubeClient.Secret(secretName).CheckIfExists(ctx)
|
||||
|
||||
By("Reading old password")
|
||||
secret := kubeClient.Secret(secretName).Get(ctx)
|
||||
oldPassword, ok := secret.Data["password"]
|
||||
Expect(ok).To(BeTrue())
|
||||
|
||||
By("Updating `" + secretName + "` 1Password item")
|
||||
err := op.UpdateItemPassword(itemName)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
newPassword, err := op.ReadItemField(itemName, vaultName, op.FieldPassword)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(newPassword).NotTo(BeEquivalentTo(oldPassword))
|
||||
|
||||
// checking that password was NOT updated
|
||||
Eventually(func(g Gomega) {
|
||||
// Derive a short-lived context so this API call won't hang indefinitely.
|
||||
attemptCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
intervalStr := kubeClient.Deployment("onepassword-connect-operator").ReadEnvVar(attemptCtx, "POLLING_INTERVAL")
|
||||
Expect(intervalStr).NotTo(BeEmpty())
|
||||
|
||||
i, err := strconv.Atoi(intervalStr)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// convert to duration in seconds
|
||||
interval := time.Duration(i) * time.Second
|
||||
// wait for one polling interval + 2 seconds to make sure updated secret is pulled
|
||||
time.Sleep(interval + 2*time.Second)
|
||||
|
||||
secret = kubeClient.Secret(secretName).Get(attemptCtx)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
currentPassword, ok := secret.Data["password"]
|
||||
Expect(ok).To(BeTrue())
|
||||
Expect(currentPassword).To(BeEquivalentTo(oldPassword))
|
||||
Expect(currentPassword).NotTo(BeEquivalentTo(newPassword))
|
||||
}, defaults.E2ETimeout, defaults.E2EInterval).Should(Succeed())
|
||||
})
|
||||
|
||||
It("AUTO_RESTART restarts deployments using 1Password secrets after item update", func() {
|
||||
By("Enabling AUTO_RESTART")
|
||||
kubeClient.Deployment("onepassword-connect-operator").PatchEnvVars(ctx, []corev1.EnvVar{
|
||||
{Name: "AUTO_RESTART", Value: "true"},
|
||||
}, nil)
|
||||
|
||||
DeferCleanup(func() {
|
||||
By("Disabling AUTO_RESTART")
|
||||
// disable AUTO_RESTART after test
|
||||
kubeClient.Deployment("onepassword-connect-operator").PatchEnvVars(ctx, []corev1.EnvVar{
|
||||
{Name: "AUTO_RESTART", Value: "false"},
|
||||
}, nil)
|
||||
})
|
||||
|
||||
// Ensure the secret exists (created in earlier test), but apply again safely just in case
|
||||
kubeClient.Apply(ctx, "secret-for-update.yaml")
|
||||
kubeClient.Secret("secret-for-update").CheckIfExists(ctx)
|
||||
|
||||
// add custom secret to the operator
|
||||
kubeClient.Deployment("onepassword-connect-operator").PatchEnvVars(ctx, []corev1.EnvVar{
|
||||
{
|
||||
Name: "CUSTOM_SECRET",
|
||||
ValueFrom: &corev1.EnvVarSource{
|
||||
SecretKeyRef: &corev1.SecretKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: "secret-for-update",
|
||||
},
|
||||
Key: "password",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
|
||||
By("Updating `secret-for-update` 1Password item")
|
||||
err := op.UpdateItemPassword("secret-for-update")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("Checking the operator is restarted")
|
||||
kubeClient.Deployment("onepassword-connect-operator").WaitDeploymentRolledOut(ctx)
|
||||
})
|
||||
}
|
99
test/e2e/manifests/manager.yaml
Normal file
99
test/e2e/manifests/manager.yaml
Normal file
@@ -0,0 +1,99 @@
|
||||
# This manager file is used for e2e tests.
|
||||
# It will be copied to `config/manager` and be used when deploying the operator in e2e tests
|
||||
# The purpose of it is to increase e2e tests speed and do not introduce additional changes in original `manager.yaml`
|
||||
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
labels:
|
||||
control-plane: onepassword-connect-operator
|
||||
app.kubernetes.io/name: namespace
|
||||
app.kubernetes.io/instance: system
|
||||
app.kubernetes.io/component: manager
|
||||
app.kubernetes.io/created-by: onepassword-connect-operator
|
||||
app.kubernetes.io/part-of: onepassword-connect-operator
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
name: system
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: onepassword-connect-operator
|
||||
namespace: system
|
||||
labels:
|
||||
control-plane: controller-manager
|
||||
app.kubernetes.io/name: deployment
|
||||
app.kubernetes.io/instance: controller-manager
|
||||
app.kubernetes.io/component: manager
|
||||
app.kubernetes.io/created-by: onepassword-connect-operator
|
||||
app.kubernetes.io/part-of: onepassword-connect-operator
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
name: onepassword-connect-operator
|
||||
control-plane: onepassword-connect-operator
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
kubectl.kubernetes.io/default-container: manager
|
||||
labels:
|
||||
name: onepassword-connect-operator
|
||||
control-plane: onepassword-connect-operator
|
||||
spec:
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
containers:
|
||||
- command:
|
||||
- /manager
|
||||
args:
|
||||
- --leader-elect
|
||||
- --health-probe-bind-address=:8081
|
||||
image: 1password/onepassword-operator:latest
|
||||
imagePullPolicy: Never
|
||||
name: manager
|
||||
env:
|
||||
- name: OPERATOR_NAME
|
||||
value: "onepassword-connect-operator"
|
||||
- name: POD_NAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.name
|
||||
- name: WATCH_NAMESPACE
|
||||
value: "default"
|
||||
- name: POLLING_INTERVAL
|
||||
value: "10"
|
||||
- name: AUTO_RESTART
|
||||
value: "false"
|
||||
- name: OP_SERVICE_ACCOUNT_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: onepassword-service-account-token
|
||||
key: token
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- "ALL"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 8081
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 20
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /readyz
|
||||
port: 8081
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
serviceAccountName: onepassword-connect-operator
|
||||
terminationGracePeriodSeconds: 10
|
6
test/e2e/manifests/secret-for-update.yaml
Normal file
6
test/e2e/manifests/secret-for-update.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
apiVersion: onepassword.com/v1
|
||||
kind: OnePasswordItem
|
||||
metadata:
|
||||
name: secret-for-update
|
||||
spec:
|
||||
itemPath: "vaults/operator-acceptance-tests/items/secret-for-update"
|
6
test/e2e/manifests/secret-ignored.yaml
Normal file
6
test/e2e/manifests/secret-ignored.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
apiVersion: onepassword.com/v1
|
||||
kind: OnePasswordItem
|
||||
metadata:
|
||||
name: secret-ignored
|
||||
spec:
|
||||
itemPath: "vaults/operator-acceptance-tests/items/secret-ignored"
|
6
test/e2e/manifests/secret.yaml
Normal file
6
test/e2e/manifests/secret.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
apiVersion: onepassword.com/v1
|
||||
kind: OnePasswordItem
|
||||
metadata:
|
||||
name: login
|
||||
spec:
|
||||
itemPath: "vaults/operator-acceptance-tests/items/test-login"
|
Reference in New Issue
Block a user