blob: 724085e61731e061850cc080411a857f6adafc83 [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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.
#
from datetime import time
import requests as req
import json
import os
import logging
from base64 import b64decode, b64encode
from openserverless.common.utils import join_host_port
from openserverless.config.app_config import AppConfig
from openserverless.error.config_exception import ConfigException
SERVICE_HOST_ENV_NAME = "KUBERNETES_SERVICE_HOST"
SERVICE_PORT_ENV_NAME = "KUBERNETES_SERVICE_PORT"
SERVICE_TOKEN_FILENAME = "/var/run/secrets/kubernetes.io/serviceaccount/token"
SERVICE_CERT_FILENAME = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
class KubeApiClient:
@staticmethod
def build_dockerconfigjson(username: str, password: str, registry: str = "https://index.docker.io/v1/") -> str:
"""
Crea una stringa .dockerconfigjson per un secret docker-registry.
:param username: Username del registry
:param password: Password del registry
:param registry: URL del registry (default Docker Hub)
:return: Stringa JSON da usare come valore per .dockerconfigjson
"""
import base64
import json as _json
auth = base64.b64encode(f"{username}:{password}".encode()).decode()
dockerconfig = {
"auths": {
registry: {
"auth": auth,
"username": username,
"password": password
}
}
}
return _json.dumps(dockerconfig)
def __init__(self, environ=os.environ):
self._environ = environ
self.SERVICE_TOKEN_FILENAME = self._environ.get("KUBERNETES_TOKEN_FILENAME") or SERVICE_TOKEN_FILENAME
self.SERVICE_CERT_FILENAME = self._environ.get("KUBERNETES_CERT_FILENAME") or SERVICE_CERT_FILENAME
self._load_incluster_config()
def _parse_b64(self, encoded_str):
try:
return b64decode(encoded_str).decode()
except:
raise ConfigException("Could not decode base64 encoded value")
def _load_incluster_config(self):
"""
Use the service account kubernetes gives to pods to connect to kubernetes
cluster. It's intended for clients that expect to be running inside a pod
running on kubernetes. It will raise an exception if called from a process
not running in a kubernetes environment.
"""
if (
SERVICE_HOST_ENV_NAME not in self._environ
or SERVICE_PORT_ENV_NAME not in self._environ
):
raise ConfigException("Service host/port is not set.")
self.host = "https://" + join_host_port(
self._environ.get(SERVICE_HOST_ENV_NAME),
self._environ.get(SERVICE_PORT_ENV_NAME),
)
self._read_token_file()
with open(self.SERVICE_CERT_FILENAME) as f:
if not f.read():
raise ConfigException("Cert file exists but empty.")
self.ssl_ca_cert = self.SERVICE_CERT_FILENAME
def _read_token_file(self):
with open(self.SERVICE_TOKEN_FILENAME) as f:
content = f.read()
if not content:
raise ConfigException("Token file exists but empty.")
self.token = "Bearer " + content
def create_whisk_user(self, whisk_user_dict, namespace="nuvolaris"):
""" "
Creates a whisk user using a POST operation
param: whisk_user_dict a dictionary representing the whisksusers resource to create
param: namespace default to nuvolaris
return: True if the operation is successfully, False otherwise
"""
url = f"{self.host}/apis/nuvolaris.org/v1/namespaces/{namespace}/whisksusers"
headers = {"Authorization": self.token}
try:
logging.info("POST request to %s", url)
response = None
response = req.post(
url,
headers=headers,
data=json.dumps(whisk_user_dict),
verify=self.ssl_ca_cert,
)
if response.status_code in [200, 201, 202]:
logging.debug(
"POST to %s succeeded with %s. Body %s",
url,
response.status_code,
response.text,
)
return True
logging.error(
"POST to %s failed with %s. Body %s",
url,
response.status_code,
response.text,
)
return False
except Exception as ex:
logging.error("create_whisk_user %s", ex)
return False
def delete_whisk_user(self, username: str, namespace="nuvolaris"):
""" "
Delete a whisk user using a DELETE operation
param: username of the whisksusers resource to delete
param: namespace default to nuvolaris
return: True if the operation is successfully, False otherwise
"""
url = f"{self.host}/apis/nuvolaris.org/v1/namespaces/{namespace}/whisksusers/{username}"
headers = {"Authorization": self.token}
try:
logging.info(f"DELETE request to {url}")
response = None
response = req.delete(url, headers=headers, verify=self.ssl_ca_cert)
if response.status_code in [200, 202]:
logging.debug(
f"DELETE to {url} succeeded with {response.status_code}. Body {response.text}"
)
return True
logging.error(
f"DELETE to {url} failed with {response.status_code}. Body {response.text}"
)
return False
except Exception as ex:
logging.error(f"delete_whisk_user {ex}")
return False
def get_whisk_user(self, username: str, namespace="nuvolaris"):
""" "
Get a whisk user using a GET operation
param: username of the whisksusers resource to delete
param: namespace default to nuvolaris
return: a dictionary representing the existing user, None otherwise
"""
url = f"{self.host}/apis/nuvolaris.org/v1/namespaces/{namespace}/whisksusers/{username}"
headers = {"Authorization": self.token}
try:
logging.info(f"GET request to {url}")
response = None
response = req.get(url, headers=headers, verify=self.ssl_ca_cert)
if response.status_code in [200, 202]:
logging.debug(
f"GET to {url} succeeded with {response.status_code}. Body {response.text}"
)
return json.loads(response.text)
logging.error(
f"GET to {url} failed with {response.status_code}. Body {response.text}"
)
return None
except Exception as ex:
logging.error(f"get_whisk_user {ex}")
return None
def update_whisk_user(self, whisk_user_dict, namespace="nuvolaris"):
""" "
Updates a whisk user using a PUT operation
param: whisk_user_dict a dictionary representing the whisksusers resource to update
param: namespace default to nuvolaris
return: True if the operation is successfully, False otherwise
"""
url = f"{self.host}/apis/nuvolaris.org/v1/namespaces/{namespace}/whisksusers/{whisk_user_dict['metadata']['name']}"
headers = {"Authorization": self.token}
try:
logging.error(f"PUT request to {url}")
response = None
response = req.put(
url,
headers=headers,
data=json.dumps(whisk_user_dict),
verify=self.ssl_ca_cert,
)
if response.status_code in [200, 201, 202]:
logging.debug(
f"PUT to {url} succeeded with {response.status_code}. Body {response.text}"
)
return True
logging.error(
f"PUT to {url} failed with {response.status_code}. Body {response.text}"
)
return False
except Exception as ex:
logging.error(f"update_whisk_user {ex}")
return False
def get_config_map(self, cm_name: str, namespace="nuvolaris"):
"""
Get a ConfigMap by name.
:param cm_name: Name of the ConfigMap.
:param namespace: Namespace where the ConfigMap is located.
:return: The ConfigMap data or None if not found.
"""
url = f"{self.host}/api/v1/namespaces/{namespace}/configmaps/{cm_name}"
headers = {"Authorization": self.token}
try:
logging.info(f"GET request to {url}")
response = req.get(url, headers=headers, verify=self.ssl_ca_cert)
if response.status_code == 200:
logging.debug(
f"GET to {url} succeeded with {response.status_code}. Body {response.text}"
)
return json.loads(response.text)
logging.error(
f"GET to {url} failed with {response.status_code}. Body {response.text}"
)
return None
except Exception as ex:
logging.error(f"get_config_map {ex}")
return None
def post_config_map(self, cm_name: str, file_or_dir: str, namespace="nuvolaris"):
"""
Create a ConfigMap from a file or directory.
:param cm_name: Name of the ConfigMap.
:param file_or_dir: Path to the file or directory containing the data.
:param namespace: Namespace where the ConfigMap will be created.
:return: The created ConfigMap or None if failed.
"""
if not os.path.exists(file_or_dir):
raise ConfigException(f"File or directory {file_or_dir} does not exist.")
configmap_data = {}
if os.path.isfile(file_or_dir):
with open(file_or_dir, "r") as f:
configmap_data[os.path.basename(file_or_dir)] = f.read()
elif os.path.isdir(file_or_dir):
for filename in os.listdir(file_or_dir):
filepath = os.path.join(file_or_dir, filename)
if os.path.isfile(filepath):
with open(filepath, "r") as f:
configmap_data[filename] = f.read()
configmap_manifest = {
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"name": cm_name
},
"data": configmap_data
}
url = f"{self.host}/api/v1/namespaces/{namespace}/configmaps"
headers = {"Authorization": self.token, "Content-Type": "application/json"}
try:
logging.info(f"POST request to {url}")
response = None
response = req.post(url, data=json.dumps(configmap_manifest), headers=headers, verify=self.ssl_ca_cert)
if response.status_code in [200, 201, 202]:
logging.debug(
f"POST to {url} succeeded with {response.status_code}. Body {response.text}"
)
return json.loads(response.text)
logging.error(
f"POST to {url} failed with {response.status_code}. Body {response.text}"
)
return None
except Exception as ex:
logging.error(f"post_config_map {ex}")
return None
def delete_config_map(self, cm_name: str, namespace="nuvolaris"):
"""
Delete a ConfigMap by name.
:param cm_name: Name of the ConfigMap to delete.
:param namespace: Namespace where the ConfigMap is located.
:return: True if deletion was successful, False otherwise.
"""
url = f"{self.host}/api/v1/namespaces/{namespace}/configmaps/{cm_name}"
headers = {"Authorization": self.token}
try:
logging.info(f"DELETE request to {url}")
response = None
response = req.delete(url, headers=headers, verify=self.ssl_ca_cert)
if response.status_code in [200, 202]:
logging.debug(
f"DELETE to {url} succeeded with {response.status_code}. Body {response.text}"
)
return True
logging.error(
f"DELETE to {url} failed with {response.status_code}. Body {response.text}"
)
return False
except Exception as ex:
logging.error(f"delete_config_map {ex}")
return False
def get_secret(self, secret_name: str, namespace="nuvolaris"):
"""
Get a Kubernetes secret by name.
:param secret_name: Name of the secret.
:param namespace: Namespace where the secret is located.
:return: The secret data or None if not found.
"""
url = f"{self.host}/api/v1/namespaces/{namespace}/secrets/{secret_name}"
headers = {"Authorization": self.token}
try:
logging.info(f"GET request to {url}")
response = req.get(url, headers=headers, verify=self.ssl_ca_cert)
if response.status_code == 200:
logging.debug(
f"GET to {url} succeeded with {response.status_code}. Body {response.text}"
)
return json.loads(response.text)
logging.error(
f"GET to {url} failed with {response.status_code}. Body {response.text}"
)
return None
except Exception as ex:
logging.error(f"get_secret {ex}")
return None
def post_secret(self, secret_name: str, secret_data: dict, type: str="Opaque", namespace="nuvolaris"):
"""
Create a Kubernetes secret.
:param secret_name: Name of the secret.
:param secret_data: Dictionary containing the secret data.
:param type: Type of the secret (Opaque, kubernetes.io/dockerconfigjson, kubernetes.io/tls)
:param namespace: Namespace where the secret will be created.
:return: The created secret or None if failed.
"""
url = f"{self.host}/api/v1/namespaces/{namespace}/secrets"
headers = {"Authorization": self.token, "Content-Type": "application/json"}
if type == "kubernetes.io/dockerconfigjson":
# secret_data should contain a key '.dockerconfigjson' with the JSON string value
manifest_data = {
".dockerconfigjson": b64encode(secret_data[".dockerconfigjson"].encode()).decode()
}
elif type == "kubernetes.io/tls":
# secret_data should contain 'tls.crt' and 'tls.key'
manifest_data = {
"tls.crt": b64encode(secret_data["tls.crt"].encode()).decode(),
"tls.key": b64encode(secret_data["tls.key"].encode()).decode()
}
else:
# Default: Opaque or other types
manifest_data = {k: b64encode(v.encode()).decode() for k, v in secret_data.items()}
secret_manifest = {
"apiVersion": "v1",
"kind": "Secret",
"metadata": {"name": secret_name},
"data": manifest_data,
"type": type
}
try:
logging.info(f"POST request to {url}")
response = req.post(url, headers=headers, json=secret_manifest, verify=self.ssl_ca_cert)
if response.status_code in [200, 201]:
logging.debug(
f"POST to {url} succeeded with {response.status_code}. Body {response.text}"
)
return json.loads(response.text)
logging.error(
f"POST to {url} failed with {response.status_code}. Body {response.text}"
)
return None
except Exception as ex:
logging.error(f"post_secret {ex}")
return None
def delete_secret(self, secret_name: str, namespace="nuvolaris"):
"""
Delete a Kubernetes secret.
:param secret_name: Name of the secret to delete.
:param namespace: Namespace where the secret is located.
:return: True if deletion was successful, False otherwise.
"""
url = f"{self.host}/api/v1/namespaces/{namespace}/secrets/{secret_name}"
headers = {"Authorization": self.token}
try:
logging.info(f"DELETE request to {url}")
response = req.delete(url, headers=headers, verify=self.ssl_ca_cert)
if response.status_code in [200, 202]:
logging.debug(
f"DELETE to {url} succeeded with {response.status_code}. Body {response.text}"
)
return True
logging.error(
f"DELETE to {url} failed with {response.status_code}. Body {response.text}"
)
return False
except Exception as ex:
logging.error(f"delete_secret {ex}")
return False
def get_jobs(self, name_filter: str = None, namespace="nuvolaris"):
"""
Get all Kubernetes jobs in a specific namespace.
:param namespace: Namespace to list jobs from.
:return: List of jobs or None if failed.
"""
url = f"{self.host}/apis/batch/v1/namespaces/{namespace}/jobs"
headers = {"Authorization": self.token}
try:
logging.info(f"GET request to {url}")
response = req.get(url, headers=headers, verify=self.ssl_ca_cert)
if response.status_code in [200, 202]:
logging.debug(
f"GET to {url} succeeded with {response.status_code}. Body {response.text}"
)
if name_filter:
jobs = json.loads(response.text)["items"]
filtered_jobs = [job for job in jobs if name_filter in job["metadata"]["name"]]
return filtered_jobs
return json.loads(response.text)["items"]
logging.error(
f"GET to {url} failed with {response.status_code}. Body {response.text}"
)
return None
except Exception as ex:
logging.error(f"get_jobs {ex}")
return None
def delete_job(self, job_name: str, namespace="nuvolaris"):
"""
Delete a Kubernetes job by name.
:param job_name: Name of the job to delete.
:param namespace: Namespace where the job is located.
:return: True if deletion was successful, False otherwise.
"""
url = f"{self.host}/apis/batch/v1/namespaces/{namespace}/jobs/{job_name}"
headers = {"Authorization": self.token}
try:
logging.info(f"DELETE request to {url}")
response = req.delete(url, headers=headers, verify=self.ssl_ca_cert)
if response.status_code in [200, 202]:
logging.debug(
f"DELETE to {url} succeeded with {response.status_code}. Body {response.text}"
)
return True
logging.error(
f"DELETE to {url} failed with {response.status_code}. Body {response.text}"
)
return False
except Exception as ex:
logging.error(f"delete_job {ex}")
return False
def post_job(self, job_manifest: json, namespace="nuvolaris"):
"""
Create a Kubernetes job.
:param job_manifest: Dictionary containing the job manifest.
:param namespace: Namespace where the job will be created.
:return: The created job or None if failed.
"""
url = f"{self.host}/apis/batch/v1/namespaces/{namespace}/jobs"
headers = {"Authorization": self.token}
try:
logging.info(f"POST request to {url}")
response = None
response = req.post(url, headers=headers, json=job_manifest, verify=self.ssl_ca_cert)
if response.status_code in [200, 201, 202]:
logging.debug(
f"POST to {url} succeeded with {response.status_code}. Body {response.text}"
)
return json.loads(response.text)
logging.error(
f"POST to {url} failed with {response.status_code}. Body {response.text}"
)
return None
except Exception as ex:
logging.error(f"post_job {ex}")
return None
def get_pod_by_job_name(self, job_name: str, namespace="nuvolaris"):
"""
Get the pod name associated with a job by its name.
:param job_name: Name of the job.
:param namespace: Namespace where the job is located.
:return: The pod name if found, None otherwise.
"""
url = f"{self.host}/api/v1/namespaces/{namespace}/pods"
headers = {"Authorization": self.token}
try:
while True:
resp = req.get(url, headers=headers, verify=self.ssl_ca_cert)
if not resp.status_code in [200, 202]:
logging.error(
f"POST to {url} failed with {resp.status_code}. Body {resp.text}"
)
return None
logging.debug(
f"POST to {url} succeeded with {resp.status_code}. Body {resp.text}"
)
pods = resp.json()["items"]
for pod in pods:
labels = pod["metadata"].get("labels", {})
if labels.get("job-name") == job_name:
return pod["metadata"]["name"]
time.sleep(1)
except Exception as ex:
logging.error(f"get_pod_by_job_name {ex}")
return None
def stream_pod_logs(self, pod_name: str, namespace="nuvolaris"):
"""
Stream logs from a specific pod.
:param pod_name: Name of the pod to stream logs from.
:param namespace: Namespace where the pod is located.
"""
url = f"{self.host}/api/v1/namespaces/{namespace}/pods/{pod_name}/log?follow=true"
headers = {"Authorization": self.token}
with req.get(url, headers=headers, verify=self.ssl_ca_cert, stream=True) as r:
for line in r.iter_lines():
if line:
print(line.decode())
def check_job_status(self, job_name: str, namespace="nuvolaris"):
"""
Check the status of a job by its name.
:param job_name: Name of the job to check.
:param namespace: Namespace where the job is located.
:return: True if the job has succeeded, False otherwise.
"""
url = f"{self.host}/apis/batch/v1/namespaces/{namespace}/jobs/{job_name}"
headers = {"Authorization": self.token}
try:
resp = req.get(url, headers=headers, verify=self.ssl_ca_cert)
resp.raise_for_status()
status = resp.json()["status"]
if status.get("succeeded", 0) > 0:
return True
else:
return False
except Exception as ex:
logging.error(f"check_job_status {ex}")
return False