Files
Felis/internal/build/k8sjobs.go
T
flyemoji 708cdfc5b8 feat(core): add naming, RCON, store, config, and image-build libraries
Foundational libraries: deterministic resource naming, the RCON client, the Postgres store with embedded SQL migrations, configuration loading, and container image-build helpers.
2026-06-26 23:31:58 +09:00

88 lines
2.9 KiB
Go

package build
import (
"context"
batchv1 "k8s.io/api/batch/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
)
// K8sJobs is the production Jobs backed by a controller-runtime client (spec
// §16). It creates the Kaniko+Trivy build Job, reads its phase for the scan-gate
// translation, and deletes it on cancel — nothing more. The cluster-bootstrap
// objects (the felis-build namespace, the weak SA, and the egress
// NetworkPolicy) are installed once by the deployment manifests (spec §21), not
// per build, so this binding never needs to create them. It is integration-
// tested against a live cluster, not the hermetic build_test.go suite.
type K8sJobs struct {
c client.Client
cfg Config
}
// NewK8sJobs builds a Jobs over c using cfg for the namespace and image refs.
func NewK8sJobs(c client.Client, cfg Config) *K8sJobs {
return &K8sJobs{c: c, cfg: cfg.withDefaults()}
}
// CreateBuildJob renders and applies the build Job, returning its name. The Job
// is the security-critical object; its shape is fixed by BuildJob (jobspec.go)
// and asserted by jobspec_test.go.
func (k *K8sJobs) CreateBuildJob(ctx context.Context, p JobParams) (string, error) {
job, err := BuildJob(p)
if err != nil {
return "", err
}
if err := k.c.Create(ctx, job); err != nil {
return "", err
}
return job.Name, nil
}
// JobPhase reads the Job and maps its status to a JobPhase. A missing Job
// (GC'd, never created) is JobUnknown, which Sync treats as failed. The mapping
// is deliberately conservative: a Job is Succeeded only when the Complete
// condition is true, so a half-finished Job is never admitted.
func (k *K8sJobs) JobPhase(ctx context.Context, jobName string) (JobPhase, error) {
var job batchv1.Job
if err := k.c.Get(ctx, types.NamespacedName{Namespace: k.cfg.Namespace, Name: jobName}, &job); err != nil {
if apierrors.IsNotFound(err) {
return JobUnknown, nil
}
return JobUnknown, err
}
for _, cond := range job.Status.Conditions {
if cond.Status != "True" {
continue
}
switch cond.Type {
case batchv1.JobComplete:
return JobSucceeded, nil
case batchv1.JobFailed:
// Covers a CRITICAL CVE (trivy --exit-code 1), a kaniko failure, and
// DeadlineExceeded — all are a rejected build.
return JobFailed, nil
}
}
if job.Status.Active > 0 {
return JobRunning, nil
}
return JobPending, nil
}
// CancelBuildJob deletes the Job and, via background propagation, its pods. A
// missing Job is not an error: cancellation is idempotent.
func (k *K8sJobs) CancelBuildJob(ctx context.Context, jobName string) error {
bg := metav1.DeletePropagationBackground
obj := &batchv1.Job{
ObjectMeta: metav1.ObjectMeta{Namespace: k.cfg.Namespace, Name: jobName},
}
if err := k.c.Delete(ctx, obj, &client.DeleteOptions{PropagationPolicy: &bg}); err != nil &&
!apierrors.IsNotFound(err) {
return err
}
return nil
}