Files
oc-monitord/models/template.go
T
2026-05-27 16:09:45 +02:00

338 lines
11 KiB
Go

package models
import (
"encoding/json"
"fmt"
"os/exec"
"sort"
"strings"
"cloud.o-forge.io/core/oc-lib/models/resources"
"cloud.o-forge.io/core/oc-lib/models/resources/native_tools"
"cloud.o-forge.io/core/oc-lib/models/workflow"
"cloud.o-forge.io/core/oc-lib/models/workflow_execution"
"cloud.o-forge.io/core/oc-lib/tools"
)
type Parameter struct {
Name string `yaml:"name,omitempty"`
Value string `yaml:"value,omitempty"`
}
type Container struct {
Image string `yaml:"image"`
ImagePullPolicy string `yaml:"imagePullPolicy,omitempty"`
Command []string `yaml:"command,omitempty,flow"`
Args []string `yaml:"args,omitempty,flow"`
VolumeMounts []VolumeMount `yaml:"volumeMounts,omitempty"`
}
func (c *Container) AddVolumeMount(volumeMount VolumeMount, volumes []VolumeMount) []VolumeMount {
for _, vm := range c.VolumeMounts {
if vm.Name == volumeMount.Name {
return volumes
}
}
c.VolumeMounts = append(c.VolumeMounts, volumeMount)
for _, vm := range c.VolumeMounts {
for _, v := range volumes {
if vm.Name == v.Name {
return volumes
}
}
}
volumes = append(volumes, volumeMount)
return volumes
}
type VolumeMount struct {
Name string `yaml:"name"`
MountPath string `yaml:"mountPath"`
ReadOnly bool `yaml:"readOnly,omitempty"`
Storage *resources.StorageResource `yaml:"-"`
IsReparted bool `yaml:"-"`
}
type Task struct {
Name string `yaml:"name"`
Template string `yaml:"template"`
Dependencies []string `yaml:"dependencies,omitempty"`
Arguments struct {
Parameters []Parameter `yaml:"parameters,omitempty"`
} `yaml:"arguments,omitempty"`
}
type Dag struct {
Tasks []Task `yaml:"tasks,omitempty"`
}
type TemplateMetadata struct {
Labels map[string]string `yaml:"labels,omitempty"`
Annotations map[string]string `yaml:"annotations,omitempty"`
}
type Secret struct {
Name string `yaml:"name"`
Key string `yaml:"key"`
}
type Key struct {
Key string `yaml:"key"`
Bucket string `yaml:"bucket"`
EndPoint string `yaml:"endpoint"`
Insecure bool `yaml:"insecure"`
AccessKeySecret *Secret `yaml:"accessKeySecret"`
SecretKeySecret *Secret `yaml:"secretKeySecret"`
}
type Artifact struct {
Name string `yaml:"name"`
Path string `yaml:"path"`
}
type ArtifactRepositoryRef struct {
ConfigMap string `yaml:"configMap,omitempty"`
Key string `yaml:"key,omitempty"`
}
type InOut struct {
Parameters []Parameter `yaml:"parameters"`
Artifacts []Artifact `yaml:"artifacts,omitempty"`
}
type Template struct {
Name string `yaml:"name"`
Inputs InOut `yaml:"inputs,omitempty"`
Outputs InOut `yaml:"outputs,omitempty"`
Container Container `yaml:"container,omitempty"`
Dag *Dag `yaml:"dag,omitempty"`
Metadata TemplateMetadata `yaml:"metadata,omitempty"`
Resource ServiceResource `yaml:"resource,omitempty"`
NodeSelector map[string]string `yaml:"nodeSelector,omitempty"`
}
func (template *Template) CreateEventContainer(execution *workflow_execution.WorkflowExecution, id string, wf *workflow.Workflow, nt *resources.NativeTool, dag *Dag, natsURL string) {
container := Container{Image: "natsio/nats-box", ImagePullPolicy: "IfNotPresent"}
container.Command = []string{"sh", "-c"} // all is bash
var event native_tools.WorkflowEventParams
b, err := json.Marshal(nt.Params)
if err != nil {
fmt.Println(err)
return
}
err = json.Unmarshal(b, &event)
if err != nil {
fmt.Println(err)
return
}
if event.WorkflowResourceID != "" {
event.Payload = event.Input
event.Input = ""
if b, err := json.Marshal(event); err == nil {
payload, err := json.Marshal(&tools.NATSResponse{
FromApp: "oc-monitord",
Datatype: tools.NATIVE_TOOL,
Method: int(tools.WORKFLOW_EVENT),
Payload: b,
})
if err == nil {
cmd := exec.Command(
"nats",
"pub",
"--server", natsURL,
tools.WORKFLOW_EVENT.GenerateKey(),
string(payload),
)
if len(wf.Args[id]) > 0 {
for _, args := range wf.Args[id] {
container.Args = append(container.Args, args)
}
} else {
for _, args := range cmd.Args {
container.Args = append(container.Args, args)
}
}
container.Args = []string{
strings.Join(container.Args, " "),
}
template.Container = container
}
}
}
}
func (template *Template) CreateContainer(exec *workflow_execution.WorkflowExecution, wf *workflow.Workflow, itemID string, processing *resources.ProcessingResource, dag *Dag) {
index := 0
if d, ok := exec.SelectedInstances[processing.GetID()]; ok {
index = d
}
instance := processing.GetSelectedInstance(&index)
if instance == nil {
return
}
inst := instance.(*resources.ProcessingInstance)
container := Container{
Image: inst.Access.Container.Image,
ImagePullPolicy: "IfNotPresent",
}
if container.Image == "" {
return
}
container.Command = []string{"sh", "-c"} // all is bash
for _, v := range processing.Env {
template.Inputs.Parameters = AppendParamIfAbsent(template.Inputs.Parameters, Parameter{Name: v.Name})
}
for _, v := range wf.Env[itemID] {
template.Inputs.Parameters = AppendParamIfAbsent(template.Inputs.Parameters, Parameter{Name: v.Name})
}
for _, v := range processing.Inputs {
template.Inputs.Parameters = AppendParamIfAbsent(template.Inputs.Parameters, Parameter{Name: v.Name})
}
for _, v := range wf.Inputs[itemID] {
template.Inputs.Parameters = AppendParamIfAbsent(template.Inputs.Parameters, Parameter{Name: v.Name})
}
for _, v := range processing.Outputs {
template.Outputs.Parameters = AppendParamIfAbsent(template.Outputs.Parameters, Parameter{
Name: v.Name,
Value: v.Value,
})
}
for _, v := range wf.Outputs[itemID] {
template.Outputs.Parameters = AppendParamIfAbsent(template.Outputs.Parameters, Parameter{
Name: v.Name,
Value: v.Value,
})
}
cmd := strings.ReplaceAll(inst.Access.Container.Command, container.Image, "")
for _, a := range strings.Split(cmd, " ") {
container.Args = append(container.Args, template.ReplacePerEnv(a, template.Inputs.Parameters))
}
if len(wf.Args[itemID]) > 0 {
for _, a := range wf.Args[itemID] {
container.Args = append(container.Args, template.ReplacePerEnv(a, template.Inputs.Parameters))
}
} else {
for _, a := range strings.Split(inst.Access.Container.Args, " ") {
container.Args = append(container.Args, template.ReplacePerEnv(a, template.Inputs.Parameters))
}
}
container.Args = []string{strings.Join(container.Args, " ")}
template.Container = container
}
// CreateServiceContainer crée le container Argo pour un ServiceResource.
// Pour HOSTED, le container appelle le service distant (endpoint connu) ;
// pour DEPLOYMENT, le container EST le service à déployer.
// La logique de paramètres est identique à CreateContainer.
func (template *Template) CreateServiceContainer(exec *workflow_execution.WorkflowExecution, wf *workflow.Workflow, itemID string, service *resources.ServiceResource, dag *Dag) {
index := 0
if d, ok := exec.SelectedInstances[service.GetID()]; ok {
index = d
}
instance := service.GetSelectedInstance(&index)
if instance == nil {
return
}
inst := instance.(*resources.ServiceInstance)
if inst.Access == nil || inst.Access.Container == nil || inst.Access.Container.Image == "" {
return
}
container := Container{
Image: inst.Access.Container.Image,
ImagePullPolicy: "IfNotPresent",
}
container.Command = []string{"sh", "-c"}
for _, v := range service.Env {
template.Inputs.Parameters = AppendParamIfAbsent(template.Inputs.Parameters, Parameter{Name: v.Name})
}
for _, v := range wf.Env[itemID] {
template.Inputs.Parameters = AppendParamIfAbsent(template.Inputs.Parameters, Parameter{Name: v.Name})
}
for _, v := range service.Inputs {
template.Inputs.Parameters = AppendParamIfAbsent(template.Inputs.Parameters, Parameter{Name: v.Name})
}
for _, v := range wf.Inputs[itemID] {
template.Inputs.Parameters = AppendParamIfAbsent(template.Inputs.Parameters, Parameter{Name: v.Name})
}
for _, v := range service.Outputs {
template.Outputs.Parameters = AppendParamIfAbsent(template.Outputs.Parameters, Parameter{
Name: v.Name,
Value: v.Value,
})
}
for _, v := range wf.Outputs[itemID] {
template.Outputs.Parameters = AppendParamIfAbsent(template.Outputs.Parameters, Parameter{
Name: v.Name,
Value: v.Value,
})
}
cmd := strings.ReplaceAll(inst.Access.Container.Command, container.Image, "")
for _, a := range strings.Split(cmd, " ") {
container.Args = append(container.Args, template.ReplacePerEnv(a, template.Inputs.Parameters))
}
if len(wf.Args[itemID]) > 0 {
for _, a := range wf.Args[itemID] {
container.Args = append(container.Args, template.ReplacePerEnv(a, template.Inputs.Parameters))
}
} else {
for _, a := range strings.Split(inst.Access.Container.Args, " ") {
container.Args = append(container.Args, template.ReplacePerEnv(a, template.Inputs.Parameters))
}
}
container.Args = []string{strings.Join(container.Args, " ")}
template.Container = container
}
// AppendParamIfAbsent ajoute p à params uniquement si aucun paramètre
// portant le même nom n'est déjà présent. Évite les doublons quand les
// variables sont définies à la fois sur le ProcessingResource et sur le
// Workflow (overrides).
func AppendParamIfAbsent(params []Parameter, p Parameter) []Parameter {
for _, existing := range params {
if existing.Name == p.Name {
return params
}
}
return append(params, p)
}
func (template *Template) ReplacePerEnv(arg string, envs []Parameter) string {
// Tri par longueur décroissante : les noms longs sont remplacés en premier
// pour éviter que $FOO matche à l'intérieur de $FOOBAR.
sorted := make([]Parameter, len(envs))
copy(sorted, envs)
sort.Slice(sorted, func(i, j int) bool {
return len(sorted[i].Name) > len(sorted[j].Name)
})
for _, v := range sorted {
needle := "$" + v.Name
if v.Name != "" && strings.Contains(arg, needle) {
value := "{{ inputs.parameters." + v.Name + " }}"
arg = strings.ReplaceAll(arg, needle, value)
}
}
return arg
}
// AddAdmiraltyAnnotations marque le template pour qu'Admiralty route le pod
// vers le cluster virtuel correspondant au peerId.
//
// - elect: "" déclenche le webhook Admiralty sur le pod créé par Argo.
// - nodeSelector cible le virtual node Admiralty dont le label
// multicluster.admiralty.io/cluster-name vaut peerId,
// ce qui contraint le scheduling au cluster distant.
func (t *Template) AddAdmiraltyAnnotations(peerId string) {
if t.Metadata.Annotations == nil {
t.Metadata.Annotations = make(map[string]string)
}
t.Metadata.Annotations["multicluster.admiralty.io/elect"] = ""
if t.NodeSelector == nil {
t.NodeSelector = make(map[string]string)
}
t.NodeSelector["multicluster.admiralty.io/cluster-name"] = peerId
}