diff --git a/PROJECT b/PROJECT index 22297db..d3db75a 100644 --- a/PROJECT +++ b/PROJECT @@ -1,6 +1,16 @@ -domain: ofen.cybozu.io +domain: cybozu.io layout: - go.kubebuilder.io/v4 projectName: ofen repo: github.com/cybozu-go/ofen +resources: +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: cybozu.io + group: ofen + kind: ImagePrefetch + path: github.com/cybozu-go/ofen/api/v1 + version: v1 version: "3" diff --git a/api/v1/groupversion_info.go b/api/v1/groupversion_info.go new file mode 100644 index 0000000..ce9670c --- /dev/null +++ b/api/v1/groupversion_info.go @@ -0,0 +1,20 @@ +// Package v1 contains API Schema definitions for the ofen v1 API group +// +kubebuilder:object:generate=true +// +groupName=ofen.cybozu.io +package v1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "ofen.cybozu.io", Version: "v1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/api/v1/imageprefetch_types.go b/api/v1/imageprefetch_types.go new file mode 100644 index 0000000..04fdc7a --- /dev/null +++ b/api/v1/imageprefetch_types.go @@ -0,0 +1,88 @@ +package v1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// ImagePrefetchSpec defines the desired state of ImagePrefetch +type ImagePrefetchSpec struct { + // Images is a list of container images that will be pre-downloaded to the target nodes + Images []string `json:"images"` + + // NodeSelector is a map of key-value pairs that specify which nodes should have the images pre-downloaded + // +optional + NodeSelector map[string]string `json:"nodeSelector,omitempty"` + + // Replicas is the number of nodes that should download the specified images + // +optional + Replicas int `json:"replicas,omitempty"` + + // ImagePullSecrets is a list of secret names that contain credentials for authenticating with container registries + // +optional + ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"` +} + +// ImagePrefetchStatus defines the observed state of ImagePrefetch +type ImagePrefetchStatus struct { + // The generation observed by the controller. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // Conditions represent the latest available observations of an object's state + // +listType=map + // +listMapKey=type + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` + + // DesiredNodes represents the number of nodes that should have the images pre-downloaded + // +optional + // +kubebuilder:default:=0 + DesiredNodes int `json:"desiredNodes,omitempty"` + + // ImagePulledNodes represents the number of nodes that have successfully pre-downloaded the images + // +optional + // +kubebuilder:default:=0 + ImagePulledNodes int `json:"imagePulledNodes,omitempty"` + + // ImagePullingNodes represents the number of nodes that are currently downloading the images + // +optional + // +kubebuilder:default:=0 + ImagePullingNodes int `json:"imagePullingNodes,omitempty"` + + // ImagePullFailedNodes represents the number of nodes that failed to download the images + // +optional + // +kubebuilder:default:=0 + ImagePullFailedNodes int `json:"imagePullFailedNodes,omitempty"` +} + +const ( + ConditionReady = "Ready" + ConditionProgressing = "Progressing" + ConditionImageDownloadFailed = "ImageDownloadFailed" +) + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// ImagePrefetch is the Schema for the imageprefetches API +type ImagePrefetch struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ImagePrefetchSpec `json:"spec,omitempty"` + Status ImagePrefetchStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// ImagePrefetchList contains a list of ImagePrefetch +type ImagePrefetchList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ImagePrefetch `json:"items"` +} + +func init() { + SchemeBuilder.Register(&ImagePrefetch{}, &ImagePrefetchList{}) +} diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go new file mode 100644 index 0000000..25df128 --- /dev/null +++ b/api/v1/zz_generated.deepcopy.go @@ -0,0 +1,124 @@ +//go:build !ignore_autogenerated + +// Code generated by controller-gen. DO NOT EDIT. + +package v1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ImagePrefetch) DeepCopyInto(out *ImagePrefetch) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImagePrefetch. +func (in *ImagePrefetch) DeepCopy() *ImagePrefetch { + if in == nil { + return nil + } + out := new(ImagePrefetch) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ImagePrefetch) 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 *ImagePrefetchList) DeepCopyInto(out *ImagePrefetchList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ImagePrefetch, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImagePrefetchList. +func (in *ImagePrefetchList) DeepCopy() *ImagePrefetchList { + if in == nil { + return nil + } + out := new(ImagePrefetchList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ImagePrefetchList) 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 *ImagePrefetchSpec) DeepCopyInto(out *ImagePrefetchSpec) { + *out = *in + if in.Images != nil { + in, out := &in.Images, &out.Images + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.NodeSelector != nil { + in, out := &in.NodeSelector, &out.NodeSelector + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.ImagePullSecrets != nil { + in, out := &in.ImagePullSecrets, &out.ImagePullSecrets + *out = make([]corev1.LocalObjectReference, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImagePrefetchSpec. +func (in *ImagePrefetchSpec) DeepCopy() *ImagePrefetchSpec { + if in == nil { + return nil + } + out := new(ImagePrefetchSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ImagePrefetchStatus) DeepCopyInto(out *ImagePrefetchStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImagePrefetchStatus. +func (in *ImagePrefetchStatus) DeepCopy() *ImagePrefetchStatus { + if in == nil { + return nil + } + out := new(ImagePrefetchStatus) + in.DeepCopyInto(out) + return out +} diff --git a/cmd/main.go b/cmd/main.go index 46e2782..085632a 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,19 +1,3 @@ -/* -Copyright 2025. - -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 main import ( @@ -32,6 +16,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/metrics/filters" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" "sigs.k8s.io/controller-runtime/pkg/webhook" + + ofenv1 "github.com/cybozu-go/ofen/api/v1" + "github.com/cybozu-go/ofen/internal/controller" // +kubebuilder:scaffold:imports ) @@ -43,6 +30,7 @@ var ( func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + utilruntime.Must(ofenv1.AddToScheme(scheme)) // +kubebuilder:scaffold:scheme } @@ -138,6 +126,13 @@ func main() { os.Exit(1) } + if err = (&controller.ImagePrefetchReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "ImagePrefetch") + os.Exit(1) + } // +kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/config/crd/bases/ofen.cybozu.io_imageprefetches.yaml b/config/crd/bases/ofen.cybozu.io_imageprefetches.yaml new file mode 100644 index 0000000..838c855 --- /dev/null +++ b/config/crd/bases/ofen.cybozu.io_imageprefetches.yaml @@ -0,0 +1,174 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.2 + name: imageprefetches.ofen.cybozu.io +spec: + group: ofen.cybozu.io + names: + kind: ImagePrefetch + listKind: ImagePrefetchList + plural: imageprefetches + singular: imageprefetch + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: ImagePrefetch is the Schema for the imageprefetches 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: ImagePrefetchSpec defines the desired state of ImagePrefetch + properties: + imagePullSecrets: + description: ImagePullSecrets is a list of secret names that contain + credentials for authenticating with container registries + items: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + type: array + images: + description: Images is a list of container images that will be pre-downloaded + to the target nodes + items: + type: string + type: array + nodeSelector: + additionalProperties: + type: string + description: NodeSelector is a map of key-value pairs that specify + which nodes should have the images pre-downloaded + type: object + replicas: + description: Replicas is the number of nodes that should download + the specified images + type: integer + required: + - images + type: object + status: + description: ImagePrefetchStatus defines the observed state of ImagePrefetch + properties: + conditions: + description: Conditions represent the latest available observations + of an object's state + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + desiredNodes: + default: 0 + description: DesiredNodes represents the number of nodes that should + have the images pre-downloaded + type: integer + imagePullFailedNodes: + default: 0 + description: ImagePullFailedNodes represents the number of nodes that + failed to download the images + type: integer + imagePulledNodes: + default: 0 + description: ImagePulledNodes represents the number of nodes that + have successfully pre-downloaded the images + type: integer + imagePullingNodes: + default: 0 + description: ImagePullingNodes represents the number of nodes that + are currently downloading the images + type: integer + observedGeneration: + description: The generation observed by the controller. + format: int64 + type: integer + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml new file mode 100644 index 0000000..a74597b --- /dev/null +++ b/config/crd/kustomization.yaml @@ -0,0 +1,22 @@ +# This kustomization.yaml is not intended to be run by itself, +# since it depends on service name and namespace that are out of this kustomize package. +# It should be run by config/default +resources: +- bases/ofen.cybozu.io_imageprefetches.yaml +# +kubebuilder:scaffold:crdkustomizeresource + +patches: +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. +# patches here are for enabling the conversion webhook for each CRD +# +kubebuilder:scaffold:crdkustomizewebhookpatch + +# [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. +# patches here are for enabling the CA injection for each CRD +#- path: patches/cainjection_in_imageprefetches.yaml +# +kubebuilder:scaffold:crdkustomizecainjectionpatch + +# [WEBHOOK] To enable webhook, uncomment the following section +# the following config is for teaching kustomize how to do kustomization for CRDs. + +#configurations: +#- kustomizeconfig.yaml diff --git a/config/crd/kustomizeconfig.yaml b/config/crd/kustomizeconfig.yaml new file mode 100644 index 0000000..ec5c150 --- /dev/null +++ b/config/crd/kustomizeconfig.yaml @@ -0,0 +1,19 @@ +# This file is for teaching kustomize how to substitute name and namespace reference in CRD +nameReference: +- kind: Service + version: v1 + fieldSpecs: + - kind: CustomResourceDefinition + version: v1 + group: apiextensions.k8s.io + path: spec/conversion/webhook/clientConfig/service/name + +namespace: +- kind: CustomResourceDefinition + version: v1 + group: apiextensions.k8s.io + path: spec/conversion/webhook/clientConfig/service/namespace + create: false + +varReference: +- path: metadata/annotations diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index 80a64a8..c5e0481 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -15,7 +15,7 @@ namePrefix: ofen- # someName: someValue resources: -#- ../crd +- ../crd - ../rbac - ../manager # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in diff --git a/config/rbac/imageprefetch_editor_role.yaml b/config/rbac/imageprefetch_editor_role.yaml new file mode 100644 index 0000000..ac5734d --- /dev/null +++ b/config/rbac/imageprefetch_editor_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to edit imageprefetches. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: ofen + app.kubernetes.io/managed-by: kustomize + name: imageprefetch-editor-role +rules: +- apiGroups: + - ofen.cybozu.io + resources: + - imageprefetches + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - ofen.cybozu.io + resources: + - imageprefetches/status + verbs: + - get diff --git a/config/rbac/imageprefetch_viewer_role.yaml b/config/rbac/imageprefetch_viewer_role.yaml new file mode 100644 index 0000000..f5d7646 --- /dev/null +++ b/config/rbac/imageprefetch_viewer_role.yaml @@ -0,0 +1,23 @@ +# permissions for end users to view imageprefetches. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: ofen + app.kubernetes.io/managed-by: kustomize + name: imageprefetch-viewer-role +rules: +- apiGroups: + - ofen.cybozu.io + resources: + - imageprefetches + verbs: + - get + - list + - watch +- apiGroups: + - ofen.cybozu.io + resources: + - imageprefetches/status + verbs: + - get diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index 5619aa0..1fd5672 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -18,3 +18,10 @@ resources: - metrics_auth_role.yaml - metrics_auth_role_binding.yaml - metrics_reader_role.yaml +# For each CRD, "Editor" and "Viewer" roles are scaffolded by +# default, aiding admins in cluster management. Those roles are +# not used by the Project itself. You can comment the following lines +# if you do not want those helpers be installed with your Project. +- imageprefetch_editor_role.yaml +- imageprefetch_viewer_role.yaml + diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index df3d873..6232c6f 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -1,11 +1,32 @@ +--- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - labels: - app.kubernetes.io/name: ofen - app.kubernetes.io/managed-by: kustomize name: manager-role rules: -- apiGroups: [""] - resources: ["pods"] - verbs: ["get", "list", "watch"] +- apiGroups: + - ofen.cybozu.io + resources: + - imageprefetches + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - ofen.cybozu.io + resources: + - imageprefetches/finalizers + verbs: + - update +- apiGroups: + - ofen.cybozu.io + resources: + - imageprefetches/status + verbs: + - get + - patch + - update diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml new file mode 100644 index 0000000..e31c42a --- /dev/null +++ b/config/samples/kustomization.yaml @@ -0,0 +1,4 @@ +## Append samples of your project ## +resources: +- ofen_v1_imageprefetch.yaml +# +kubebuilder:scaffold:manifestskustomizesamples diff --git a/config/samples/ofen_v1_imageprefetch.yaml b/config/samples/ofen_v1_imageprefetch.yaml new file mode 100644 index 0000000..fde8583 --- /dev/null +++ b/config/samples/ofen_v1_imageprefetch.yaml @@ -0,0 +1,12 @@ +apiVersion: ofen.cybozu.io/v1 +kind: ImagePrefetch +metadata: + labels: + app.kubernetes.io/name: ofen + app.kubernetes.io/managed-by: kustomize + name: imageprefetch-sample +spec: + images: + - ghcr.io/cybozu/ubuntu:24.04 + replicas: 1 + diff --git a/docs/design.md b/docs/design.md index 64ad0ae..ddf9ad4 100644 --- a/docs/design.md +++ b/docs/design.md @@ -149,12 +149,12 @@ spegel-pod2<-->|p2p|spegel-pod3 ### API ImagePrefetch Resource -| Field | Type | Required | Description | -| ---------------- | ----------------- | -------- | ---------------------------------------------------------- | -| images | []string | true | List of images to pre-download | -| nodeSelector | map[string]string | false | Specify the nodes to which the image should be downloaded | -| replicas | int | false | Set the number of image download nodes | -| imagePullSecrets | []string | false | Secret used for authentication with the container registry | +| Field | Type | Required | Description | +| ---------------- | ----------------------------- | -------- | ---------------------------------------------------------- | +| images | []string | true | List of images to pre-download | +| nodeSelector | map[string]string | false | Specify the nodes to which the image should be downloaded | +| replicas | int | false | Set the number of image download nodes | +| imagePullSecrets | []corev1.LocalObjectReference | false | Secret used for authentication with the container registry | ``` kind: ImagePrefetch diff --git a/hack/boilerplate.go.txt b/hack/boilerplate.go.txt index 221dcbe..e69de29 100644 --- a/hack/boilerplate.go.txt +++ b/hack/boilerplate.go.txt @@ -1,15 +0,0 @@ -/* -Copyright 2025. - -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. -*/ \ No newline at end of file diff --git a/internal/controller/imageprefetch_controller.go b/internal/controller/imageprefetch_controller.go new file mode 100644 index 0000000..149ba2e --- /dev/null +++ b/internal/controller/imageprefetch_controller.go @@ -0,0 +1,46 @@ +package controller + +import ( + "context" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + ofenv1 "github.com/cybozu-go/ofen/api/v1" +) + +// ImagePrefetchReconciler reconciles a ImagePrefetch object +type ImagePrefetchReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +// +kubebuilder:rbac:groups=ofen.cybozu.io,resources=imageprefetches,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=ofen.cybozu.io,resources=imageprefetches/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=ofen.cybozu.io,resources=imageprefetches/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 ImagePrefetch 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.19.0/pkg/reconcile +func (r *ImagePrefetchReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + _ = log.FromContext(ctx) + + // TODO(user): your logic here + + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *ImagePrefetchReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&ofenv1.ImagePrefetch{}). + Complete(r) +} diff --git a/internal/controller/imageprefetch_controller_test.go b/internal/controller/imageprefetch_controller_test.go new file mode 100644 index 0000000..bcf7d7a --- /dev/null +++ b/internal/controller/imageprefetch_controller_test.go @@ -0,0 +1,70 @@ +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" + + ofenv1 "github.com/cybozu-go/ofen/api/v1" +) + +var _ = Describe("ImagePrefetch 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 + } + imageprefetch := &ofenv1.ImagePrefetch{} + + BeforeEach(func() { + By("creating the custom resource for the Kind ImagePrefetch") + err := k8sClient.Get(ctx, typeNamespacedName, imageprefetch) + if err != nil && errors.IsNotFound(err) { + resource := &ofenv1.ImagePrefetch{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + Spec: ofenv1.ImagePrefetchSpec{ + Images: []string{"test"}, + }, + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } + }) + + AfterEach(func() { + // TODO(user): Cleanup logic after each test, like removing the resource instance. + resource := &ofenv1.ImagePrefetch{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance ImagePrefetch") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + }) + It("should successfully reconcile the resource", func() { + By("Reconciling the created resource") + controllerReconciler := &ImagePrefetchReconciler{ + 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. + }) + }) +}) diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go new file mode 100644 index 0000000..9302630 --- /dev/null +++ b/internal/controller/suite_test.go @@ -0,0 +1,80 @@ +package controller + +import ( + "context" + "fmt" + "path/filepath" + "runtime" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + ofenv1 "github.com/cybozu-go/ofen/api/v1" + // +kubebuilder:scaffold:imports +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var cfg *rest.Config +var k8sClient client.Client +var testEnv *envtest.Environment +var ctx context.Context +var cancel context.CancelFunc + +func TestControllers(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Controller Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + ctx, cancel = context.WithCancel(context.TODO()) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: true, + + // The BinaryAssetsDirectory is only required if you want to run the tests directly + // without call the makefile target test. If not informed it will look for the + // default path defined in controller-runtime which is /usr/local/kubebuilder/. + // Note that you must have the required binaries setup under the bin directory to perform + // the tests directly. When we run make test it will be setup and used automatically. + BinaryAssetsDirectory: filepath.Join("..", "..", "bin", "k8s", + fmt.Sprintf("1.31.0-%s-%s", runtime.GOOS, runtime.GOARCH)), + } + + var err error + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + err = ofenv1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + // +kubebuilder:scaffold:scheme + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + +}) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + cancel() + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +})