diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..ef3dd01 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,18 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${fileDirname}", + "args": ["-e","e3b7772e-dc9f-4bc1-9f5d-533e23e6cd57" ,"-u","http://127.0.0.1:3100","-m" ,"mongodb://127.0.0.1:27017","-d","DC_myDC"], + "env": {"OCMONITOR_LOKIURL":"http://127.0.0.1:3100","OCMONITOR_WORKFLOW":"8d0f1814-b5ca-436c-ba3f-116d99198fd2","KUBERNETES_SERVICE_HOST":"","OCMONITOR_MONGOURL":"mongodb://127.0.0.1:27017","OCMONITOR_DATABASE":"DC_myDC","test_service":"true"} + } + ] +} + diff --git a/README.md b/README.md index 3394ee4..379076a 100644 --- a/README.md +++ b/README.md @@ -86,8 +86,9 @@ EOF ``` - Launch the installs -> helm upgrade --install metallb metallb/metallb -> helm upgrade --install metallb metallb/metallb +> helm upgrade --install metallb metallb/metallb --namespace metallb-system + +> helm install --namespace=traefik-ingress traefik traefik/traefik --values=./traefik-values.yaml ### Configure metallb @@ -113,6 +114,10 @@ spec: EOF ``` +- Check that the services created in traefik-ingress have an external IP + +> kubectl get service -n traefik-ingress -o wide + ## TODO - [ ] Logs the output of each pods : diff --git a/models/ingress.go b/models/ingress.go new file mode 100644 index 0000000..f93274f --- /dev/null +++ b/models/ingress.go @@ -0,0 +1,101 @@ +package models + +import "strconv" + +// apiVersion: networking.k8s.io/v1 +// kind: Ingress +// metadata: +// name: example-ingress +// namespace: argo +// annotations: +// traefik.ingress.kubernetes.io/router.entrypoints: web # Utilisation de l'entrypoint HTTP standard +// spec: +// rules: +// - http: +// paths: +// - path: /dtf +// pathType: Prefix +// backend: +// service: +// name: workflow-service-qtjk2 +// port: +// number: 80 + +var ingress_manifest = &Manifest{ + ApiVersion: "networking.k8s.io/v1", + Kind: "Ingress", + Metadata: Metadata{ + GenerateName: "ingress-argo-", + }, +} + +type Ingress struct { + ApiVersion string `yaml:"apiVersion,omitempty"` + Kind string `yaml:"kind,omitempty"` + Metadata Metadata `yaml:"metadata,omitempty"` + Spec IngressSpec `yaml:"spec,omitempty"` +} + + + +type IngressSpec struct { + Rules []Rule `yaml:"rules,omitempty"` +} + +type Rule struct { + HTTP HTTP `yaml:"http,omitempty"` +} + +type HTTP struct { + Paths []Path `yaml:"paths,omitempty"` +} + +type Path struct { + Path string `yaml:"path,omitempty"` + PathType string `yaml:"pathType,omitempty"` + Backend Backend `yaml:"backend,omitempty"` +} + +type Backend struct { + ServiceName string `yaml:"serviceName,omitempty"` + ServicePort int64 `yaml:"servicePort,omitempty"` +} + +func NewIngress(contract map[string]map[string]string, serviceName string) Ingress { + new_ingr := Ingress{ + ApiVersion: "networking.k8s.io/v1", + Kind: "Ingress", + Metadata: Metadata{ + GenerateName: "ingress-argo-", + }, + Spec: IngressSpec{ + Rules: []Rule{ + { + HTTP: HTTP{ + Paths: []Path{}, + }, + }, + }, + }, + } + + for port_to_reverse_str, translations := range contract{ + + port, _ := strconv.ParseInt(port_to_reverse_str,10,64) + + port_reverse := Path{ + Path: translations["reverse"], + PathType: "Prefix", + Backend: Backend{ + ServiceName: serviceName, + ServicePort:port, + }, + } + + new_ingr.Spec.Rules[0].HTTP.Paths = append(new_ingr.Spec.Rules[0].HTTP.Paths, port_reverse) + + } + + return new_ingr +} + diff --git a/models/k8s_manifest.go b/models/k8s_manifest.go new file mode 100644 index 0000000..34b17b3 --- /dev/null +++ b/models/k8s_manifest.go @@ -0,0 +1,12 @@ +package models + +type Manifest struct { + ApiVersion string `yaml:"apiVersion,omitempty"` + Kind string `yaml:"kind"` + Metadata Metadata `yaml:"metadata,omitempty"` +} + +type Metadata struct { + Name string `yaml:"name"` + GenerateName string `yaml:"generateName"` +} \ No newline at end of file diff --git a/models/services.go b/models/services.go index e525545..685ffe5 100644 --- a/models/services.go +++ b/models/services.go @@ -10,16 +10,11 @@ type ServiceResource struct { } type Service struct { - APIVersion string `yaml:"apiVersion"` - Kind string `yaml:"kind"` - Metadata Metadata `yaml:"metadata"` + Manifest Spec ServiceSpec `yaml:"spec"` } -type Metadata struct { - Name string `yaml:"generateName"` - -} + // ServiceSpec is the specification of the Kubernetes Service type ServiceSpec struct { diff --git a/traefik-values.yaml b/traefik-values.yaml new file mode 100644 index 0000000..49572f0 --- /dev/null +++ b/traefik-values.yaml @@ -0,0 +1,11 @@ +globalArguments: +deployment: + kind: DaemonSet +providers: + kubernetesCRD: + enabled: true +service: + type: LoadBalancer +ingressRoute: + dashboard: + enabled: false diff --git a/workflow_builder/argo_builder.go b/workflow_builder/argo_builder.go index 6156b42..c27fc31 100644 --- a/workflow_builder/argo_builder.go +++ b/workflow_builder/argo_builder.go @@ -15,6 +15,7 @@ import ( oclib "cloud.o-forge.io/core/oc-lib" "cloud.o-forge.io/core/oc-lib/models/resource_model" + "cloud.o-forge.io/core/oc-lib/models/resources/processing" "cloud.o-forge.io/core/oc-lib/models/resources/workflow/graph" w "cloud.o-forge.io/core/oc-lib/models/workflow" "github.com/nwtgck/go-fakelish" @@ -24,6 +25,13 @@ import ( var logger zerolog.Logger +type ServiceExposure int +const ( + PAT ServiceExposure = iota + Reverse + Both +) + type ArgoBuilder struct { OriginWorkflow w.Workflow Workflow Workflow @@ -32,15 +40,11 @@ type ArgoBuilder struct { } type Workflow struct { - ApiVersion string `yaml:"apiVersion"` - Kind string `yaml:"kind"` - Metadata struct { - Name string `yaml:"name"` - } `yaml:"metadata"` - Spec Spec `yaml:"spec,omitempty"` + Manifest + Spec ArgoSpec `yaml:"spec,omitempty"` } -type Spec struct { +type ArgoSpec struct { Entrypoint string `yaml:"entrypoint"` Arguments []Parameter `yaml:"arguments,omitempty"` Volumes []VolumeClaimTemplate `yaml:"volumeClaimTemplates,omitempty"` @@ -48,6 +52,8 @@ type Spec struct { Timeout int `yaml:"activeDeadlineSeconds,omitempty"` } + + func (b *ArgoBuilder) CreateDAG() (string, error) { // handle services by checking if there is only one processing with hostname and port @@ -61,7 +67,7 @@ func (b *ArgoBuilder) CreateDAG() (string, error) { b.Workflow.Spec.Timeout = b.Timeout } b.Workflow.Spec.Entrypoint = "dag" - b.Workflow.ApiVersion = "argoproj.io/v1alpha1" + b.Workflow.Manifest.ApiVersion = "argoproj.io/v1alpha1" b.Workflow.Kind = "Workflow" random_name := generateWfName() b.Workflow.Metadata.Name = "oc-monitor-" + random_name @@ -116,11 +122,28 @@ func (b *ArgoBuilder) createTemplates() { new_temp.Container.VolumeMounts = append(new_temp.Container.VolumeMounts, VolumeMount{Name: "workdir", MountPath: "/mnt/vol"}) // TODO : replace this with a search of the storage / data source name if (b.isService(comp.ID)){ - serv = b.CreateService(comp) - b.addServiceToWorkflow(serv, argo_name, comp.ID) - new_temp.Metadata.Labels = make(map[string]string) - new_temp.Metadata.Labels["app"] = serv.Spec.Selector["app"] // Construct the template for the k8s service and add a link in graph between k8s service and processing - b.addServiceToArgo(serv) + + serv_type := getServiceExposure(*comp.Processing) + if serv_type == PAT || serv_type == Both{ + serv = b.CreateKubeService(comp, NodePort) + b.addKubeServiceToWorkflow(serv, argo_name, comp.ID) + new_temp.Metadata.Labels = make(map[string]string) + new_temp.Metadata.Labels["app"] = serv.Spec.Selector["app"] // Construct the template for the k8s service and add a link in graph between k8s service and processing + b.addServiceToArgo(serv) + ingress := b.CreateIngress(comp,serv) + b.addIngressToWorfklow(ingress, argo_name, comp.ID) + + } + if serv_type == Reverse || serv_type == Both{ + serv = b.CreateKubeService(comp, ClusterIP) + // create ingress by passing the service and the processing (or reverse) + b.addKubeServiceToWorkflow(serv, argo_name, comp.ID) + new_temp.Metadata.Labels = make(map[string]string) + new_temp.Metadata.Labels["app"] = serv.Spec.Selector["app"] // Construct the template for the k8s service and add a link in graph between k8s service and processing + b.addServiceToArgo(serv) + } + + // if err != nil { // // TODO // } @@ -309,5 +332,29 @@ func (b *ArgoBuilder) isService(id string) bool{ return is_exposed } +func getServiceExposure(service processing.ProcessingResource) ServiceExposure{ + var exposure_type ServiceExposure + contract := getExposeContract(service.ResourceModel.Model["expose"]) + _, pat := contract["PAT"] + _, reverse := contract["reverse"] + + if pat && reverse { + exposure_type= Both + } + if pat { + exposure_type = PAT + } + if reverse{ + exposure_type = Reverse + } + return exposure_type + +} + +func (b *ArgoBuilder) CreateIngress(processing processing.ProcessingResource, service Service) Ingress{ + contract := getExposeContract(processing.ResourceModel.Model["expose"]) + new_ingress := models.NewIngress(contract,service.Metadata.Name) + return new_ingress +} \ No newline at end of file diff --git a/workflow_builder/argo_services.go b/workflow_builder/argo_services.go index 40bd122..c9552ec 100644 --- a/workflow_builder/argo_services.go +++ b/workflow_builder/argo_services.go @@ -13,6 +13,13 @@ import ( "gopkg.in/yaml.v3" ) +type ServiceType string + +const ( + NodePort ServiceType = "NodePort" + ClusterIP ServiceType = "ClusterIP" +) + // TODO : refactor this method or the deserialization process in oc-lib to get rid of the mongo code func getExposeContract(expose resource_model.Model) map[string]map[string]string { contract := make(map[string]map[string]string,0) @@ -41,7 +48,7 @@ func getExposeContract(expose resource_model.Model) map[string]map[string]string } -func (b *ArgoBuilder) CreateService(processing graph.GraphItem) models.Service{ +func (b *ArgoBuilder) CreateKubeService(processing graph.GraphItem, service_type ServiceType) models.Service{ // model { // Type : "dict", @@ -58,23 +65,24 @@ func (b *ArgoBuilder) CreateService(processing graph.GraphItem) models.Service{ // } - new_service := models.Service{APIVersion: "v1", - Kind: "Service", - Metadata: models.Metadata{ - Name: "workflow-service-" , - }, - Spec: models.ServiceSpec{ - Selector: map[string]string{"app": "service-" + fakelish.GenerateFakeWord(5, 8)}, - Ports: []models.ServicePort{ - }, - Type: "NodePort", - }, - } - + new_service := models.Service{ + Manifest: models.Manifest{ + ApiVersion: "v1", + Kind: "Service", + Metadata: models.Metadata{ + Name: "workflow-service-"+ processing.Processing.Name + "-" + processing.ID , + }, + }, + Spec: models.ServiceSpec{ + Selector: map[string]string{"app": "service-" + fakelish.GenerateFakeWord(5, 8)}, + Ports: []models.ServicePort{ + }, + Type: string(service_type), + }, + } + completeServicePorts(&new_service, processing) - yamlified, _ := yaml.Marshal(new_service) - x := string(yamlified) - _ = x + return new_service } @@ -91,7 +99,7 @@ func completeServicePorts(service *models.Service, processing graph.GraphItem) { return } - + // This condition allows us to create NodePort if PAT is filled or a ClusterIP that only expose port 80 and 443 (for Ingress) if _, ok := translation_dict["PAT"]; ok{ port_translation, err := strconv.ParseInt(translation_dict["PAT"], 10, 64) if err != nil { @@ -99,8 +107,6 @@ func completeServicePorts(service *models.Service, processing graph.GraphItem) { return } - - new_port_translation := models.ServicePort{ Name: strings.ToLower(processing.Processing.Name) + processing.ID, Port: port_translation-30000, @@ -109,17 +115,34 @@ func completeServicePorts(service *models.Service, processing graph.GraphItem) { Protocol: "TCP", } service.Spec.Ports = append(service.Spec.Ports, new_port_translation) - } + } else { + port_spec := []models.ServicePort{ + models.ServicePort{ + Name: strings.ToLower(processing.Processing.Name) + processing.ID + "-80", + Port: 80, + TargetPort: 80, + Protocol: "TCP", + }, + models.ServicePort{ + Name: strings.ToLower(processing.Processing.Name) + processing.ID + "-443", + Port: 443, + TargetPort: 443, + Protocol: "TCP", + }, + + } + service.Spec.Ports = append(service.Spec.Ports,port_spec...) + } } - return + } // The k8s service passed as the parameter only expose one port because it is the result of CreateService() // we check if this port is already exposed by a service in the workflow and we proceed to the creation of a new service OR // add the port to the list of port exposed by an existing if portAlreadyExposed is false -func (b *ArgoBuilder) addServiceToWorkflow(service models.Service, processing_name string, processing_id string) (label string) { +func (b *ArgoBuilder) addKubeServiceToWorkflow(service models.Service, processing_name string, processing_id string) (label string) { if exposed, service_available_port := b.portAlreadyExposed(service.Spec.Ports[0].TargetPort); exposed && service_available_port != nil{ // The port you want to expose is already exposed by all the existing services service_available_port.Spec.Ports = append(service_available_port.Spec.Ports, service.Spec.Ports...) @@ -178,4 +201,15 @@ func (b *ArgoBuilder) portAlreadyExposed(port int64) (exposed bool, service *mod } return true, nil +} + +// If contract has a reverse value + // Test if service already exist for the argo service + // Yes : create ingress, associate it with the existing kube service (name + exposed port on service) + // No : + // - create template for a service that expose the port of the argo service (pod) + // - store the name of the service and its port exposed + // - create template for an ingress : manifest must contain the path, name of the service and port exposed on the service +func (b *ArgoBuilder) addIngress(){ + } \ No newline at end of file