diff --git a/.github/workflows/contributor-governance.yml b/.github/workflows/contributor-governance.yml new file mode 100644 index 0000000..67b99b3 --- /dev/null +++ b/.github/workflows/contributor-governance.yml @@ -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 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 339202b..41aef48 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -20,4 +20,4 @@ jobs: - name: Run linter uses: golangci/golangci-lint-action@v9 with: - version: v2.6 + version: v2.12 diff --git a/.golangci.yml b/.golangci.yml index e5b21b0..389995b 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -8,7 +8,6 @@ linters: - dupl - errcheck - ginkgolinter - - goconst - gocyclo - govet - ineffassign diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..2a2e3ee --- /dev/null +++ b/CONTRIBUTING.md @@ -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. diff --git a/Dockerfile b/Dockerfile index b8e0c29..6205c41 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/Makefile b/Makefile index 925efff..8c5b815 100644 --- a/Makefile +++ b/Makefile @@ -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. diff --git a/README.md b/README.md index 569011a..31b8560 100644 --- a/README.md +++ b/README.md @@ -630,6 +630,10 @@ spec: EOF ``` +## Contributing + +See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines. + ## License Copyright 2025. diff --git a/go.mod b/go.mod index 353dbb5..297e06c 100644 --- a/go.mod +++ b/go.mod @@ -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 @@ -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 diff --git a/go.sum b/go.sum index 0a7d7ab..109339e 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/controller/apikey_status_controller.go b/internal/controller/apikey_status_controller.go index b1bfa0f..d011e9a 100644 --- a/internal/controller/apikey_status_controller.go +++ b/internal/controller/apikey_status_controller.go @@ -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 @@ -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) } diff --git a/internal/controller/apikey_status_controller_test.go b/internal/controller/apikey_status_controller_test.go index 4169540..06aa279 100644 --- a/internal/controller/apikey_status_controller_test.go +++ b/internal/controller/apikey_status_controller_test.go @@ -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,