diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 7dc82a30..29046a7c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -23,5 +23,4 @@ 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 8323daec..6ae866a2 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -14,22 +14,19 @@ 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 @@ -55,7 +52,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 . All +reported by contacting the project team at products@grycap.upv.es. 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/README.md b/README.md index c92d4a4e..730e88f4 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # OSCAR - Open Source Serverless Computing for Data-Processing Applications -[![Go Report Card](https://goreportcard.com/badge/github.com/grycap/oscar/v3)](https://goreportcard.com/report/github.com/grycap/oscar/v3) -[![Codacy Badge](https://app.codacy.com/project/badge/Coverage/8145efdfb9d24af1b5b53e21c6e2df99)](https://app.codacy.com/gh/grycap/oscar/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_coverage) +[![Go Report Card](https://goreportcard.com/badge/github.com/grycap/oscar)](https://goreportcard.com/report/github.com/grycap/oscar) +[![Codacy Badge](https://app.codacy.com/project/badge/Coverage/8145efdfb9d24af1b5b53e21c6e2df99)](https://www.codacy.com/gh/grycap/oscar/dashboard?utm_source=github.com&utm_medium=referral&utm_content=grycap/oscar&utm_campaign=Badge_Coverage) [![tests](https://github.com/grycap/oscar/actions/workflows/tests.yaml/badge.svg?branch=master)](https://github.com/grycap/oscar/actions/workflows/tests.yaml) [![build](https://github.com/grycap/oscar/workflows/build/badge.svg)](https://github.com/grycap/oscar/actions?query=workflow%3Abuild) [![GitHub release (latest by date)](https://img.shields.io/github/v/release/grycap/oscar)](https://github.com/grycap/oscar/pkgs/container/oscar) diff --git a/deploy/ansible/README.md b/deploy/ansible/README.md index bb79f54a..cd10fd90 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. +Please refer to the [docs](https://docs.oscar.grycap.net/deploy-ansible/) for instructions. \ No newline at end of file diff --git a/docs/api.md b/docs/api.md index 6bef9b69..07afb9af 100644 --- a/docs/api.md +++ b/docs/api.md @@ -4,8 +4,4 @@ OSCAR exposes a secure REST API available at the Kubernetes master's node IP through an Ingress Controller. This API has been described following the [OpenAPI Specification](https://www.openapis.org/) and it is available below. -> ℹ️ -> -> The bearer token used to run a service can be either the OSCAR [service access token](invoking-sync.md#service-access-tokens) or the [user's Access Token](integration-egi.md#obtaining-an-access-token) if the OSCAR cluster is integrated with EGI Check-in. - !!swagger api.yaml!! diff --git a/docs/api.yaml b/docs/api.yaml index be36ec1a..90d5be14 100644 --- a/docs/api.yaml +++ b/docs/api.yaml @@ -33,7 +33,6 @@ paths: description: List all created services security: - basicAuth: [] - - token: [] tags: - services post: @@ -51,7 +50,6 @@ paths: description: Create a service security: - basicAuth: [] - - token: [] requestBody: content: application/json: @@ -76,7 +74,6 @@ paths: description: Update a service security: - basicAuth: [] - - token: [] requestBody: content: application/json: @@ -111,7 +108,6 @@ paths: operationId: ReadService security: - basicAuth: [] - - token: [] description: Read a service delete: summary: Delete service @@ -128,7 +124,6 @@ paths: description: Delete a service security: - basicAuth: [] - - token: [] tags: - services '/system/logs/{serviceName}': @@ -162,7 +157,6 @@ paths: operationId: ListJobs security: - basicAuth: [] - - token: [] description: List all jobs with their status delete: summary: Delete jobs @@ -179,7 +173,6 @@ paths: description: Delete all jobs from a service. security: - basicAuth: [] - - token: [] parameters: - schema: type: boolean @@ -221,7 +214,6 @@ paths: description: Get the logs from a job security: - basicAuth: [] - - token: [] parameters: - schema: type: boolean @@ -242,7 +234,6 @@ paths: description: Delete a job security: - basicAuth: [] - - token: [] tags: - logs /system/info: @@ -265,7 +256,6 @@ paths: description: Get system info security: - basicAuth: [] - - token: [] /health: get: summary: Health @@ -326,7 +316,6 @@ paths: description: Get system configuration security: - basicAuth: [] - - token: [] '/run/{serviceName}': parameters: - schema: @@ -618,6 +607,4 @@ servers: - url: 'https://localhost' description: 'Local testing' - url: 'https://inference.cloud.ai4eosc.eu' - description: 'AI4EOSC OSCAR cluster' - - url: 'https://inference-walton.cloud.imagine-ai.eu' - description: 'iMagine OSCAR cluster' \ No newline at end of file + description: 'AI4EOSC OSCAR cluster' \ No newline at end of file diff --git a/docs/images/bucket-list.png b/docs/images/bucket-list.png deleted file mode 100644 index dba9aa46..00000000 Binary files a/docs/images/bucket-list.png and /dev/null differ diff --git a/docs/images/minio-ui.png b/docs/images/minio-ui.png deleted file mode 100644 index f90d710a..00000000 Binary files a/docs/images/minio-ui.png and /dev/null differ diff --git a/docs/images/oidc/egi-checkin-token-portal.png b/docs/images/oidc/egi-checkin-token-portal.png deleted file mode 100644 index 41e0d08b..00000000 Binary files a/docs/images/oidc/egi-checkin-token-portal.png and /dev/null differ diff --git a/docs/images/oscar-info.png b/docs/images/oscar-info.png deleted file mode 100644 index 1c9b969e..00000000 Binary files a/docs/images/oscar-info.png and /dev/null differ diff --git a/docs/integration-egi.md b/docs/integration-egi.md index f7a348c2..18434035 100644 --- a/docs/integration-egi.md +++ b/docs/integration-egi.md @@ -67,7 +67,7 @@ grant access for all users from that VO. The static web interface of OSCAR has been integrated with EGI Check-in and published in [ui.oscar.grycap.net](https://ui.oscar.grycap.net) to facilitate -the authorization of users. To login through EGI Check-In using OIDC tokens, +the authorization of users. To login through EGI Checkín using OIDC tokens, users only have to put the endpoint of its OSCAR cluster and click on the "EGI CHECK-IN" button. diff --git a/docs/invoking-async.md b/docs/invoking-async.md index b480cfb0..60119924 100644 --- a/docs/invoking-async.md +++ b/docs/invoking-async.md @@ -2,11 +2,11 @@ For event-driven file processing, OSCAR automatically manages the creation and [notification system](https://docs.min.io/minio/baremetal/monitoring/bucket-notifications/bucket-notifications.html#minio-bucket-notifications) -of MinIO buckets. This allow the event-driven invocation of services -using asynchronous requests for every file uploaded to the bucket, which generates a Kubernetes job for every file to be processed. +of MinIO buckets in order to allow the event-driven invocation of services +using asynchronous requests, generating a Kubernetes job for every file to be +processed. + ![oscar-async.png](images/oscar-async.png) -These jobs will be queued up in the Kubernetes scheduler and will be processed whenever there are resources available. If OSCAR cluster has been deployed as an elastic Kubernetes cluster (see [Deployment with IM](https://docs.oscar.grycap.net/deploy-im-dashboard/)), then new Virtual Machines will be provisioned (up to the maximum number of nodes defined) in the underlying Cloud platform and seamlessly integrated in the Kubernetes clusters to proceed with the execution of jobs. These nodes will be terminated as the worload is reduced. Notice that the output files can be stores in MinIO or in any other storage back-end supported by the [FaaS supervisor](oscar-service.md#faas-supervisor). -If you want to process a large number of data files, consider using [OSCAR Batch](https://github.com/grycap/oscar-batch), a tool designed to perform batch-based processing in OSCAR clusters. It includes a coordinator tool where the user provides a MinIO bucket containing files for processing. This service calculates the optimal number of parallel service invocations that can be accommodated within the cluster, according to its current status, and distributes the image processing workload accordingly among the service invocations. This is mainly intended to process large amounts of files, for example, historical data. diff --git a/docs/invoking-sync.md b/docs/invoking-sync.md index 9dd75329..56fcc000 100644 --- a/docs/invoking-sync.md +++ b/docs/invoking-sync.md @@ -83,8 +83,8 @@ base64 input.png | curl -X POST -H "Authorization: Bearer " \ ## Service access tokens -As detailed in the [API specification](api.md), invocation paths require either the -service access token or the Access Token of the user when the cluster is integrated with EGI Check-in, in the request header for authentication (any of them is valid). Service access +As detailed in the [API specification](api.md), invocation paths require the +service access token in the request header for authentication. Service access tokens are auto-generated in service creation and update, and MinIO eventing system is automatically configured to use them for event-driven file processing. Tokens can be obtained through the API, using the diff --git a/docs/invoking.md b/docs/invoking.md index 1c0c43b9..25a0664a 100644 --- a/docs/invoking.md +++ b/docs/invoking.md @@ -2,16 +2,7 @@ OSCAR services can be executed: - - [Synchronously](invoking-sync.md), so that the invocation to the service blocks the client until the response is obtained. + - [Synchronously](invoking-sync.md), so that the invocation to the service blocks the client until the response is obtained. Useful for short-lived service invocations. - [Asynchronously](invoking-async.md), typically in response to a file upload to MinIO or via the OSCAR API. - - As an [exposed service](exposed-services.md), where the application executed already provides its own API or user interface (e.g. Jupyter Notebook) - - -After reading the different service execution types, take into account the following considerations to better decide the most appropriate execution type for your use case: - -* **Scalability**: Asynchronous invocations provide the best throughput when dealing with multiple concurrent data processing requests, since these are processed by independent jobs which are managed by the Kubernetes scheduler. A two-level elasticity approach is used (increase in the number of pods and increase in the number of Virtual Machines, if the OSCAR cluster was configured to be elastic). This is the recommended approach when each processing request exceeds the order of tens of seconds. - -* **Reduced Latency** Synchronous invocations are oriented for short-lived (< tens of seconds) bursty requests. A certain number of containers can be configured to be kept alive to avoid the performance penalty of spawning new ones while providing an upper bound limit (see [`min_scale` and `max_scale` in the FDL](fdl.md#synchronoussettings), at the expense of always consuming resources in the OSCAR cluster. If the processing file is in the order of several MBytes it may not fit in the payload of the HTTP request. - -* **Easy Access** For services that provide their own user interface or their own API, exposed services provide the ability to execute them in OSCAR and benefit for an auto-scaled configuration in case they are [stateless](https://en.wikipedia.org/wiki/Service_statelessness_principle). This way, users can directly access the service using its well-known interfaces by the users. + - As an [exposed service](exposed-services.md), where the application executed already provides its own API or user interface (e.g. a Jupyter Notebook) diff --git a/docs/minio-usage.md b/docs/minio-usage.md deleted file mode 100644 index 32fd22a6..00000000 --- a/docs/minio-usage.md +++ /dev/null @@ -1,50 +0,0 @@ -# Using the MinIO Storage Provider - -Each OSCAR cluster includes a deployed MinIO instance, which is used to trigger service executions. When a service is configured to use MinIO as its storage provider, it monitors a specified input folder for new data. Whenever new data is added to this folder, it triggers the associated service to execute. - -## Using graphical interfaces - -These folders can be accessed via both the OSCAR UI and the MinIO console UI. - -- **Using OSCAR-UI**: The following image highlights the section where MinIO buckets are accessible. Users can view a list of available buckets and perform operations such as uploading and deleting files. - -![minio-buckets](images/bucket-list.png) - -- **Using the MinIO Console UI**: Access details for this interface are available in the "Info" tab within the OSCAR UI. This tab provides the MinIO console endpoint and the necessary credentials to log in, where the *Access Key* serves as the username, and the *Secret Key* functions as the password. - -![oscar-info](images/oscar-info.png) - -Finally, the following image provides an overview of the MinIO login panel and the "Object Browser" tab. Once logged in, the "Object Browser" tab allows users to navigate their available buckets, view stored objects, and perform various operations such as uploading, downloading, or deleting files. - -![oscar-info](images/minio-ui.png) - -Further information about the MinIO Console avaliable on [MinIO Console documentation](https://min.io/docs/minio/linux/administration/minio-console.html). - -## Using command-line interfaces - -MinIO buckets can also be managed through [oscar-cli command-line](https://github.com/grycap/oscar-cli) or the official [MinIO client](https://min.io/docs/minio/linux/reference/minio-mc.html). - -- **oscar-cli**: The OSCAR client provides a dedicated set of commands for accessing files within buckets. It is important to note that this interface does not support DELETE or UPDATE operations. Below is a brief overview of the available commands and their functionalities. - - [get-file](https://docs.oscar.grycap.net/oscar-cli/#get-file): Get file from a service's storage provider. - - [list-files](https://docs.oscar.grycap.net/oscar-cli/#list-files): List files from a service's storage provider path. - - [put-file](https://docs.oscar.grycap.net/oscar-cli/#put-file): Upload a file on a service storage provider. - - An example of a put-file operation: - - ``` bash - oscar-cli service put-file fish-detector.yaml minio .path/to/your/images ./fish-detector/input/ - ``` - -- **mc**: If a user wants to use the MinIO client it needs to follow some previous steps. - - *Install the client*: Detailed instructions for installing the MinIO client (mc) are available in [the official documentation](https://min.io/docs/minio/linux/reference/minio-mc.html#install-mc). - - *Configure the MinIO instance*: The client requires credentials to connect and interact with the MinIO instance. This configuration can be set with the following command: - - ``` bash - mc alias set myminio https://minio.gracious-varahamihira6.im.grycap.net YOUR-ACCESS-KEY YOUR-SECRET-KEY - ``` - - Once the client is configured, users can perform various operations supported by the MinIO client. For a complete list of available commands and their usage, refer to the [MinIO client reference](https://min.io/docs/minio/linux/reference/minio-mc.html#command-quick-reference). The following example demonstrates a PUT operation, where a file is uploaded to a specific folder within a bucket. - - ```bash - mc cp /path/to/your/images/*.jpg myminio/fish-detector/input/ - ``` diff --git a/docs/oscar-service.md b/docs/oscar-service.md index afcee018..b06e0ec1 100644 --- a/docs/oscar-service.md +++ b/docs/oscar-service.md @@ -15,7 +15,7 @@ is in charge of: -### FaaS Supervisor +### Input/Output [FaaS Supervisor](https://github.com/grycap/faas-supervisor), the component in charge of managing the input and output of services, allows JSON or base64 @@ -37,16 +37,6 @@ The output of synchronous invocations will depend on the application itself: This way users can adapt OSCAR's services to their own needs. -The FaaS Supervisor supports the following storage back-ends: -* [MinIO](https://min.io) -* [Amazon S3](https://aws.amazon.com/s3/) -* Webdav (and, therefore, [dCache](https://dcache.org)) -* Onedata (and, therefore, [EGI DataHub](https://www.egi.eu/service/datahub/)) - -### Container images - -Container images on asynchronous services use the tag `imagePullPolicy: Always`, which means that Kubernetes will check for the image digest on the image registry and download it if it is not present. -So, if you are using an image without a specific tag or with the latest tag, the service will automatically download and use the most recent version of the image on its executions, whenever the image is updated. You can follow one of the [examples](https://github.com/grycap/oscar/tree/master/examples) diff --git a/examples/plant-classification-theano/README.md b/examples/plant-classification-theano/README.md index 94248e20..53dc5b5a 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 output from here for further processing. + download the ouput from here for further processing. ![minio-out.png](img/Minio-OUT.png) diff --git a/examples/plants-classification-tensorflow/script.sh b/examples/plants-classification-tensorflow/script.sh index 26835bb6..ab76ecc8 100644 --- a/examples/plants-classification-tensorflow/script.sh +++ b/examples/plants-classification-tensorflow/script.sh @@ -1,6 +1,6 @@ #!/bin/bash -IMAGE_NAME=`basename "$INPUT_FILE_PATH" | cut -d. -f1` +IMAGE_NAME=`basename "$INPUT_FILE_PATH"` OUTPUT_FILE="$TMP_OUTPUT_DIR/output.json" deepaas-cli predict --files "$INPUT_FILE_PATH" 2>&1 | grep -Po '{.*}' > "$OUTPUT_FILE" diff --git a/examples/yolov8/README.md b/examples/yolov8/README.md deleted file mode 100644 index a4645e5f..00000000 --- a/examples/yolov8/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# Object Detection with YOLOv8 - -Detect objects in images using the state-of-the-art YOLOv8 model. - -## About YOLO - -This node utilizes the YOLOv8 (You Only Look Once version 8) model to detect objects within images. YOLOv8 is a cutting-edge, real-time object detection system known for its speed and accuracy, capable of identifying thousands of object categories efficiently. - -## About YOLOV8 Service in OSCAR - -This service uses the pre-trained YOLOv8 model provided by DEEP-Hybrid-DataCloud for object detection. It is designed to handle synchronous invocations and real-time image processing with high scalability, managed automatically by an elastic Kubernetes cluster. - -In order to invoke the function, first you have to create a service, either by the OSCAR UI or by using the FDL within the following command. - - -``` sh -oscar-cli apply yolov8.yaml -``` - -Once the service is created you can make the invocation with the following -command, which will store the output on a minio bucket. - -``` sh -oscar-cli service put-file yolov8.yaml minio img/cat.jpg yolov8/input/cat.jpg \ No newline at end of file diff --git a/examples/yolov8/img/cat.jpg b/examples/yolov8/img/cat.jpg deleted file mode 100644 index 016cdd82..00000000 Binary files a/examples/yolov8/img/cat.jpg and /dev/null differ diff --git a/examples/yolov8/img/cat.jpg:Zone.Identifier b/examples/yolov8/img/cat.jpg:Zone.Identifier deleted file mode 100644 index db4f2e84..00000000 --- a/examples/yolov8/img/cat.jpg:Zone.Identifier +++ /dev/null @@ -1,4 +0,0 @@ -[ZoneTransfer] -ZoneId=3 -ReferrerUrl=https://www.google.com/ -HostUrl=https://upload.wikimedia.org/wikipedia/commons/thumb/4/4d/Cat_November_2010-1a.jpg/220px-Cat_November_2010-1a.jpg diff --git a/examples/yolov8/script.sh b/examples/yolov8/script.sh deleted file mode 100644 index 631e4552..00000000 --- a/examples/yolov8/script.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -IMAGE_NAME=`basename "$INPUT_FILE_PATH"` -OUTPUT_FILE="$TMP_OUTPUT_DIR/output.png" - -deepaas-cli --deepaas_method_output="$OUTPUT_FILE" predict --files "$INPUT_FILE_PATH" --accept image/png 2>&1 - -echo "Prediction was saved in: $OUTPUT_FILE" \ No newline at end of file diff --git a/examples/yolov8/yolov8.yaml b/examples/yolov8/yolov8.yaml deleted file mode 100644 index 0db5095c..00000000 --- a/examples/yolov8/yolov8.yaml +++ /dev/null @@ -1,17 +0,0 @@ -functions: - oscar: - - oscar-cluster: - name: yolov8 - memory: 4Gi - cpu: '2.0' - image: ai4oshub/ai4os-yolov8-torch:latest - script: script.sh - vo: vo.imagine-ai.eu - allowed_users: [] - log_level: CRITICAL - input: - - storage_provider: minio.default - path: yolov8/input - output: - - storage_provider: minio.default - path: yolov8/output \ No newline at end of file diff --git a/go.mod b/go.mod index c29f3ffe..ddb974fc 100644 --- a/go.mod +++ b/go.mod @@ -34,7 +34,6 @@ require ( require ( github.com/fatih/color v1.14.1 // indirect - github.com/golang-jwt/jwt/v4 v4.5.1 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/rs/xid v1.4.0 // indirect @@ -53,7 +52,6 @@ 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 0fadf17e..3d436868 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -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= @@ -108,8 +106,6 @@ github.com/goccy/go-yaml v1.9.8/go.mod h1:JubOolP3gh0HpiBc4BLRD4YmjEjHAmIIB2aaXK github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= -github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -153,6 +149,8 @@ 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= @@ -276,6 +274,18 @@ 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/main.go b/main.go index a98e2a65..53bca6d4 100644 --- a/main.go +++ b/main.go @@ -105,12 +105,12 @@ func main() { system.GET("/status", handlers.MakeStatusHandler(kubeClientset, metricsClientset)) // Job path for async invocations - r.POST("/job/:serviceName", auth.GetLoggerMiddleware(), handlers.MakeJobHandler(cfg, kubeClientset, back, resMan)) + r.POST("/job/:serviceName", handlers.MakeJobHandler(cfg, kubeClientset, back, resMan)) // Service path for sync invocations (only if ServerlessBackend is enabled) syncBack, ok := back.(types.SyncBackend) if cfg.ServerlessBackend != "" && ok { - r.POST("/run/:serviceName", auth.GetLoggerMiddleware(), handlers.MakeRunHandler(cfg, syncBack)) + r.POST("/run/:serviceName", handlers.MakeRunHandler(cfg, syncBack)) } // System info path diff --git a/mkdocs.yml b/mkdocs.yml index 18bb0df2..fa259848 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -21,7 +21,6 @@ nav: - oscar-cli.md - usage-ui.md - api.md - - MinIO: minio-usage.md - Service Execution: - invoking.md @@ -64,4 +63,4 @@ plugins: - search - render_swagger -copyright: "© GRyCAP - I3M - Universitat Politècnica de València, Spain." +copyright: "© GRyCAP - I3M - Universitat Politècnica de València, Spain." \ No newline at end of file diff --git a/pkg/backends/fake.go b/pkg/backends/fake.go index 3c2ad304..c9ec6b26 100644 --- a/pkg/backends/fake.go +++ b/pkg/backends/fake.go @@ -31,8 +31,7 @@ var errFake = errors.New("fake error") // FakeBackend fake struct to mock the beahaviour of the ServerlessBackend interface type FakeBackend struct { - errors map[string][]error - Service *types.Service // service to be returned by the ReadService function + errors map[string][]error } // MakeFakeBackend returns the pointer of a new FakeBackend struct @@ -82,12 +81,7 @@ func (f *FakeBackend) CreateService(service types.Service) error { // ReadService returns a Service (fake) func (f *FakeBackend) ReadService(name string) (*types.Service, error) { - // default service returned by the function - service := &types.Service{Token: "11e387cf727630d899925d57fceb4578f478c44be6cde0ae3fe886d8be513acf"} - if f.Service != nil { - service = f.Service - } - return service, f.returnError(getCurrentFuncName()) + return &types.Service{Token: "AbCdEf123456"}, f.returnError(getCurrentFuncName()) } // UpdateService updates an existent service (fake) diff --git a/pkg/backends/k8s.go b/pkg/backends/k8s.go index 0879991d..c41a1f2a 100644 --- a/pkg/backends/k8s.go +++ b/pkg/backends/k8s.go @@ -57,21 +57,23 @@ func (k *KubeBackend) GetInfo() *types.ServerlessBackendInfo { // ListServices returns a slice with all services registered in the provided namespace func (k *KubeBackend) ListServices() ([]*types.Service, error) { - // Get the list with all Knative services - configmaps, err := getAllServicesConfigMaps(k.namespace, k.kubeClientset) + // Get the list with all podTemplates + podTemplates, err := k.kubeClientset.CoreV1().PodTemplates(k.namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { - log.Printf("WARNING: %v\n", err) return nil, err } - services := []*types.Service{} - for _, cm := range configmaps.Items { - service, err := getServiceFromConfigMap(&cm) + services := []*types.Service{} + for _, podTemplate := range podTemplates.Items { + // Get service from configMap's FDL + svc, err := getServiceFromFDL(podTemplate.Name, k.namespace, k.kubeClientset) if err != nil { - return nil, err + log.Printf("WARNING: %v\n", err) + } else { + services = append(services, svc) } - services = append(services, service) } + return services, nil } @@ -146,14 +148,8 @@ func (k *KubeBackend) ReadService(name string) (*types.Service, error) { return nil, err } - // Get the configMap of the Service - cm, err := k.kubeClientset.CoreV1().ConfigMaps(k.namespace).Get(context.TODO(), name, metav1.GetOptions{}) - if err != nil { - return nil, fmt.Errorf("the service \"%s\" does not have a registered ConfigMap", name) - } - // Get service from configMap's FDL - svc, err := getServiceFromConfigMap(cm) + svc, err := getServiceFromFDL(name, k.namespace, k.kubeClientset) if err != nil { return nil, err } @@ -216,14 +212,6 @@ func (k *KubeBackend) UpdateService(service types.Service) error { } } - //Create deaemonset to cache the service image on all the nodes - if service.ImagePrefetch { - err = imagepuller.CreateDaemonset(k.config, service, k.kubeClientset) - if err != nil { - return err - } - } - return nil } @@ -254,12 +242,17 @@ func (k *KubeBackend) DeleteService(service types.Service) error { return nil } -func getServiceFromConfigMap(cm *v1.ConfigMap) (*types.Service, error) { +func getServiceFromFDL(name string, namespace string, kubeClientset kubernetes.Interface) (*types.Service, error) { + // Get the configMap of the Service + cm, err := kubeClientset.CoreV1().ConfigMaps(namespace).Get(context.TODO(), name, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("the service \"%s\" does not have a registered ConfigMap", name) + } service := &types.Service{} // Unmarshal the FDL stored in the configMap - if err := yaml.Unmarshal([]byte(cm.Data[types.FDLFileName]), service); err != nil { - return nil, fmt.Errorf("the FDL of the service \"%s\" cannot be read", cm.Name) + if err = yaml.Unmarshal([]byte(cm.Data[types.FDLFileName]), service); err != nil { + return nil, fmt.Errorf("the FDL of the service \"%s\" cannot be read", name) } // Add the script to the service from configmap's script value @@ -372,17 +365,6 @@ func deleteServiceConfigMap(name string, namespace string, kubeClientset kuberne return nil } -func getAllServicesConfigMaps(namespace string, kubeClientset kubernetes.Interface) (*v1.ConfigMapList, error) { - listOpts := metav1.ListOptions{ - LabelSelector: "oscar_service", - } - configMapsList, err := kubeClientset.CoreV1().ConfigMaps(namespace).List(context.TODO(), listOpts) - if err != nil { - return nil, err - } - return configMapsList, nil -} - func deleteServiceJobs(name string, namespace string, kubeClientset kubernetes.Interface) error { // ListOptions to select all the associated jobs with the specified service listOpts := metav1.ListOptions{ diff --git a/pkg/backends/k8s_test.go b/pkg/backends/k8s_test.go index e9e4f30b..1a967bda 100644 --- a/pkg/backends/k8s_test.go +++ b/pkg/backends/k8s_test.go @@ -113,12 +113,29 @@ func TestKubeGetInfo(t *testing.T) { } func TestKubeListServices(t *testing.T) { + validPodTemplateListReactor := func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) { + podTemplateList := &v1.PodTemplateList{ + Items: []v1.PodTemplate{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "testnamespace", + }, + Template: v1.PodTemplateSpec{}, + }, + }, + } + return true, podTemplateList, nil + } t.Run("valid list", func(t *testing.T) { clientset := fake.NewSimpleClientset() back := MakeKubeBackend(clientset, testConfig) + // Return a valid PodTemplateList + back.kubeClientset.(*fake.Clientset).Fake.PrependReactor("list", "podtemplates", validPodTemplateListReactor) + // Return a valid configMap back.kubeClientset.(*fake.Clientset).Fake.PrependReactor("get", "configmaps", validConfigMapReaction) @@ -129,11 +146,29 @@ func TestKubeListServices(t *testing.T) { } }) + t.Run("listing podTemplates throws an error", func(t *testing.T) { + clientset := fake.NewSimpleClientset() + + back := MakeKubeBackend(clientset, testConfig) + + // Return an error listing PodTemplates + back.kubeClientset.(*fake.Clientset).Fake.PrependReactor("list", "podtemplates", errorReaction) + + // Call + _, err := back.ListServices() + if err == nil { + t.Error("expecting error, got: nil") + } + }) + t.Run("getServiceFromFDL throws error getting configMap", func(t *testing.T) { clientset := fake.NewSimpleClientset() back := MakeKubeBackend(clientset, testConfig) + // Return a valid PodTemplateList + back.kubeClientset.(*fake.Clientset).Fake.PrependReactor("list", "podtemplates", validPodTemplateListReactor) + // Return an error getting the configMap back.kubeClientset.(*fake.Clientset).Fake.PrependReactor("get", "configmaps", errorReaction) @@ -163,6 +198,9 @@ func TestKubeListServices(t *testing.T) { return true, validCM, nil } + // Return a valid PodTemplateList + back.kubeClientset.(*fake.Clientset).Fake.PrependReactor("list", "podtemplates", validPodTemplateListReactor) + // Return a valid configMap with invalid FDL back.kubeClientset.(*fake.Clientset).Fake.PrependReactor("get", "configmaps", validConfigMapWithInvalidFDLReactor) diff --git a/pkg/backends/knative.go b/pkg/backends/knative.go index 566352c4..9e1391e9 100644 --- a/pkg/backends/knative.go +++ b/pkg/backends/knative.go @@ -76,20 +76,22 @@ func (kn *KnativeBackend) GetInfo() *types.ServerlessBackendInfo { // ListServices returns a slice with all services registered in the provided namespace func (kn *KnativeBackend) ListServices() ([]*types.Service, error) { // Get the list with all Knative services - configmaps, err := getAllServicesConfigMaps(kn.namespace, kn.kubeClientset) + knSvcs, err := kn.knClientset.ServingV1().Services(kn.namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { - log.Printf("WARNING: %v\n", err) return nil, err } - services := []*types.Service{} - for _, cm := range configmaps.Items { - service, err := getServiceFromConfigMap(&cm) + services := []*types.Service{} + for _, knSvc := range knSvcs.Items { + // Get service from configMap's FDL + svc, err := getServiceFromFDL(knSvc.Name, kn.namespace, kn.kubeClientset) if err != nil { - return nil, err + log.Printf("WARNING: %v\n", err) + } else { + services = append(services, svc) } - services = append(services, service) } + return services, nil } @@ -149,13 +151,8 @@ func (kn *KnativeBackend) ReadService(name string) (*types.Service, error) { return nil, err } - // Get the configMap of the Service - cm, err := kn.kubeClientset.CoreV1().ConfigMaps(kn.namespace).Get(context.TODO(), name, metav1.GetOptions{}) - if err != nil { - return nil, fmt.Errorf("the service \"%s\" does not have a registered ConfigMap", name) - } // Get service from configMap's FDL - svc, err := getServiceFromConfigMap(cm) + svc, err := getServiceFromFDL(name, kn.namespace, kn.kubeClientset) if err != nil { return nil, err } @@ -226,14 +223,6 @@ func (kn *KnativeBackend) UpdateService(service types.Service) error { } } - //Create deaemonset to cache the service image on all the nodes - if service.ImagePrefetch { - err = imagepuller.CreateDaemonset(kn.config, service, kn.kubeClientset) - if err != nil { - return err - } - } - return nil } diff --git a/pkg/backends/knative_test.go b/pkg/backends/knative_test.go index 064da7af..d21f16b2 100644 --- a/pkg/backends/knative_test.go +++ b/pkg/backends/knative_test.go @@ -144,6 +144,17 @@ func TestKnativeListServices(t *testing.T) { []k8stesting.SimpleReactor{knServiceListReactor}, false, }, + { + "Error listing knative services", + []k8stesting.SimpleReactor{}, + []k8stesting.SimpleReactor{ + { + Verb: "list", + Resource: "services", + Reaction: errorReaction, + }}, + true, + }, { "Error getting the configMap", []k8stesting.SimpleReactor{ diff --git a/pkg/backends/openfaas.go b/pkg/backends/openfaas.go index 5743e21a..89ab0402 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.Interface + ofClientset *ofclientset.Clientset namespace string gatewayEndpoint string scaler *utils.OpenfaasScaler @@ -83,20 +83,21 @@ func (of *OpenfaasBackend) GetInfo() *types.ServerlessBackendInfo { // ListServices returns a slice with all services registered in the provided namespace func (of *OpenfaasBackend) ListServices() ([]*types.Service, error) { - // Get the list with all Knative services - configmaps, err := getAllServicesConfigMaps(of.namespace, of.kubeClientset) + // Get the list with all deployments + deployments, err := of.kubeClientset.AppsV1().Deployments(of.namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { - log.Printf("WARNING: %v\n", err) return nil, err } - services := []*types.Service{} - for _, cm := range configmaps.Items { - service, err := getServiceFromConfigMap(&cm) + services := []*types.Service{} + for _, deployment := range deployments.Items { + // Get service from configMap's FDL + svc, err := getServiceFromFDL(deployment.Name, of.namespace, of.kubeClientset) if err != nil { - return nil, err + log.Printf("WARNING: %v\n", err) + } else { + services = append(services, svc) } - services = append(services, service) } return services, nil @@ -229,13 +230,8 @@ func (of *OpenfaasBackend) ReadService(name string) (*types.Service, error) { return nil, err } - // Get the configMap of the Service - cm, err := of.kubeClientset.CoreV1().ConfigMaps(of.namespace).Get(context.TODO(), name, metav1.GetOptions{}) - if err != nil { - return nil, fmt.Errorf("the service \"%s\" does not have a registered ConfigMap", name) - } // Get service from configMap's FDL - svc, err := getServiceFromConfigMap(cm) + svc, err := getServiceFromFDL(name, of.namespace, of.kubeClientset) if err != nil { return nil, err } diff --git a/pkg/backends/openfaas_test.go b/pkg/backends/openfaas_test.go deleted file mode 100644 index 4228a6b6..00000000 --- a/pkg/backends/openfaas_test.go +++ /dev/null @@ -1,326 +0,0 @@ -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 deleted file mode 100644 index 01af08c1..00000000 --- a/pkg/backends/serverlessbackend_test.go +++ /dev/null @@ -1,65 +0,0 @@ -/* -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 deleted file mode 100644 index 7c74fcb5..00000000 --- a/pkg/handlers/config_test.go +++ /dev/null @@ -1,165 +0,0 @@ -/* -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, - "oidc_groups": nil, - }, - "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 30e16bdc..9d2bc194 100644 --- a/pkg/handlers/create.go +++ b/pkg/handlers/create.go @@ -40,7 +40,6 @@ const ( defaultMemory = "256Mi" defaultCPU = "0.2" defaultLogLevel = "INFO" - createPath = "/system/services" ) var errInput = errors.New("unrecognized input (valid inputs are MinIO and dCache)") @@ -57,7 +56,7 @@ func MakeCreateHandler(cfg *types.Config, back types.ServerlessBackend) gin.Hand if len(strings.Split(authHeader, "Bearer")) == 1 { isAdminUser = true service.Owner = "cluster_admin" - createLogger.Printf("Creating service '%s' for user '%s'", service.Name, service.Owner) + createLogger.Printf("Creating service for user: %s", service.Owner) } if err := c.ShouldBindJSON(&service); err != nil { @@ -70,22 +69,25 @@ func MakeCreateHandler(cfg *types.Config, back types.ServerlessBackend) gin.Hand // Check if users in allowed_users have a MinIO associated user minIOAdminClient, _ := utils.MakeMinIOAdminClient(cfg) + // === DEBUG code === + loguid, _ := auth.GetUIDFromContext(c) + createLogger.Printf(">>> uid from context: %s", loguid) + // ============= + // Service is created by an EGI user if !isAdminUser { uid, err := auth.GetUIDFromContext(c) if err != nil { c.String(http.StatusInternalServerError, fmt.Sprintln(err)) - return } // Set UID from owner service.Owner = uid - createLogger.Printf("Creating service '%s' for user '%s'", service.Name, service.Owner) + createLogger.Printf("Creating service for user: %s", service.Owner) mc, err := auth.GetMultitenancyConfigFromContext(c) if err != nil { c.String(http.StatusInternalServerError, fmt.Sprintln(err)) - return } full_uid := auth.FormatUID(uid) @@ -96,7 +98,6 @@ func MakeCreateHandler(cfg *types.Config, back types.ServerlessBackend) gin.Hand err := checkIdentity(&service, cfg, authHeader) if err != nil { c.String(http.StatusBadRequest, fmt.Sprintln(err)) - return } break } @@ -122,7 +123,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 associated MinIO user + // Check if the uid's from allowed_users have and asociated MinIO user // and create it if not uids := mc.CheckUsersInCache(service.AllowedUsers) if len(uids) > 0 { @@ -170,11 +171,7 @@ func MakeCreateHandler(cfg *types.Config, back types.ServerlessBackend) gin.Hand log.Println(err.Error()) } } - uid := service.Owner - if service.Owner == "" { - uid = "nil" - } - createLogger.Printf("%s | %v | %s | %s | %s", "POST", 200, createPath, service.Name, uid) + createLogger.Println("Service created with name: ", service.Name) c.Status(http.StatusCreated) } } @@ -287,7 +284,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 becomes public + // If new allowed users list is empty the service becames public if !isUpdate { if !isAdminUser { if len(allowed_users) == 0 { diff --git a/pkg/handlers/create_test.go b/pkg/handlers/create_test.go deleted file mode 100644 index 912763dd..00000000 --- a/pkg/handlers/create_test.go +++ /dev/null @@ -1,123 +0,0 @@ -/* -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 ( - "fmt" - "strings" - "testing" - - "net/http" - "net/http/httptest" - - "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 != "/test" && hreq.URL.Path != "/test/input/" && hreq.URL.Path != "/test/output/" && hreq.URL.Path != "/test/mount/" && !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"}`)) - } - })) - - // 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, - }, - } - 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() - 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": "/test/input/" - } - ], - "output": [ - { - "storage_provider": "minio", - "path": "/test/output" - } - ], - "mount": { - "storage_provider": "minio", - "path": "/test/mount" - }, - "storage_providers": { - "webdav": { - "id": { - "hostname": "` + server.URL + `", - "login": "user", - "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 - defer server.Close() - - if w.Code != http.StatusCreated { - fmt.Println(w.Body) - t.Errorf("expecting code %d, got %d", http.StatusCreated, w.Code) - } -} diff --git a/pkg/handlers/delete_test.go b/pkg/handlers/delete_test.go deleted file mode 100644 index 2f6bb0ca..00000000 --- a/pkg/handlers/delete_test.go +++ /dev/null @@ -1,102 +0,0 @@ -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 deleted file mode 100644 index ca405ad4..00000000 --- a/pkg/handlers/health_test.go +++ /dev/null @@ -1,50 +0,0 @@ -/* -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 e0319d63..1fa48b5b 100644 --- a/pkg/handlers/job.go +++ b/pkg/handlers/job.go @@ -18,12 +18,11 @@ package handlers import ( "context" - "encoding/json" + "encoding/base64" "fmt" "io" "log" "net/http" - "os" "strconv" "strings" @@ -31,8 +30,6 @@ import ( "github.com/google/uuid" "github.com/grycap/oscar/v3/pkg/resourcemanager" "github.com/grycap/oscar/v3/pkg/types" - "github.com/grycap/oscar/v3/pkg/utils/auth" - genericErrors "github.com/pkg/errors" batchv1 "k8s.io/api/batch/v1" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" @@ -47,8 +44,7 @@ var ( // Don't restart jobs in order to keep logs restartPolicy = v1.RestartPolicyNever // command used for passing the event to faas-supervisor - command = []string{"/bin/sh"} - jobLogger = log.New(os.Stdout, "[JOB-HANDLER] ", log.Flags()) + command = []string{"/bin/sh"} ) const ( @@ -62,8 +58,8 @@ const ( InterLinkTolerationOperator = "Exists" ) -// MakeJobHandler makes a handler to manage async invocations -func MakeJobHandler(cfg *types.Config, kubeClientset kubernetes.Interface, back types.ServerlessBackend, rm resourcemanager.ResourceManager) gin.HandlerFunc { +// 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 { return func(c *gin.Context) { service, err := back.ReadService(c.Param("serviceName")) if err != nil { @@ -90,45 +86,10 @@ func MakeJobHandler(cfg *types.Config, kubeClientset kubernetes.Interface, back c.Status(http.StatusUnauthorized) return } - - // Check if reqToken is the service token - rawToken := strings.TrimSpace(splitToken[1]) - if len(rawToken) == tokenLength { - - if rawToken != service.Token { - c.Status(http.StatusUnauthorized) - return - } - } - - // If isn't service token check if it is an oidc token - var uidFromToken string - if len(rawToken) != tokenLength { - oidcManager, _ := auth.NewOIDCManager(cfg.OIDCIssuer, cfg.OIDCSubject, cfg.OIDCGroups) - - if !oidcManager.IsAuthorised(rawToken) { - c.Status(http.StatusUnauthorized) - return - } - - hasVO, err := oidcManager.UserHasVO(rawToken, service.VO) - - if err != nil { - c.String(http.StatusInternalServerError, err.Error()) - return - } - - if !hasVO { - c.String(http.StatusUnauthorized, "this user isn't enrrolled on the vo: %v", service.VO) - return - } - - // Get UID from token - var uidErr error - uidFromToken, uidErr = oidcManager.GetUID(rawToken) - if uidErr != nil { - jobLogger.Println("WARNING:", uidErr) - } + reqToken := strings.TrimSpace(splitToken[1]) + if reqToken != service.Token { + c.Status(http.StatusUnauthorized) + return } // Get the event from request body @@ -138,48 +99,37 @@ func MakeJobHandler(cfg *types.Config, kubeClientset kubernetes.Interface, back return } - // Check if it has the MinIO event format - uid, sourceIPAddress, err := decodeEventBytes(eventBytes) - if err != nil { - // Check if the request was made with OIDC token to get user UID - if uidFromToken != "" { - c.Set("uidOrigin", uidFromToken) - } else { - // Set as nil string if unable to get an UID - jobLogger.Println("WARNING:", err) - c.Set("uidOrigin", "nil") - } - } else { - c.Set("IPAddress", sourceIPAddress) - c.Set("uidOrigin", uid) - } - - c.Next() - - // Initialize event envVar and args var + // Make event envVar event := v1.EnvVar{} - var args []string + var args string if cfg.InterLinkAvailable && service.InterLinkNodeName != "" { - command, event, args = types.SetInterlinkJob(podSpec, service, cfg, eventBytes) - } else { - - if service.Mount.Provider != "" { - args = []string{"-c", fmt.Sprintf("echo $%s | %s", types.EventVariable, service.GetSupervisorPath()) + ";echo \"I finish\" > /tmpfolder/finish-file;"} - types.SetMount(podSpec, *service, cfg) - } else { - args = []string{"-c", fmt.Sprintf("echo $%s | %s", types.EventVariable, service.GetSupervisorPath())} + event = v1.EnvVar{ + Name: types.EventVariable, + Value: base64.StdEncoding.EncodeToString([]byte(eventBytes)), } - + args = fmt.Sprintf("\" wget %s -O %s && chmod 0755 %s && echo \\$%s | base64 -d | %s \"", cfg.SupervisorURL, SupervisorPath, SupervisorPath, types.EventVariable, SupervisorPath) + podSpec.NodeSelector = map[string]string{ + NodeSelectorKey: service.InterLinkNodeName, + } + podSpec.DNSPolicy = InterLinkDNSPolicy + podSpec.RestartPolicy = InterLinkRestartPolicy + podSpec.Tolerations = []v1.Toleration{ + { + Key: InterLinkTolerationKey, + Operator: InterLinkTolerationOperator, + }, + } + } else { event = v1.EnvVar{ Name: types.EventVariable, Value: string(eventBytes), } + args = fmt.Sprintf("echo $%s | %s", types.EventVariable, service.GetSupervisorPath()) } // Make JOB_UUID envVar jobUUID := uuid.New().String() - jobUUID = service.Name + "-" + jobUUID jobUUIDVar := v1.EnvVar{ Name: types.JobUUIDVariable, Value: jobUUID, @@ -200,12 +150,16 @@ func MakeJobHandler(cfg *types.Config, kubeClientset kubernetes.Interface, back for i, c := range podSpec.Containers { if c.Name == types.ContainerName { podSpec.Containers[i].Command = command - podSpec.Containers[i].Args = args + podSpec.Containers[i].Args = []string{"-c", args} podSpec.Containers[i].Env = append(podSpec.Containers[i].Env, event) podSpec.Containers[i].Env = append(podSpec.Containers[i].Env, jobUUIDVar) podSpec.Containers[i].Env = append(podSpec.Containers[i].Env, resourceIDVar) } } + if service.Mount.Provider != "" { + types.SetMount(podSpec, *service, cfg) + podSpec.Containers[0].Args = []string{"-c", args + ";echo \"I finish\" > /tmpfolder/finish-file;"} + } // Delegate job if can't be scheduled and has defined replicas if rm != nil && service.HasReplicas() { @@ -216,7 +170,7 @@ func MakeJobHandler(cfg *types.Config, kubeClientset kubernetes.Interface, back c.Status(http.StatusCreated) return } - jobLogger.Printf("unable to delegate job. Error: %v\n", err) + log.Printf("unable to delegate job. Error: %v\n", err) } } @@ -251,6 +205,7 @@ func MakeJobHandler(cfg *types.Config, kubeClientset kubernetes.Interface, back } } + // Create job _, err = kubeClientset.BatchV1().Jobs(cfg.ServicesNamespace).Create(context.TODO(), job, metav1.CreateOptions{}) if err != nil { c.String(http.StatusInternalServerError, err.Error()) @@ -259,31 +214,3 @@ func MakeJobHandler(cfg *types.Config, kubeClientset kubernetes.Interface, back c.Status(http.StatusCreated) } } - -func decodeEventBytes(eventBytes []byte) (string, string, error) { - - defer func() { - // recover from panic, if one occurs - if r := recover(); r != nil { - jobLogger.Println("Recovered from panic:", r) - } - }() - // Extract user UID from MinIO event - var decoded map[string]interface{} - if err := json.Unmarshal(eventBytes, &decoded); err != nil { - return "", "", err - } - - if records, panicErr := decoded["Records"].([]interface{}); panicErr { - r := records[0].(map[string]interface{}) - - eventInfo := r["requestParameters"].(map[string]interface{}) - uid := eventInfo["principalId"] - sourceIPAddress := eventInfo["sourceIPAddress"] - - return uid.(string), sourceIPAddress.(string), nil - } else { - return "", "", genericErrors.New("Failed to decode records") - } - -} diff --git a/pkg/handlers/job_test.go b/pkg/handlers/job_test.go deleted file mode 100644 index 6150617a..00000000 --- a/pkg/handlers/job_test.go +++ /dev/null @@ -1,44 +0,0 @@ -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/list.go b/pkg/handlers/list.go index ba8ae64e..2a5cb130 100644 --- a/pkg/handlers/list.go +++ b/pkg/handlers/list.go @@ -19,7 +19,6 @@ package handlers import ( "fmt" "net/http" - "slices" "strings" "github.com/gin-gonic/gin" @@ -43,13 +42,19 @@ func MakeListHandler(back types.ServerlessBackend) gin.HandlerFunc { uid, err := auth.GetUIDFromContext(c) if err != nil { c.String(http.StatusInternalServerError, fmt.Sprintln(err)) - return } var allowedServicesForUser []*types.Service for _, service := range services { - if len(service.AllowedUsers) == 0 || slices.Contains(service.AllowedUsers, uid) { + if len(service.AllowedUsers) == 0 { allowedServicesForUser = append(allowedServicesForUser, service) + continue + } + for _, id := range service.AllowedUsers { + if uid == id { + allowedServicesForUser = append(allowedServicesForUser, service) + break + } } } diff --git a/pkg/handlers/logs.go b/pkg/handlers/logs.go index cc809827..5767af31 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.Interface, namespace string) gin.HandlerFunc { +func MakeJobsInfoHandler(back types.ServerlessBackend, kubeClientset *kubernetes.Clientset, 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.Interface, namespace string) gin.HandlerFunc { +func MakeDeleteJobsHandler(back types.ServerlessBackend, kubeClientset *kubernetes.Clientset, 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 kubernete } // 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.Interface, namespace string) gin.HandlerFunc { +func MakeGetLogsHandler(back types.ServerlessBackend, kubeClientset *kubernetes.Clientset, 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.I } // MakeDeleteJobHandler makes a handler for removing a job -func MakeDeleteJobHandler(back types.ServerlessBackend, kubeClientset kubernetes.Interface, namespace string) gin.HandlerFunc { +func MakeDeleteJobHandler(back types.ServerlessBackend, kubeClientset *kubernetes.Clientset, 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(serviceName) + service, _ := back.ReadService(c.Param("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 deleted file mode 100644 index e500a29e..00000000 --- a/pkg/handlers/logs_test.go +++ /dev/null @@ -1,234 +0,0 @@ -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.go b/pkg/handlers/run.go index ee7dec56..f289713a 100644 --- a/pkg/handlers/run.go +++ b/pkg/handlers/run.go @@ -23,14 +23,9 @@ import ( "github.com/gin-gonic/gin" "github.com/grycap/oscar/v3/pkg/types" - "github.com/grycap/oscar/v3/pkg/utils/auth" "k8s.io/apimachinery/pkg/api/errors" ) -const ( - tokenLength = 64 -) - // MakeRunHandler makes a handler to manage sync invocations sending them to the gateway of the ServerlessBackend func MakeRunHandler(cfg *types.Config, back types.SyncBackend) gin.HandlerFunc { return func(c *gin.Context) { @@ -52,44 +47,10 @@ func MakeRunHandler(cfg *types.Config, back types.SyncBackend) gin.HandlerFunc { c.Status(http.StatusUnauthorized) return } - - // Check if reqToken is the service token - rawToken := strings.TrimSpace(splitToken[1]) - if len(rawToken) == tokenLength { - - if rawToken != service.Token { - c.Status(http.StatusUnauthorized) - return - } - } else { - oidcManager, _ := auth.NewOIDCManager(cfg.OIDCIssuer, cfg.OIDCSubject, cfg.OIDCGroups) - - if !oidcManager.IsAuthorised(rawToken) { - c.Status(http.StatusUnauthorized) - return - } - - hasVO, err := oidcManager.UserHasVO(rawToken, service.VO) - - if err != nil { - c.String(http.StatusInternalServerError, err.Error()) - return - } - - if !hasVO { - c.String(http.StatusUnauthorized, "this user isn't enrrolled on the vo: %v", service.VO) - return - } - - ui, err := oidcManager.GetUserInfo(rawToken) - if err != nil { - c.String(http.StatusInternalServerError, err.Error()) - return - } - uid := ui.Subject - c.Set("uidOrigin", uid) - c.Next() - + reqToken := strings.TrimSpace(splitToken[1]) + if reqToken != service.Token { + c.Status(http.StatusUnauthorized) + return } proxy := &httputil.ReverseProxy{ diff --git a/pkg/handlers/run_test.go b/pkg/handlers/run_test.go index 3d9d647d..e1b9e626 100644 --- a/pkg/handlers/run_test.go +++ b/pkg/handlers/run_test.go @@ -63,10 +63,10 @@ func TestMakeRunHandler(t *testing.T) { for _, s := range scenarios { t.Run(s.name, func(t *testing.T) { w := httptest.NewRecorder() - serviceName := "testName" + serviceName := "test" req, _ := http.NewRequest("POST", "/run/"+serviceName, nil) - req.Header.Set("Authorization", "Bearer 11e387cf727630d899925d57fceb4578f478c44be6cde0ae3fe886d8be513acf") + req.Header.Set("Authorization", "Bearer AbCdEf123456") if s.returnError { switch s.errType { @@ -76,9 +76,9 @@ func TestMakeRunHandler(t *testing.T) { err := errors.New("Not found") back.AddError("ReadService", k8serr.NewInternalError(err)) case "splitErr": - req.Header.Set("Authorization", "11e387cf727630d899925d57fceb4578f478c44be6cde0ae3fe886d8be513acf") + req.Header.Set("Authorization", "AbCdEf123456") case "diffErr": - req.Header.Set("Authorization", "Bearer 11e387cf727630d899925d57fceb4578f478c44be6cde0ae3fe886d8be513dfg") + req.Header.Set("Authorization", "Bearer AbC123456") } } diff --git a/pkg/handlers/status.go b/pkg/handlers/status.go index 17fed02b..52061ecb 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.Interface, metricsClientset versioned.MetricsV1beta1Interface) gin.HandlerFunc { +func MakeStatusHandler(kubeClientset *kubernetes.Clientset, metricsClientset *versioned.MetricsV1beta1Client) 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 deleted file mode 100644 index d211583f..00000000 --- a/pkg/handlers/status_test.go +++ /dev/null @@ -1,132 +0,0 @@ -/* -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 e7dea13e..6a5a96f0 100644 --- a/pkg/handlers/update.go +++ b/pkg/handlers/update.go @@ -45,11 +45,7 @@ func MakeUpdateHandler(cfg *types.Config, back types.ServerlessBackend) gin.Hand // Check service values and set defaults checkValues(&newService, cfg) - authHeader := c.GetHeader("Authorization") - if len(strings.Split(authHeader, "Bearer")) == 1 { - isAdminUser = true - createLogger.Printf("[*] Updating service as admin user") - } + // Read the current service oldService, err := back.ReadService(newService.Name) @@ -62,34 +58,34 @@ func MakeUpdateHandler(cfg *types.Config, back types.ServerlessBackend) gin.Hand } return } - if !isAdminUser { - uid, err := auth.GetUIDFromContext(c) - if err != nil { - c.String(http.StatusInternalServerError, fmt.Sprintln("Couldn't get UID from context")) - } - if oldService.Owner != uid { - c.String(http.StatusForbidden, "User %s doesn't have permision to modify this service", uid) - return - } + uid, err := auth.GetUIDFromContext(c) + if err != nil { + c.String(http.StatusInternalServerError, fmt.Sprintln("Couldn't get UID from context")) + } - // Set the owner on the new service definition - newService.Owner = oldService.Owner + if oldService.Owner != uid { + c.String(http.StatusForbidden, "User %s doesn't have permision to modify this service", uid) + return + } - // 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 { - authHeader := c.GetHeader("Authorization") - err := checkIdentity(&newService, cfg, authHeader) - if err != nil { - c.String(http.StatusBadRequest, fmt.Sprintln(err)) - } - break + // Set the owner on the new service definition + newService.Owner = oldService.Owner + + // If the service has changed VO check permisions again + if newService.VO != "" && newService.VO != oldService.VO { + for _, vo := range cfg.OIDCGroups { + if vo == newService.VO { + authHeader := c.GetHeader("Authorization") + err := checkIdentity(&newService, cfg, authHeader) + if err != nil { + c.String(http.StatusBadRequest, fmt.Sprintln(err)) } + break } } } + minIOAdminClient, _ := utils.MakeMinIOAdminClient(cfg) // Update the service if err := back.UpdateService(newService); err != nil { diff --git a/pkg/handlers/update_test.go b/pkg/handlers/update_test.go deleted file mode 100644 index 034b3fed..00000000 --- a/pkg/handlers/update_test.go +++ /dev/null @@ -1,114 +0,0 @@ -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": ["somelonguid1@egi.eu", "somelonguid2@egi.eu"] - } - `) - 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.go b/pkg/imagepuller/daemonset.go index 7d5a6f9f..037685ca 100644 --- a/pkg/imagepuller/daemonset.go +++ b/pkg/imagepuller/daemonset.go @@ -59,7 +59,7 @@ var stopper chan struct{} // Create daemonset func CreateDaemonset(cfg *types.Config, service types.Service, kubeClientset kubernetes.Interface) error { - DaemonSetLoggerInfo.Println("Creating daemonset for service:", service.Name) + //Set needed variables setWorkingNodes(kubeClientset) podGroup = generatePodGroupName() @@ -73,6 +73,8 @@ func CreateDaemonset(cfg *types.Config, service types.Service, kubeClientset kub if err != nil { DaemonSetLoggerInfo.Println(err) return fmt.Errorf("failed to create daemonset: %s", err.Error()) + } else { + DaemonSetLoggerInfo.Println("Created daemonset for service:", service.Name) } //Set watcher informer diff --git a/pkg/imagepuller/daemonset_test.go b/pkg/imagepuller/daemonset_test.go deleted file mode 100644 index 1b29e709..00000000 --- a/pkg/imagepuller/daemonset_test.go +++ /dev/null @@ -1,74 +0,0 @@ -/* -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 deleted file mode 100644 index 03ee2581..00000000 --- a/pkg/resourcemanager/delegate_test.go +++ /dev/null @@ -1,204 +0,0 @@ -/* -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 deleted file mode 100644 index 6f3939b7..00000000 --- a/pkg/resourcemanager/rescheduler_test.go +++ /dev/null @@ -1,193 +0,0 @@ -/* -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/config.go b/pkg/types/config.go index 6ad0a3a9..3777791e 100644 --- a/pkg/types/config.go +++ b/pkg/types/config.go @@ -183,13 +183,13 @@ type Config struct { // OIDCGroups OpenID comma-separated group list to grant access in the cluster. // Groups defined in the "eduperson_entitlement" OIDC scope, // as described here: https://docs.egi.eu/providers/check-in/sp/#10-groups - OIDCGroups []string `json:"oidc_groups"` + OIDCGroups []string `json:"-"` // IngressHost string `json:"-"` // Github path of FaaS Supervisor (needed for Interlink config) - SupervisorKitImage string `json:"-"` + SupervisorURL string `json:"-"` //Path to additional OSCAR configuration setted by users AdditionalConfigPath string `json:"-"` @@ -238,7 +238,7 @@ var configVars = []configVar{ {"OIDCSubject", "OIDC_SUBJECT", false, stringType, ""}, {"OIDCGroups", "OIDC_GROUPS", false, stringSliceType, ""}, {"IngressHost", "INGRESS_HOST", false, stringType, ""}, - {"SupervisorKitImage", "SUPERVISOR_KIT_IMAGE", false, stringType, ""}, + {"SupervisorURL", "SUPERVISOR_URL", false, stringType, "https://github.com/grycap/faas-supervisor/releases/download/1.5.8/supervisor"}, {"AdditionalConfigPath", "ADDITIONAL_CONFIG_PATH", false, stringType, "config.yaml"}, } diff --git a/pkg/types/expose.go b/pkg/types/expose.go index 09b758b4..0be38895 100644 --- a/pkg/types/expose.go +++ b/pkg/types/expose.go @@ -152,8 +152,8 @@ 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 useful -func ListExpose(kubeClientset kubernetes.Interface, cfg *Config) error { +// This function is not used, in the future could be usefull +func ListExpose(service Service, kubeClientset kubernetes.Interface, cfg *Config) error { deploy, hpa, err := listDeployments(kubeClientset, cfg) services, err2 := listServices(kubeClientset, cfg) @@ -246,7 +246,7 @@ func getHortizontalAutoScaleSpec(service Service, cfg *Config) *autos.Horizontal func getPodTemplateSpec(service Service, cfg *Config) v1.PodTemplateSpec { podSpec, _ := service.ToPodSpec(cfg) - for i := range podSpec.Containers { + for i, _ := range podSpec.Containers { podSpec.Containers[i].Ports = []v1.ContainerPort{ { Name: podPortName, @@ -414,7 +414,7 @@ func deleteService(name string, kubeClientset kubernetes.Interface, cfg *Config) func createIngress(service Service, kubeClientset kubernetes.Interface, cfg *Config) error { // Create Secret - ingress := getIngressSpec(service, cfg) + ingress := getIngressSpec(service, kubeClientset, cfg) _, err := kubeClientset.NetworkingV1().Ingresses(cfg.ServicesNamespace).Create(context.TODO(), ingress, metav1.CreateOptions{}) if err != nil { return err @@ -432,7 +432,7 @@ func updateIngress(service Service, kubeClientset kubernetes.Interface, cfg *Con //if exist continue and need -> Update //if exist and not need -> delete //if not exist create - kube_ingress := getIngressSpec(service, cfg) + kube_ingress := getIngressSpec(service, kubeClientset, cfg) _, err := kubeClientset.NetworkingV1().Ingresses(cfg.ServicesNamespace).Update(context.TODO(), kube_ingress, metav1.UpdateOptions{}) if err != nil { return err @@ -455,7 +455,7 @@ func updateIngress(service Service, kubeClientset kubernetes.Interface, cfg *Con } // Return a kubernetes ingress component, ready to deploy or update -func getIngressSpec(service Service, cfg *Config) *net.Ingress { +func getIngressSpec(service Service, kubeClientset kubernetes.Interface, cfg *Config) *net.Ingress { name_ingress := getIngressName(service.Name) pathofapi := getAPIPath(service.Name) name_service := getServiceName(service.Name) @@ -554,7 +554,7 @@ func deleteIngress(name string, kubeClientset kubernetes.Interface, cfg *Config) // Secret func createSecret(service Service, kubeClientset kubernetes.Interface, cfg *Config) error { - secret := getSecretSpec(service, cfg) + secret := getSecretSpec(service, kubeClientset, cfg) _, err := kubeClientset.CoreV1().Secrets(cfg.ServicesNamespace).Create(context.TODO(), secret, metav1.CreateOptions{}) if err != nil { return err @@ -563,7 +563,7 @@ func createSecret(service Service, kubeClientset kubernetes.Interface, cfg *Conf } func updateSecret(service Service, kubeClientset kubernetes.Interface, cfg *Config) error { - secret := getSecretSpec(service, cfg) + secret := getSecretSpec(service, kubeClientset, cfg) _, err := kubeClientset.CoreV1().Secrets(cfg.ServicesNamespace).Update(context.TODO(), secret, metav1.UpdateOptions{}) if err != nil { return err @@ -579,12 +579,12 @@ func deleteSecret(name string, kubeClientset kubernetes.Interface, cfg *Config) } return nil } -func getSecretSpec(service Service, cfg *Config) *v1.Secret { +func getSecretSpec(service Service, kubeClientset kubernetes.Interface, cfg *Config) *v1.Secret { //setPassword hash := make(htpasswd.HashedPasswords) err := hash.SetPassword(service.Name, service.Token, htpasswd.HashAPR1) if err != nil { - ExposeLogger.Print(err.Error()) + ExposeLogger.Printf(err.Error()) } //Create Secret inmutable := false @@ -620,7 +620,10 @@ func existsSecret(serviceName string, kubeClientset kubernetes.Interface, cfg *C func existsIngress(serviceName string, namespace string, kubeClientset kubernetes.Interface) bool { _, err := kubeClientset.NetworkingV1().Ingresses(namespace).Get(context.TODO(), getIngressName(serviceName), metav1.GetOptions{}) - return err == nil + if err == nil { + return true + } + return false } /// These are auxiliary functions diff --git a/pkg/types/expose_test.go b/pkg/types/expose_test.go deleted file mode 100644 index 69bab5b3..00000000 --- a/pkg/types/expose_test.go +++ /dev/null @@ -1,371 +0,0 @@ -/* -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" - - appsv1 "k8s.io/api/apps/v1" - autoscalingv1 "k8s.io/api/autoscaling/v1" - corev1 "k8s.io/api/core/v1" - netv1 "k8s.io/api/networking/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - testclient "k8s.io/client-go/kubernetes/fake" - k8stesting "k8s.io/client-go/testing" -) - -type Action struct { - Verb string - Resource string -} - -func CompareActions(actions []k8stesting.Action, expected_actions []Action) bool { - if len(actions) != len(expected_actions) { - return false - } - - for i, action := range actions { - if action.GetVerb() != expected_actions[i].Verb || action.GetResource().Resource != expected_actions[i].Resource { - return false - } - } - return true -} - -func TestCreateExpose(t *testing.T) { - - kubeClientset := testclient.NewSimpleClientset() - - service := Service{ - Name: "test-service", - Expose: Expose{ - MinScale: 1, - MaxScale: 3, - CpuThreshold: 80, - SetAuth: true, - }, - } - cfg := &Config{ServicesNamespace: "namespace"} - - err := CreateExpose(service, kubeClientset, cfg) - - if err != nil { - t.Errorf("Error creating expose: %v", err) - } - - actions := kubeClientset.Actions() - expected_actions := []Action{ - {Verb: "create", Resource: "deployments"}, - {Verb: "create", Resource: "horizontalpodautoscalers"}, - {Verb: "create", Resource: "services"}, - {Verb: "create", Resource: "ingresses"}, - {Verb: "create", Resource: "secrets"}, - } - - if CompareActions(actions, expected_actions) == false { - t.Errorf("Expected %v actions but got %v", expected_actions, actions) - } -} - -func TestDeleteExpose(t *testing.T) { - - K8sObjects := []runtime.Object{ - &autoscalingv1.HorizontalPodAutoscaler{ - ObjectMeta: metav1.ObjectMeta{ - Name: "service-hpa", - Namespace: "namespace", - }, - }, - &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: "service-dlp", - Namespace: "namespace", - }, - }, - &corev1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Name: "service-svc", - Namespace: "namespace", - }, - }, - } - - kubeClientset := testclient.NewSimpleClientset(K8sObjects...) - cfg := &Config{ServicesNamespace: "namespace"} - - err := DeleteExpose("service", kubeClientset, cfg) - - if err != nil { - t.Errorf("Error creating expose: %v", err) - } - - actions := kubeClientset.Actions() - - expected_actions := []Action{ - {Verb: "delete", Resource: "horizontalpodautoscalers"}, - {Verb: "delete", Resource: "deployments"}, - {Verb: "delete", Resource: "services"}, - {Verb: "get", Resource: "ingresses"}, - {Verb: "delete-collection", Resource: "pods"}, - } - - if CompareActions(actions, expected_actions) == false { - t.Errorf("Expected %v actions but got %v", expected_actions, actions) - } -} - -func TestUpdateExpose(t *testing.T) { - - K8sObjects := []runtime.Object{ - &autoscalingv1.HorizontalPodAutoscaler{ - ObjectMeta: metav1.ObjectMeta{ - Name: "service-hpa", - Namespace: "namespace", - }, - }, - &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: "service-dlp", - Namespace: "namespace", - }, - }, - &corev1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Name: "service-svc", - Namespace: "namespace", - }, - }, - } - - kubeClientset := testclient.NewSimpleClientset(K8sObjects...) - cfg := &Config{ServicesNamespace: "namespace"} - - service := Service{ - Name: "service", - Expose: Expose{ - MinScale: 1, - MaxScale: 3, - CpuThreshold: 80, - SetAuth: true, - }, - } - - err := UpdateExpose(service, kubeClientset, cfg) - - if err != nil { - t.Errorf("Error creating expose: %v", err) - } - - actions := kubeClientset.Actions() - - expected_actions := []Action{ - {Verb: "get", Resource: "deployments"}, - {Verb: "update", Resource: "deployments"}, - {Verb: "get", Resource: "horizontalpodautoscalers"}, - {Verb: "update", Resource: "horizontalpodautoscalers"}, - {Verb: "update", Resource: "services"}, - {Verb: "get", Resource: "ingresses"}, - {Verb: "create", Resource: "ingresses"}, - {Verb: "create", Resource: "secrets"}, - } - - if CompareActions(actions, expected_actions) == false { - t.Errorf("Expected %v actions but got %v", expected_actions, actions) - } -} - -func TestServiceSpec(t *testing.T) { - - service := Service{ - Name: "test-service", - Expose: Expose{ - MinScale: 1, - MaxScale: 3, - CpuThreshold: 40, - APIPort: 8080, - SetAuth: true, - }, - } - cfg := &Config{Namespace: "namespace"} - res := getServiceSpec(service, cfg) - if res.Spec.Ports[0].TargetPort.IntVal != 8080 { - t.Errorf("Expected port 8080 but got %d", res.Spec.Ports[0].Port) - } -} - -func TestHortizontalAutoScaleSpec(t *testing.T) { - - service := Service{ - Name: "test-service", - Expose: Expose{ - MinScale: 1, - MaxScale: 3, - CpuThreshold: 40, - }, - } - cfg := &Config{Namespace: "namespace"} - res := getHortizontalAutoScaleSpec(service, cfg) - if *res.Spec.MinReplicas != 1 { - t.Errorf("Expected min replicas 1 but got %d", res.Spec.MinReplicas) - } - if res.Spec.MaxReplicas != 3 { - t.Errorf("Expected max replicas 3 but got %d", res.Spec.MaxReplicas) - } - if *res.Spec.TargetCPUUtilizationPercentage != 40 { - t.Errorf("Expected target cpu 40 but got %d", res.Spec.TargetCPUUtilizationPercentage) - } -} - -func TestListIngress(t *testing.T) { - - K8sObjects := []runtime.Object{ - &netv1.Ingress{ - ObjectMeta: metav1.ObjectMeta{ - Name: "service-ing", - Namespace: "namespace", - }, - }, - } - - kubeClientset := testclient.NewSimpleClientset(K8sObjects...) - cfg := &Config{ServicesNamespace: "namespace"} - - _, err := listIngress(kubeClientset, cfg) - - if err != nil { - t.Errorf("Error listing ingresses: %v", err) - } -} - -func TestUpdateIngress(t *testing.T) { - - K8sObjects := []runtime.Object{ - &netv1.Ingress{ - ObjectMeta: metav1.ObjectMeta{ - Name: "service-ing", - Namespace: "namespace", - }, - }, - } - - service := Service{ - Name: "service", - } - - kubeClientset := testclient.NewSimpleClientset(K8sObjects...) - cfg := &Config{ServicesNamespace: "namespace"} - - err := updateIngress(service, kubeClientset, cfg) - - if err != nil { - t.Errorf("Error updating ingress: %v", err) - } -} - -func TestDeleteIngress(t *testing.T) { - - K8sObjects := []runtime.Object{ - &netv1.Ingress{ - ObjectMeta: metav1.ObjectMeta{ - Name: "service-ing", - Namespace: "namespace", - }, - }, - } - - kubeClientset := testclient.NewSimpleClientset(K8sObjects...) - cfg := &Config{ServicesNamespace: "namespace"} - - err := deleteIngress("service-ing", kubeClientset, cfg) - - if err != nil { - t.Errorf("Error deleting ingress: %v", err) - } -} - -func TestUpdateSecret(t *testing.T) { - - K8sObjects := []runtime.Object{ - &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "service-auth-expose", - Namespace: "namespace", - }, - }, - } - service := Service{ - Name: "service", - } - - kubeClientset := testclient.NewSimpleClientset(K8sObjects...) - cfg := &Config{ServicesNamespace: "namespace"} - - err := updateSecret(service, kubeClientset, cfg) - - if err != nil { - t.Errorf("Error updating secret: %v", err) - } -} - -func TestDeleteSecret(t *testing.T) { - - K8sObjects := []runtime.Object{ - &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "service-auth-expose", - Namespace: "namespace", - }, - }, - } - - kubeClientset := testclient.NewSimpleClientset(K8sObjects...) - cfg := &Config{ServicesNamespace: "namespace"} - - err := deleteSecret("service", kubeClientset, cfg) - - if err != nil { - t.Errorf("Error deleting secret: %v", err) - } -} - -func TestExistsSecret(t *testing.T) { - - K8sObjects := []runtime.Object{ - &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "service-auth-expose", - Namespace: "namespace", - }, - }, - } - - kubeClientset := testclient.NewSimpleClientset(K8sObjects...) - cfg := &Config{ServicesNamespace: "namespace"} - - exists := existsSecret("service", kubeClientset, cfg) - - if exists != true { - t.Errorf("Expected secret to exist but got %v", exists) - } - - notexists := existsSecret("service1", kubeClientset, cfg) - - if notexists != false { - t.Errorf("Expected secret not to exist but got %v", notexists) - } -} diff --git a/pkg/types/interlink.go b/pkg/types/interlink.go deleted file mode 100644 index e80cbbea..00000000 --- a/pkg/types/interlink.go +++ /dev/null @@ -1,101 +0,0 @@ -/* -Copyright (C) GRyCAP - I3M - UPV - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package types - -import ( - "encoding/base64" - - v1 "k8s.io/api/core/v1" -) - -const ( - ContainerSupervisorName = "supervisor-container" - SupervisorMountPath = "/data" - SupervisorArg = "cp -r /supervisor/* " + SupervisorMountPath - NameSupervisorVolume = "supervisor-share-data" - NodeSelectorKey = "kubernetes.io/hostname" - - // Annotations for InterLink nodes - InterLinkDNSPolicy = "ClusterFirst" - InterLinkRestartPolicy = "OnFailure" - InterLinkTolerationKey = "virtual-node.interlink/no-schedule" - InterLinkTolerationOperator = "Exists" -) - -var SupervisorCommand = []string{"/bin/sh", "-c"} -var OscarContainerCommand = []string{"echo $EVENT | base64 -d | " + SupervisorMountPath + "/supervisor"} - -// SetInterlinkJob Return interlink configuration for kubernetes job and add Interlink variables to podSpec -func SetInterlinkJob(podSpec *v1.PodSpec, service *Service, cfg *Config, eventBytes []byte) ([]string, v1.EnvVar, []string) { - command := SupervisorCommand - event := v1.EnvVar{ - Name: EventVariable, - Value: base64.StdEncoding.EncodeToString([]byte(eventBytes)), - } - args := OscarContainerCommand - podSpec.NodeSelector = map[string]string{ - NodeSelectorKey: service.InterLinkNodeName, - } - podSpec.DNSPolicy = InterLinkDNSPolicy - podSpec.RestartPolicy = InterLinkRestartPolicy - podSpec.Tolerations = []v1.Toleration{ - { - Key: InterLinkTolerationKey, - Operator: InterLinkTolerationOperator, - }, - } - - addInitContainer(podSpec, cfg) - return command, event, args -} - -// SetInterlinkService Add InterLink configuration to podSpec -func SetInterlinkService(podSpec *v1.PodSpec) { - podSpec.Containers[0].ImagePullPolicy = "Always" - shareDataVolumeMount := v1.VolumeMount{ - Name: NameSupervisorVolume, - MountPath: SupervisorMountPath, - } - - podSpec.Containers[0].VolumeMounts = append(podSpec.Containers[0].VolumeMounts, shareDataVolumeMount) - - shareDataVolume := v1.Volume{ - Name: NameSupervisorVolume, - VolumeSource: v1.VolumeSource{ - EmptyDir: &v1.EmptyDirVolumeSource{}, - }, - } - podSpec.Volumes = append(podSpec.Volumes, shareDataVolume) - -} - -func addInitContainer(podSpec *v1.PodSpec, cfg *Config) { - initContainer := v1.Container{ - Name: ContainerSupervisorName, - Command: SupervisorCommand, - Args: []string{SupervisorArg}, - Image: cfg.SupervisorKitImage, - ImagePullPolicy: v1.PullIfNotPresent, - VolumeMounts: []v1.VolumeMount{ - { - Name: NameSupervisorVolume, - MountPath: SupervisorMountPath, - }, - }, - } - podSpec.InitContainers = []v1.Container{initContainer} -} diff --git a/pkg/types/interlink_test.go b/pkg/types/interlink_test.go deleted file mode 100644 index 3b15f8ba..00000000 --- a/pkg/types/interlink_test.go +++ /dev/null @@ -1,120 +0,0 @@ -/* -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.go b/pkg/types/mount.go index 71915fe2..4c2e5621 100644 --- a/pkg/types/mount.go +++ b/pkg/types/mount.go @@ -49,6 +49,8 @@ done` // SetMount Creates the sidecar container that mounts the source volume onto the pod volume func SetMount(podSpec *v1.PodSpec, service Service, cfg *Config) { podSpec.Containers = append(podSpec.Containers, sidecarPodSpec(service)) + termination := int64(5) + podSpec.TerminationGracePeriodSeconds = &termination addVolume(podSpec) } diff --git a/pkg/types/mount_test.go b/pkg/types/mount_test.go deleted file mode 100644 index df7a3945..00000000 --- a/pkg/types/mount_test.go +++ /dev/null @@ -1,157 +0,0 @@ -/* -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 deleted file mode 100644 index efa5d896..00000000 --- a/pkg/types/replica_test.go +++ /dev/null @@ -1,69 +0,0 @@ -/* -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 20098ab3..9645b41e 100644 --- a/pkg/types/service.go +++ b/pkg/types/service.go @@ -108,17 +108,6 @@ const ( // YAMLMarshal package-level yaml marshal function var YAMLMarshal = yaml.Marshal -type Expose struct { - MinScale int32 `json:"min_scale" default:"1"` - MaxScale int32 `json:"max_scale" default:"10"` - APIPort int `json:"api_port,omitempty" ` - CpuThreshold int32 `json:"cpu_threshold" default:"80" ` - RewriteTarget bool `json:"rewrite_target" default:"false" ` - NodePort int32 `json:"nodePort" default:"0" ` - DefaultCommand bool `json:"default_command" ` - SetAuth bool `json:"set_auth" ` -} - // Service represents an OSCAR service following the SCAR Function Definition Language type Service struct { // Name the name of the service @@ -224,7 +213,16 @@ type Service struct { // Optional ImagePullSecrets []string `json:"image_pull_secrets,omitempty"` - Expose Expose `json:"expose"` + Expose struct { + MinScale int32 `json:"min_scale" default:"1"` + MaxScale int32 `json:"max_scale" default:"10"` + APIPort int `json:"api_port,omitempty" ` + CpuThreshold int32 `json:"cpu_threshold" default:"80" ` + RewriteTarget bool `json:"rewrite_target" default:"false" ` + NodePort int32 `json:"nodePort" default:"0" ` + DefaultCommand bool `json:"default_command" ` + SetAuth bool `json:"set_auth" ` + } `json:"expose"` // The user-defined environment variables assigned to the service // Optional @@ -260,7 +258,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 assume the visibility is public for all cluster users) + // Optional (If the list is empty we asume 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 @@ -309,7 +307,7 @@ func (service *Service) ToPodSpec(cfg *Config) (*v1.PodSpec, error) { } if cfg.InterLinkAvailable && service.InterLinkNodeName != "" { // Add specs of InterLink - SetInterlinkService(podSpec) + podSpec.Containers[0].ImagePullPolicy = "Always" } else { // Add specs volumeMount := v1.VolumeMount{ diff --git a/pkg/utils/auth/auth.go b/pkg/utils/auth/auth.go index 57795a48..40883f7c 100644 --- a/pkg/utils/auth/auth.go +++ b/pkg/utils/auth/auth.go @@ -18,9 +18,7 @@ package auth import ( "fmt" - "log" "strings" - "time" "github.com/gin-gonic/gin" "github.com/grycap/oscar/v3/pkg/types" @@ -29,7 +27,7 @@ import ( ) // GetAuthMiddleware returns the appropriate gin auth middleware -func GetAuthMiddleware(cfg *types.Config, kubeClientset kubernetes.Interface) gin.HandlerFunc { +func GetAuthMiddleware(cfg *types.Config, kubeClientset *kubernetes.Clientset) gin.HandlerFunc { if !cfg.OIDCEnable { return gin.BasicAuth(gin.Accounts{ // Use the config's username and password for basic auth @@ -40,7 +38,7 @@ func GetAuthMiddleware(cfg *types.Config, kubeClientset kubernetes.Interface) gi } // CustomAuth returns a custom auth handler (gin middleware) -func CustomAuth(cfg *types.Config, kubeClientset kubernetes.Interface) gin.HandlerFunc { +func CustomAuth(cfg *types.Config, kubeClientset *kubernetes.Clientset) gin.HandlerFunc { basicAuthHandler := gin.BasicAuth(gin.Accounts{ // Use the config's username and password for basic auth cfg.Username: cfg.Password, @@ -53,7 +51,7 @@ func CustomAuth(cfg *types.Config, kubeClientset kubernetes.Interface) gin.Handl minIOAdminClient.CreateAllUsersGroup() minIOAdminClient.UpdateUsersInGroup(oscarUser, "all_users_group", false) - oidcHandler := getOIDCMiddleware(kubeClientset, minIOAdminClient, cfg.OIDCIssuer, cfg.OIDCSubject, cfg.OIDCGroups, nil) + oidcHandler := getOIDCMiddleware(kubeClientset, minIOAdminClient, cfg.OIDCIssuer, cfg.OIDCSubject, cfg.OIDCGroups) return func(c *gin.Context) { authHeader := c.GetHeader("Authorization") if strings.HasPrefix(authHeader, "Bearer ") { @@ -64,49 +62,6 @@ func CustomAuth(cfg *types.Config, kubeClientset kubernetes.Interface) gin.Handl } } -// GetLoggerMiddleware returns a gin handler as middleware to log custom info about sync/async executions -func GetLoggerMiddleware() gin.HandlerFunc { - return func(c *gin.Context) { - - // Disable default printf timestamp to avoid inconsistencies on logs - log.SetFlags(0) - - startTime := time.Now() - - // Process request - c.Next() - - endTime := time.Now() - - // Log custom information after the request is processed - logTime := endTime.Format("2006/01/02 - 15:04:05") - latency := time.Since(startTime) - status := c.Writer.Status() - clientIP := c.ClientIP() - method := c.Request.Method - path := c.Request.URL.Path - - // Get EGI UID from context (if OIDC auth is used) - uid, uidExists := c.Get("uidOrigin") - var user string - if uidExists { - user, _ = uid.(string) - } else { - // Set OSCAR as default user when no UID is found - user = "oscar" - } - - // Get source IP from context for jobs triggered through MinIO events - IPAddress, AddressExists := c.Get("IPAddress") - if AddressExists { - clientIP, _ = IPAddress.(string) - } - - log.Printf("[GIN-EXECUTIONS-LOGGER] %s | %3d | %13v | %s | %-7s %s | %s", - logTime, status, latency, clientIP, method, path, user) - } -} - func GetUIDFromContext(c *gin.Context) (string, error) { uidOrigin, uidExists := c.Get("uidOrigin") if !uidExists { diff --git a/pkg/utils/auth/auth_test.go b/pkg/utils/auth/auth_test.go deleted file mode 100644 index 1a9c50b5..00000000 --- a/pkg/utils/auth/auth_test.go +++ /dev/null @@ -1,145 +0,0 @@ -/* -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 auth - -import ( - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/gin-gonic/gin" - "github.com/grycap/oscar/v3/pkg/types" - "k8s.io/client-go/kubernetes/fake" -) - -func TestGetAuthMiddleware(t *testing.T) { - cfg := &types.Config{ - OIDCEnable: false, - Username: "testuser", - Password: "testpass", - } - kubeClientset := fake.NewSimpleClientset() - - router := gin.New() - router.Use(GetAuthMiddleware(cfg, kubeClientset)) - router.GET("/", func(c *gin.Context) { - c.JSON(http.StatusOK, "") - }) - - w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/", nil) - req.SetBasicAuth("testuser", "testpass") - router.ServeHTTP(w, req) - - if w.Code != http.StatusOK { - t.Errorf("expected status %v, got %v", http.StatusOK, w.Code) - } - - we := httptest.NewRecorder() - reqe, _ := http.NewRequest("GET", "/", nil) - reqe.SetBasicAuth("testuser", "otherpass") - router.ServeHTTP(we, reqe) - - if we.Code != http.StatusUnauthorized { - t.Errorf("expected status %v, got %v", http.StatusUnauthorized, we.Code) - } -} - -func TestGetLoggerMiddleware(t *testing.T) { - router := gin.New() - router.Use(GetLoggerMiddleware()) - router.GET("/", func(c *gin.Context) { - c.JSON(http.StatusOK, "") - }) - - w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/", nil) - router.ServeHTTP(w, req) - - if w.Code != http.StatusOK { - t.Errorf("expected status %v, got %v", http.StatusOK, w.Code) - } -} - -func TestGetUIDFromContext(t *testing.T) { - c, _ := gin.CreateTestContext(httptest.NewRecorder()) - c.Set("uidOrigin", "testuid") - - uid, err := GetUIDFromContext(c) - if err != nil { - t.Errorf("unexpected error: %v", err) - } - if uid != "testuid" { - t.Errorf("expected uid %v, got %v", "testuid", uid) - } -} - -func TestGetMultitenancyConfigFromContext(t *testing.T) { - c, _ := gin.CreateTestContext(httptest.NewRecorder()) - mc := &MultitenancyConfig{} - c.Set("multitenancyConfig", mc) - - mcFromContext, err := GetMultitenancyConfigFromContext(c) - if err != nil { - t.Errorf("unexpected error: %v", err) - } - if mcFromContext != mc { - t.Errorf("expected multitenancyConfig %v, got %v", mc, mcFromContext) - } -} - -func TestCustomAuth(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, hreq *http.Request) { - if !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"}`)) - } - })) - - cfg := &types.Config{ - OIDCEnable: false, - Username: "testuser", - Password: "testpass", - MinIOProvider: &types.MinIOProvider{ - Endpoint: server.URL, - AccessKey: "minio", - SecretKey: "minio123", - }, - } - kubeClientset := fake.NewSimpleClientset() - - router := gin.New() - router.Use(CustomAuth(cfg, kubeClientset)) - router.GET("/", func(c *gin.Context) { - c.JSON(http.StatusOK, "") - }) - - w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/", nil) - req.SetBasicAuth("testuser", "testpass") - router.ServeHTTP(w, req) - - if w.Code != http.StatusOK { - t.Errorf("expected status %v, got %v", http.StatusOK, w.Code) - } -} diff --git a/pkg/utils/auth/multitenancy.go b/pkg/utils/auth/multitenancy.go index a9f50d6e..8e5a8118 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.Interface + kubeClientset *kubernetes.Clientset owner_uid string usersCache []string } -func NewMultitenancyConfig(kubeClientset kubernetes.Interface, uid string) *MultitenancyConfig { +func NewMultitenancyConfig(kubeClientset *kubernetes.Clientset, uid string) *MultitenancyConfig { return &MultitenancyConfig{ kubeClientset: kubeClientset, owner_uid: uid, diff --git a/pkg/utils/auth/multitenancy_test.go b/pkg/utils/auth/multitenancy_test.go deleted file mode 100644 index 2f93a055..00000000 --- a/pkg/utils/auth/multitenancy_test.go +++ /dev/null @@ -1,149 +0,0 @@ -/* -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 auth - -import ( - "context" - "encoding/base64" - "testing" - - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes/fake" -) - -func TestNewMultitenancyConfig(t *testing.T) { - clientset := fake.NewSimpleClientset() - uid := "test-uid" - mc := NewMultitenancyConfig(clientset, uid) - - if mc.owner_uid != uid { - t.Errorf("expected owner_uid to be %s, got %s", uid, mc.owner_uid) - } -} - -func TestUpdateCache(t *testing.T) { - clientset := fake.NewSimpleClientset() - mc := NewMultitenancyConfig(clientset, "test-uid") - - mc.UpdateCache("user1") - if len(mc.usersCache) != 1 { - t.Errorf("expected usersCache length to be 1, got %d", len(mc.usersCache)) - } -} - -func TestClearCache(t *testing.T) { - clientset := fake.NewSimpleClientset() - mc := NewMultitenancyConfig(clientset, "test-uid") - - mc.UpdateCache("user1") - mc.ClearCache() - if len(mc.usersCache) != 0 { - t.Errorf("expected usersCache length to be 0, got %d", len(mc.usersCache)) - } -} - -func TestUserExists(t *testing.T) { - clientset := fake.NewSimpleClientset() - mc := NewMultitenancyConfig(clientset, "test-uid") - - secret := &v1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "user1", - Namespace: ServicesNamespace, - }, - } - clientset.CoreV1().Secrets(ServicesNamespace).Create(context.TODO(), secret, metav1.CreateOptions{}) - - exists := mc.UserExists("user1@egi.eu") - if !exists { - t.Errorf("expected user1 to exist") - } -} - -func TestCreateSecretForOIDC(t *testing.T) { - clientset := fake.NewSimpleClientset() - mc := NewMultitenancyConfig(clientset, "test-uid") - - err := mc.CreateSecretForOIDC("user1@egi.eu", "secret-key") - if err != nil { - t.Errorf("expected no error, got %v", err) - } - - secret, err := clientset.CoreV1().Secrets(ServicesNamespace).Get(context.TODO(), "user1", metav1.GetOptions{}) - if err != nil { - t.Errorf("expected no error, got %v", err) - } - - if secret.StringData["secretKey"] != "secret-key" { - t.Errorf("expected secretKey to be 'secret-key', got %s", secret.StringData["secretKey"]) - } -} - -func TestGetUserCredentials(t *testing.T) { - clientset := fake.NewSimpleClientset() - mc := NewMultitenancyConfig(clientset, "test-uid") - - secret := &v1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "user1", - Namespace: ServicesNamespace, - }, - Data: map[string][]byte{ - "accessKey": []byte("access-key"), - "secretKey": []byte("secret-key"), - }, - } - clientset.CoreV1().Secrets(ServicesNamespace).Create(context.TODO(), secret, metav1.CreateOptions{}) - - accessKey, secretKey, err := mc.GetUserCredentials("user1@egi.eu") - if err != nil { - t.Errorf("expected no error, got %v", err) - } - - if accessKey != "access-key" { - t.Errorf("expected accessKey to be 'access-key', got %s", accessKey) - } - - if secretKey != "secret-key" { - t.Errorf("expected secretKey to be 'secret-key', got %s", secretKey) - } -} - -func TestGenerateRandomKey(t *testing.T) { - key, err := GenerateRandomKey(32) - if err != nil { - t.Errorf("expected no error, got %v", err) - } - dkey, _ := base64.RawURLEncoding.DecodeString(key) - if len(dkey) != 32 { - t.Errorf("expected key length to be 32, got %d", len(key)) - } -} - -func TestCheckUsersInCache(t *testing.T) { - clientset := fake.NewSimpleClientset() - mc := NewMultitenancyConfig(clientset, "test-uid") - - mc.UpdateCache("user1") - mc.UpdateCache("user2") - - notFoundUsers := mc.CheckUsersInCache([]string{"user1", "user3"}) - if len(notFoundUsers) != 1 { - t.Errorf("expected notFoundUsers length to be 1, got %d", len(notFoundUsers)) - } -} diff --git a/pkg/utils/auth/oidc.go b/pkg/utils/auth/oidc.go index 40dbd54d..9fb2a21f 100644 --- a/pkg/utils/auth/oidc.go +++ b/pkg/utils/auth/oidc.go @@ -51,8 +51,8 @@ type oidcManager struct { // userInfo custom struct to store essential fields from UserInfo type userInfo struct { - Subject string - Groups []string + subject string + groups []string } // newOIDCManager returns a new oidcManager or error if the oidc.Provider can't be created @@ -76,11 +76,8 @@ func NewOIDCManager(issuer string, subject string, groups []string) (*oidcManage } // getIODCMiddleware returns the Gin's handler middleware to validate OIDC-based auth -func getOIDCMiddleware(kubeClientset kubernetes.Interface, minIOAdminClient *utils.MinIOAdminClient, issuer string, subject string, groups []string, oidcConfig *oidc.Config) gin.HandlerFunc { +func getOIDCMiddleware(kubeClientset *kubernetes.Clientset, minIOAdminClient *utils.MinIOAdminClient, issuer string, subject string, groups []string) gin.HandlerFunc { oidcManager, err := NewOIDCManager(issuer, subject, groups) - if oidcConfig != nil { - oidcManager.config = oidcConfig - } if err != nil { return func(c *gin.Context) { c.AbortWithStatus(http.StatusUnauthorized) @@ -99,17 +96,17 @@ func getOIDCMiddleware(kubeClientset kubernetes.Interface, minIOAdminClient *uti rawToken := strings.TrimPrefix(authHeader, "Bearer ") // Check the token - if !oidcManager.IsAuthorised(rawToken) { + if !oidcManager.isAuthorised(rawToken) { c.AbortWithStatus(http.StatusUnauthorized) return } - ui, err := oidcManager.GetUserInfo(rawToken) + ui, err := oidcManager.getUserInfo(rawToken) if err != nil { c.String(http.StatusInternalServerError, fmt.Sprintf("%v", err)) return } - uid := ui.Subject + uid := ui.subject // Check if exist MinIO user in cached users list minioUserExists := mc.UserExists(uid) @@ -145,8 +142,8 @@ func (om *oidcManager) clearExpired() { } } -// GetUserInfo obtains UserInfo from the issuer -func (om *oidcManager) GetUserInfo(rawToken string) (*userInfo, error) { +// getUserInfo obtains UserInfo from the issuer +func (om *oidcManager) getUserInfo(rawToken string) (*userInfo, error) { ot := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: rawToken}) // Get OIDC UserInfo @@ -163,8 +160,8 @@ func (om *oidcManager) GetUserInfo(rawToken string) (*userInfo, error) { // Create "userInfo" struct and add the groups return &userInfo{ - Subject: ui.Subject, - Groups: getGroups(claims.EdupersonEntitlement), + subject: ui.Subject, + groups: getGroups(claims.EdupersonEntitlement), }, nil } @@ -187,11 +184,11 @@ func getGroups(urns []string) []string { // UserHasVO checks if the user contained on the request token is enrolled on a specific VO func (om *oidcManager) UserHasVO(rawToken string, vo string) (bool, error) { - ui, err := om.GetUserInfo(rawToken) + ui, err := om.getUserInfo(rawToken) if err != nil { return false, err } - for _, gr := range ui.Groups { + for _, gr := range ui.groups { if vo == gr { return true, nil } @@ -200,15 +197,16 @@ func (om *oidcManager) UserHasVO(rawToken string, vo string) (bool, error) { } func (om *oidcManager) GetUID(rawToken string) (string, error) { - ui, err := om.GetUserInfo(rawToken) + ui, err := om.getUserInfo(rawToken) + oidcLogger.Println("received uid: ", ui.subject) if err != nil { - return "", err + return ui.subject, nil } - return ui.Subject, nil + return "", err } -// IsAuthorised checks if a token is authorised to access the API -func (om *oidcManager) IsAuthorised(rawToken string) bool { +// isAuthorised checks if a token is authorised to access the API +func (om *oidcManager) isAuthorised(rawToken string) bool { // Check if the token is valid _, err := om.provider.Verifier(om.config).Verify(context.TODO(), rawToken) if err != nil { @@ -219,7 +217,7 @@ func (om *oidcManager) IsAuthorised(rawToken string) bool { ui, found := om.tokenCache[rawToken] if !found { // Get userInfo from the issuer - ui, err = om.GetUserInfo(rawToken) + ui, err = om.getUserInfo(rawToken) if err != nil { return false } @@ -233,12 +231,12 @@ func (om *oidcManager) IsAuthorised(rawToken string) bool { // Check if is authorised // Same subject - if ui.Subject == om.subject { + if ui.subject == om.subject { return true } // Groups - for _, tokenGroup := range ui.Groups { + for _, tokenGroup := range ui.groups { for _, authGroup := range om.groups { if tokenGroup == authGroup { return true diff --git a/pkg/utils/auth/oidc_test.go b/pkg/utils/auth/oidc_test.go deleted file mode 100644 index 142e520f..00000000 --- a/pkg/utils/auth/oidc_test.go +++ /dev/null @@ -1,219 +0,0 @@ -/* -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 auth - -import ( - "crypto/rand" - "crypto/rsa" - "fmt" - "net/http" - "net/http/httptest" - "testing" - "time" - - "github.com/coreos/go-oidc/v3/oidc" - "github.com/gin-gonic/gin" - "github.com/golang-jwt/jwt/v4" - "github.com/grycap/oscar/v3/pkg/types" - "github.com/grycap/oscar/v3/pkg/utils" - "k8s.io/client-go/kubernetes/fake" -) - -func TestNewOIDCManager(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, hreq *http.Request) { - if hreq.URL.Path == "/.well-known/openid-configuration" { - rw.Write([]byte(`{"issuer": "http://` + hreq.Host + `"}`)) - } - })) - - issuer := server.URL - subject := "test-subject" - groups := []string{"group1", "group2"} - - oidcManager, err := NewOIDCManager(issuer, subject, groups) - if err != nil { - t.Errorf("expected no error, got %v", err) - } - if oidcManager == nil { - t.Errorf("expected oidcManager to be non-nil") - } -} - -func TestGetUserInfo(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, hreq *http.Request) { - fmt.Println(hreq.URL.Path) - rw.Header().Set("Content-Type", "application/json") - if hreq.URL.Path == "/.well-known/openid-configuration" { - rw.Write([]byte(`{"issuer": "http://` + hreq.Host + `", "userinfo_endpoint": "http://` + hreq.Host + `/userinfo"}`)) - } else if hreq.URL.Path == "/userinfo" { - rw.Write([]byte(`{"sub": "test-subject", "eduperson_entitlement": ["urn:mace:egi.eu:group:group1"]}`)) - } - })) - - issuer := server.URL - subject := "test-subject" - groups := []string{"group1", "group2"} - - oidcManager, err := NewOIDCManager(issuer, subject, groups) - if err != nil { - t.Errorf("expected no error, got %v", err) - } - - rawToken := "test-token" - ui, err := oidcManager.GetUserInfo(rawToken) - if err != nil { - t.Errorf("expected no error, got %v", err) - } - if ui.Subject != "test-subject" { - t.Errorf("expected subject to be %v, got %v", "test-subject", ui.Subject) - } - if len(ui.Groups) != 1 || ui.Groups[0] != "group1" { - t.Errorf("expected groups to be %v, got %v", []string{"group1"}, ui.Groups) - } -} - -func TestIsAuthorised(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, hreq *http.Request) { - rw.Header().Set("Content-Type", "application/json") - if hreq.URL.Path == "/.well-known/openid-configuration" { - rw.Write([]byte(`{"issuer": "http://` + hreq.Host + `", "userinfo_endpoint": "http://` + hreq.Host + `/userinfo"}`)) - } else if hreq.URL.Path == "/userinfo" { - rw.Write([]byte(`{"sub": "user1@egi.eu", "eduperson_entitlement": ["urn:mace:egi.eu:group:group1"]}`)) - } - })) - - issuer := server.URL - subject := "user1@egi.eu" - groups := []string{"group1", "group2"} - - oidcManager, err := NewOIDCManager(issuer, subject, groups) - if err != nil { - t.Errorf("expected no error, got %v", err) - } - - rawToken := GetToken(issuer, subject) - oidcManager.config.InsecureSkipSignatureCheck = true - - if !oidcManager.IsAuthorised(rawToken) { - t.Errorf("expected token to be authorised") - } - - resg1, err2 := oidcManager.UserHasVO(rawToken, "group1") - if err2 != nil { - t.Errorf("expected no error, got %v", err) - } - if !resg1 { - t.Errorf("expected user to have VO") - } - resg2, err3 := oidcManager.UserHasVO(rawToken, "group2") - if err3 != nil { - t.Errorf("expected no error, got %v", err) - } - if resg2 { - t.Errorf("expected user not to have VO") - } - - uid, _ := oidcManager.GetUID(rawToken) - if uid != subject { - t.Errorf("expected uid to be %v, got %v", subject, uid) - } -} - -func TestGetOIDCMiddleware(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, hreq *http.Request) { - if hreq.URL.Path == "/.well-known/openid-configuration" { - rw.Write([]byte(`{"issuer": "http://` + hreq.Host + `", "userinfo_endpoint": "http://` + hreq.Host + `/userinfo"}`)) - } else if hreq.URL.Path == "/userinfo" { - rw.Write([]byte(`{"sub": "user@egi.eu", "eduperson_entitlement": ["urn:mace:egi.eu:group:group1"]}`)) - } else 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"}`)) - } - })) - - kubeClientset := fake.NewSimpleClientset() - cfg := types.Config{ - MinIOProvider: &types.MinIOProvider{ - Endpoint: server.URL, - Verify: false, - }, - } - minIOAdminClient, _ := utils.MakeMinIOAdminClient(&cfg) - issuer := server.URL - subject := "user@egi.eu" - groups := []string{"group1", "group2"} - - oidcConfig := &oidc.Config{ - InsecureSkipSignatureCheck: true, - SkipClientIDCheck: true, - } - middleware := getOIDCMiddleware(kubeClientset, minIOAdminClient, issuer, subject, groups, oidcConfig) - if middleware == nil { - t.Errorf("expected middleware to be non-nil") - } - - scenarios := []struct { - token string - code int - name string - }{ - { - name: "invalid-token", - token: "invalid-token", - code: http.StatusUnauthorized, - }, - { - name: "valid-token", - token: GetToken(issuer, subject), - code: http.StatusOK, - }, - } - for _, s := range scenarios { - t.Run(s.name, func(t *testing.T) { - // Create a new Gin context - gin.SetMode(gin.TestMode) - w := httptest.NewRecorder() - c, _ := gin.CreateTestContext(w) - - // Test the middleware with an invalid token - c.Request = &http.Request{ - Header: http.Header{ - "Authorization": []string{"Bearer " + s.token}, - }, - } - middleware(c) - if c.Writer.Status() != s.code { - t.Errorf("expected status to be %v, got %v", s.code, c.Writer.Status()) - } - }) - } -} - -func GetToken(issuer string, subject string) string { - claims := jwt.MapClaims{ - "iss": issuer, - "sub": subject, - "exp": time.Now().Add(1 * time.Hour).Unix(), - "iat": time.Now().Unix(), - } - token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) - privateKey, _ := rsa.GenerateKey(rand.Reader, 1024) - rawToken, _ := token.SignedString(privateKey) - return rawToken -} diff --git a/pkg/utils/minio_test.go b/pkg/utils/minio_test.go deleted file mode 100644 index 8121c579..00000000 --- a/pkg/utils/minio_test.go +++ /dev/null @@ -1,123 +0,0 @@ -/* -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 deleted file mode 100644 index 1a35370b..00000000 --- a/pkg/utils/of_scaler_test.go +++ /dev/null @@ -1,196 +0,0 @@ -/* -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 deleted file mode 100644 index c2b25867..00000000 --- a/pkg/utils/token_test.go +++ /dev/null @@ -1,49 +0,0 @@ -/* -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 fbf9fb07..38f3f6f0 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 existence of the default partition and the root queue is assumed) +// (the existance 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 deleted file mode 100644 index bef3181d..00000000 --- a/pkg/utils/yunikorn_test.go +++ /dev/null @@ -1,122 +0,0 @@ -/* -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) - } -} diff --git a/ui b/ui index 3ce8470b..98de936c 160000 --- a/ui +++ b/ui @@ -1 +1 @@ -Subproject commit 3ce8470b994e73789ec086e612af66b9721f2aff +Subproject commit 98de936c5923d3b3ccaef500c1a92f35009bbf53