diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 29046a7c..7dc82a30 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -23,4 +23,5 @@ about: Create a report to help us improve ### Possible Solution -Any thoughts as to potential solutions or ideas to go about finding one. Please include links to any research. +Any thoughts as to potential solutions or ideas to go about finding one. +Please include links to any research. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 6ae866a2..8323daec 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -14,19 +14,22 @@ orientation. Examples of behavior that contributes to creating a positive environment include: -- Using welcoming and inclusive language -- Being respectful of differing viewpoints and experiences -- Gracefully accepting constructive criticism -- Focusing on what is best for the community -- Showing empathy towards other community members +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members Examples of unacceptable behavior by participants include: -- The use of sexualized language or imagery and unwelcome sexual attention or advances -- Trolling, insulting/derogatory comments, and personal or political attacks -- Public or private harassment -- Publishing others' private information, such as a physical or electronic address, without explicit permission -- Other conduct which could reasonably be considered inappropriate in a professional setting +- The use of sexualized language or imagery and unwelcome sexual attention or + advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic + address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting ## Our Responsibilities @@ -52,7 +55,7 @@ further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at products@grycap.upv.es. All +reported by contacting the project team at . All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. diff --git a/deploy/ansible/README.md b/deploy/ansible/README.md index cd10fd90..bb79f54a 100644 --- a/deploy/ansible/README.md +++ b/deploy/ansible/README.md @@ -1,3 +1,3 @@ # Ansible playbook to deploy K3s and the OSCAR platform -Please refer to the [docs](https://docs.oscar.grycap.net/deploy-ansible/) for instructions. \ No newline at end of file +Please refer to the [docs](https://docs.oscar.grycap.net/deploy-ansible/) for instructions. diff --git a/examples/plant-classification-theano/README.md b/examples/plant-classification-theano/README.md index 53dc5b5a..94248e20 100644 --- a/examples/plant-classification-theano/README.md +++ b/examples/plant-classification-theano/README.md @@ -63,5 +63,5 @@ To run this example you need: 1. Once the function is executed, the output is automatically copied to the output bucket in minio, in this case `plant-classifier-out`. You can - download the ouput from here for further processing. + download the output from here for further processing. ![minio-out.png](img/Minio-OUT.png) diff --git a/go.mod b/go.mod index ddb974fc..508637a0 100644 --- a/go.mod +++ b/go.mod @@ -52,6 +52,7 @@ require ( ) require ( + bou.ke/monkey v1.0.2 github.com/GehirnInc/crypt v0.0.0-20190301055215-6c0105aabd46 // indirect github.com/apache/yunikorn-scheduler-interface v1.2.0 // indirect github.com/blendle/zapdriver v1.3.1 // indirect diff --git a/go.sum b/go.sum index 3d436868..67ddba40 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +bou.ke/monkey v1.0.2 h1:kWcnsrCNUatbxncxR/ThdYqbytgOIArtYWqcQLQzKLI= +bou.ke/monkey v1.0.2/go.mod h1:OqickVX3tNx6t33n1xvtTtu85YN5s6cKwVug+oHMaIA= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= contrib.go.opencensus.io/exporter/ocagent v0.7.1-0.20200907061046-05415f1de66d h1:LblfooH1lKOpp1hIhukktmSAxFkqMPFk9KR6iZ0MJNI= @@ -149,8 +151,6 @@ github.com/grycap/cdmi-client-go v0.1.1/go.mod h1:ZqWeQS3YBJVXxg3HOIkAu1MLNJ4+7s github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= @@ -274,18 +274,6 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM= diff --git a/pkg/backends/fake.go b/pkg/backends/fake.go index d39b6bba..3c2ad304 100644 --- a/pkg/backends/fake.go +++ b/pkg/backends/fake.go @@ -31,7 +31,8 @@ var errFake = errors.New("fake error") // FakeBackend fake struct to mock the beahaviour of the ServerlessBackend interface type FakeBackend struct { - errors map[string][]error + errors map[string][]error + Service *types.Service // service to be returned by the ReadService function } // MakeFakeBackend returns the pointer of a new FakeBackend struct @@ -81,7 +82,12 @@ func (f *FakeBackend) CreateService(service types.Service) error { // ReadService returns a Service (fake) func (f *FakeBackend) ReadService(name string) (*types.Service, error) { - return &types.Service{Token: "11e387cf727630d899925d57fceb4578f478c44be6cde0ae3fe886d8be513acf"}, f.returnError(getCurrentFuncName()) + // default service returned by the function + service := &types.Service{Token: "11e387cf727630d899925d57fceb4578f478c44be6cde0ae3fe886d8be513acf"} + if f.Service != nil { + service = f.Service + } + return service, f.returnError(getCurrentFuncName()) } // UpdateService updates an existent service (fake) diff --git a/pkg/backends/openfaas.go b/pkg/backends/openfaas.go index d03db44c..5743e21a 100644 --- a/pkg/backends/openfaas.go +++ b/pkg/backends/openfaas.go @@ -43,7 +43,7 @@ var errOpenfaasOperator = errors.New("the OpenFaaS Operator is not creating the // OpenfaasBackend struct to represent an Openfaas client type OpenfaasBackend struct { kubeClientset kubernetes.Interface - ofClientset *ofclientset.Clientset + ofClientset ofclientset.Interface namespace string gatewayEndpoint string scaler *utils.OpenfaasScaler diff --git a/pkg/backends/openfaas_test.go b/pkg/backends/openfaas_test.go new file mode 100644 index 00000000..4228a6b6 --- /dev/null +++ b/pkg/backends/openfaas_test.go @@ -0,0 +1,326 @@ +package backends + +import ( + "testing" + "time" + + "github.com/grycap/oscar/v3/pkg/types" + ofv1 "github.com/openfaas/faas-netes/pkg/apis/openfaas/v1" + ofclientset "github.com/openfaas/faas-netes/pkg/client/clientset/versioned/fake" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/watch" + + "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/rest" + k8stesting "k8s.io/client-go/testing" +) + +func TestMakeOpenfaasBackend(t *testing.T) { + kubeClientset := fake.NewSimpleClientset() + kubeConfig := &rest.Config{} + cfg := &types.Config{ + ServicesNamespace: "default", + OpenfaasNamespace: "openfaas", + OpenfaasPort: 8080, + } + + ofBackend := MakeOpenfaasBackend(kubeClientset, kubeConfig, cfg) + + if ofBackend.namespace != "default" { + t.Errorf("Expected namespace to be 'default', got '%s'", ofBackend.namespace) + } + if ofBackend.gatewayEndpoint != "gateway.openfaas:8080" { + t.Errorf("Expected gatewayEndpoint to be 'gateway.openfaas:8080', got '%s'", ofBackend.gatewayEndpoint) + } +} + +func TestGetInfo(t *testing.T) { + kubeClientset := fake.NewSimpleClientset() + cfg := &types.Config{ + ServicesNamespace: "default", + OpenfaasNamespace: "openfaas", + OpenfaasPort: 8080, + } + + ofClientset := ofclientset.NewSimpleClientset() + ofBackend := &OpenfaasBackend{ + kubeClientset: kubeClientset, + ofClientset: ofClientset, + namespace: cfg.ServicesNamespace, + config: cfg, + } + + info := ofBackend.GetInfo() + if info.Name != "OpenFaaS" { + t.Errorf("Expected Name to be 'OpenFaaS', got '%s'", info.Name) + } +} + +func TestCreateService(t *testing.T) { + ofClientset := ofclientset.NewSimpleClientset() + cfg := &types.Config{ + ServicesNamespace: "default", + OpenfaasNamespace: "openfaas", + OpenfaasPort: 8080, + } + + deployment := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Namespace: cfg.ServicesNamespace, + }, + } + kubeClientset := fake.NewSimpleClientset(deployment) + + ofBackend := &OpenfaasBackend{ + kubeClientset: kubeClientset, + ofClientset: ofClientset, + namespace: cfg.ServicesNamespace, + config: cfg, + } + + service := types.Service{ + Name: "test-service", + Image: "test-image", + Labels: map[string]string{ + "test": "label", + }, + } + + // Create a fake watcher + fakeWatcher := watch.NewFake() + + // Set up a reactor to intercept the Watch action and return the fake watcher + kubeClientset.PrependWatchReactor("deployments", func(action k8stesting.Action) (handled bool, ret watch.Interface, err error) { + return true, fakeWatcher, nil + }) + + // Run watcher in a goroutine + go func() { + // Simulate the creation of the deployment by triggering an event on the fake watcher + time.Sleep(1 * time.Second) // Ensure the CreateService method is waiting on the watcher + fakeWatcher.Add(&appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: service.Name, + Namespace: cfg.ServicesNamespace, + }, + }) + + // Allow some time for the CreateService method to process the event + time.Sleep(1 * time.Second) + }() + + err := ofBackend.CreateService(service) + if err != nil { + t.Errorf("Expected no error, got '%v'", err) + } + + actions := ofClientset.Actions() + if len(actions) != 1 { + t.Errorf("Expected 1 action, got %d", len(actions)) + } + if actions[0].GetResource().Resource != "functions" || actions[0].GetVerb() != "create" { + t.Errorf("Expected action to be 'create functions', got '%s %s'", actions[0].GetVerb(), actions[0].GetResource().Resource) + } +} + +func TestReadService(t *testing.T) { + ofClientset := ofclientset.NewSimpleClientset() + cfg := &types.Config{ + ServicesNamespace: "default", + OpenfaasNamespace: "openfaas", + OpenfaasPort: 8080, + } + + cm := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Namespace: cfg.ServicesNamespace, + }, + Data: map[string]string{ + types.FDLFileName: `{"name": "test-service"}`, + types.ScriptFileName: "script.sh", + }, + } + deployment := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Namespace: cfg.ServicesNamespace, + }, + } + kubeClientset := fake.NewSimpleClientset(cm, deployment) + + ofBackend := &OpenfaasBackend{ + kubeClientset: kubeClientset, + ofClientset: ofClientset, + namespace: cfg.ServicesNamespace, + config: cfg, + } + + service, err := ofBackend.ReadService("test-service") + if err != nil { + t.Errorf("Expected no error, got '%v'", err) + } + if service.Name != "test-service" { + t.Errorf("Expected service name to be 'test-service', got '%s'", service.Name) + } +} + +func TestDeleteService(t *testing.T) { + + kubeClientset := fake.NewSimpleClientset() + offunction := &ofv1.Function{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Namespace: "default", + }, + Spec: ofv1.FunctionSpec{ + Image: "test-image", + }, + } + ofClientset := ofclientset.NewSimpleClientset(offunction) + cfg := &types.Config{ + ServicesNamespace: "default", + OpenfaasNamespace: "openfaas", + OpenfaasPort: 8080, + } + + ofBackend := &OpenfaasBackend{ + kubeClientset: kubeClientset, + ofClientset: ofClientset, + namespace: cfg.ServicesNamespace, + config: cfg, + } + + service := types.Service{ + Name: "test-service", + Image: "test-image", + } + + // Delete the service + err := ofBackend.DeleteService(service) + if err != nil { + t.Errorf("Expected no error, got '%v'", err) + } + + actions := ofClientset.Actions() + if len(actions) != 1 { + t.Errorf("Expected 1 action, got %d", len(actions)) + } + if actions[0].GetResource().Resource != "functions" || actions[0].GetVerb() != "delete" { + t.Errorf("Expected action to be 'delete functions', got '%s %s'", actions[0].GetVerb(), actions[0].GetResource().Resource) + } +} + +func TestUpdateService(t *testing.T) { + ofClientset := ofclientset.NewSimpleClientset() + cfg := &types.Config{ + ServicesNamespace: "default", + OpenfaasNamespace: "openfaas", + OpenfaasPort: 8080, + } + + oldCm := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Namespace: cfg.ServicesNamespace, + }, + Data: map[string]string{ + types.FDLFileName: `{"name": "test-service"}`, + types.ScriptFileName: "script.sh", + }, + } + deployment := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Namespace: cfg.ServicesNamespace, + }, + } + kubeClientset := fake.NewSimpleClientset(oldCm, deployment) + + ofBackend := &OpenfaasBackend{ + kubeClientset: kubeClientset, + ofClientset: ofClientset, + namespace: cfg.ServicesNamespace, + config: cfg, + } + + service := types.Service{ + Name: "test-service", + Image: "test-image", + Labels: map[string]string{ + "test": "label", + }, + } + + err := ofBackend.UpdateService(service) + if err != nil { + t.Errorf("Expected no error, got '%v'", err) + } + + actions := kubeClientset.Actions() + if len(actions) != 4 { + t.Errorf("Expected 4 actions, got %d", len(actions)) + } + if actions[0].GetResource().Resource != "configmaps" || actions[0].GetVerb() != "get" { + t.Errorf("Expected action to be 'get configmaps', got '%s %s'", actions[0].GetVerb(), actions[0].GetResource().Resource) + } + if actions[1].GetResource().Resource != "configmaps" || actions[1].GetVerb() != "update" { + t.Errorf("Expected action to be 'update configmaps', got '%s %s'", actions[2].GetVerb(), actions[2].GetResource().Resource) + } + if actions[2].GetResource().Resource != "deployments" || actions[2].GetVerb() != "get" { + t.Errorf("Expected action to be 'get deployments', got '%s %s'", actions[2].GetVerb(), actions[2].GetResource().Resource) + } + if actions[3].GetResource().Resource != "deployments" || actions[3].GetVerb() != "update" { + t.Errorf("Expected action to be 'update deployments', got '%s %s'", actions[3].GetVerb(), actions[3].GetResource().Resource) + } +} + +func TestListServices(t *testing.T) { + cfg := &types.Config{ + ServicesNamespace: "default", + OpenfaasNamespace: "openfaas", + OpenfaasPort: 8080, + } + + ofClientset := ofclientset.NewSimpleClientset() + + cml := &v1.ConfigMapList{ + Items: []v1.ConfigMap{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Namespace: cfg.ServicesNamespace, + Labels: map[string]string{ + "oscar_service": "true", + }, + }, + Data: map[string]string{ + types.FDLFileName: `{"name": "test-service"}`, + types.ScriptFileName: "script.sh", + }, + }, + }, + } + kubeClientset := fake.NewSimpleClientset(cml) + + ofBackend := &OpenfaasBackend{ + kubeClientset: kubeClientset, + ofClientset: ofClientset, + namespace: cfg.ServicesNamespace, + config: cfg, + } + + services, err := ofBackend.ListServices() + if err != nil { + t.Errorf("Expected no error, got '%v'", err) + } + if len(services) != 1 { + t.Errorf("Expected 1 service, got %d", len(services)) + } + if services[0].Name != "test-service" { + t.Errorf("Expected service name to be 'test-service', got '%s'", services[0].Name) + } +} diff --git a/pkg/backends/serverlessbackend_test.go b/pkg/backends/serverlessbackend_test.go new file mode 100644 index 00000000..01af08c1 --- /dev/null +++ b/pkg/backends/serverlessbackend_test.go @@ -0,0 +1,65 @@ +/* +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 backends + +import ( + "fmt" + "testing" + + "github.com/grycap/oscar/v3/pkg/types" + "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/rest" +) + +func TestMakeServerlessBackend(t *testing.T) { + kubeClientset := fake.NewSimpleClientset() + kubeConfig := &rest.Config{} + + tests := []struct { + name string + serverlessBackend string + expectedBackendType string + }{ + { + name: "OpenFaaS Backend", + serverlessBackend: "openfaas", + expectedBackendType: "*backends.OpenfaasBackend", + }, + { + name: "Knative Backend", + serverlessBackend: "knative", + expectedBackendType: "*backends.KnativeBackend", + }, + { + name: "Default Kube Backend", + serverlessBackend: "unknown", + expectedBackendType: "*backends.KubeBackend", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := &types.Config{ + ServerlessBackend: tt.serverlessBackend, + } + backend := MakeServerlessBackend(kubeClientset, kubeConfig, cfg) + if backendType := fmt.Sprintf("%T", backend); backendType != tt.expectedBackendType { + t.Errorf("expected %s, got %s", tt.expectedBackendType, backendType) + } + }) + } +} diff --git a/pkg/handlers/config_test.go b/pkg/handlers/config_test.go new file mode 100644 index 00000000..388c3c1a --- /dev/null +++ b/pkg/handlers/config_test.go @@ -0,0 +1,164 @@ +/* +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 handlers + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "reflect" + "strings" + "testing" + + "bou.ke/monkey" + "github.com/gin-gonic/gin" + "github.com/grycap/oscar/v3/pkg/types" + "github.com/grycap/oscar/v3/pkg/utils/auth" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + testclient "k8s.io/client-go/kubernetes/fake" +) + +func createExpectedBody(access_key string, secret_key string, cfg *types.Config) map[string]interface{} { + return map[string]interface{}{ + "config": map[string]interface{}{ + "name": "", + "namespace": "", + "services_namespace": "", + "gpu_available": false, + "interLink_available": false, + "yunikorn_enable": false, + }, + "minio_provider": map[string]interface{}{ + "endpoint": cfg.MinIOProvider.Endpoint, + "verify": cfg.MinIOProvider.Verify, + "access_key": access_key, + "secret_key": secret_key, + "region": cfg.MinIOProvider.Region, + }, + } +} + +func TestMakeConfigHandler(t *testing.T) { + gin.SetMode(gin.TestMode) + + cfg := &types.Config{ + // Initialize with necessary fields + MinIOProvider: &types.MinIOProvider{ + Endpoint: "http://minio.example.com", + Verify: true, + Region: "us-east-1", + AccessKey: "accessKey1", + SecretKey: "secretKey1", + }, + } + + t.Run("Without Authorization Header", func(t *testing.T) { + router := gin.New() + router.GET("/config", MakeConfigHandler(cfg)) + + req, _ := http.NewRequest("GET", "/config", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected status code 200, got %d", w.Code) + } + if !strings.Contains(w.Body.String(), "http://minio.example.com") { + t.Fatalf("Unexpected response body") + } + + }) + + K8sObjects := []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "somelonguserid", + Namespace: auth.ServicesNamespace, + }, + Data: map[string][]byte{ + "accessKey": []byte("accessKey"), + "secretKey": []byte("secretKey"), + }, + }, + } + + kubeClientset := testclient.NewSimpleClientset(K8sObjects...) + t.Run("With Bearer Authorization Header", func(t *testing.T) { + router := gin.New() + router.GET("/config", MakeConfigHandler(cfg)) + + req, _ := http.NewRequest("GET", "/config", nil) + req.Header.Set("Authorization", "Bearer some-token") + w := httptest.NewRecorder() + + // Mocking auth functions + monkey.Patch(auth.GetUIDFromContext, func(c *gin.Context) (string, error) { + return "somelonguserid@egi.eu", nil + }) + + monkey.Patch(auth.GetMultitenancyConfigFromContext, func(c *gin.Context) (*auth.MultitenancyConfig, error) { + return auth.NewMultitenancyConfig(kubeClientset, "somelonguserid@egi.eu"), nil + }) + + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected status code 200, got %d", w.Code) + } + + expected_body := createExpectedBody("accessKey", "secretKey", cfg) + + var responseBody map[string]interface{} + if err := json.Unmarshal(w.Body.Bytes(), &responseBody); err != nil { + t.Fatalf("Failed to parse response body: %v", err) + } + + if !reflect.DeepEqual(responseBody, expected_body) { + t.Fatalf("Unexpected response body: %s", w.Body.String()) + } + + defer monkey.Unpatch(auth.GetUIDFromContext) + defer monkey.Unpatch(auth.GetMultitenancyConfigFromContext) + }) + + t.Run("With Token Authorization Header", func(t *testing.T) { + router := gin.New() + router.GET("/config", MakeConfigHandler(cfg)) + + req, _ := http.NewRequest("GET", "/config", nil) + req.Header.Set("Authorization", "SomeToken") + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected status code 200, got %d", w.Code) + } + + expected_body := createExpectedBody("accessKey1", "secretKey1", cfg) + + var responseBody map[string]interface{} + if err := json.Unmarshal(w.Body.Bytes(), &responseBody); err != nil { + t.Fatalf("Failed to parse response body: %v", err) + } + + if !reflect.DeepEqual(responseBody, expected_body) { + t.Fatalf("Unexpected response body: %s", w.Body.String()) + } + }) +} diff --git a/pkg/handlers/create.go b/pkg/handlers/create.go index 3bb2e260..095dbe09 100644 --- a/pkg/handlers/create.go +++ b/pkg/handlers/create.go @@ -121,7 +121,7 @@ func MakeCreateHandler(cfg *types.Config, back types.ServerlessBackend) gin.Hand if !ownerOnList { service.AllowedUsers = append(service.AllowedUsers, uid) } - // Check if the uid's from allowed_users have and asociated MinIO user + // Check if the uid's from allowed_users have and associated MinIO user // and create it if not uids := mc.CheckUsersInCache(service.AllowedUsers) if len(uids) > 0 { @@ -282,7 +282,7 @@ func createBuckets(service *types.Service, cfg *types.Config, minIOAdminClient * // Create group for the service and add users // Check if users in allowed_users have a MinIO associated user - // If new allowed users list is empty the service becames public + // If new allowed users list is empty the service becomes public if !isUpdate { if !isAdminUser { if len(allowed_users) == 0 { diff --git a/pkg/handlers/create_test.go b/pkg/handlers/create_test.go index a3f3d68b..335ab2d3 100644 --- a/pkg/handlers/create_test.go +++ b/pkg/handlers/create_test.go @@ -27,20 +27,21 @@ import ( "github.com/gin-gonic/gin" "github.com/grycap/oscar/v3/pkg/backends" "github.com/grycap/oscar/v3/pkg/types" + "github.com/grycap/oscar/v3/pkg/utils/auth" + testclient "k8s.io/client-go/kubernetes/fake" ) func TestMakeCreateHandler(t *testing.T) { back := backends.MakeFakeBackend() + kubeClientset := testclient.NewSimpleClientset() // Create a fake MinIO server server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, hreq *http.Request) { - if hreq.URL.Path != "/input" && hreq.URL.Path != "/output" && !strings.HasPrefix(hreq.URL.Path, "/minio/admin/v3/") { + if hreq.URL.Path != "/test" && hreq.URL.Path != "/test/input/" && hreq.URL.Path != "/output" && !strings.HasPrefix(hreq.URL.Path, "/minio/admin/v3/") { t.Errorf("Unexpected path in request, got: %s", hreq.URL.Path) } - fmt.Println(hreq.URL.Path) - if hreq.URL.Path == "/minio/admin/v3/info" { rw.WriteHeader(http.StatusOK) rw.Write([]byte(`{"Mode": "local", "Region": "us-east-1"}`)) @@ -61,6 +62,11 @@ func TestMakeCreateHandler(t *testing.T) { }, } r := gin.Default() + r.Use(func(c *gin.Context) { + c.Set("uidOrigin", "somelonguid@egi.eu") + c.Set("multitenancyConfig", auth.NewMultitenancyConfig(kubeClientset, "somelonguid@egi.eu")) + c.Next() + }) r.POST("/system/services", MakeCreateHandler(&cfg, back)) w := httptest.NewRecorder() @@ -77,7 +83,7 @@ func TestMakeCreateHandler(t *testing.T) { "input": [ { "storage_provider": "minio", - "path": "/input" + "path": "/test/input/" } ], "output": [ @@ -94,11 +100,13 @@ func TestMakeCreateHandler(t *testing.T) { "password": "pass" } } - } + }, + "allowed_users": ["somelonguid@egi.eu", "somelonguid2@egi.eu"] } `) req, _ := http.NewRequest("POST", "/system/services", body) + req.Header.Add("Authorization", "Bearer token") r.ServeHTTP(w, req) // Close the fake MinIO server diff --git a/pkg/handlers/delete_test.go b/pkg/handlers/delete_test.go new file mode 100644 index 00000000..2f6bb0ca --- /dev/null +++ b/pkg/handlers/delete_test.go @@ -0,0 +1,102 @@ +package handlers + +import ( + "errors" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/gin-gonic/gin" + "github.com/grycap/oscar/v3/pkg/backends" + "github.com/grycap/oscar/v3/pkg/types" + k8serr "k8s.io/apimachinery/pkg/api/errors" +) + +func TestMakeDeleteHandler(t *testing.T) { + back := backends.MakeFakeBackend() + + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, hreq *http.Request) { + if hreq.URL.Path != "/input" && hreq.URL.Path != "/output" && !strings.HasPrefix(hreq.URL.Path, "/minio/admin/v3/") { + t.Errorf("Unexpected path in request, got: %s", hreq.URL.Path) + } + })) + + // and set the MinIO endpoint to the fake server + cfg := types.Config{ + MinIOProvider: &types.MinIOProvider{ + Endpoint: server.URL, + Region: "us-east-1", + AccessKey: "minioadmin", + SecretKey: "minioadmin", + Verify: false, + }, + } + + svc := &types.Service{ + Token: "11e387cf727630d899925d57fceb4578f478c44be6cde0ae3fe886d8be513acf", + Input: []types.StorageIOConfig{ + {Provider: "minio." + types.DefaultProvider, Path: "/input"}, + }, + Output: []types.StorageIOConfig{ + {Provider: "minio." + types.DefaultProvider, Path: "/output"}, + }, + StorageProviders: &types.StorageProviders{ + MinIO: map[string]*types.MinIOProvider{types.DefaultProvider: { + Region: "us-east-1", + Endpoint: server.URL, + AccessKey: "ak", + SecretKey: "sk"}}, + }} + back.Service = svc + + r := gin.Default() + r.DELETE("/system/services/:serviceName", MakeDeleteHandler(&cfg, back)) + + scenarios := []struct { + name string + returnError bool + errType string + }{ + {"valid", false, ""}, + {"Service Not Found test", true, "404"}, + {"Internal Server Error test", true, "500"}, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + w := httptest.NewRecorder() + + if s.returnError { + switch s.errType { + case "404": + back.AddError("DeleteService", k8serr.NewGone("Not Found")) + case "500": + err := errors.New("Not found") + back.AddError("DeleteService", k8serr.NewInternalError(err)) + } + } + serviceName := "testName" + req, _ := http.NewRequest("DELETE", "/system/services/"+serviceName, nil) + + r.ServeHTTP(w, req) + + if s.returnError { + if s.errType == "404" && w.Code != http.StatusNotFound { + t.Errorf("expecting code %d, got %d", http.StatusNotFound, w.Code) + } + + if s.errType == "500" && w.Code != http.StatusInternalServerError { + t.Errorf("expecting code %d, got %d", http.StatusInternalServerError, w.Code) + } + } else { + if w.Code != http.StatusNoContent { + t.Errorf("expecting code %d, got %d", http.StatusNoContent, w.Code) + } + } + }) + } + + // Close the fake MinIO server + defer server.Close() +} diff --git a/pkg/handlers/health_test.go b/pkg/handlers/health_test.go new file mode 100644 index 00000000..ca405ad4 --- /dev/null +++ b/pkg/handlers/health_test.go @@ -0,0 +1,50 @@ +/* +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 handlers + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" +) + +func TestHealthHandler(t *testing.T) { + // Set up the Gin router + router := gin.Default() + router.GET("/health", HealthHandler) + + // Create a request to send to the above route + req, _ := http.NewRequest("GET", "/health", nil) + + // Create a response recorder to record the response + w := httptest.NewRecorder() + + // Perform the request + router.ServeHTTP(w, req) + + // Check the status code is what we expect + if w.Code != http.StatusOK { + t.Errorf("expected status OK, got %v", w.Code) + } + + // Check the response body is what we expect + if w.Body.String() != "Ok" { + t.Errorf("expected body 'Ok', got %v", w.Body.String()) + } +} diff --git a/pkg/handlers/job.go b/pkg/handlers/job.go index 58e2cbaf..962bcc55 100644 --- a/pkg/handlers/job.go +++ b/pkg/handlers/job.go @@ -59,8 +59,8 @@ const ( InterLinkTolerationOperator = "Exists" ) -// MakeJobHandler makes a han/home/slangarita/Escritorio/interlink-cluster/PodCern/PodCern.yamldler to manage async invocations -func MakeJobHandler(cfg *types.Config, kubeClientset *kubernetes.Clientset, back types.ServerlessBackend, rm resourcemanager.ResourceManager) gin.HandlerFunc { +// MakeJobHandler makes a handler to manage async invocations +func MakeJobHandler(cfg *types.Config, kubeClientset kubernetes.Interface, back types.ServerlessBackend, rm resourcemanager.ResourceManager) gin.HandlerFunc { return func(c *gin.Context) { service, err := back.ReadService(c.Param("serviceName")) if err != nil { diff --git a/pkg/handlers/job_test.go b/pkg/handlers/job_test.go new file mode 100644 index 00000000..6150617a --- /dev/null +++ b/pkg/handlers/job_test.go @@ -0,0 +1,44 @@ +package handlers + +import ( + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + testclient "k8s.io/client-go/kubernetes/fake" + + "github.com/gin-gonic/gin" + "github.com/grycap/oscar/v3/pkg/backends" + "github.com/grycap/oscar/v3/pkg/types" +) + +func TestMakeJobHandler(t *testing.T) { + back := backends.MakeFakeBackend() + cfg := types.Config{} + kubeClient := testclient.NewSimpleClientset() + + r := gin.Default() + r.POST("/job/:serviceName", MakeJobHandler(&cfg, kubeClient, back, nil)) + + w := httptest.NewRecorder() + body := strings.NewReader(`{"Records": [{"requestParameters": {"principalId": "uid", "sourceIPAddress": "ip"}}]}`) + serviceName := "testName" + req, _ := http.NewRequest("POST", "/job/services"+serviceName, body) + req.Header.Set("Authorization", "Bearer 11e387cf727630d899925d57fceb4578f478c44be6cde0ae3fe886d8be513acf") + r.ServeHTTP(w, req) + + if w.Code != http.StatusCreated { + fmt.Println(w.Body) + t.Errorf("expecting code %d, got %d", http.StatusCreated, w.Code) + } + + actions := kubeClient.Actions() + if len(actions) != 1 { + t.Errorf("Expected 1 action but got %d", len(actions)) + } + if actions[0].GetVerb() != "create" || actions[0].GetResource().Resource != "jobs" { + t.Errorf("Expected create job action but got %v", actions[0]) + } +} diff --git a/pkg/handlers/logs.go b/pkg/handlers/logs.go index 5767af31..cc809827 100644 --- a/pkg/handlers/logs.go +++ b/pkg/handlers/logs.go @@ -35,7 +35,7 @@ import ( // TODO Try using cookies to avoid excesive calls to the k8s API // // MakeJobsInfoHandler makes a handler for listing all existing jobs from a service and show their JobInfo -func MakeJobsInfoHandler(back types.ServerlessBackend, kubeClientset *kubernetes.Clientset, namespace string) gin.HandlerFunc { +func MakeJobsInfoHandler(back types.ServerlessBackend, kubeClientset kubernetes.Interface, namespace string) gin.HandlerFunc { return func(c *gin.Context) { jobsInfo := make(map[string]*types.JobInfo) // Get serviceName @@ -103,7 +103,7 @@ func MakeJobsInfoHandler(back types.ServerlessBackend, kubeClientset *kubernetes // MakeDeleteJobsHandler makes a handler for deleting all jobs created by the provided service. // If 'all' querystring is set to 'true' pending, running and failed jobs will also be deleted -func MakeDeleteJobsHandler(back types.ServerlessBackend, kubeClientset *kubernetes.Clientset, namespace string) gin.HandlerFunc { +func MakeDeleteJobsHandler(back types.ServerlessBackend, kubeClientset kubernetes.Interface, namespace string) gin.HandlerFunc { return func(c *gin.Context) { // Get serviceName and jobName serviceName := c.Param("serviceName") @@ -147,7 +147,7 @@ func MakeDeleteJobsHandler(back types.ServerlessBackend, kubeClientset *kubernet } // MakeGetLogsHandler makes a handler for getting logs from the 'oscar-container' inside the pod created by the specified job -func MakeGetLogsHandler(back types.ServerlessBackend, kubeClientset *kubernetes.Clientset, namespace string) gin.HandlerFunc { +func MakeGetLogsHandler(back types.ServerlessBackend, kubeClientset kubernetes.Interface, namespace string) gin.HandlerFunc { return func(c *gin.Context) { // Get serviceName and jobName serviceName := c.Param("serviceName") @@ -200,7 +200,7 @@ func MakeGetLogsHandler(back types.ServerlessBackend, kubeClientset *kubernetes. } // MakeDeleteJobHandler makes a handler for removing a job -func MakeDeleteJobHandler(back types.ServerlessBackend, kubeClientset *kubernetes.Clientset, namespace string) gin.HandlerFunc { +func MakeDeleteJobHandler(back types.ServerlessBackend, kubeClientset kubernetes.Interface, namespace string) gin.HandlerFunc { return func(c *gin.Context) { // Get serviceName and jobName serviceName := c.Param("serviceName") @@ -251,7 +251,7 @@ func isOIDCAuthorised(c *gin.Context, back types.ServerlessBackend, serviceName // If is oidc auth get service and check on allowed users authHeader := c.GetHeader("Authorization") if len(strings.Split(authHeader, "Bearer")) > 1 { - service, _ := back.ReadService(c.Param("serviceName")) + service, _ := back.ReadService(serviceName) uid, err := auth.GetUIDFromContext(c) if err != nil { c.String(http.StatusInternalServerError, fmt.Sprintln(err)) diff --git a/pkg/handlers/logs_test.go b/pkg/handlers/logs_test.go new file mode 100644 index 00000000..e500a29e --- /dev/null +++ b/pkg/handlers/logs_test.go @@ -0,0 +1,234 @@ +package handlers + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "reflect" + "testing" + "time" + + "github.com/gin-gonic/gin" + "github.com/grycap/oscar/v3/pkg/backends" + "github.com/grycap/oscar/v3/pkg/types" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + testclient "k8s.io/client-go/kubernetes/fake" +) + +func TestMakeJobsInfoHandler(t *testing.T) { + back := backends.MakeFakeBackend() + now := time.Now() + + K8sObjects := []runtime.Object{ + &batchv1.Job{ + Status: batchv1.JobStatus{ + StartTime: &metav1.Time{Time: now}, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "job", + Namespace: "namespace", + Labels: map[string]string{ + types.ServiceLabel: "test", + }, + }, + }, + &corev1.PodList{ + Items: []corev1.Pod{ + { + Status: corev1.PodStatus{ + Phase: corev1.PodRunning, + ContainerStatuses: []corev1.ContainerStatus{ + { + Name: types.ContainerName, + State: corev1.ContainerState{ + Running: &corev1.ContainerStateRunning{ + StartedAt: metav1.Time{Time: now}, + }, + }, + }, + }, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "pod", + Namespace: "namespace", + Labels: map[string]string{ + "oscar_service": "test", + "job-name": "job"}, + }, + }, + }, + }, + } + kubeClientset := testclient.NewSimpleClientset(K8sObjects...) + + r := gin.Default() + r.GET("/system/logs/:serviceName", MakeJobsInfoHandler(back, kubeClientset, "namespace")) + + w := httptest.NewRecorder() + serviceName := "test" + req, _ := http.NewRequest("GET", "/system/logs/"+serviceName, nil) + r.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + fmt.Println(w.Body) + t.Errorf("expecting code %d, got %d", http.StatusOK, w.Code) + } + + var response map[string]interface{} + if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { + t.Errorf("response is not valid JSON: %v", err) + } + + expected := map[string]interface{}{ + "job": map[string]interface{}{ + "status": "Running", + "creation_time": now.UTC().Format(time.RFC3339), + "start_time": now.UTC().Format(time.RFC3339), + }, + } + + if !reflect.DeepEqual(response, expected) { + t.Errorf("expecting %v, got %v", expected, response) + } + + actions := kubeClientset.Actions() + if len(actions) != 2 { + t.Errorf("expecting 2 actions, got %d", len(actions)) + } + + if actions[0].GetVerb() != "list" || actions[0].GetResource().Resource != "jobs" { + t.Errorf("expecting list jobs, got %s %s", actions[0].GetVerb(), actions[0].GetResource().Resource) + } + if actions[1].GetVerb() != "list" || actions[1].GetResource().Resource != "pods" { + t.Errorf("expecting list pods, got %s %s", actions[1].GetVerb(), actions[1].GetResource().Resource) + } +} + +func TestMakeDeleteJobsHandler(t *testing.T) { + back := backends.MakeFakeBackend() + kubeClientset := testclient.NewSimpleClientset() + + r := gin.Default() + r.DELETE("/system/logs/:serviceName", MakeDeleteJobsHandler(back, kubeClientset, "namespace")) + + w := httptest.NewRecorder() + serviceName := "test" + req, _ := http.NewRequest("DELETE", "/system/logs/"+serviceName, nil) + r.ServeHTTP(w, req) + + if w.Code != http.StatusNoContent { + fmt.Println(w.Body) + t.Errorf("expecting code %d, got %d", http.StatusNoContent, w.Code) + } + + actions := kubeClientset.Actions() + if len(actions) != 1 { + t.Errorf("expecting 1 actions, got %d", len(actions)) + } + + if actions[0].GetVerb() != "delete-collection" || actions[0].GetResource().Resource != "jobs" { + t.Errorf("expecting list jobs, got %s %s", actions[0].GetVerb(), actions[0].GetResource().Resource) + } +} + +func TestMakeGetLogsHandler(t *testing.T) { + back := backends.MakeFakeBackend() + + K8sObjects := []runtime.Object{ + &corev1.PodList{ + Items: []corev1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod", + Namespace: "namespace", + Labels: map[string]string{ + "oscar_service": "test", + "job-name": "job"}, + }, + }, + }, + }, + } + kubeClientset := testclient.NewSimpleClientset(K8sObjects...) + + r := gin.Default() + r.GET("/system/logs/:serviceName/:jobName", MakeGetLogsHandler(back, kubeClientset, "namespace")) + + w := httptest.NewRecorder() + serviceName := "test" + jobName := "job" + req, _ := http.NewRequest("GET", "/system/logs/"+serviceName+"/"+jobName, nil) + r.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + fmt.Println(w.Body) + t.Errorf("expecting code %d, got %d", http.StatusOK, w.Code) + } + if w.Body.String() != "fake logs" { + t.Errorf("expecting 'fake logs', got %s", w.Body.String()) + } + + actions := kubeClientset.Actions() + if len(actions) != 2 { + t.Errorf("expecting 2 actions, got %d", len(actions)) + } + + if actions[0].GetVerb() != "list" || actions[0].GetResource().Resource != "pods" { + t.Errorf("expecting list pods, got %s %s", actions[0].GetVerb(), actions[0].GetResource().Resource) + } + if actions[1].GetVerb() != "get" || actions[1].GetResource().Resource != "pods" { + t.Errorf("expecting get pods, got %s %s", actions[1].GetVerb(), actions[1].GetResource().Resource) + } +} +func TestMakeDeleteJobHandler(t *testing.T) { + back := backends.MakeFakeBackend() + + K8sObjects := []runtime.Object{ + &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "job", + Namespace: "namespace", + Labels: map[string]string{ + types.ServiceLabel: "test", + }, + }, + }, + } + kubeClientset := testclient.NewSimpleClientset(K8sObjects...) + + r := gin.Default() + r.Use(func(c *gin.Context) { + c.Set("uidOrigin", "some-uid-value") + c.Next() + }) + r.DELETE("/system/logs/:serviceName/:jobName", MakeDeleteJobHandler(back, kubeClientset, "namespace")) + + w := httptest.NewRecorder() + serviceName := "test" + jobName := "job" + req, _ := http.NewRequest("DELETE", "/system/logs/"+serviceName+"/"+jobName, nil) + req.Header.Set("Authorization", "Bearer 11e387cf727630d899925d57fceb4578f478c44be6cde0ae3fe886d8be513acf") + r.ServeHTTP(w, req) + + if w.Code != http.StatusNoContent { + fmt.Println(w.Body) + t.Errorf("expecting code %d, got %d", http.StatusNoContent, w.Code) + } + + actions := kubeClientset.Actions() + if len(actions) != 2 { + t.Errorf("expecting 2 actions, got %d", len(actions)) + } + + if actions[0].GetVerb() != "get" || actions[0].GetResource().Resource != "jobs" { + t.Errorf("expecting get jobs, got %s %s", actions[0].GetVerb(), actions[0].GetResource().Resource) + } + + if actions[1].GetVerb() != "delete" || actions[1].GetResource().Resource != "jobs" { + t.Errorf("expecting delete jobs, got %s %s", actions[1].GetVerb(), actions[1].GetResource().Resource) + } +} diff --git a/pkg/handlers/run_test.go b/pkg/handlers/run_test.go index e7c9fe3b..3d9d647d 100644 --- a/pkg/handlers/run_test.go +++ b/pkg/handlers/run_test.go @@ -76,7 +76,7 @@ func TestMakeRunHandler(t *testing.T) { err := errors.New("Not found") back.AddError("ReadService", k8serr.NewInternalError(err)) case "splitErr": - req.Header.Set("Authorization", "AbCdEf123456") + req.Header.Set("Authorization", "11e387cf727630d899925d57fceb4578f478c44be6cde0ae3fe886d8be513acf") case "diffErr": req.Header.Set("Authorization", "Bearer 11e387cf727630d899925d57fceb4578f478c44be6cde0ae3fe886d8be513dfg") } diff --git a/pkg/handlers/status.go b/pkg/handlers/status.go index 52061ecb..17fed02b 100644 --- a/pkg/handlers/status.go +++ b/pkg/handlers/status.go @@ -49,7 +49,7 @@ type NodeInfo struct { } // MakeStatusHandler Status handler for kubernetes deployment. -func MakeStatusHandler(kubeClientset *kubernetes.Clientset, metricsClientset *versioned.MetricsV1beta1Client) gin.HandlerFunc { +func MakeStatusHandler(kubeClientset kubernetes.Interface, metricsClientset versioned.MetricsV1beta1Interface) gin.HandlerFunc { return func(c *gin.Context) { // Get nodes list nodes, err := kubeClientset.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{}) diff --git a/pkg/handlers/status_test.go b/pkg/handlers/status_test.go new file mode 100644 index 00000000..d211583f --- /dev/null +++ b/pkg/handlers/status_test.go @@ -0,0 +1,132 @@ +/* +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 handlers + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "reflect" + "testing" + + "github.com/gin-gonic/gin" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/fake" + k8stesting "k8s.io/client-go/testing" + metricsv1beta1api "k8s.io/metrics/pkg/apis/metrics/v1beta1" + metricsfake "k8s.io/metrics/pkg/client/clientset/versioned/fake" +) + +func TestMakeStatusHandler(t *testing.T) { + // Create a fake Kubernetes clientset + kubeClientset := fake.NewSimpleClientset( + &v1.NodeList{ + Items: []v1.Node{ + { + ObjectMeta: metav1.ObjectMeta{Name: "node1"}, + Status: v1.NodeStatus{ + Allocatable: v1.ResourceList{ + "cpu": *resource.NewMilliQuantity(2000, resource.DecimalSI), + "memory": *resource.NewQuantity(8*1024*1024*1024, resource.BinarySI), + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "node2"}, + Status: v1.NodeStatus{ + Allocatable: v1.ResourceList{ + "cpu": *resource.NewMilliQuantity(4000, resource.DecimalSI), + "memory": *resource.NewQuantity(16*1024*1024*1024, resource.BinarySI), + }, + }, + }, + }, + }, + ) + + // Create a fake Metrics clientset + metricsClientset := metricsfake.NewSimpleClientset() + // Add NodeMetrics objects to the fake clientset's store + metricsClientset.Fake.PrependReactor("list", "nodes", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) { + return true, &metricsv1beta1api.NodeMetricsList{ + Items: []metricsv1beta1api.NodeMetrics{ + { + ObjectMeta: metav1.ObjectMeta{Name: "node1"}, + Usage: v1.ResourceList{ + "cpu": *resource.NewMilliQuantity(1000, resource.DecimalSI), + "memory": *resource.NewQuantity(4*1024*1024*1024, resource.BinarySI), + }, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "node2"}, + Usage: v1.ResourceList{ + "cpu": *resource.NewMilliQuantity(2000, resource.DecimalSI), + "memory": *resource.NewQuantity(8*1024*1024*1024, resource.BinarySI), + }, + }, + }, + }, nil + }) + + // Create a new Gin router + router := gin.Default() + router.GET("/status", MakeStatusHandler(kubeClientset, metricsClientset.MetricsV1beta1())) + + // Create a new HTTP request + req, _ := http.NewRequest("GET", "/status", nil) + w := httptest.NewRecorder() + + // Perform the request + router.ServeHTTP(w, req) + + // Check the response + if w.Code != http.StatusOK { + t.Errorf("Expected status code %d, but got %d", http.StatusOK, w.Code) + } + + var jsonResponse map[string]interface{} + err := json.Unmarshal(w.Body.Bytes(), &jsonResponse) + if err != nil { + t.Fatalf("Failed to unmarshal response: %v", err) + } + + expectedResponse := map[string]interface{}{ + "numberNodes": 1.0, + "cpuFreeTotal": 2000.0, + "cpuMaxFree": 2000.0, + "memoryFreeTotal": 16.0 * 1024 * 1024 * 1024, + "memoryMaxFree": 8.0 * 1024 * 1024 * 1024, + "detail": []interface{}{ + map[string]interface{}{ + "nodeName": "node2", + "cpuCapacity": "4000", + "cpuUsage": "2000", + "cpuPercentage": "50.00", + "memoryCapacity": "17179869184", + "memoryUsage": "8589934592", + "memoryPercentage": "50.00", + }, + }, + } + + if !reflect.DeepEqual(jsonResponse, expectedResponse) { + t.Errorf("Expected response %v, but got %v", expectedResponse, jsonResponse) + } +} diff --git a/pkg/handlers/update.go b/pkg/handlers/update.go index cd97f826..e7dea13e 100644 --- a/pkg/handlers/update.go +++ b/pkg/handlers/update.go @@ -76,7 +76,7 @@ func MakeUpdateHandler(cfg *types.Config, back types.ServerlessBackend) gin.Hand // Set the owner on the new service definition newService.Owner = oldService.Owner - // If the service has changed VO check permisions again + // If the service has changed VO check permission again if newService.VO != "" && newService.VO != oldService.VO { for _, vo := range cfg.OIDCGroups { if vo == newService.VO { diff --git a/pkg/handlers/update_test.go b/pkg/handlers/update_test.go new file mode 100644 index 00000000..636caa9a --- /dev/null +++ b/pkg/handlers/update_test.go @@ -0,0 +1,114 @@ +package handlers + +import ( + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/gin-gonic/gin" + "github.com/grycap/oscar/v3/pkg/backends" + "github.com/grycap/oscar/v3/pkg/types" +) + +func TestMakeUpdateHandler(t *testing.T) { + back := backends.MakeFakeBackend() + + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, hreq *http.Request) { + if hreq.URL.Path != "/input" && hreq.URL.Path != "/output" && !strings.HasPrefix(hreq.URL.Path, "/minio/admin/v3/") { + t.Errorf("Unexpected path in request, got: %s", hreq.URL.Path) + } + if hreq.URL.Path == "/minio/admin/v3/info" { + rw.WriteHeader(http.StatusOK) + rw.Write([]byte(`{"Mode": "local", "Region": "us-east-1"}`)) + } else { + rw.WriteHeader(http.StatusOK) + rw.Write([]byte(`{"status": "success"}`)) + } + })) + + svc := &types.Service{ + Token: "11e387cf727630d899925d57fceb4578f478c44be6cde0ae3fe886d8be513acf", + Input: []types.StorageIOConfig{ + {Provider: "minio." + types.DefaultProvider, Path: "/input"}, + }, + Output: []types.StorageIOConfig{ + {Provider: "minio." + types.DefaultProvider, Path: "/output"}, + }, + StorageProviders: &types.StorageProviders{ + MinIO: map[string]*types.MinIOProvider{types.DefaultProvider: { + Region: "us-east-1", + Endpoint: server.URL, + AccessKey: "ak", + SecretKey: "sk"}}, + }, + Owner: "somelonguid@egi.eu", + AllowedUsers: []string{"somelonguid1@egi.eu"}} + back.Service = svc + + // and set the MinIO endpoint to the fake server + cfg := types.Config{ + MinIOProvider: &types.MinIOProvider{ + Region: "us-east-1", + Endpoint: server.URL, + AccessKey: "ak", + SecretKey: "sk", + }, + } + + r := gin.Default() + r.Use(func(c *gin.Context) { + c.Set("uidOrigin", "somelonguid@egi.eu") + c.Next() + }) + r.PUT("/system/services", MakeUpdateHandler(&cfg, back)) + + w := httptest.NewRecorder() + body := strings.NewReader(` + { + "name": "cowsay", + "cluster_id": "oscar", + "memory": "1Gi", + "cpu": "1.0", + "log_level": "CRITICAL", + "image": "ghcr.io/grycap/cowsay", + "alpine": false, + "script": "test", + "input": [ + { + "storage_provider": "minio", + "path": "/input" + } + ], + "output": [ + { + "storage_provider": "webdav.id", + "path": "/output" + } + ], + "storage_providers": { + "webdav": { + "id": { + "hostname": "` + server.URL + `", + "login": "user", + "password": "pass" + } + } + }, + "allowed_users": ["user1", "user2"] + } + `) + req, _ := http.NewRequest("PUT", "/system/services", body) + req.Header.Set("Authorization", "Bearer token") + r.ServeHTTP(w, req) + + // Close the fake MinIO server + defer server.Close() + + if w.Code != http.StatusNoContent { + fmt.Println(w.Body) + t.Errorf("expecting code %d, got %d", http.StatusNoContent, w.Code) + } + +} diff --git a/pkg/imagepuller/daemonset_test.go b/pkg/imagepuller/daemonset_test.go new file mode 100644 index 00000000..1b29e709 --- /dev/null +++ b/pkg/imagepuller/daemonset_test.go @@ -0,0 +1,74 @@ +/* +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 imagepuller + +import ( + "testing" + + "bou.ke/monkey" + "github.com/grycap/oscar/v3/pkg/types" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/fake" +) + +func TestCreateDaemonset(t *testing.T) { + cfg := &types.Config{ + ServicesNamespace: "default", + } + service := types.Service{ + Name: "test-service", + Image: "test-image", + ImagePullSecrets: []string{"test-secret"}, + } + kubeClientset := fake.NewSimpleClientset() + + // Patch the watchPods function to return a mock result + monkey.Patch(watchPods, func(kubernetes.Interface, *types.Config) { + }) + + err := CreateDaemonset(cfg, service, kubeClientset) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + actions := kubeClientset.Actions() + if len(actions) != 2 { + t.Errorf("Expected 2 action but got %d", len(actions)) + } + if actions[0].GetVerb() != "list" || actions[0].GetResource().Resource != "nodes" { + t.Errorf("Expected create job action but got %v", actions[0]) + } + if actions[1].GetVerb() != "create" || actions[1].GetResource().Resource != "daemonsets" { + t.Errorf("Expected create job action but got %v", actions[1]) + } + + daemonset := getDaemonset(cfg, service) + + if daemonset.Name != "image-puller-test-service" { + t.Errorf("expected daemonset name to be 'image-puller-test-service', got %s", daemonset.Name) + } + + if daemonset.Namespace != cfg.ServicesNamespace { + t.Errorf("expected daemonset namespace to be '%s', got %s", cfg.ServicesNamespace, daemonset.Namespace) + } + + if daemonset.Spec.Template.Spec.Containers[0].Image != service.Image { + t.Errorf("expected container image to be '%s', got %s", service.Image, daemonset.Spec.Template.Spec.Containers[0].Image) + } + + defer monkey.Unpatch(watchPods) +} diff --git a/pkg/resourcemanager/delegate_test.go b/pkg/resourcemanager/delegate_test.go new file mode 100644 index 00000000..03ee2581 --- /dev/null +++ b/pkg/resourcemanager/delegate_test.go @@ -0,0 +1,204 @@ +/* +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 resourcemanager + +import ( + "bytes" + "encoding/json" + "log" + "net/http" + "net/http/httptest" + "testing" + + "github.com/grycap/oscar/v3/pkg/types" +) + +func TestDelegateJob(t *testing.T) { + logger := log.New(bytes.NewBuffer([]byte{}), "", log.LstdFlags) + event := "test-event" + + // Mock server to simulate the cluster endpoint + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost && r.URL.Path == "/" { + w.WriteHeader(http.StatusOK) + return + } + if r.Method == http.MethodPost && r.URL.Path == "/job/test-service" { + w.WriteHeader(http.StatusCreated) + return + } + if r.Method == http.MethodGet && r.URL.Path == "/system/services/test-service" { + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(&types.Service{Token: "test-token"}) + return + } + if r.Method == http.MethodGet && r.URL.Path == "/system/status" { + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(&GeneralInfo{ + CPUMaxFree: 1000, + CPUFreeTotal: 2000, + }) + return + } + w.WriteHeader(http.StatusNotFound) + })) + defer server.Close() + + service := &types.Service{ + Name: "test-service", + ClusterID: "test-cluster", + CPU: "1", + Delegation: "static", + Replicas: []types.Replica{ + { + Type: "oscar", + ClusterID: "test-cluster", + ServiceName: "test-service", + Priority: 50, + Headers: map[string]string{"Content-Type": "application/json"}, + }, + }, + Clusters: map[string]types.Cluster{ + "test-cluster": { + Endpoint: server.URL, + AuthUser: "user", + AuthPassword: "password", + SSLVerify: false, + }, + }, + } + + t.Run("Replica type oscar", func(t *testing.T) { + err := DelegateJob(service, event, logger) + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + }) + + t.Run("Replica type oscar with delegation random", func(t *testing.T) { + service.Delegation = "random" + err := DelegateJob(service, event, logger) + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + }) + + t.Run("Replica type oscar with delegation load-based", func(t *testing.T) { + service.Delegation = "load-based" + err := DelegateJob(service, event, logger) + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + }) + + t.Run("Replica type endpoint", func(t *testing.T) { + service.Replicas[0].Type = "endpoint" + service.Replicas[0].URL = server.URL + err := DelegateJob(service, event, logger) + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + }) +} + +func TestWrapEvent(t *testing.T) { + providerID := "test-provider" + event := "test-event" + + expected := DelegatedEvent{ + StorageProviderID: providerID, + Event: event, + } + + result := WrapEvent(providerID, event) + + if result != expected { + t.Errorf("Expected %v, got %v", expected, result) + } +} + +func TestGetServiceToken(t *testing.T) { + replica := types.Replica{ + ServiceName: "test-service", + } + cluster := types.Cluster{ + Endpoint: "http://localhost:8080", + AuthUser: "user", + AuthPassword: "password", + SSLVerify: false, + } + + // Mock server to simulate the cluster endpoint + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet && r.URL.Path == "/system/services/test-service" { + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(&types.Service{Token: "test-token"}) + return + } + w.WriteHeader(http.StatusNotFound) + })) + defer server.Close() + + // Update the cluster endpoint to the mock server URL + cluster.Endpoint = server.URL + + token, err := getServiceToken(replica, cluster) + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + expectedToken := "test-token" + if token != expectedToken { + t.Errorf("Expected %v, got %v", expectedToken, token) + } +} + +func TestUpdateServiceToken(t *testing.T) { + replica := types.Replica{ + ServiceName: "test-service", + } + cluster := types.Cluster{ + Endpoint: "http://localhost:8080", + AuthUser: "user", + AuthPassword: "password", + SSLVerify: false, + } + + // Mock server to simulate the cluster endpoint + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet && r.URL.Path == "/system/services/test-service" { + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(&types.Service{Token: "test-token"}) + return + } + w.WriteHeader(http.StatusNotFound) + })) + defer server.Close() + + // Update the cluster endpoint to the mock server URL + cluster.Endpoint = server.URL + + token, err := updateServiceToken(replica, cluster) + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + expectedToken := "test-token" + if token != expectedToken { + t.Errorf("Expected %v, got %v", expectedToken, token) + } +} diff --git a/pkg/resourcemanager/rescheduler_test.go b/pkg/resourcemanager/rescheduler_test.go new file mode 100644 index 00000000..6f3939b7 --- /dev/null +++ b/pkg/resourcemanager/rescheduler_test.go @@ -0,0 +1,193 @@ +/* +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 resourcemanager + +import ( + "bytes" + "log" + "testing" + "time" + + "bou.ke/monkey" + "github.com/grycap/oscar/v3/pkg/backends" + "github.com/grycap/oscar/v3/pkg/types" + jobv1 "k8s.io/api/batch/v1" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" +) + +func TestGetReSchedulablePods(t *testing.T) { + // Define test namespace + namespace := "test-namespace" + + // Create test pods + pods := &v1.PodList{ + Items: []v1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + Namespace: namespace, + Labels: map[string]string{ + types.ServiceLabel: "service1", + types.ReSchedulerLabelKey: "10", + }, + CreationTimestamp: metav1.Time{Time: time.Now().Add(-15 * time.Second)}, + }, + Status: v1.PodStatus{ + Phase: v1.PodPending, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod2", + Namespace: namespace, + Labels: map[string]string{ + types.ServiceLabel: "service2", + types.ReSchedulerLabelKey: "20", + }, + CreationTimestamp: metav1.Time{Time: time.Now().Add(-5 * time.Second)}, + }, + Status: v1.PodStatus{ + Phase: v1.PodPending, + }, + }, + }, + } + + // Create a fake Kubernetes client + kubeClientset := fake.NewSimpleClientset(pods) + + // Call the function to test + reSchedulablePods, err := getReSchedulablePods(kubeClientset, namespace) + if err != nil { + t.Fatalf("error getting reschedulable pods: %v", err) + } + + // Check the results + if len(reSchedulablePods) != 1 { + t.Errorf("expected 1 reschedulable pod, got %d", len(reSchedulablePods)) + } + + if reSchedulablePods[0].Name != "pod1" { + t.Errorf("expected pod1 to be reschedulable, got %s", reSchedulablePods[0].Name) + } +} + +func TestGetReScheduleInfos(t *testing.T) { + // Define test namespace + namespace := "test-namespace" + + // Create test pods + pods := []v1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + Namespace: namespace, + Labels: map[string]string{ + types.ServiceLabel: "service1", + types.ReSchedulerLabelKey: "10", + }, + CreationTimestamp: metav1.Time{Time: time.Now().Add(-15 * time.Second)}, + }, + Status: v1.PodStatus{ + Phase: v1.PodPending, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod2", + Namespace: namespace, + Labels: map[string]string{ + types.ServiceLabel: "service2", + types.ReSchedulerLabelKey: "20", + }, + CreationTimestamp: metav1.Time{Time: time.Now().Add(-5 * time.Second)}, + }, + Status: v1.PodStatus{ + Phase: v1.PodPending, + }, + }, + } + + back := backends.MakeFakeBackend() + // Call the function to test + reScheduleInfos := getReScheduleInfos(pods, back) + if reScheduleInfos == nil { + t.Fatalf("error getting reschedule infos") + } + +} + +func TestStartReScheduler(t *testing.T) { + // Define test namespace + namespace := "test-namespace" + + // Create test pods + pods := &v1.PodList{ + Items: []v1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + Namespace: namespace, + Labels: map[string]string{ + types.ServiceLabel: "service1", + types.ReSchedulerLabelKey: "10", + "job-name": "job1", + }, + CreationTimestamp: metav1.Time{Time: time.Now().Add(-15 * time.Second)}, + }, + Status: v1.PodStatus{ + Phase: v1.PodPending, + }, + }, + }, + } + jobs := &jobv1.JobList{ + Items: []jobv1.Job{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "job1", + Namespace: namespace, + }, + }, + }, + } + + // Create a fake Kubernetes client + kubeClientset := fake.NewSimpleClientset(pods, jobs) + back := backends.MakeFakeBackend() + cfg := &types.Config{ + ReSchedulerInterval: 5, + ServicesNamespace: namespace, + } + + // Mock the Delegate function using monkey patching + monkey.Patch(DelegateJob, func(service *types.Service, event string, logger *log.Logger) error { + return nil + }) + var buf bytes.Buffer + reSchedulerLogger = log.New(&buf, "[RE-SCHEDULER] ", log.Flags()) + // Call the function to test + go StartReScheduler(cfg, back, kubeClientset) + time.Sleep(2 * time.Second) + + defer monkey.Unpatch(DelegateJob) + if buf.String() != "" { + t.Fatalf("error starting rescheduler: %v", buf.String()) + } +} diff --git a/pkg/types/expose.go b/pkg/types/expose.go index 6ce2499c..09b758b4 100644 --- a/pkg/types/expose.go +++ b/pkg/types/expose.go @@ -152,7 +152,7 @@ func UpdateExpose(service Service, kubeClientset kubernetes.Interface, cfg *Conf // TODO check and refactor // Main function that list all the kubernetes components -// This function is not used, in the future could be usefull +// This function is not used, in the future could be useful func ListExpose(kubeClientset kubernetes.Interface, cfg *Config) error { deploy, hpa, err := listDeployments(kubeClientset, cfg) diff --git a/pkg/types/interlink_test.go b/pkg/types/interlink_test.go new file mode 100644 index 00000000..3b15f8ba --- /dev/null +++ b/pkg/types/interlink_test.go @@ -0,0 +1,120 @@ +/* +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" + "testing" + + v1 "k8s.io/api/core/v1" +) + +func TestSetInterlinkJob(t *testing.T) { + podSpec := &v1.PodSpec{} + service := &Service{InterLinkNodeName: "test-node"} + cfg := &Config{SupervisorKitImage: "test-image"} + eventBytes := []byte("test-event") + + command, event, args := SetInterlinkJob(podSpec, service, cfg, eventBytes) + + if len(command) != 2 || command[0] != "/bin/sh" || command[1] != "-c" { + t.Errorf("Unexpected command: %v", command) + } + + expectedEventValue := base64.StdEncoding.EncodeToString(eventBytes) + if event.Name != EventVariable || event.Value != expectedEventValue { + t.Errorf("Unexpected event: %v", event) + } + + expectedArgs := "echo $EVENT | base64 -d | " + SupervisorMountPath + "/supervisor" + if len(args) != 1 || args[0] != expectedArgs { + t.Errorf("Unexpected args: %v", args) + } + + if podSpec.NodeSelector[NodeSelectorKey] != service.InterLinkNodeName { + t.Errorf("Unexpected NodeSelector: %v", podSpec.NodeSelector) + } + + if podSpec.DNSPolicy != InterLinkDNSPolicy { + t.Errorf("Unexpected DNSPolicy: %v", podSpec.DNSPolicy) + } + + if podSpec.RestartPolicy != InterLinkRestartPolicy { + t.Errorf("Unexpected RestartPolicy: %v", podSpec.RestartPolicy) + } + + if len(podSpec.Tolerations) != 1 || podSpec.Tolerations[0].Key != InterLinkTolerationKey || podSpec.Tolerations[0].Operator != InterLinkTolerationOperator { + t.Errorf("Unexpected Tolerations: %v", podSpec.Tolerations) + } +} + +func TestSetInterlinkService(t *testing.T) { + podSpec := &v1.PodSpec{ + Containers: []v1.Container{ + {}, + }, + } + + SetInterlinkService(podSpec) + + if podSpec.Containers[0].ImagePullPolicy != "Always" { + t.Errorf("Unexpected ImagePullPolicy: %v", podSpec.Containers[0].ImagePullPolicy) + } + + if len(podSpec.Containers[0].VolumeMounts) != 1 || podSpec.Containers[0].VolumeMounts[0].Name != NameSupervisorVolume || podSpec.Containers[0].VolumeMounts[0].MountPath != SupervisorMountPath { + t.Errorf("Unexpected VolumeMounts: %v", podSpec.Containers[0].VolumeMounts) + } + + if len(podSpec.Volumes) != 1 || podSpec.Volumes[0].Name != NameSupervisorVolume || podSpec.Volumes[0].VolumeSource.EmptyDir == nil { + t.Errorf("Unexpected Volumes: %v", podSpec.Volumes) + } +} + +func TestAddInitContainer(t *testing.T) { + podSpec := &v1.PodSpec{} + cfg := &Config{SupervisorKitImage: "test-image"} + + addInitContainer(podSpec, cfg) + + if len(podSpec.InitContainers) != 1 { + t.Fatalf("Expected 1 init container, got %d", len(podSpec.InitContainers)) + } + + initContainer := podSpec.InitContainers[0] + if initContainer.Name != ContainerSupervisorName { + t.Errorf("Unexpected init container name: %v", initContainer.Name) + } + + if len(initContainer.Command) != 2 || initContainer.Command[0] != "/bin/sh" || initContainer.Command[1] != "-c" { + t.Errorf("Unexpected init container command: %v", initContainer.Command) + } + + if len(initContainer.Args) != 1 || initContainer.Args[0] != SupervisorArg { + t.Errorf("Unexpected init container args: %v", initContainer.Args) + } + + if initContainer.Image != cfg.SupervisorKitImage { + t.Errorf("Unexpected init container image: %v", initContainer.Image) + } + + if initContainer.ImagePullPolicy != v1.PullIfNotPresent { + t.Errorf("Unexpected init container image pull policy: %v", initContainer.ImagePullPolicy) + } + + if len(initContainer.VolumeMounts) != 1 || initContainer.VolumeMounts[0].Name != NameSupervisorVolume || initContainer.VolumeMounts[0].MountPath != SupervisorMountPath { + t.Errorf("Unexpected init container volume mounts: %v", initContainer.VolumeMounts) + } +} diff --git a/pkg/types/mount_test.go b/pkg/types/mount_test.go new file mode 100644 index 00000000..df7a3945 --- /dev/null +++ b/pkg/types/mount_test.go @@ -0,0 +1,157 @@ +/* +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 ( + "testing" + + v1 "k8s.io/api/core/v1" +) + +func TestSetMount(t *testing.T) { + podSpec := &v1.PodSpec{} + service := Service{ + Mount: StorageIOConfig{ + Provider: "minio.provider", + Path: "test-bucket", + }, + StorageProviders: &StorageProviders{ + MinIO: map[string]*MinIOProvider{ + "provider": { + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + Endpoint: "test-endpoint", + }, + }, + }, + } + cfg := &Config{} + + SetMount(podSpec, service, cfg) + + if len(podSpec.Containers) != 1 { + t.Fatalf("expected 1 container, got %d", len(podSpec.Containers)) + } + + container := podSpec.Containers[0] + if container.Name != rcloneContainerName { + t.Errorf("expected container name %s, got %s", rcloneContainerName, container.Name) + } + + if container.Image != rcloneContainerImage { + t.Errorf("expected container image %s, got %s", rcloneContainerImage, container.Image) + } + + expectedEnvVars := map[string]string{ + "MNT_POINT": rcloneFolderMount, + "MINIO_BUCKET": "test-bucket", + "AWS_ACCESS_KEY_ID": "test-access-key", + "AWS_SECRET_ACCESS_KEY": "test-secret-key", + "MINIO_ENDPOINT": "test-endpoint", + } + + for _, envVar := range container.Env { + if expectedValue, ok := expectedEnvVars[envVar.Name]; ok { + if envVar.Value != expectedValue { + t.Errorf("expected env var %s to have value %s, got %s", envVar.Name, expectedValue, envVar.Value) + } + } else { + t.Errorf("unexpected env var %s", envVar.Name) + } + } + + if len(container.VolumeMounts) != 4 { + t.Fatalf("expected 4 volume mounts, got %d", len(container.VolumeMounts)) + } + + if len(podSpec.Volumes) != 2 { + t.Fatalf("expected 2 volumes, got %d", len(podSpec.Volumes)) + } +} + +func TestSetMinIOEnvVars(t *testing.T) { + service := Service{ + Mount: StorageIOConfig{ + Path: "test-bucket", + }, + StorageProviders: &StorageProviders{ + MinIO: map[string]*MinIOProvider{ + "provider": { + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + Endpoint: "test-endpoint", + }, + }, + }, + } + providerId := "provider" + + envVars := setMinIOEnvVars(service, providerId) + + expectedEnvVars := map[string]string{ + "MINIO_BUCKET": "test-bucket", + "AWS_ACCESS_KEY_ID": "test-access-key", + "AWS_SECRET_ACCESS_KEY": "test-secret-key", + "MINIO_ENDPOINT": "test-endpoint", + } + + for _, envVar := range envVars { + if expectedValue, ok := expectedEnvVars[envVar.Name]; ok { + if envVar.Value != expectedValue { + t.Errorf("expected env var %s to have value %s, got %s", envVar.Name, expectedValue, envVar.Value) + } + } else { + t.Errorf("unexpected env var %s", envVar.Name) + } + } +} + +func TestSetWebDavEnvVars(t *testing.T) { + service := Service{ + Mount: StorageIOConfig{ + Path: "test-folder", + }, + StorageProviders: &StorageProviders{ + WebDav: map[string]*WebDavProvider{ + "provider": { + Login: "test-login", + Password: "test-password", + Hostname: "test-hostname", + }, + }, + }, + } + providerId := "provider" + + envVars := setWebDavEnvVars(service, providerId) + + expectedEnvVars := map[string]string{ + "WEBDAV_FOLDER": "test-folder", + "WEBDAV_LOGIN": "test-login", + "WEBDAV_PASSWORD": "test-password", + "WEBDAV_HOSTNAME": "https://test-hostname", + } + + for _, envVar := range envVars { + if expectedValue, ok := expectedEnvVars[envVar.Name]; ok { + if envVar.Value != expectedValue { + t.Errorf("expected env var %s to have value %s, got %s", envVar.Name, expectedValue, envVar.Value) + } + } else { + t.Errorf("unexpected env var %s", envVar.Name) + } + } +} diff --git a/pkg/types/replica_test.go b/pkg/types/replica_test.go new file mode 100644 index 00000000..efa5d896 --- /dev/null +++ b/pkg/types/replica_test.go @@ -0,0 +1,69 @@ +/* +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 ( + "sort" + "testing" +) + +func TestReplicaList_Len(t *testing.T) { + replicas := ReplicaList{ + {Type: "oscar", Priority: 1}, + {Type: "endpoint", Priority: 2}, + } + expected := 2 + if replicas.Len() != expected { + t.Errorf("expected %d, got %d", expected, len(replicas)) + } +} + +func TestReplicaList_Swap(t *testing.T) { + replicas := ReplicaList{ + {Type: "oscar", Priority: 1}, + {Type: "endpoint", Priority: 2}, + } + replicas.Swap(0, 1) + if replicas[0].Priority != 2 || replicas[1].Priority != 1 { + t.Errorf("Swap did not work as expected") + } +} + +func TestReplicaList_Less(t *testing.T) { + replicas := ReplicaList{ + {Type: "oscar", Priority: 1}, + {Type: "endpoint", Priority: 2}, + } + if !replicas.Less(0, 1) { + t.Errorf("expected replicas[0] to be less than replicas[1]") + } + if replicas.Less(1, 0) { + t.Errorf("expected replicas[1] to not be less than replicas[0]") + } +} + +func TestReplicaList_Sort(t *testing.T) { + replicas := ReplicaList{ + {Type: "endpoint", Priority: 2}, + {Type: "oscar", Priority: 1}, + {Type: "oscar", Priority: 0}, + } + sort.Sort(replicas) + if replicas[0].Priority != 0 || replicas[1].Priority != 1 || replicas[2].Priority != 2 { + t.Errorf("Sort did not work as expected") + } +} diff --git a/pkg/types/service.go b/pkg/types/service.go index f8d51e67..20098ab3 100644 --- a/pkg/types/service.go +++ b/pkg/types/service.go @@ -260,7 +260,7 @@ type Service struct { InterLinkNodeName string `json:"interlink_node_name"` // List of EGI UID's identifying the users that will have visibility of the service and its MinIO storage provider - // Optional (If the list is empty we asume the visibility is public for all cluster users) + // Optional (If the list is empty we assume the visibility is public for all cluster users) AllowedUsers []string `json:"allowed_users"` // Configuration to create a storage provider as a volume inside the service container diff --git a/pkg/utils/auth/multitenancy.go b/pkg/utils/auth/multitenancy.go index 8e5a8118..a9f50d6e 100644 --- a/pkg/utils/auth/multitenancy.go +++ b/pkg/utils/auth/multitenancy.go @@ -32,12 +32,12 @@ const ServicesNamespace = "oscar-svc" const ServiceLabelLength = 8 type MultitenancyConfig struct { - kubeClientset *kubernetes.Clientset + kubeClientset kubernetes.Interface owner_uid string usersCache []string } -func NewMultitenancyConfig(kubeClientset *kubernetes.Clientset, uid string) *MultitenancyConfig { +func NewMultitenancyConfig(kubeClientset kubernetes.Interface, uid string) *MultitenancyConfig { return &MultitenancyConfig{ kubeClientset: kubeClientset, owner_uid: uid, diff --git a/pkg/utils/minio_test.go b/pkg/utils/minio_test.go new file mode 100644 index 00000000..8121c579 --- /dev/null +++ b/pkg/utils/minio_test.go @@ -0,0 +1,123 @@ +/* +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 utils + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/grycap/oscar/v3/pkg/types" +) + +func createMinIOConfig() (types.Config, *httptest.Server) { + // Create a fake MinIO server + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, hreq *http.Request) { + if !strings.HasPrefix(hreq.URL.Path, "/minio/admin/v3/") { + rw.WriteHeader(http.StatusNotFound) + } + + if hreq.URL.Path == "/minio/admin/v3/info-canned-policy" { + rw.WriteHeader(http.StatusOK) + rw.Write([]byte(`{"PolicyName": "testpolicy", "Policy": {"Version": "version","Statement": [{"Resource": ["res"]}]}}`)) + } else { + rw.WriteHeader(http.StatusOK) + rw.Write([]byte(`{"Status": "success"}`)) + } + })) + + cfg := types.Config{ + MinIOProvider: &types.MinIOProvider{ + Endpoint: server.URL, + Region: "us-east-1", + AccessKey: "minioadmin", + SecretKey: "minioadmin", + Verify: false, + }, + Name: "test", + Namespace: "default", + ServicePort: 8080, + } + + return cfg, server +} + +func TestCreateMinIOUser(t *testing.T) { + // Create a fake MinIO server + cfg, server := createMinIOConfig() + + client, err := MakeMinIOAdminClient(&cfg) + + if err != nil { + t.Errorf("Error creating MinIO client: %v", err) + } + + err = client.CreateMinIOUser("testuser", "testpassword") + + if err != nil { + t.Errorf("Error creating MinIO user: %v", err) + } + + // Close the fake MinIO server + defer server.Close() +} + +func TestPublicToPrivateBucket(t *testing.T) { + // Create a fake MinIO server + cfg, server := createMinIOConfig() + + client, _ := MakeMinIOAdminClient(&cfg) + err := client.PublicToPrivateBucket("testbucket", []string{"testuser"}) + + if err != nil { + t.Errorf("Error creating MinIO user: %v", err) + } + + // Close the fake MinIO server + defer server.Close() +} + +func TestCreateServiceGroup(t *testing.T) { + // Create a fake MinIO server + cfg, server := createMinIOConfig() + + client, _ := MakeMinIOAdminClient(&cfg) + err := client.CreateServiceGroup("bucket") + + if err != nil { + t.Errorf("Error creating MinIO user: %v", err) + } + + // Close the fake MinIO server + defer server.Close() +} + +func TestPrivateToPublicBucket(t *testing.T) { + // Create a fake MinIO server + cfg, server := createMinIOConfig() + + client, _ := MakeMinIOAdminClient(&cfg) + err := client.PrivateToPublicBucket("testbucket") + + if err != nil { + t.Errorf("Error creating MinIO user: %v", err) + } + + // Close the fake MinIO server + defer server.Close() +} diff --git a/pkg/utils/of_scaler_test.go b/pkg/utils/of_scaler_test.go new file mode 100644 index 00000000..1a35370b --- /dev/null +++ b/pkg/utils/of_scaler_test.go @@ -0,0 +1,196 @@ +/* +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 utils + +import ( + "bytes" + "log" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/grycap/oscar/v3/pkg/types" + "github.com/prometheus/client_golang/api" + v1 "github.com/prometheus/client_golang/api/prometheus/v1" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" +) + +func TestNewOFScaler(t *testing.T) { + kubeClientset := fake.NewSimpleClientset() + cfg := &types.Config{ + OpenfaasNamespace: "openfaas", + ServicesNamespace: "default", + OpenfaasPort: 8080, + OpenfaasBasicAuthSecret: "basic-auth", + OpenfaasPrometheusPort: 9090, + OpenfaasScalerInactivityDuration: "5m", + OpenfaasScalerInterval: "1m", + } + + scaler := NewOFScaler(kubeClientset, cfg) + + if scaler.openfaasNamespace != "openfaas" { + t.Errorf("Expected openfaasNamespace to be 'openfaas', got %s", scaler.openfaasNamespace) + } + if scaler.namespace != "default" { + t.Errorf("Expected namespace to be 'default', got %s", scaler.namespace) + } + if scaler.gatewayEndpoint != "http://gateway.openfaas:8080" { + t.Errorf("Expected gatewayEndpoint to be 'http://gateway.openfaas:8080', got %s", scaler.gatewayEndpoint) + } + if scaler.prometheusEndpoint != "http://prometheus.openfaas:9090" { + t.Errorf("Expected prometheusEndpoint to be 'http://prometheus.openfaas:9090', got %s", scaler.prometheusEndpoint) + } +} + +func TestGetScalableFunctions(t *testing.T) { + // Create a deployment with the label "com.openfaas.scale.zero" set to "true" + deployment := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-function", + Namespace: "default", + }, + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "com.openfaas.scale.zero": "true", + }, + }, + }, + }, + Status: appsv1.DeploymentStatus{ + Replicas: 1, + }, + } + + kubeClientset := fake.NewSimpleClientset(deployment) + scaler := &OpenfaasScaler{ + kubeClientset: kubeClientset, + namespace: "default", + } + + functions, err := scaler.getScalableFunctions() + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + if len(functions) != 1 { + t.Errorf("Expected 1 function, got %d", len(functions)) + } + if functions[0] != "test-function" { + t.Errorf("Expected function name to be 'test-function', got %s", functions[0]) + } +} + +func TestScaleToZero(t *testing.T) { + kubeClientset := fake.NewSimpleClientset() + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, hreq *http.Request) { + })) + + scaler := &OpenfaasScaler{ + kubeClientset: kubeClientset, + gatewayEndpoint: server.URL, + } + + err := scaler.scaleToZero("test-function", "user", "pass", server.Client()) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } +} + +func TestIsIdle(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, hreq *http.Request) { + if hreq.URL.Path == "/api/v1/query" { + rw.Write([]byte(`{"status":"success","data":{"resultType":"vector","result":[{"metric":{},"value":[1620810000,"0"]}]},"error":null}`)) + } + })) + + prometheusClient, _ := api.NewClient(api.Config{ + Address: server.URL, + }) + prometheusAPIClient := v1.NewAPI(prometheusClient) + + idle := isIdle("test-function", "default", "5m", prometheusAPIClient) + if !idle { + t.Errorf("Expected function to be idle") + } +} + +func TestStart(t *testing.T) { + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "basic-auth", + Namespace: "openfaas", + }, + Data: map[string][]byte{ + "basic-auth-user": []byte("user"), + "basic-auth-password": []byte("pass"), + }, + } + deployment := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-function", + Namespace: "default", + }, + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "com.openfaas.scale.zero": "true", + }, + }, + }, + }, + Status: appsv1.DeploymentStatus{ + Replicas: 1, + }, + } + kubeClientset := fake.NewSimpleClientset(secret, deployment) + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, hreq *http.Request) { + if hreq.URL.Path == "/api/v1/query" { + rw.Write([]byte(`{"status":"success","data":{"resultType":"vector","result":[{"metric":{},"value":[1620810000,"1"]}]},"error":null}`)) + } + })) + + cfg := &types.Config{ + OpenfaasNamespace: "openfaas", + ServicesNamespace: "default", + OpenfaasPort: 8080, + OpenfaasBasicAuthSecret: "basic-auth", + OpenfaasPrometheusPort: 9090, + OpenfaasScalerInactivityDuration: "5m", + OpenfaasScalerInterval: "0.5s", + } + + scaler := NewOFScaler(kubeClientset, cfg) + scaler.gatewayEndpoint = server.URL + scaler.prometheusEndpoint = server.URL + + var buf bytes.Buffer + scalerLogger = log.New(&buf, "[OF-SCALER] ", log.Flags()) + + go scaler.Start() + time.Sleep(1 * time.Second) + + if buf.String() != "" { + t.Errorf("Unexpected log output: %s", buf.String()) + } +} diff --git a/pkg/utils/token_test.go b/pkg/utils/token_test.go new file mode 100644 index 00000000..c2b25867 --- /dev/null +++ b/pkg/utils/token_test.go @@ -0,0 +1,49 @@ +/* +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 utils + +import ( + "encoding/hex" + "testing" +) + +// TestGenerateTokenLength checks if the generated token has the correct length +func TestGenerateTokenLength(t *testing.T) { + token := GenerateToken() + expectedLength := 64 // 32 bytes * 2 (hex encoding) + if len(token) != expectedLength { + t.Errorf("Expected token length of %d, but got %d", expectedLength, len(token)) + } +} + +// TestGenerateTokenUniqueness checks if multiple generated tokens are unique +func TestGenerateTokenUniqueness(t *testing.T) { + token1 := GenerateToken() + token2 := GenerateToken() + if token1 == token2 { + t.Error("Expected tokens to be unique, but they are the same") + } +} + +// TestGenerateTokenHexEncoding checks if the generated token is a valid hex string +func TestGenerateTokenHexEncoding(t *testing.T) { + token := GenerateToken() + _, err := hex.DecodeString(token) + if err != nil { + t.Errorf("Expected a valid hex string, but got an error: %v", err) + } +} diff --git a/pkg/utils/yunikorn.go b/pkg/utils/yunikorn.go index 38f3f6f0..fbf9fb07 100644 --- a/pkg/utils/yunikorn.go +++ b/pkg/utils/yunikorn.go @@ -151,7 +151,7 @@ func DeleteYunikornQueue(cfg *types.Config, kubeClientset kubernetes.Interface, // getOscarQueue returns a pointer to the OSCAR's Yunikorn queue (configs.QueueConfig) // If the Queue doesn't exists, create a new one in the SchedulerConfig -// (the existance of the default partition and the root queue is assumed) +// (the existence of the default partition and the root queue is assumed) func getOscarQueue(schedulerConfig *configs.SchedulerConfig) *configs.QueueConfig { // First get a pointer to the root queue root := &configs.QueueConfig{} diff --git a/pkg/utils/yunikorn_test.go b/pkg/utils/yunikorn_test.go new file mode 100644 index 00000000..bef3181d --- /dev/null +++ b/pkg/utils/yunikorn_test.go @@ -0,0 +1,122 @@ +/* +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 utils + +import ( + "testing" + + "github.com/apache/yunikorn-core/pkg/common/configs" + "github.com/grycap/oscar/v3/pkg/types" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" +) + +func getFakeClientset() (*types.Config, *fake.Clientset) { + cfg := &types.Config{ + YunikornNamespace: "default", + YunikornConfigMap: "yunikorn-config", + YunikornConfigFileName: "yunikorn.yaml", + } + + cfgmap := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: cfg.YunikornConfigMap, + Namespace: cfg.YunikornNamespace, + }, + Data: map[string]string{ + cfg.YunikornConfigFileName: ` +partitions: + - name: default + queues: + - name: root + queues: + - name: oscar + queues: + - name: test-service +`, + }, + } + return cfg, fake.NewSimpleClientset(cfgmap) +} + +func TestReadYunikornConfig(t *testing.T) { + cfg, clientset := getFakeClientset() + + schedulerConfig, err := readYunikornConfig(cfg, clientset) + if err != nil { + t.Errorf("Error Reading Yunikorn config: %v", err) + } + + if schedulerConfig.Partitions[0].Name != "default" { + t.Errorf("Error Reading Yunikorn config. SchedulerConfig is nil") + } +} + +func TestUpdateYunikornConfig(t *testing.T) { + cfg, clientset := getFakeClientset() + + schedulerConfig := &configs.SchedulerConfig{ + Partitions: []configs.PartitionConfig{ + { + Name: "default", + Queues: []configs.QueueConfig{ + { + Name: "root", + Queues: []configs.QueueConfig{ + { + Name: "oscar", + }, + }, + }, + }, + }, + }, + } + + err := updateYunikornConfig(cfg, clientset, schedulerConfig) + if err != nil { + t.Errorf("Error Updating Yunikorn config: %v", err) + } +} + +func TestAddYunikornQueue(t *testing.T) { + cfg, clientset := getFakeClientset() + + svc := &types.Service{ + Name: "test-service", + TotalMemory: "4Gi", + TotalCPU: "2", + } + + err := AddYunikornQueue(cfg, clientset, svc) + if err != nil { + t.Errorf("Error Adding Yunikorn config: %v", err) + } +} + +func TestDeleteYunikornQueue(t *testing.T) { + cfg, clientset := getFakeClientset() + + svc := &types.Service{ + Name: "test-service", + } + + err := DeleteYunikornQueue(cfg, clientset, svc) + if err != nil { + t.Errorf("Error Deleting Yunikorn config: %v", err) + } +}