Files
unkin-agent f1bcb8cd3a
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Add the initial repospawner service
repospawner turns JSON new-repo requests into terraform-git pull requests
via kubernetes Jobs, follows those PRs to merge and optionally activates
the repository in Woodpecker.
2026-08-30 14:33:31 +10:00

81 lines
2.5 KiB
Go

package server
import (
"context"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"git.unkin.net/unkin/repospawner/internal/jobs"
)
// Cluster is the slice of the Kubernetes API the server needs. Keeping it an
// interface lets the reconciler and handlers be tested without a cluster.
type Cluster interface {
// CreateJob creates a Job, reporting AlreadyExists as nil so a repeated
// reconcile is harmless.
CreateJob(ctx context.Context, job *batchv1.Job) error
// ListJobs returns every repospawner-owned Job in the namespace.
ListJobs(ctx context.Context) ([]batchv1.Job, error)
// ListPods returns every repospawner-owned Job pod in the namespace.
ListPods(ctx context.Context) ([]corev1.Pod, error)
// Ping reports whether the API server is reachable.
Ping(ctx context.Context) error
}
// KubeCluster is the in-cluster Cluster implementation.
type KubeCluster struct {
client kubernetes.Interface
namespace string
}
// NewKubeCluster builds a Cluster from the pod's in-cluster credentials.
func NewKubeCluster(namespace string) (*KubeCluster, error) {
cfg, err := rest.InClusterConfig()
if err != nil {
return nil, err
}
client, err := kubernetes.NewForConfig(cfg)
if err != nil {
return nil, err
}
return &KubeCluster{client: client, namespace: namespace}, nil
}
// ownedSelector matches everything repospawner creates.
const ownedSelector = jobs.LabelApp + "=" + jobs.AppName + "," + jobs.LabelRequest
func (k *KubeCluster) CreateJob(ctx context.Context, job *batchv1.Job) error {
_, err := k.client.BatchV1().Jobs(k.namespace).Create(ctx, job, metav1.CreateOptions{})
if apierrors.IsAlreadyExists(err) {
return nil
}
return err
}
func (k *KubeCluster) ListJobs(ctx context.Context) ([]batchv1.Job, error) {
list, err := k.client.BatchV1().Jobs(k.namespace).List(ctx, metav1.ListOptions{LabelSelector: ownedSelector})
if err != nil {
return nil, err
}
return list.Items, nil
}
func (k *KubeCluster) ListPods(ctx context.Context) ([]corev1.Pod, error) {
list, err := k.client.CoreV1().Pods(k.namespace).List(ctx, metav1.ListOptions{LabelSelector: ownedSelector})
if err != nil {
return nil, err
}
return list.Items, nil
}
func (k *KubeCluster) Ping(ctx context.Context) error {
limit := int64(1)
_, err := k.client.BatchV1().Jobs(k.namespace).List(ctx, metav1.ListOptions{Limit: limit})
return err
}