Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/contributor-governance.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name: Contributor Governance

on:
issues:
types: [opened, labeled, unlabeled]
pull_request_target:
types: [opened, reopened]

jobs:
governance:
uses: Kuadrant/.github/.github/workflows/contributor-governance.yml@main
secrets: inherit
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ jobs:
- name: Run linter
uses: golangci/golangci-lint-action@v9
with:
version: v2.6
version: v2.12
1 change: 0 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ linters:
- dupl
- errcheck
- ginkgolinter
- goconst
- gocyclo
- govet
- ineffassign
Expand Down
128 changes: 128 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# Contributing

For general contribution guidelines, PR requirements, and issue triage rules, see the [Kuadrant contributing guide](https://github.com/Kuadrant/.github/blob/main/CONTRIBUTING.md).

## Issue Policy

**Do not start work on an issue unless it has been assigned to you by a maintainer.**

Issues go through a triage and planning process before they are ready for contribution. Working on unassigned issues — whether manually or via AI coding agents — wastes your time and ours.

**Pull requests submitted against unassigned issues will be automatically closed and will not be reopened.**

If you're interested in contributing to an issue, leave a comment and wait for a maintainer to assign it to you before starting any work.

Issues labelled `maintainers-only` are reserved for the maintainer team and are not available for external contribution.

## Contributions

We welcome code and non-code contributions to our project. Non-code contributions can come in the form of documentation updates, bug reports, enhancement requests, and feature requests.

### Finding Issues to Work On

The best place to start is to look through our issues for [bugs](https://github.com/Kuadrant/developer-portal-controller/issues?q=is%3Aopen+is%3Aissue+label%3Akind%2Fbug) and [good first issues](https://github.com/Kuadrant/developer-portal-controller/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22). These are a great starting point for new contributors.

Remember: only work on issues that are `triage/accepted` and assigned to you.

### Bug Reporting

If you found a bug, please submit an [issue](https://github.com/Kuadrant/developer-portal-controller/issues/new) describing the problem. Include:

- Steps to reproduce the bug
- The Kubernetes/OpenShift version you are running
- CRD versions installed
- Any relevant logs

### Enhancement Requests

If you want an enhancement of a feature or workflow, submit an [issue](https://github.com/Kuadrant/developer-portal-controller/issues/new) describing the enhancement. Include:

- What you are wanting to see improved
- The current behavior
- The new behavior you wish to see

### Feature Requests

If you want to see a new feature, file an [issue](https://github.com/Kuadrant/developer-portal-controller/issues/new) detailing the new feature. Include:

- What you are trying to achieve with the new feature
- What you will need
- Any relevant documentation or information on the new feature

### Documentation

If there is documentation that is unclear or could use some improvements, please raise an issue or submit a pull request.

### Pull Requests

If you want to submit code changes to the project, here are some guidelines:

1. **Create a Branch**

```bash
git checkout -b your-feature-branch
```

2. **Implement Your Changes**

Make your code changes, ensuring that you follow the project's coding standards and best practices.

If you modify API types in `api/v1alpha1/*_types.go`, regenerate code and CRDs:

```bash
make manifests generate
```

3. **Testing**

Ensure all tests pass before committing.

```bash
make test # run unit tests with coverage
make test-e2e # run e2e tests (creates/deletes Kind cluster)
go test ./internal/controller -v # run controller tests only
```

4. **Linting and Formatting**

Ensure your code passes linting and formatting checks.

```bash
make fmt # run go fmt
make vet # run go vet
make lint # run golangci-lint
```

5. **Ensure CI Passes**

Your contributions will need to pass the Continuous Integration (CI) tests for pull requests.

6. **Commit Changes**

Use meaningful commit messages following the [Conventional Commits](https://www.conventionalcommits.org/) specification.

```bash
git commit -m "feat: add new feature"
```

7. **Push to Your Fork**

```bash
git push origin your-feature-branch
```

8. **Open a Pull Request**

Go to the original repository and click on **New Pull Request**. Provide a clear description of your changes, including any issues your PR fixes, acceptance criteria, and any special notes to the reviewers.

## Commit Messages

Follow the [Conventional Commits](https://www.conventionalcommits.org/) specification:

- `feat`: A new feature.
- `fix`: A bug fix.
- `docs`: Documentation changes.
- `style`: Code style changes (formatting, missing semi-colons, etc.).
- `refactor`: Code changes that neither fix a bug nor add a feature.
- `test`: Adding or correcting tests.
- `chore`: Changes to the build process or auxiliary tools.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Build the manager binary
FROM --platform=$BUILDPLATFORM golang:1.25 AS builder
FROM --platform=$BUILDPLATFORM golang:1.26 AS builder
ARG TARGETOS
ARG TARGETARCH
ARG GIT_SHA
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ CONTROLLER_TOOLS_VERSION ?= v0.18.0
ENVTEST_VERSION ?= $(shell go list -m -f "{{ .Version }}" sigs.k8s.io/controller-runtime | awk -F'[v.]' '{printf "release-%d.%d", $$2, $$3}')
#ENVTEST_K8S_VERSION is the version of Kubernetes to use for setting up ENVTEST binaries (i.e. 1.31)
ENVTEST_K8S_VERSION ?= $(shell go list -m -f "{{ .Version }}" k8s.io/api | awk -F'[v.]' '{printf "1.%d", $$3}')
GOLANGCI_LINT_VERSION ?= v2.1.0
GOLANGCI_LINT_VERSION ?= v2.12.2

.PHONY: kustomize
kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary.
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,10 @@ spec:
EOF
```

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines.

## License

Copyright 2025.
Expand Down
14 changes: 7 additions & 7 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/kuadrant/developer-portal-controller

go 1.25.9
go 1.26.4

require (
github.com/go-logr/logr v1.4.3
Expand Down Expand Up @@ -82,14 +82,14 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/term v0.38.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/term v0.43.0 // indirect
golang.org/x/text v0.37.0 // indirect
golang.org/x/time v0.11.0 // indirect
golang.org/x/tools v0.39.0 // indirect
golang.org/x/tools v0.44.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
Expand Down
24 changes: 12 additions & 12 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -199,35 +199,35 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
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.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
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.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
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.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
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=
Expand Down
3 changes: 2 additions & 1 deletion internal/controller/apikey_status_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ type APIKeyStatusReconciler struct {
// +kubebuilder:rbac:groups=devportal.kuadrant.io,resources=apiproducts,verbs=get;list;watch
// +kubebuilder:rbac:groups=devportal.kuadrant.io,resources=apikeyapprovals,verbs=get;list;watch
// +kubebuilder:rbac:groups=devportal.kuadrant.io,resources=apikeyrequests,verbs=get;list;watch
// +kubebuilder:rbac:groups="",resources=secrets,verbs=get
// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch
// +kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=httproutes,verbs=get;list;watch
// +kubebuilder:rbac:groups=kuadrant.io,resources=kuadrants,verbs=get;list;watch

Expand Down Expand Up @@ -557,6 +557,7 @@ func (r *APIKeyStatusReconciler) SetupWithManager(mgr ctrl.Manager) error {
Watches(&devportalv1alpha1.APIKeyRequest{}, handler.EnqueueRequestsFromMapFunc(r.enqueueClass)).
Watches(&devportalv1alpha1.APIKeyApproval{}, handler.EnqueueRequestsFromMapFunc(r.enqueueClass)).
Watches(&gwapiv1.HTTPRoute{}, handler.EnqueueRequestsFromMapFunc(r.enqueueClass)).
Watches(&corev1.Secret{}, handler.EnqueueRequestsFromMapFunc(r.enqueueClass)).
Named("apikey-status").
Complete(r)
}
Expand Down
85 changes: 85 additions & 0 deletions internal/controller/apikey_status_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,91 @@ var _ = Describe("APIKey Status Controller", func() {
}, time.Second*10, time.Millisecond*250).Should(Succeed())
})

// Regression test for https://github.com/Kuadrant/developer-portal-controller/issues/78
//
// When the console-plugin creates an APIKey before its Secret (to set
// an ownerReference for garbage collection), the status controller
// sets SecretNotFound on the first reconciliation. Without a Secret
// watch, no subsequent event triggers re-reconciliation, so the
// APIKey stays stuck in Failed permanently even after the Secret
// appears. This test verifies that creating the Secret clears the
// failure.
It("should clear Failed condition when Secret is created after the APIKey", func() {
controllerReconciler := &APIKeyStatusReconciler{
Client: k8sClient,
Scheme: k8sClient.Scheme(),
}

secretName := "late-arriving-secret"

By("Creating an APIKey that references a Secret that does not exist yet")
apiKeyObj := &devportalv1alpha1.APIKey{
ObjectMeta: metav1.ObjectMeta{
Name: "apikey-late-secret",
Namespace: consumerNamespace,
},
Spec: devportalv1alpha1.APIKeySpec{
APIProductRef: devportalv1alpha1.APIProductReference{
Name: apiProductName,
Namespace: apiProductNamespace,
},
SecretRef: corev1.LocalObjectReference{
Name: secretName,
},
PlanTier: "premium",
UseCase: "Testing secret recovery",
RequestedBy: devportalv1alpha1.RequestedBy{
UserID: "test-user",
Email: "test@example.com",
},
},
}
Expect(k8sClient.Create(ctx, apiKeyObj)).To(Succeed())

By("Running reconciliation — should set SecretNotFound")
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{})
Expect(err).NotTo(HaveOccurred())

updatedAPIKey := &devportalv1alpha1.APIKey{}
Eventually(func(g Gomega) {
err := k8sClient.Get(ctx, types.NamespacedName{
Name: "apikey-late-secret",
Namespace: consumerNamespace,
}, updatedAPIKey)
g.Expect(err).NotTo(HaveOccurred())
failedCondition := meta.FindStatusCondition(updatedAPIKey.Status.Conditions, devportalv1alpha1.APIKeyConditionFailed)
g.Expect(failedCondition).NotTo(BeNil())
g.Expect(failedCondition.Reason).To(Equal("SecretNotFound"))
}, time.Second*10, time.Millisecond*250).Should(Succeed())

By("Creating the Secret that the APIKey references")
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: consumerNamespace,
},
Type: corev1.SecretTypeOpaque,
Data: map[string][]byte{
"api_key": []byte("test-key-value"),
},
}
Expect(k8sClient.Create(ctx, secret)).To(Succeed())

By("Running reconciliation — should clear the Failed condition")
_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{})
Expect(err).NotTo(HaveOccurred())

Eventually(func(g Gomega) {
err := k8sClient.Get(ctx, types.NamespacedName{
Name: "apikey-late-secret",
Namespace: consumerNamespace,
}, updatedAPIKey)
g.Expect(err).NotTo(HaveOccurred())
failedCondition := meta.FindStatusCondition(updatedAPIKey.Status.Conditions, devportalv1alpha1.APIKeyConditionFailed)
g.Expect(failedCondition).To(BeNil(), "Failed condition should be cleared after Secret is created")
}, time.Second*10, time.Millisecond*250).Should(Succeed())
})

It("should set Pending condition when no approval exists", func() {
controllerReconciler := &APIKeyStatusReconciler{
Client: k8sClient,
Expand Down
Loading