mirror of
https://github.com/run-llama/workflows-py.git
synced 2026-08-24 20:01:34 -04:00
231 lines
8.4 KiB
Go
231 lines
8.4 KiB
Go
package controller
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
. "github.com/onsi/gomega"
|
|
appsv1 "k8s.io/api/apps/v1"
|
|
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/apimachinery/pkg/types"
|
|
"k8s.io/client-go/tools/record"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
|
|
|
llamadeployv1 "llama-agents-operator/api/v1"
|
|
)
|
|
|
|
// GetDeploymentEventually fetches a Deployment with Eventually retry.
|
|
func GetDeploymentEventually(ctx context.Context, name, ns string) *appsv1.Deployment {
|
|
depl := &appsv1.Deployment{}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(ctx, types.NamespacedName{Name: name, Namespace: ns}, depl)
|
|
}).Should(Succeed())
|
|
return depl
|
|
}
|
|
|
|
// GetConfigMapEventually fetches a ConfigMap with Eventually retry.
|
|
func GetConfigMapEventually(ctx context.Context, name, ns string) *corev1.ConfigMap {
|
|
cm := &corev1.ConfigMap{}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(ctx, types.NamespacedName{Name: name, Namespace: ns}, cm)
|
|
}).Should(Succeed())
|
|
return cm
|
|
}
|
|
|
|
// FindContainer returns a container by name from a PodSpec.
|
|
func FindContainer(podSpec corev1.PodSpec, name string) (corev1.Container, bool) {
|
|
for _, c := range podSpec.Containers {
|
|
if c.Name == name {
|
|
return c, true
|
|
}
|
|
}
|
|
return corev1.Container{}, false
|
|
}
|
|
|
|
// FindInitContainer returns an init container by name from a PodSpec.
|
|
func FindInitContainer(podSpec corev1.PodSpec, name string) (corev1.Container, bool) {
|
|
for _, c := range podSpec.InitContainers {
|
|
if c.Name == name {
|
|
return c, true
|
|
}
|
|
}
|
|
return corev1.Container{}, false
|
|
}
|
|
|
|
// EnvMap builds a map of non-empty env var values from a container.
|
|
func EnvMap(c corev1.Container) map[string]string {
|
|
out := make(map[string]string, len(c.Env))
|
|
for _, e := range c.Env {
|
|
if e.Value != "" {
|
|
out[e.Name] = e.Value
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// ExpectEnv asserts an exact env var value on a container.
|
|
func ExpectEnv(c corev1.Container, key, value string) {
|
|
envs := EnvMap(c)
|
|
Expect(envs[key]).To(Equal(value), fmt.Sprintf("env %s mismatch", key))
|
|
}
|
|
|
|
// ExpectEnvMatches asserts an env var matches a regex pattern.
|
|
func ExpectEnvMatches(c corev1.Container, key, pattern string) {
|
|
envs := EnvMap(c)
|
|
Expect(envs[key]).To(MatchRegexp(pattern), fmt.Sprintf("env %s mismatch", key))
|
|
}
|
|
|
|
// ExpectServicePort asserts a service exposes port->target mapping.
|
|
func ExpectServicePort(ctx context.Context, name, ns string, port int32, target int) {
|
|
svc := &corev1.Service{}
|
|
Eventually(func() error {
|
|
return k8sClient.Get(ctx, types.NamespacedName{Name: name, Namespace: ns}, svc)
|
|
}).Should(Succeed())
|
|
Expect(svc.Spec.Ports).To(HaveLen(1))
|
|
Expect(svc.Spec.Ports[0].Port).To(Equal(port))
|
|
Expect(svc.Spec.Ports[0].TargetPort.IntValue()).To(Equal(target))
|
|
}
|
|
|
|
// CleanupLlama deletes a LlamaDeployment and associated ServiceAccount if present.
|
|
func CleanupLlama(ctx context.Context, name, ns string) {
|
|
// Delete LlamaDeployment with finalizer handling
|
|
llamaDeploy := &llamadeployv1.LlamaDeployment{}
|
|
if err := k8sClient.Get(ctx, types.NamespacedName{Name: name, Namespace: ns}, llamaDeploy); err == nil {
|
|
if len(llamaDeploy.Finalizers) > 0 {
|
|
llamaDeploy.Finalizers = []string{}
|
|
_ = k8sClient.Update(ctx, llamaDeploy)
|
|
}
|
|
_ = k8sClient.Delete(ctx, llamaDeploy)
|
|
// Wait until deleted
|
|
Eventually(func() bool {
|
|
err := k8sClient.Get(ctx, types.NamespacedName{Name: name, Namespace: ns}, llamaDeploy)
|
|
return apierrors.IsNotFound(err)
|
|
}, "5s", "100ms").Should(BeTrue())
|
|
}
|
|
|
|
// Delete ServiceAccount if exists
|
|
sa := &corev1.ServiceAccount{}
|
|
if err := k8sClient.Get(ctx, types.NamespacedName{Name: name + "-sa", Namespace: ns}, sa); err == nil {
|
|
_ = k8sClient.Delete(ctx, sa)
|
|
}
|
|
}
|
|
|
|
// Functional options for building LlamaDeployment spec
|
|
type LlamaSpecOption func(*llamadeployv1.LlamaDeploymentSpec)
|
|
|
|
func WithImage(image string) LlamaSpecOption {
|
|
return func(s *llamadeployv1.LlamaDeploymentSpec) { s.Image = image }
|
|
}
|
|
func WithImageTag(tag string) LlamaSpecOption {
|
|
return func(s *llamadeployv1.LlamaDeploymentSpec) { s.ImageTag = tag }
|
|
}
|
|
func WithSecret(name string) LlamaSpecOption {
|
|
return func(s *llamadeployv1.LlamaDeploymentSpec) { s.SecretName = name }
|
|
}
|
|
func WithDeploymentFilePath(path string) LlamaSpecOption {
|
|
return func(s *llamadeployv1.LlamaDeploymentSpec) { s.DeploymentFilePath = path }
|
|
}
|
|
func WithGitRef(ref string) LlamaSpecOption {
|
|
return func(s *llamadeployv1.LlamaDeploymentSpec) { s.GitRef = ref }
|
|
}
|
|
func WithGitSha(sha string) LlamaSpecOption {
|
|
return func(s *llamadeployv1.LlamaDeploymentSpec) { s.GitSha = sha }
|
|
}
|
|
func WithRepoUrl(url string) LlamaSpecOption {
|
|
return func(s *llamadeployv1.LlamaDeploymentSpec) { s.RepoUrl = url }
|
|
}
|
|
func WithAssetsPath(path string) LlamaSpecOption {
|
|
return func(s *llamadeployv1.LlamaDeploymentSpec) { s.StaticAssetsPath = path }
|
|
}
|
|
func WithBuildGeneration(gen int64) LlamaSpecOption {
|
|
return func(s *llamadeployv1.LlamaDeploymentSpec) { s.BuildGeneration = gen }
|
|
}
|
|
|
|
// NewLlama creates a LlamaDeployment with defaults and applies options.
|
|
func NewLlama(name, ns, projectId, repoURL string, opts ...LlamaSpecOption) *llamadeployv1.LlamaDeployment {
|
|
spec := llamadeployv1.LlamaDeploymentSpec{
|
|
ProjectId: projectId,
|
|
RepoUrl: repoURL,
|
|
}
|
|
for _, opt := range opts {
|
|
opt(&spec)
|
|
}
|
|
return &llamadeployv1.LlamaDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: ns},
|
|
Spec: spec,
|
|
}
|
|
}
|
|
|
|
// SetDeploymentAvailableReplicas sets AvailableReplicas on a Deployment's status.
|
|
// Use this in tests that need checkRolloutTimeout to pick PhaseRolloutFailed (>0)
|
|
// vs PhaseFailed (0), since envtest defaults to 0 available replicas.
|
|
func SetDeploymentAvailableReplicas(ctx context.Context, name, ns string, replicas int32) {
|
|
deployment := &appsv1.Deployment{}
|
|
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: name, Namespace: ns}, deployment)).To(Succeed())
|
|
deployment.Status.AvailableReplicas = replicas
|
|
if replicas > 0 {
|
|
deployment.Status.ReadyReplicas = replicas
|
|
deployment.Status.Replicas = replicas
|
|
}
|
|
Expect(k8sClient.Status().Update(ctx, deployment)).To(Succeed())
|
|
}
|
|
|
|
// ReconcilerOption configures a LlamaDeploymentReconciler created by NewTestReconciler.
|
|
type ReconcilerOption func(*LlamaDeploymentReconciler)
|
|
|
|
func WithMaxConcurrentRollouts(n int) ReconcilerOption {
|
|
return func(r *LlamaDeploymentReconciler) { r.MaxConcurrentRollouts = n }
|
|
}
|
|
|
|
func WithMaxDeployments(n int) ReconcilerOption {
|
|
return func(r *LlamaDeploymentReconciler) { r.MaxDeployments = n }
|
|
}
|
|
|
|
// NewTestReconciler creates a reconciler wired to the test envtest client.
|
|
func NewTestReconciler(opts ...ReconcilerOption) *LlamaDeploymentReconciler {
|
|
r := &LlamaDeploymentReconciler{
|
|
Client: k8sClient,
|
|
Scheme: k8sClient.Scheme(),
|
|
Recorder: record.NewFakeRecorder(100),
|
|
DirectClient: k8sClient,
|
|
}
|
|
for _, o := range opts {
|
|
o(r)
|
|
}
|
|
return r
|
|
}
|
|
|
|
// CreateAndReconcile creates the CR and runs one reconciliation.
|
|
func CreateAndReconcile(ctx context.Context, r *LlamaDeploymentReconciler, obj *llamadeployv1.LlamaDeployment) {
|
|
Expect(k8sClient.Create(ctx, obj)).To(Succeed())
|
|
_, err := r.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: obj.Name, Namespace: obj.Namespace}})
|
|
Expect(err).NotTo(HaveOccurred())
|
|
}
|
|
|
|
// CompleteBuild marks the build Job for a LlamaDeployment as succeeded and reconciles
|
|
// again so that reconcileBuild proceeds past the build phase and creates the Deployment.
|
|
// Call this after the first Reconcile when spec.GitSha is set.
|
|
func CompleteBuild(ctx context.Context, r *LlamaDeploymentReconciler, llamaDeploy *llamadeployv1.LlamaDeployment) {
|
|
// Re-read to get latest status (buildId set by first reconcile)
|
|
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: llamaDeploy.Name, Namespace: llamaDeploy.Namespace}, llamaDeploy)).To(Succeed())
|
|
buildId := computeBuildId(llamaDeploy)
|
|
jobName := fmt.Sprintf("%s-build-%s", llamaDeploy.Name, buildId)
|
|
if len(jobName) > 63 {
|
|
jobName = jobName[:63]
|
|
}
|
|
|
|
// Mark the build Job as succeeded
|
|
job := &batchv1.Job{}
|
|
Expect(k8sClient.Get(ctx, client.ObjectKey{Name: jobName, Namespace: llamaDeploy.Namespace}, job)).To(Succeed())
|
|
job.Status.Succeeded = 1
|
|
Expect(k8sClient.Status().Update(ctx, job)).To(Succeed())
|
|
|
|
// Reconcile again to proceed past build
|
|
_, err := r.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: llamaDeploy.Name, Namespace: llamaDeploy.Namespace}})
|
|
Expect(err).NotTo(HaveOccurred())
|
|
}
|