diff --git a/pkg/backends/k8s.go b/pkg/backends/k8s.go index 3c43088b..725616d1 100644 --- a/pkg/backends/k8s.go +++ b/pkg/backends/k8s.go @@ -57,23 +57,21 @@ func (k *KubeBackend) GetInfo() *types.ServerlessBackendInfo { // ListServices returns a slice with all services registered in the provided namespace func (k *KubeBackend) ListServices() ([]*types.Service, error) { - // Get the list with all podTemplates - podTemplates, err := k.kubeClientset.CoreV1().PodTemplates(k.namespace).List(context.TODO(), metav1.ListOptions{}) + // Get the list with all Knative services + configmaps, err := getAllServicesConfigMaps(k.namespace, k.kubeClientset) if err != nil { + log.Printf("WARNING: %v\n", err) return nil, err } - services := []*types.Service{} - for _, podTemplate := range podTemplates.Items { - // Get service from configMap's FDL - svc, err := getServiceFromFDL(podTemplate.Name, k.namespace, k.kubeClientset) + + for _, cm := range configmaps.Items { + service, err := getServiceFromConfigMap(&cm) if err != nil { - log.Printf("WARNING: %v\n", err) - } else { - services = append(services, svc) + return nil, err } + services = append(services, service) } - return services, nil } @@ -148,8 +146,14 @@ func (k *KubeBackend) ReadService(name string) (*types.Service, error) { return nil, err } + // Get the configMap of the Service + cm, err := k.kubeClientset.CoreV1().ConfigMaps(k.namespace).Get(context.TODO(), name, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("the service \"%s\" does not have a registered ConfigMap", name) + } + // Get service from configMap's FDL - svc, err := getServiceFromFDL(name, k.namespace, k.kubeClientset) + svc, err := getServiceFromConfigMap(cm) if err != nil { return nil, err } @@ -242,17 +246,12 @@ func (k *KubeBackend) DeleteService(service types.Service) error { return nil } -func getServiceFromFDL(name string, namespace string, kubeClientset kubernetes.Interface) (*types.Service, error) { - // Get the configMap of the Service - cm, err := kubeClientset.CoreV1().ConfigMaps(namespace).Get(context.TODO(), name, metav1.GetOptions{}) - if err != nil { - return nil, fmt.Errorf("the service \"%s\" does not have a registered ConfigMap", name) - } +func getServiceFromConfigMap(cm *v1.ConfigMap) (*types.Service, error) { service := &types.Service{} // Unmarshal the FDL stored in the configMap - if err = yaml.Unmarshal([]byte(cm.Data[types.FDLFileName]), service); err != nil { - return nil, fmt.Errorf("the FDL of the service \"%s\" cannot be read", name) + if err := yaml.Unmarshal([]byte(cm.Data[types.FDLFileName]), service); err != nil { + return nil, fmt.Errorf("the FDL of the service \"%s\" cannot be read", cm.Name) } // Add the script to the service from configmap's script value @@ -364,6 +363,17 @@ func deleteServiceConfigMap(name string, namespace string, kubeClientset kuberne return nil } +func getAllServicesConfigMaps(namespace string, kubeClientset kubernetes.Interface) (*v1.ConfigMapList, error) { + listOpts := metav1.ListOptions{ + LabelSelector: "oscar_service", + } + configMapsList, err := kubeClientset.CoreV1().ConfigMaps(namespace).List(context.TODO(), listOpts) + if err != nil { + return nil, err + } + return configMapsList, nil +} + func deleteServiceJobs(name string, namespace string, kubeClientset kubernetes.Interface) error { // ListOptions to select all the associated jobs with the specified service listOpts := metav1.ListOptions{ diff --git a/pkg/backends/k8s_test.go b/pkg/backends/k8s_test.go index 1a967bda..e9e4f30b 100644 --- a/pkg/backends/k8s_test.go +++ b/pkg/backends/k8s_test.go @@ -113,29 +113,12 @@ func TestKubeGetInfo(t *testing.T) { } func TestKubeListServices(t *testing.T) { - validPodTemplateListReactor := func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) { - podTemplateList := &v1.PodTemplateList{ - Items: []v1.PodTemplate{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - Namespace: "testnamespace", - }, - Template: v1.PodTemplateSpec{}, - }, - }, - } - return true, podTemplateList, nil - } t.Run("valid list", func(t *testing.T) { clientset := fake.NewSimpleClientset() back := MakeKubeBackend(clientset, testConfig) - // Return a valid PodTemplateList - back.kubeClientset.(*fake.Clientset).Fake.PrependReactor("list", "podtemplates", validPodTemplateListReactor) - // Return a valid configMap back.kubeClientset.(*fake.Clientset).Fake.PrependReactor("get", "configmaps", validConfigMapReaction) @@ -146,29 +129,11 @@ func TestKubeListServices(t *testing.T) { } }) - t.Run("listing podTemplates throws an error", func(t *testing.T) { - clientset := fake.NewSimpleClientset() - - back := MakeKubeBackend(clientset, testConfig) - - // Return an error listing PodTemplates - back.kubeClientset.(*fake.Clientset).Fake.PrependReactor("list", "podtemplates", errorReaction) - - // Call - _, err := back.ListServices() - if err == nil { - t.Error("expecting error, got: nil") - } - }) - t.Run("getServiceFromFDL throws error getting configMap", func(t *testing.T) { clientset := fake.NewSimpleClientset() back := MakeKubeBackend(clientset, testConfig) - // Return a valid PodTemplateList - back.kubeClientset.(*fake.Clientset).Fake.PrependReactor("list", "podtemplates", validPodTemplateListReactor) - // Return an error getting the configMap back.kubeClientset.(*fake.Clientset).Fake.PrependReactor("get", "configmaps", errorReaction) @@ -198,9 +163,6 @@ func TestKubeListServices(t *testing.T) { return true, validCM, nil } - // Return a valid PodTemplateList - back.kubeClientset.(*fake.Clientset).Fake.PrependReactor("list", "podtemplates", validPodTemplateListReactor) - // Return a valid configMap with invalid FDL back.kubeClientset.(*fake.Clientset).Fake.PrependReactor("get", "configmaps", validConfigMapWithInvalidFDLReactor) diff --git a/pkg/backends/knative.go b/pkg/backends/knative.go index 9e1391e9..08497817 100644 --- a/pkg/backends/knative.go +++ b/pkg/backends/knative.go @@ -76,22 +76,20 @@ func (kn *KnativeBackend) GetInfo() *types.ServerlessBackendInfo { // ListServices returns a slice with all services registered in the provided namespace func (kn *KnativeBackend) ListServices() ([]*types.Service, error) { // Get the list with all Knative services - knSvcs, err := kn.knClientset.ServingV1().Services(kn.namespace).List(context.TODO(), metav1.ListOptions{}) + configmaps, err := getAllServicesConfigMaps(kn.namespace, kn.kubeClientset) if err != nil { + log.Printf("WARNING: %v\n", err) return nil, err } - services := []*types.Service{} - for _, knSvc := range knSvcs.Items { - // Get service from configMap's FDL - svc, err := getServiceFromFDL(knSvc.Name, kn.namespace, kn.kubeClientset) + + for _, cm := range configmaps.Items { + service, err := getServiceFromConfigMap(&cm) if err != nil { - log.Printf("WARNING: %v\n", err) - } else { - services = append(services, svc) + return nil, err } + services = append(services, service) } - return services, nil } @@ -151,8 +149,13 @@ func (kn *KnativeBackend) ReadService(name string) (*types.Service, error) { return nil, err } + // Get the configMap of the Service + cm, err := kn.kubeClientset.CoreV1().ConfigMaps(kn.namespace).Get(context.TODO(), name, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("the service \"%s\" does not have a registered ConfigMap", name) + } // Get service from configMap's FDL - svc, err := getServiceFromFDL(name, kn.namespace, kn.kubeClientset) + svc, err := getServiceFromConfigMap(cm) if err != nil { return nil, err } diff --git a/pkg/backends/knative_test.go b/pkg/backends/knative_test.go index d21f16b2..064da7af 100644 --- a/pkg/backends/knative_test.go +++ b/pkg/backends/knative_test.go @@ -144,17 +144,6 @@ func TestKnativeListServices(t *testing.T) { []k8stesting.SimpleReactor{knServiceListReactor}, false, }, - { - "Error listing knative services", - []k8stesting.SimpleReactor{}, - []k8stesting.SimpleReactor{ - { - Verb: "list", - Resource: "services", - Reaction: errorReaction, - }}, - true, - }, { "Error getting the configMap", []k8stesting.SimpleReactor{ diff --git a/pkg/backends/openfaas.go b/pkg/backends/openfaas.go index 89ab0402..d03db44c 100644 --- a/pkg/backends/openfaas.go +++ b/pkg/backends/openfaas.go @@ -83,21 +83,20 @@ func (of *OpenfaasBackend) GetInfo() *types.ServerlessBackendInfo { // ListServices returns a slice with all services registered in the provided namespace func (of *OpenfaasBackend) ListServices() ([]*types.Service, error) { - // Get the list with all deployments - deployments, err := of.kubeClientset.AppsV1().Deployments(of.namespace).List(context.TODO(), metav1.ListOptions{}) + // Get the list with all Knative services + configmaps, err := getAllServicesConfigMaps(of.namespace, of.kubeClientset) if err != nil { + log.Printf("WARNING: %v\n", err) return nil, err } - services := []*types.Service{} - for _, deployment := range deployments.Items { - // Get service from configMap's FDL - svc, err := getServiceFromFDL(deployment.Name, of.namespace, of.kubeClientset) + + for _, cm := range configmaps.Items { + service, err := getServiceFromConfigMap(&cm) if err != nil { - log.Printf("WARNING: %v\n", err) - } else { - services = append(services, svc) + return nil, err } + services = append(services, service) } return services, nil @@ -230,8 +229,13 @@ func (of *OpenfaasBackend) ReadService(name string) (*types.Service, error) { return nil, err } + // Get the configMap of the Service + cm, err := of.kubeClientset.CoreV1().ConfigMaps(of.namespace).Get(context.TODO(), name, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("the service \"%s\" does not have a registered ConfigMap", name) + } // Get service from configMap's FDL - svc, err := getServiceFromFDL(name, of.namespace, of.kubeClientset) + svc, err := getServiceFromConfigMap(cm) if err != nil { return nil, err } diff --git a/pkg/handlers/create.go b/pkg/handlers/create.go index 79844911..37eb5453 100644 --- a/pkg/handlers/create.go +++ b/pkg/handlers/create.go @@ -70,10 +70,12 @@ func MakeCreateHandler(cfg *types.Config, back types.ServerlessBackend) gin.Hand minIOAdminClient, _ := utils.MakeMinIOAdminClient(cfg) // Service is created by an EGI user + if !isAdminUser { uid, err := auth.GetUIDFromContext(c) if err != nil { c.String(http.StatusInternalServerError, fmt.Sprintln(err)) + return } // Set UID from owner @@ -83,6 +85,7 @@ func MakeCreateHandler(cfg *types.Config, back types.ServerlessBackend) gin.Hand mc, err := auth.GetMultitenancyConfigFromContext(c) if err != nil { c.String(http.StatusInternalServerError, fmt.Sprintln(err)) + return } full_uid := auth.FormatUID(uid) @@ -93,6 +96,7 @@ func MakeCreateHandler(cfg *types.Config, back types.ServerlessBackend) gin.Hand err := checkIdentity(&service, cfg, authHeader) if err != nil { c.String(http.StatusBadRequest, fmt.Sprintln(err)) + return } break } diff --git a/pkg/handlers/job.go b/pkg/handlers/job.go index 1fa48b5b..53654aeb 100644 --- a/pkg/handlers/job.go +++ b/pkg/handlers/job.go @@ -18,7 +18,6 @@ package handlers import ( "context" - "encoding/base64" "fmt" "io" "log" @@ -99,33 +98,24 @@ func MakeJobHandler(cfg *types.Config, kubeClientset *kubernetes.Clientset, back return } - // Make event envVar + // Initialize event envVar and args var event := v1.EnvVar{} + var args []string - var args string if cfg.InterLinkAvailable && service.InterLinkNodeName != "" { - event = v1.EnvVar{ - Name: types.EventVariable, - Value: base64.StdEncoding.EncodeToString([]byte(eventBytes)), - } - args = fmt.Sprintf("\" wget %s -O %s && chmod 0755 %s && echo \\$%s | base64 -d | %s \"", cfg.SupervisorURL, SupervisorPath, SupervisorPath, types.EventVariable, SupervisorPath) - podSpec.NodeSelector = map[string]string{ - NodeSelectorKey: service.InterLinkNodeName, - } - podSpec.DNSPolicy = InterLinkDNSPolicy - podSpec.RestartPolicy = InterLinkRestartPolicy - podSpec.Tolerations = []v1.Toleration{ - { - Key: InterLinkTolerationKey, - Operator: InterLinkTolerationOperator, - }, - } + command, event, args = types.SetInterlinkJob(podSpec, service, cfg, eventBytes) } else { - event = v1.EnvVar{ - Name: types.EventVariable, - Value: string(eventBytes), + + if service.Mount.Provider != "" { + args = []string{"-c", fmt.Sprintf("echo $%s | %s", types.EventVariable, service.GetSupervisorPath()) + ";echo \"I finish\" > /tmpfolder/finish-file;"} + types.SetMount(podSpec, *service, cfg) + } else { + event = v1.EnvVar{ + Name: types.EventVariable, + Value: string(eventBytes), + } + args = []string{"-c", fmt.Sprintf("echo $%s | %s", types.EventVariable, service.GetSupervisorPath())} } - args = fmt.Sprintf("echo $%s | %s", types.EventVariable, service.GetSupervisorPath()) } // Make JOB_UUID envVar @@ -150,16 +140,12 @@ func MakeJobHandler(cfg *types.Config, kubeClientset *kubernetes.Clientset, back for i, c := range podSpec.Containers { if c.Name == types.ContainerName { podSpec.Containers[i].Command = command - podSpec.Containers[i].Args = []string{"-c", args} + podSpec.Containers[i].Args = args podSpec.Containers[i].Env = append(podSpec.Containers[i].Env, event) podSpec.Containers[i].Env = append(podSpec.Containers[i].Env, jobUUIDVar) podSpec.Containers[i].Env = append(podSpec.Containers[i].Env, resourceIDVar) } } - if service.Mount.Provider != "" { - types.SetMount(podSpec, *service, cfg) - podSpec.Containers[0].Args = []string{"-c", args + ";echo \"I finish\" > /tmpfolder/finish-file;"} - } // Delegate job if can't be scheduled and has defined replicas if rm != nil && service.HasReplicas() { diff --git a/pkg/handlers/list.go b/pkg/handlers/list.go index 2a5cb130..ba8ae64e 100644 --- a/pkg/handlers/list.go +++ b/pkg/handlers/list.go @@ -19,6 +19,7 @@ package handlers import ( "fmt" "net/http" + "slices" "strings" "github.com/gin-gonic/gin" @@ -42,19 +43,13 @@ func MakeListHandler(back types.ServerlessBackend) gin.HandlerFunc { uid, err := auth.GetUIDFromContext(c) if err != nil { c.String(http.StatusInternalServerError, fmt.Sprintln(err)) + return } var allowedServicesForUser []*types.Service for _, service := range services { - if len(service.AllowedUsers) == 0 { + if len(service.AllowedUsers) == 0 || slices.Contains(service.AllowedUsers, uid) { allowedServicesForUser = append(allowedServicesForUser, service) - continue - } - for _, id := range service.AllowedUsers { - if uid == id { - allowedServicesForUser = append(allowedServicesForUser, service) - break - } } } diff --git a/pkg/handlers/update.go b/pkg/handlers/update.go index 6a5a96f0..cd97f826 100644 --- a/pkg/handlers/update.go +++ b/pkg/handlers/update.go @@ -45,7 +45,11 @@ func MakeUpdateHandler(cfg *types.Config, back types.ServerlessBackend) gin.Hand // Check service values and set defaults checkValues(&newService, cfg) - + authHeader := c.GetHeader("Authorization") + if len(strings.Split(authHeader, "Bearer")) == 1 { + isAdminUser = true + createLogger.Printf("[*] Updating service as admin user") + } // Read the current service oldService, err := back.ReadService(newService.Name) @@ -58,34 +62,34 @@ func MakeUpdateHandler(cfg *types.Config, back types.ServerlessBackend) gin.Hand } return } + if !isAdminUser { + uid, err := auth.GetUIDFromContext(c) + if err != nil { + c.String(http.StatusInternalServerError, fmt.Sprintln("Couldn't get UID from context")) + } - uid, err := auth.GetUIDFromContext(c) - if err != nil { - c.String(http.StatusInternalServerError, fmt.Sprintln("Couldn't get UID from context")) - } + if oldService.Owner != uid { + c.String(http.StatusForbidden, "User %s doesn't have permision to modify this service", uid) + return + } - if oldService.Owner != uid { - c.String(http.StatusForbidden, "User %s doesn't have permision to modify this service", uid) - return - } + // Set the owner on the new service definition + newService.Owner = oldService.Owner - // Set the owner on the new service definition - newService.Owner = oldService.Owner - - // If the service has changed VO check permisions again - if newService.VO != "" && newService.VO != oldService.VO { - for _, vo := range cfg.OIDCGroups { - if vo == newService.VO { - authHeader := c.GetHeader("Authorization") - err := checkIdentity(&newService, cfg, authHeader) - if err != nil { - c.String(http.StatusBadRequest, fmt.Sprintln(err)) + // If the service has changed VO check permisions again + if newService.VO != "" && newService.VO != oldService.VO { + for _, vo := range cfg.OIDCGroups { + if vo == newService.VO { + authHeader := c.GetHeader("Authorization") + err := checkIdentity(&newService, cfg, authHeader) + if err != nil { + c.String(http.StatusBadRequest, fmt.Sprintln(err)) + } + break } - break } } } - minIOAdminClient, _ := utils.MakeMinIOAdminClient(cfg) // Update the service if err := back.UpdateService(newService); err != nil { diff --git a/pkg/types/config.go b/pkg/types/config.go index 3777791e..b871ba86 100644 --- a/pkg/types/config.go +++ b/pkg/types/config.go @@ -189,7 +189,7 @@ type Config struct { IngressHost string `json:"-"` // Github path of FaaS Supervisor (needed for Interlink config) - SupervisorURL string `json:"-"` + SupervisorKitImage string `json:"-"` //Path to additional OSCAR configuration setted by users AdditionalConfigPath string `json:"-"` @@ -238,7 +238,7 @@ var configVars = []configVar{ {"OIDCSubject", "OIDC_SUBJECT", false, stringType, ""}, {"OIDCGroups", "OIDC_GROUPS", false, stringSliceType, ""}, {"IngressHost", "INGRESS_HOST", false, stringType, ""}, - {"SupervisorURL", "SUPERVISOR_URL", false, stringType, "https://github.com/grycap/faas-supervisor/releases/download/1.5.8/supervisor"}, + {"SupervisorKitImage", "SUPERVISOR_KIT_IMAGE", false, stringType, ""}, {"AdditionalConfigPath", "ADDITIONAL_CONFIG_PATH", false, stringType, "config.yaml"}, } diff --git a/pkg/types/interlink.go b/pkg/types/interlink.go new file mode 100644 index 00000000..e80cbbea --- /dev/null +++ b/pkg/types/interlink.go @@ -0,0 +1,101 @@ +/* +Copyright (C) GRyCAP - I3M - UPV + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package types + +import ( + "encoding/base64" + + v1 "k8s.io/api/core/v1" +) + +const ( + ContainerSupervisorName = "supervisor-container" + SupervisorMountPath = "/data" + SupervisorArg = "cp -r /supervisor/* " + SupervisorMountPath + NameSupervisorVolume = "supervisor-share-data" + NodeSelectorKey = "kubernetes.io/hostname" + + // Annotations for InterLink nodes + InterLinkDNSPolicy = "ClusterFirst" + InterLinkRestartPolicy = "OnFailure" + InterLinkTolerationKey = "virtual-node.interlink/no-schedule" + InterLinkTolerationOperator = "Exists" +) + +var SupervisorCommand = []string{"/bin/sh", "-c"} +var OscarContainerCommand = []string{"echo $EVENT | base64 -d | " + SupervisorMountPath + "/supervisor"} + +// SetInterlinkJob Return interlink configuration for kubernetes job and add Interlink variables to podSpec +func SetInterlinkJob(podSpec *v1.PodSpec, service *Service, cfg *Config, eventBytes []byte) ([]string, v1.EnvVar, []string) { + command := SupervisorCommand + event := v1.EnvVar{ + Name: EventVariable, + Value: base64.StdEncoding.EncodeToString([]byte(eventBytes)), + } + args := OscarContainerCommand + podSpec.NodeSelector = map[string]string{ + NodeSelectorKey: service.InterLinkNodeName, + } + podSpec.DNSPolicy = InterLinkDNSPolicy + podSpec.RestartPolicy = InterLinkRestartPolicy + podSpec.Tolerations = []v1.Toleration{ + { + Key: InterLinkTolerationKey, + Operator: InterLinkTolerationOperator, + }, + } + + addInitContainer(podSpec, cfg) + return command, event, args +} + +// SetInterlinkService Add InterLink configuration to podSpec +func SetInterlinkService(podSpec *v1.PodSpec) { + podSpec.Containers[0].ImagePullPolicy = "Always" + shareDataVolumeMount := v1.VolumeMount{ + Name: NameSupervisorVolume, + MountPath: SupervisorMountPath, + } + + podSpec.Containers[0].VolumeMounts = append(podSpec.Containers[0].VolumeMounts, shareDataVolumeMount) + + shareDataVolume := v1.Volume{ + Name: NameSupervisorVolume, + VolumeSource: v1.VolumeSource{ + EmptyDir: &v1.EmptyDirVolumeSource{}, + }, + } + podSpec.Volumes = append(podSpec.Volumes, shareDataVolume) + +} + +func addInitContainer(podSpec *v1.PodSpec, cfg *Config) { + initContainer := v1.Container{ + Name: ContainerSupervisorName, + Command: SupervisorCommand, + Args: []string{SupervisorArg}, + Image: cfg.SupervisorKitImage, + ImagePullPolicy: v1.PullIfNotPresent, + VolumeMounts: []v1.VolumeMount{ + { + Name: NameSupervisorVolume, + MountPath: SupervisorMountPath, + }, + }, + } + podSpec.InitContainers = []v1.Container{initContainer} +} diff --git a/pkg/types/mount.go b/pkg/types/mount.go index 4c2e5621..71915fe2 100644 --- a/pkg/types/mount.go +++ b/pkg/types/mount.go @@ -49,8 +49,6 @@ done` // SetMount Creates the sidecar container that mounts the source volume onto the pod volume func SetMount(podSpec *v1.PodSpec, service Service, cfg *Config) { podSpec.Containers = append(podSpec.Containers, sidecarPodSpec(service)) - termination := int64(5) - podSpec.TerminationGracePeriodSeconds = &termination addVolume(podSpec) } diff --git a/pkg/types/service.go b/pkg/types/service.go index c4a1f021..0fc8c157 100644 --- a/pkg/types/service.go +++ b/pkg/types/service.go @@ -300,7 +300,7 @@ func (service *Service) ToPodSpec(cfg *Config) (*v1.PodSpec, error) { } if cfg.InterLinkAvailable && service.InterLinkNodeName != "" { // Add specs of InterLink - podSpec.Containers[0].ImagePullPolicy = "Always" + SetInterlinkService(podSpec) } else { // Add specs volumeMount := v1.VolumeMount{ diff --git a/ui b/ui index 98de936c..045c25fb 160000 --- a/ui +++ b/ui @@ -1 +1 @@ -Subproject commit 98de936c5923d3b3ccaef500c1a92f35009bbf53 +Subproject commit 045c25fbf3acfeaaefcb44c0b183098bf196c7f1