mirror of
https://github.com/hanzo-fi/stack.git
synced 2026-10-30 20:34:43 +00:00
366 lines
9.9 KiB
Go
366 lines
9.9 KiB
Go
package benthos
|
|
|
|
import (
|
|
"embed"
|
|
"fmt"
|
|
"sort"
|
|
|
|
. "github.com/formancehq/go-libs/collectionutils"
|
|
"github.com/formancehq/operator/api/formance.com/v1beta1"
|
|
. "github.com/formancehq/operator/internal/core"
|
|
"github.com/formancehq/operator/internal/resources/applications"
|
|
"github.com/formancehq/operator/internal/resources/registries"
|
|
"github.com/formancehq/operator/internal/resources/resourcereferences"
|
|
benthosOperator "github.com/formancehq/operator/internal/resources/searches/benthos"
|
|
"github.com/formancehq/operator/internal/resources/services"
|
|
"github.com/formancehq/operator/internal/resources/settings"
|
|
"github.com/formancehq/search/benthos"
|
|
"github.com/pkg/errors"
|
|
appsv1 "k8s.io/api/apps/v1"
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/apimachinery/pkg/util/intstr"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
)
|
|
|
|
//+kubebuilder:rbac:groups=formance.com,resources=benthos,verbs=get;list;watch;create;update;patch;delete
|
|
//+kubebuilder:rbac:groups=formance.com,resources=benthos/status,verbs=get;update;patch
|
|
//+kubebuilder:rbac:groups=formance.com,resources=benthos/finalizers,verbs=update
|
|
|
|
func Reconcile(ctx Context, stack *v1beta1.Stack, b *v1beta1.Benthos) error {
|
|
|
|
if err := createDeployment(ctx, stack, b); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := createService(ctx, b); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func createService(ctx Context, b *v1beta1.Benthos) error {
|
|
_, err := services.Create(ctx, b, "benthos", func(t *corev1.Service) error {
|
|
t.Labels = map[string]string{
|
|
"app.kubernetes.io/service-name": "benthos",
|
|
}
|
|
t.Spec = corev1.ServiceSpec{
|
|
Ports: []corev1.ServicePort{{
|
|
Name: "http",
|
|
Port: 4195,
|
|
Protocol: "TCP",
|
|
TargetPort: intstr.FromString("http"),
|
|
}},
|
|
Selector: map[string]string{
|
|
"app.kubernetes.io/name": "benthos",
|
|
},
|
|
}
|
|
|
|
return nil
|
|
})
|
|
return err
|
|
}
|
|
|
|
// TODO(gfyrag): there is a ton of search related configuration
|
|
// We need to this controller and keep it focused on benthos
|
|
func createDeployment(ctx Context, stack *v1beta1.Stack, b *v1beta1.Benthos) error {
|
|
brokerURI, err := settings.RequireURL(ctx, stack.Name, "broker", "dsn")
|
|
if err != nil {
|
|
return errors.Wrap(err, "searching broker configuration")
|
|
}
|
|
|
|
elasticSearchURI, err := settings.RequireURL(ctx, stack.Name, "elasticsearch", "dsn")
|
|
if err != nil {
|
|
return errors.Wrap(err, "searching elasticsearch configuration")
|
|
}
|
|
|
|
serviceAccountName, err := settings.GetAWSServiceAccount(ctx, stack.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
awsIAMEnabled := serviceAccountName != ""
|
|
var resourceReference *v1beta1.ResourceReference
|
|
if secret := elasticSearchURI.Query().Get("secret"); !awsIAMEnabled && secret != "" {
|
|
resourceReference, err = resourcereferences.Create(ctx, b, "elasticsearch", secret, &corev1.Secret{})
|
|
} else {
|
|
err = resourcereferences.Delete(ctx, b, "elasticsearch")
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
broker := &v1beta1.Broker{}
|
|
if err := ctx.GetClient().Get(ctx, types.NamespacedName{
|
|
Name: stack.Name,
|
|
}, broker); err != nil {
|
|
return err
|
|
}
|
|
if !broker.Status.Ready {
|
|
return NewPendingError().WithMessage("broker not ready")
|
|
}
|
|
|
|
var topicPrefix string
|
|
switch broker.Status.Mode {
|
|
case v1beta1.ModeOneStreamByService:
|
|
topicPrefix = b.Spec.Stack + "-"
|
|
case v1beta1.ModeOneStreamByStack:
|
|
topicPrefix = b.Spec.Stack + "."
|
|
}
|
|
|
|
env := []corev1.EnvVar{
|
|
Env("OPENSEARCH_URL", elasticSearchURI.WithoutQuery().String()),
|
|
Env("TOPIC_PREFIX", topicPrefix),
|
|
Env("OPENSEARCH_INDEX", "stacks"),
|
|
Env("STACK", b.Spec.Stack),
|
|
}
|
|
if awsIAMEnabled {
|
|
env = append(env, Env("AWS_IAM_ENABLED", "true"))
|
|
}
|
|
|
|
if b.Spec.Batching != nil {
|
|
if b.Spec.Batching.Count != 0 {
|
|
env = append(env, Env("OPENSEARCH_BATCHING_COUNT", fmt.Sprint(b.Spec.Batching.Count)))
|
|
}
|
|
if b.Spec.Batching.Period != "" {
|
|
env = append(env, Env("OPENSEARCH_BATCHING_PERIOD", b.Spec.Batching.Period))
|
|
}
|
|
}
|
|
|
|
otelEnvVars, err := settings.GetOTELEnvVars(ctx, stack.Name, "benthos")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
env = append(env, otelEnvVars...)
|
|
|
|
if brokerURI.Scheme == "kafka" {
|
|
env = append(env, Env("KAFKA_ADDRESS", brokerURI.Host))
|
|
if settings.IsTrue(brokerURI.Query().Get("tls")) {
|
|
env = append(env, Env("KAFKA_TLS_ENABLED", "true"))
|
|
}
|
|
if settings.IsTrue(brokerURI.Query().Get("saslEnabled")) {
|
|
env = append(env,
|
|
Env("KAFKA_SASL_USERNAME", brokerURI.Query().Get("saslUsername")),
|
|
Env("KAFKA_SASL_PASSWORD", brokerURI.Query().Get("saslPassword")),
|
|
Env("KAFKA_SASL_MECHANISM", brokerURI.Query().Get("saslMechanism")),
|
|
)
|
|
}
|
|
}
|
|
if brokerURI.Scheme == "nats" {
|
|
env = append(env, Env("NATS_URL", brokerURI.Host))
|
|
if broker.Status.Mode == v1beta1.ModeOneStreamByStack {
|
|
env = append(env, Env("NATS_BIND", "true"))
|
|
}
|
|
}
|
|
if secret := elasticSearchURI.Query().Get("secret"); elasticSearchURI.User != nil || secret != "" {
|
|
env = append(env, Env("BASIC_AUTH_ENABLED", "true"))
|
|
if secret == "" {
|
|
password, _ := brokerURI.User.Password()
|
|
env = append(env,
|
|
Env("BASIC_AUTH_USERNAME", brokerURI.User.Username()),
|
|
Env("BASIC_AUTH_PASSWORD", password),
|
|
)
|
|
} else {
|
|
env = append(env,
|
|
EnvFromSecret("BASIC_AUTH_USERNAME", secret, "username"),
|
|
EnvFromSecret("BASIC_AUTH_PASSWORD", secret, "password"),
|
|
)
|
|
}
|
|
} else {
|
|
// Even if basic auth is not enabled, we need to set the env vars
|
|
// to avoid benthos to crash due to linting errors
|
|
env = append(env,
|
|
Env("BASIC_AUTH_ENABLED", "false"),
|
|
Env("BASIC_AUTH_USERNAME", "username"),
|
|
Env("BASIC_AUTH_PASSWORD", "password"),
|
|
)
|
|
}
|
|
|
|
cmd := []string{
|
|
"/benthos",
|
|
"-r", "/resources/*.yaml",
|
|
"-t", "/templates/*.yaml",
|
|
}
|
|
|
|
openTelemetryEnabled, err := settings.HasOpenTelemetryTracesEnabled(ctx, stack.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if openTelemetryEnabled {
|
|
cmd = append(cmd, "-c", "/global/config.yaml")
|
|
}
|
|
|
|
cmd = append(cmd, "--log.level", "trace", "streams", "/streams/*.yaml")
|
|
|
|
volumes := make([]corev1.Volume, 0)
|
|
volumeMounts := make([]corev1.VolumeMount, 0)
|
|
|
|
type directory struct {
|
|
name string
|
|
fs embed.FS
|
|
}
|
|
|
|
directories := []directory{
|
|
{
|
|
name: "templates",
|
|
fs: benthos.Templates,
|
|
},
|
|
{
|
|
name: "resources",
|
|
fs: benthos.Resources,
|
|
},
|
|
}
|
|
if openTelemetryEnabled {
|
|
directories = append(directories, directory{
|
|
name: "global",
|
|
fs: benthosOperator.Global,
|
|
})
|
|
}
|
|
|
|
if stack.Spec.EnableAudit {
|
|
directories = append(directories, directory{
|
|
name: "audit",
|
|
fs: benthosOperator.Audit,
|
|
})
|
|
} else {
|
|
kinds, _, err := ctx.GetScheme().ObjectKinds(&corev1.ConfigMap{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
object := &unstructured.Unstructured{}
|
|
object.SetGroupVersionKind(kinds[0])
|
|
object.SetNamespace(stack.Name)
|
|
object.SetName("benthos-audit")
|
|
if err := client.IgnoreNotFound(ctx.GetClient().Delete(ctx, object)); err != nil {
|
|
return errors.Wrap(err, "deleting audit config map")
|
|
}
|
|
}
|
|
|
|
configMaps := make([]*corev1.ConfigMap, 0)
|
|
|
|
for _, x := range directories {
|
|
data := make(map[string]string)
|
|
|
|
CopyDir(x.fs, x.name, x.name, &data)
|
|
|
|
configMap, _, err := CreateOrUpdate[*corev1.ConfigMap](ctx, types.NamespacedName{
|
|
Namespace: b.Spec.Stack,
|
|
Name: "benthos-" + x.name,
|
|
},
|
|
func(t *corev1.ConfigMap) error {
|
|
t.Data = data
|
|
|
|
return nil
|
|
},
|
|
WithController[*corev1.ConfigMap](ctx.GetScheme(), b),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
configMaps = append(configMaps, configMap)
|
|
|
|
volumes = append(volumes, corev1.Volume{
|
|
Name: x.name,
|
|
VolumeSource: corev1.VolumeSource{
|
|
ConfigMap: &corev1.ConfigMapVolumeSource{
|
|
LocalObjectReference: corev1.LocalObjectReference{
|
|
Name: "benthos-" + x.name,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
volumeMounts = append(volumeMounts, corev1.VolumeMount{
|
|
Name: x.name,
|
|
ReadOnly: true,
|
|
MountPath: "/" + x.name,
|
|
})
|
|
}
|
|
|
|
if stack.Spec.EnableAudit {
|
|
cmd = append(cmd, "/audit/gateway_audit.yaml")
|
|
}
|
|
|
|
streamList := &v1beta1.BenthosStreamList{}
|
|
if err := ctx.GetClient().List(ctx, streamList, client.MatchingFields{
|
|
"stack": b.Spec.Stack,
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
streams := streamList.Items
|
|
sort.Slice(streams, func(i, j int) bool {
|
|
return streams[i].Name < streams[j].Name
|
|
})
|
|
|
|
benthosImage, err := registries.GetBenthosImage(ctx, stack, "v4.23.1-es")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
podAnnotations := map[string]string{
|
|
"config-hash": HashFromConfigMaps(configMaps...),
|
|
}
|
|
if resourceReference != nil {
|
|
podAnnotations["elasticsearch-secret-hash"] = resourceReference.Status.Hash
|
|
}
|
|
|
|
return applications.
|
|
New(b, &appsv1.Deployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "benthos",
|
|
},
|
|
Spec: appsv1.DeploymentSpec{
|
|
Template: corev1.PodTemplateSpec{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Annotations: podAnnotations,
|
|
},
|
|
Spec: corev1.PodSpec{
|
|
InitContainers: b.Spec.InitContainers,
|
|
Containers: []corev1.Container{{
|
|
Name: "benthos",
|
|
Image: benthosImage,
|
|
Env: env,
|
|
Command: cmd,
|
|
Ports: []corev1.ContainerPort{{
|
|
Name: "http",
|
|
ContainerPort: 4195,
|
|
}},
|
|
VolumeMounts: append(volumeMounts, corev1.VolumeMount{
|
|
Name: "streams",
|
|
ReadOnly: true,
|
|
MountPath: "/streams",
|
|
}),
|
|
}},
|
|
Volumes: append(volumes, corev1.Volume{
|
|
Name: "streams",
|
|
VolumeSource: corev1.VolumeSource{
|
|
Projected: &corev1.ProjectedVolumeSource{
|
|
Sources: Map(streams, func(stream v1beta1.BenthosStream) corev1.VolumeProjection {
|
|
return corev1.VolumeProjection{
|
|
ConfigMap: &corev1.ConfigMapProjection{
|
|
LocalObjectReference: corev1.LocalObjectReference{
|
|
Name: fmt.Sprintf("stream-%s", stream.Name),
|
|
},
|
|
Items: []corev1.KeyToPath{{
|
|
Key: "stream.yaml",
|
|
Path: stream.Spec.Name + ".yaml",
|
|
}},
|
|
},
|
|
}
|
|
}),
|
|
},
|
|
},
|
|
}),
|
|
ServiceAccountName: serviceAccountName,
|
|
},
|
|
},
|
|
},
|
|
}).
|
|
Install(ctx)
|
|
}
|