From 5e10c6d32cd645cc7dac0cede323a6323ada4c36 Mon Sep 17 00:00:00 2001 From: Sam Simpson Date: Wed, 20 May 2026 14:54:48 +0100 Subject: [PATCH] Define JobRequestReview resource --- PROJECT | 9 ++ api/v1/jobrequestreview_types.go | 72 +++++++++++++++ api/v1/zz_generated.deepcopy.go | 89 +++++++++++++++++++ cmd/main.go | 7 ++ ...hing.service.gov.uk_jobrequestreviews.yaml | 72 +++++++++++++++ .../controller/jobrequestreview_controller.go | 63 +++++++++++++ .../jobrequestreview_controller_test.go | 88 ++++++++++++++++++ 7 files changed, 400 insertions(+) create mode 100644 api/v1/jobrequestreview_types.go create mode 100644 config/crd/bases/platform.publishing.service.gov.uk_jobrequestreviews.yaml create mode 100644 internal/controller/jobrequestreview_controller.go create mode 100644 internal/controller/jobrequestreview_controller_test.go diff --git a/PROJECT b/PROJECT index a33e428..a2a9dcb 100644 --- a/PROJECT +++ b/PROJECT @@ -22,4 +22,13 @@ resources: kind: JobRequest path: github.com/alphagov/govuk-job-request-operator/api/v1 version: v1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: publishing.service.gov.uk + group: platform + kind: JobRequestReview + path: github.com/alphagov/govuk-job-request-operator/api/v1 + version: v1 version: "3" diff --git a/api/v1/jobrequestreview_types.go b/api/v1/jobrequestreview_types.go new file mode 100644 index 0000000..df4653e --- /dev/null +++ b/api/v1/jobrequestreview_types.go @@ -0,0 +1,72 @@ +/* +Copyright 2026. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// JobRequestReviewSpec defines the desired state of JobRequestReview +type JobRequestReviewSpec struct { + // Name of the JobRequest resource being reviewed. + JobRequestName string `json:"jobRequestName"` + // +kubebuilder:validation:Enum=Approved;Rejected + Decision string `json:"decision"` + // A description of the review decision. + // +optional + Description string `json:"description,omitempty"` +} + +// JobRequestReviewStatus defines the observed state of JobRequestReview. +type JobRequestReviewStatus struct { + // Kubernetes username of the reviewer. + ReviewedBy string `json:"reviewedBy,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:resource:shortName=jrr +// +kubebuilder:subresource:status + +// JobRequestReview is the Schema for the jobrequestreviews API +type JobRequestReview struct { + metav1.TypeMeta `json:",inline"` + + // metadata is a standard object metadata + // +optional + metav1.ObjectMeta `json:"metadata,omitzero"` + + // spec defines the desired state of JobRequestReview + // +required + Spec JobRequestReviewSpec `json:"spec"` + + // status defines the observed state of JobRequestReview + // +optional + Status JobRequestReviewStatus `json:"status,omitzero"` +} + +// +kubebuilder:object:root=true + +// JobRequestReviewList contains a list of JobRequestReview +type JobRequestReviewList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitzero"` + Items []JobRequestReview `json:"items"` +} + +func init() { + SchemeBuilder.Register(&JobRequestReview{}, &JobRequestReviewList{}) +} diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index f2b70ae..8335906 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -115,6 +115,95 @@ func (in *JobRequestPodSpecFrom) DeepCopy() *JobRequestPodSpecFrom { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JobRequestReview) DeepCopyInto(out *JobRequestReview) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JobRequestReview. +func (in *JobRequestReview) DeepCopy() *JobRequestReview { + if in == nil { + return nil + } + out := new(JobRequestReview) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *JobRequestReview) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JobRequestReviewList) DeepCopyInto(out *JobRequestReviewList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]JobRequestReview, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JobRequestReviewList. +func (in *JobRequestReviewList) DeepCopy() *JobRequestReviewList { + if in == nil { + return nil + } + out := new(JobRequestReviewList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *JobRequestReviewList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JobRequestReviewSpec) DeepCopyInto(out *JobRequestReviewSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JobRequestReviewSpec. +func (in *JobRequestReviewSpec) DeepCopy() *JobRequestReviewSpec { + if in == nil { + return nil + } + out := new(JobRequestReviewSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JobRequestReviewStatus) DeepCopyInto(out *JobRequestReviewStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JobRequestReviewStatus. +func (in *JobRequestReviewStatus) DeepCopy() *JobRequestReviewStatus { + if in == nil { + return nil + } + out := new(JobRequestReviewStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *JobRequestSpec) DeepCopyInto(out *JobRequestSpec) { *out = *in diff --git a/cmd/main.go b/cmd/main.go index 0192c28..e357b38 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -185,6 +185,13 @@ func main() { setupLog.Error(err, "Failed to create controller", "controller", "jobrequest") os.Exit(1) } + if err := (&controller.JobRequestReviewReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "Failed to create controller", "controller", "jobrequestreview") + os.Exit(1) + } // +kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/config/crd/bases/platform.publishing.service.gov.uk_jobrequestreviews.yaml b/config/crd/bases/platform.publishing.service.gov.uk_jobrequestreviews.yaml new file mode 100644 index 0000000..172293d --- /dev/null +++ b/config/crd/bases/platform.publishing.service.gov.uk_jobrequestreviews.yaml @@ -0,0 +1,72 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.20.1 + name: jobrequestreviews.platform.publishing.service.gov.uk +spec: + group: platform.publishing.service.gov.uk + names: + kind: JobRequestReview + listKind: JobRequestReviewList + plural: jobrequestreviews + shortNames: + - jrr + singular: jobrequestreview + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: JobRequestReview is the Schema for the jobrequestreviews API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: spec defines the desired state of JobRequestReview + properties: + decision: + enum: + - Approved + - Rejected + type: string + description: + description: A description of the review decision. + type: string + jobRequestName: + description: Name of the JobRequest resource being reviewed. + type: string + required: + - decision + - jobRequestName + type: object + status: + description: status defines the observed state of JobRequestReview + properties: + reviewedBy: + description: Kubernetes username of the reviewer. + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/internal/controller/jobrequestreview_controller.go b/internal/controller/jobrequestreview_controller.go new file mode 100644 index 0000000..a0b28e9 --- /dev/null +++ b/internal/controller/jobrequestreview_controller.go @@ -0,0 +1,63 @@ +/* +Copyright 2026. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + logf "sigs.k8s.io/controller-runtime/pkg/log" + + platformv1 "github.com/alphagov/govuk-job-request-operator/api/v1" +) + +// JobRequestReviewReconciler reconciles a JobRequestReview object +type JobRequestReviewReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +// +kubebuilder:rbac:groups=platform.publishing.service.gov.uk,resources=jobrequestreviews,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=platform.publishing.service.gov.uk,resources=jobrequestreviews/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=platform.publishing.service.gov.uk,resources=jobrequestreviews/finalizers,verbs=update + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// TODO(user): Modify the Reconcile function to compare the state specified by +// the JobRequestReview object against the actual cluster state, and then +// perform operations to make the cluster state reflect the state specified by +// the user. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.23.3/pkg/reconcile +func (r *JobRequestReviewReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + _ = logf.FromContext(ctx) + + // TODO(user): your logic here + + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *JobRequestReviewReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&platformv1.JobRequestReview{}). + Named("jobrequestreview"). + Complete(r) +} diff --git a/internal/controller/jobrequestreview_controller_test.go b/internal/controller/jobrequestreview_controller_test.go new file mode 100644 index 0000000..e2e5468 --- /dev/null +++ b/internal/controller/jobrequestreview_controller_test.go @@ -0,0 +1,88 @@ +/* +Copyright 2026. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + platformv1 "github.com/alphagov/govuk-job-request-operator/api/v1" +) + +var _ = Describe("JobRequestReview Controller", func() { + Context("When reconciling a resource", func() { + const resourceName = "test-resource" + + ctx := context.Background() + + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", // TODO(user):Modify as needed + } + jobrequestreview := &platformv1.JobRequestReview{} + + BeforeEach(func() { + By("creating the custom resource for the Kind JobRequestReview") + err := k8sClient.Get(ctx, typeNamespacedName, jobrequestreview) + if err != nil && errors.IsNotFound(err) { + resource := &platformv1.JobRequestReview{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + Spec: platformv1.JobRequestReviewSpec{ + Decision: "Approved", + Description: "A description", + }, + // TODO(user): Specify other spec details if needed. + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } + }) + + AfterEach(func() { + // TODO(user): Cleanup logic after each test, like removing the resource instance. + resource := &platformv1.JobRequestReview{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance JobRequestReview") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + }) + It("should successfully reconcile the resource", func() { + By("Reconciling the created resource") + controllerReconciler := &JobRequestReviewReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. + // Example: If you expect a certain status condition after reconciliation, verify it here. + }) + }) +})