feat: add FunctionJob CRD for one-shot function runs

- api/v1alpha1/job_types.go: new CRD FunctionJob (Pending/Running/Succeeded/Failed)
- controllers/functionjob_controller.go: reconciler creates k8s Job from FunctionRef + EventJSON
- zz_generated.deepcopy.go: DeepCopy methods for FunctionJob types
- config/crd/bases: generated CRD YAML, applied to cluster
- main.go: register FunctionJobReconciler
- rbac.yaml: add functionjobs permissions
- operator.yaml: v0.1.2 -> v0.1.3
- operator:v0.1.3 deployed and running in cluster
This commit is contained in:
“Naeel”
2026-03-07 17:20:21 +04:00
parent d6dc676dfd
commit 22685a72a9
7 changed files with 529 additions and 6 deletions
+89
View File
@@ -0,0 +1,89 @@
// Изменено: 2026-03-07
// Описание CRD FunctionJob — одноразовый запуск функции.
// Отдельный ресурс (не Trigger) потому что семантика принципиально другая:
// - Trigger: постоянно живёт, описывает КАК функцию вызывают (http/cron)
// - FunctionJob: запускается один раз, завершается, хранит результат
//
// Terraform ресурс: sless_job
// k8s ресурс: k8s Job в namespace sless-fn-{namespace}
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// FunctionJobSpec — параметры одноразового запуска функции.
type FunctionJobSpec struct {
// FunctionRef — имя Function ресурса в том же namespace
// +kubebuilder:validation:Required
FunctionRef string `json:"functionRef"`
// EventJSON — данные передаваемые в handle(event) в JSON формате.
// Если не задан — передаётся пустой объект {}.
// Пример: {"action": "migrate", "version": "002"}
// +kubebuilder:default="{}"
EventJSON string `json:"eventJson,omitempty"`
}
// FunctionJobPhase — фаза жизненного цикла одноразового запуска.
type FunctionJobPhase string
const (
// FunctionJobPhasePending — ожидает пока Function станет Ready
FunctionJobPhasePending FunctionJobPhase = "Pending"
// FunctionJobPhaseRunning — k8s Job запущен, функция выполняется
FunctionJobPhaseRunning FunctionJobPhase = "Running"
// FunctionJobPhaseSucceeded — функция успешно завершилась
FunctionJobPhaseSucceeded FunctionJobPhase = "Succeeded"
// FunctionJobPhaseFailed — функция завершилась с ошибкой
FunctionJobPhaseFailed FunctionJobPhase = "Failed"
)
// FunctionJobStatus — наблюдаемое состояние (заполняет контроллер).
type FunctionJobStatus struct {
// Phase — текущая фаза: Pending, Running, Succeeded, Failed
Phase FunctionJobPhase `json:"phase,omitempty"`
// JobName — имя созданного k8s Job
JobName string `json:"jobName,omitempty"`
// StartTime — время запуска k8s Job
StartTime *metav1.Time `json:"startTime,omitempty"`
// CompletionTime — время завершения
CompletionTime *metav1.Time `json:"completionTime,omitempty"`
// Message — результат выполнения или сообщение об ошибке
Message string `json:"message,omitempty"`
}
//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
//+kubebuilder:printcolumn:name="Function",type=string,JSONPath=`.spec.functionRef`
//+kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase`
//+kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
// FunctionJob — ресурс для одноразового запуска serverless функции.
// Создаётся пользователем через terraform ресурс sless_job.
// После завершения (Succeeded/Failed) ресурс остаётся в кластере для аудита.
type FunctionJob struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec FunctionJobSpec `json:"spec,omitempty"`
Status FunctionJobStatus `json:"status,omitempty"`
}
//+kubebuilder:object:root=true
// FunctionJobList contains a list of FunctionJob
type FunctionJobList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []FunctionJob `json:"items"`
}
func init() {
SchemeBuilder.Register(&FunctionJob{}, &FunctionJobList{})
}
+98 -1
View File
@@ -21,7 +21,7 @@ limitations under the License.
package v1alpha1
import (
"k8s.io/apimachinery/pkg/apis/meta/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
)
@@ -224,3 +224,100 @@ func (in *TriggerStatus) DeepCopy() *TriggerStatus {
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *FunctionJob) DeepCopyInto(out *FunctionJob) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
out.Spec = in.Spec
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FunctionJob.
func (in *FunctionJob) DeepCopy() *FunctionJob {
if in == nil {
return nil
}
out := new(FunctionJob)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *FunctionJob) 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 *FunctionJobList) DeepCopyInto(out *FunctionJobList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]FunctionJob, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FunctionJobList.
func (in *FunctionJobList) DeepCopy() *FunctionJobList {
if in == nil {
return nil
}
out := new(FunctionJobList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *FunctionJobList) 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 *FunctionJobSpec) DeepCopyInto(out *FunctionJobSpec) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FunctionJobSpec.
func (in *FunctionJobSpec) DeepCopy() *FunctionJobSpec {
if in == nil {
return nil
}
out := new(FunctionJobSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *FunctionJobStatus) DeepCopyInto(out *FunctionJobStatus) {
*out = *in
if in.StartTime != nil {
in, out := &in.StartTime, &out.StartTime
*out = (*in).DeepCopy()
}
if in.CompletionTime != nil {
in, out := &in.CompletionTime, &out.CompletionTime
*out = (*in).DeepCopy()
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FunctionJobStatus.
func (in *FunctionJobStatus) DeepCopy() *FunctionJobStatus {
if in == nil {
return nil
}
out := new(FunctionJobStatus)
in.DeepCopyInto(out)
return out
}