From 6fd8e37704a4e94041daab3cc46106986f82f4c3 Mon Sep 17 00:00:00 2001 From: Syed Murtaza Hassan Date: Thu, 14 Nov 2024 02:18:25 +0500 Subject: [PATCH 1/2] Cleaned XkOps Repo: Removed old files and folder and updated documents and folder structure. --- .pre-commit-config.yaml | 4 +- CONTRIBUTING.md | 5 +- Dockerfile | 36 -- Makefile | 121 ------ README.md | 87 ++--- SECURITY.md | 21 +- charts/stable/index.yaml | 14 - charts/stable/xkops-0.1.0.tgz | Bin 4293 -> 0 bytes documentation/About XkOps/FAQs.md | 30 ++ documentation/About XkOps/Overview.md | 79 ++++ documentation/About XkOps/Tools we use.md | 19 + documentation/About XkOps/XkOps Roadmap.md | 33 ++ documentation/HOME.md | 20 + .../Installing XkOps/FAQs for Installation.md | 53 +++ .../Installing XkOps/Helm Parameters.md | 47 +++ .../Installing XkOps/Requirements.md | 15 + .../Self Hosted Installation.md | 0 .../Troubleshooting Installation Problems.md | 29 ++ .../Using XkOps/Available Workflows.md | 37 ++ .../Creating Your Own Workflows.md | 17 + .../Using XkOps/FAQs for Workflow Creation.md | 45 +++ .../Using XkOps/Understanding Workflows.md | 38 ++ documentation/Using XkOps/Using Workflows.md | 39 ++ .../Using XkOps/Workdlow's Architecture.md | 8 + .../Using XkOps/Workflow Creation Criteria.md | 19 + helm/.helmignore | 23 ++ helm/Chart.yaml | 44 +-- helm/templates/_helpers.tpl | 9 - .../templates/clusterrole-xkops-helmfile.yaml | 134 +++++++ .../clusterrolebinding-xkops-helmfile.yaml | 12 + helm/templates/configmap-xkops-helmfile.yaml | 8 + helm/templates/configmap-xkops-mongodb.yaml | 6 + helm/templates/configmap-xkops-query.yaml | 30 ++ helm/templates/configmaps.yaml | 25 -- helm/templates/dashboard-service.yaml | 27 -- helm/templates/dashboard.yaml | 34 -- helm/templates/deployment -xkops-query.yaml | 49 +++ helm/templates/deployment-xkops-backend.yaml | 41 ++ helm/templates/deployment-xkops-frontend.yaml | 25 ++ helm/templates/job-xkops-helmfile.yaml | 19 + helm/templates/manifests.yaml | 62 --- .../persistentvolume-xkops-mongodb.yaml | 13 + .../persistentvolumeclaim-xkops-mongodb.yaml | 11 + .../role-xkops-deployment-editor.yaml | 9 + .../rolebinding-xkops-robusta-runner.yaml | 13 + helm/templates/secret-xkops-mongodb.yaml | 8 + helm/templates/secretproviderclass.yaml | 59 --- helm/templates/service-xkops-backend.yaml | 13 + helm/templates/service-xkops-mongodb.yaml | 13 + helm/templates/statefulset-xkops-mongodb.yaml | 47 +++ helm/templates/storage-class.yaml | 23 -- helm/templates/workflows-config.yaml | 11 - helm/templates/xkops-clusterrole.yaml | 211 ---------- helm/templates/xkops-clusterrolebindings.yaml | 43 --- helm/templates/xkops-deployment.yaml | 133 ------- helm/values.yaml | 129 +------ images/arch diagram.png | Bin 114219 -> 50020 bytes secret-manager/cleaner.sh | 360 ------------------ secret-manager/secret-manager.sh | 252 ------------ src/checker/README.md | 104 ----- src/checker/kc-checker.sh | 150 -------- src/checker/px-checker.sh | 164 -------- src/checker/rb-checker.sh | 181 --------- src/commons/README.md | 33 -- src/commons/common-functions.sh | 220 ----------- src/config/config.sh | 33 -- src/config/kc-config.sh | 24 -- src/config/px-config.sh | 40 -- src/config/rb-config.sh | 50 --- src/installer/README.md | 61 --- src/installer/kc-installer.sh | 66 ---- src/installer/px-installer.sh | 81 ---- .../delete_persistent_volume.py | 61 --- src/installer/rb-actions/pyproject.toml | 15 - src/installer/rb-installer.sh | 101 ----- src/pre-flight/README.md | 52 --- src/pre-flight/kc-pre-flight/kc-preflight.sh | 66 ---- src/pre-flight/px-pre-flight/px-preflight.sh | 68 ---- src/pre-flight/rb-pre-flight/rb-preflight.sh | 124 ------ .../unclaimed-volume-test.sh | 77 ---- .../unclaimed-volume.yaml | 30 -- src/rollback/kc-rollback.sh | 41 -- src/rollback/px-rollback.sh | 42 -- src/rollback/rb-rollback.sh | 53 --- src/test/kc-test.sh | 83 ---- src/test/px-test.sh | 89 ----- src/test/rb-test.sh | 49 --- src/testing/ebs-provisioning-test.py | 91 ----- terraform-scripts/Dockerfile | 25 -- terraform-scripts/README | 42 -- terraform-scripts/ebs-provisioning.tf | 90 ----- terraform-scripts/user-creation.tf | 78 ---- 92 files changed, 1079 insertions(+), 4117 deletions(-) delete mode 100644 Dockerfile delete mode 100644 Makefile delete mode 100644 charts/stable/index.yaml delete mode 100644 charts/stable/xkops-0.1.0.tgz create mode 100644 documentation/About XkOps/FAQs.md create mode 100644 documentation/About XkOps/Overview.md create mode 100644 documentation/About XkOps/Tools we use.md create mode 100644 documentation/About XkOps/XkOps Roadmap.md create mode 100644 documentation/HOME.md create mode 100644 documentation/Installing XkOps/FAQs for Installation.md create mode 100644 documentation/Installing XkOps/Helm Parameters.md create mode 100644 documentation/Installing XkOps/Requirements.md rename manifests/secrets.yaml => documentation/Installing XkOps/Self Hosted Installation.md (100%) create mode 100644 documentation/Installing XkOps/Troubleshooting Installation Problems.md create mode 100644 documentation/Using XkOps/Available Workflows.md create mode 100644 documentation/Using XkOps/Creating Your Own Workflows.md create mode 100644 documentation/Using XkOps/FAQs for Workflow Creation.md create mode 100644 documentation/Using XkOps/Understanding Workflows.md create mode 100644 documentation/Using XkOps/Using Workflows.md create mode 100644 documentation/Using XkOps/Workdlow's Architecture.md create mode 100644 documentation/Using XkOps/Workflow Creation Criteria.md create mode 100644 helm/.helmignore delete mode 100644 helm/templates/_helpers.tpl create mode 100644 helm/templates/clusterrole-xkops-helmfile.yaml create mode 100644 helm/templates/clusterrolebinding-xkops-helmfile.yaml create mode 100644 helm/templates/configmap-xkops-helmfile.yaml create mode 100644 helm/templates/configmap-xkops-mongodb.yaml create mode 100644 helm/templates/configmap-xkops-query.yaml delete mode 100644 helm/templates/configmaps.yaml delete mode 100644 helm/templates/dashboard-service.yaml delete mode 100644 helm/templates/dashboard.yaml create mode 100644 helm/templates/deployment -xkops-query.yaml create mode 100644 helm/templates/deployment-xkops-backend.yaml create mode 100644 helm/templates/deployment-xkops-frontend.yaml create mode 100644 helm/templates/job-xkops-helmfile.yaml delete mode 100644 helm/templates/manifests.yaml create mode 100644 helm/templates/persistentvolume-xkops-mongodb.yaml create mode 100644 helm/templates/persistentvolumeclaim-xkops-mongodb.yaml create mode 100644 helm/templates/role-xkops-deployment-editor.yaml create mode 100644 helm/templates/rolebinding-xkops-robusta-runner.yaml create mode 100644 helm/templates/secret-xkops-mongodb.yaml delete mode 100644 helm/templates/secretproviderclass.yaml create mode 100644 helm/templates/service-xkops-backend.yaml create mode 100644 helm/templates/service-xkops-mongodb.yaml create mode 100644 helm/templates/statefulset-xkops-mongodb.yaml delete mode 100644 helm/templates/storage-class.yaml delete mode 100644 helm/templates/workflows-config.yaml delete mode 100644 helm/templates/xkops-clusterrole.yaml delete mode 100644 helm/templates/xkops-clusterrolebindings.yaml delete mode 100644 helm/templates/xkops-deployment.yaml delete mode 100755 secret-manager/cleaner.sh delete mode 100755 secret-manager/secret-manager.sh delete mode 100644 src/checker/README.md delete mode 100755 src/checker/kc-checker.sh delete mode 100644 src/checker/px-checker.sh delete mode 100644 src/checker/rb-checker.sh delete mode 100644 src/commons/README.md delete mode 100644 src/commons/common-functions.sh delete mode 100644 src/config/config.sh delete mode 100755 src/config/kc-config.sh delete mode 100644 src/config/px-config.sh delete mode 100644 src/config/rb-config.sh delete mode 100644 src/installer/README.md delete mode 100755 src/installer/kc-installer.sh delete mode 100644 src/installer/px-installer.sh delete mode 100644 src/installer/rb-actions/my_playbook_repo/delete_persistent_volume.py delete mode 100644 src/installer/rb-actions/pyproject.toml delete mode 100644 src/installer/rb-installer.sh delete mode 100644 src/pre-flight/README.md delete mode 100755 src/pre-flight/kc-pre-flight/kc-preflight.sh delete mode 100644 src/pre-flight/px-pre-flight/px-preflight.sh delete mode 100644 src/pre-flight/rb-pre-flight/rb-preflight.sh delete mode 100755 src/remediation/unclaimed-volumes/unclaimed-volume-tests/unclaimed-volume-test.sh delete mode 100644 src/remediation/unclaimed-volumes/unclaimed-volume-tests/unclaimed-volume.yaml delete mode 100644 src/rollback/kc-rollback.sh delete mode 100644 src/rollback/px-rollback.sh delete mode 100644 src/rollback/rb-rollback.sh delete mode 100755 src/test/kc-test.sh delete mode 100644 src/test/px-test.sh delete mode 100755 src/test/rb-test.sh delete mode 100644 src/testing/ebs-provisioning-test.py delete mode 100644 terraform-scripts/Dockerfile delete mode 100644 terraform-scripts/README delete mode 100644 terraform-scripts/ebs-provisioning.tf delete mode 100644 terraform-scripts/user-creation.tf diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e22598c..480d169 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,6 @@ -# pre-commit configuration file +# This file configures Git pre-commit hooks to automatically run code quality checks +# and format code before commits, ensuring consistent standards across the project. +# Define each hook, its source repo, and version below. # Repositories containing hooks repos: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8c54bd9..29cc4e3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,6 +41,9 @@ Before getting started and setup with contributing, you'll want to have a look a If you have gotten that far, then you can go ahead and work on the issue. Below are more detailed instructions based on the basic workflow above. +### Commit Messages +Commit messages should provide enough information about what has changed and why. + ### Pull Request Process Please adhere to the following guideline for submitting a pull request: @@ -74,4 +77,4 @@ If you have any questions or need help, Join our Slack channel [XkOps Community] ## Acknowledgements -We would like to acknowledge and thank all contributors for their work on this project. +We would like to acknowledge and thank all contributors for their work on this project. \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 5e0ad0e..0000000 --- a/Dockerfile +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright (c) 2023, Xgrid Inc, https://xgrid.co - -# 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. - -FROM ubuntu:22.04 - -COPY src /src -COPY Makefile / - -SHELL ["/bin/bash", "-o", "pipefail", "-c"] -RUN apt-get update && \ - apt-get install -y make=4.3-4.1build1 --no-install-recommends && \ - apt-get install -y curl=7.81.0-1ubuntu1.10 --no-install-recommends && \ - apt-get install -y --no-install-recommends ca-certificates curl=7.81.0-1ubuntu1.10 && \ - apt-get upgrade -y && \ - apt-get clean && \ - rm -rf /var/lib/apt/lists/*y && \ - curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" && \ - chmod +x ./kubectl && \ - mv ./kubectl /usr/local/bin/kubectl && \ - apt-get install -y unzip=6.0-26ubuntu3.1 --no-install-recommends && \ - apt-get install -y jq=1.6-2.1ubuntu3 --no-install-recommends && \ - export PATH="/usr/local/bin:$PATH" && \ - chmod -R +x /src - -CMD [ "make", "all" ] diff --git a/Makefile b/Makefile deleted file mode 100644 index f087107..0000000 --- a/Makefile +++ /dev/null @@ -1,121 +0,0 @@ -# Copyright (c) 2023, Xgrid Inc, https://xgrid.co - -# 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. - -# We'll be using following prefixes with rules and variables. -# Pixie ==> px -# Robusta ==> rb -# Kubecost ==> kc - -INTERPRETER = bash - -#Pixie script paths -PX_CHECKER = /src/checker/px-checker.sh -PX_PRE_FLIGHT = /src/pre-flight/px-pre-flight/px-preflight.sh -PX_INSTALLER = /src/installer/px-installer.sh -PX_ROLLBACK = /src/rollback/px-rollback.sh -PX_TEST= /src/test/px-test.sh - -#Robusta script paths -RB_CHECKER = /src/checker/rb-checker.sh -RB_PRE_FLIGHT = /src/pre-flight/rb-pre-flight/rb-preflight.sh -RB_INSTALLER = /src/installer/rb-installer.sh -RB_ROLLBACK = /src/rollback/rb-rollback.sh -RB_TEST = /src/test/rb-test.sh - -#Kubecost script paths -KC_CHECKER = /src/checker/kc-checker.sh -KC_PRE_FLIGHT = /src/pre-flight/kc-pre-flight/kc-preflight.sh -KC_INSTALLER = /src/installer/kc-installer.sh -KC_ROLLBACK = /src/rollback/kc-rollback.sh -KC_TEST = /src/test/kc-test.sh - - -# Pixie relevant rules. -px_check: - $(INTERPRETER) $(PX_CHECKER) - -px_preflight: - $(INTERPRETER) $(PX_PRE_FLIGHT) - -px_install: - $(INTERPRETER) $(PX_INSTALLER) "$(PX_API_KEY)" - -#Flow control of px_all -# RUN PX_CHECKER if returns Exit 0 -> RUN PX_PRE_FLIGHT else if returns Exit 1 -> echo "PX_CHECKER Exit 1" -# RUN PX_PRE_FLIGHT if returns Exit 0 -> RUN PX_INSTALLER else if returns Exit 1 -> echo "PX_PRE_FLIGHT Exit 1" -# RUN PX_INSTALLER if returns Exit 1 -> echo "PX_INSTALLER Exit 1" -px_all: - $(INTERPRETER) $(PX_CHECKER) && ( $(INTERPRETER) $(PX_PRE_FLIGHT) && ( $(INTERPRETER) $(PX_INSTALLER) "$(PX_API_KEY)" || echo "PX_INSTALLER Exit 1") || echo "PX_PRE_FLIGHT Exit 1" ) || echo "PX_CHECKER Exit 1" - -#Flow control of px_rollback -#RUN PX_ROLLBACK if returns Exit 1 -> echo "PX_ROLLBACK Exit 1" -px_rollback: - $(INTERPRETER) $(PX_ROLLBACK) || echo "PX_ROLLBACK Exit 1" - -# Robusta relevant rules -rb_check: - $(INTERPRETER) $(RB_CHECKER) - -rb_preflight: - $(INTERPRETER) $(RB_PRE_FLIGHT) - -rb_install: - $(INTERPRETER) $(RB_INSTALLER) - -#Flow control of rb_all -# RUN RB_CHECKER if returns Exit 0 -> RUN RB_PRE_FLIGHT else if returns Exit 1 -> echo "RB_CHECKER Exit 1" -# RUN RB_PRE_FLIGHT if returns Exit 0 -> RUN RB_INSTALLER else if returns Exit 1 -> echo "RB_PRE_FLIGHT Exit 1" -# RUN RB_INSTALLER if returns Exit 1 -> echo "RB_INSTALLER Exit 1" -rb_all: - $(INTERPRETER) $(RB_CHECKER) && ( $(INTERPRETER) $(RB_PRE_FLIGHT) && ( $(INTERPRETER) $(RB_INSTALLER) && ( $(INTERPRETER) $(RB_TEST) || echo "RB_TEST Exit 1")|| echo "RB_INSTALLER Exit 1") || echo "RB_PRE_FLIGHT Exit 1" ) || echo "RB_CHECKER Exit 1" - -#Flow control of rb_rollback -#RUN RB_ROLLBACK if returns Exit 1 -> echo "RB_ROLLBACK Exit 1" -rb_rollback: - $(INTERPRETER) $(RB_ROLLBACK) || echo "RB_ROLLBACK Exit 1" - -# Kubecost relevant rules -kc_check: - $(INTERPRETER) $(KC_CHECKER) - -kc_preflight: - $(INTERPRETER) $(KC_PRE_FLIGHT) - -kc_install: - $(INTERPRETER) $(KC_INSTALLER) - -kc_test: - $(INTERPRETER) $(KC_TEST) -#Flow control of kc_all -# RUN KC_CHECKER if returns Exit 0 -> RUN KC_PRE_FLIGHT else if returns Exit 1 -> echo "KC_CHECKER Exit 1" -# RUN KC_PRE_FLIGHT if returns Exit 0 -> RUN KC_INSTALLER else if returns Exit 1 -> echo "KC_PRE_FLIGHT Exit 1" -# RUN KC_INSTALLER if returns Exit 1 -> echo "KC_INSTALLER Exit 1" -kc_all: - $(INTERPRETER) $(KC_CHECKER) && ( $(INTERPRETER) $(KC_PRE_FLIGHT) && ( $(INTERPRETER) $(KC_INSTALLER) && ( $(INTERPRETER) $(KC_TEST) || echo "KC_TEST Exit 1")|| echo "KC_INSTALLER Exit 1") || echo "KC_PRE_FLIGHT Exit 1" ) || echo "KC_CHECKER Exit 1" - -#Flow control of kc_rollback -#RUN KC_ROLLBACK if returns Exit 1 -> echo "KC_ROLLBACK Exit 1" -kc_rollback: - $(INTERPRETER) $(KC_ROLLBACK) || echo "KC_ROLLBACK Exit 1" - -#target to install all tools -all: - $(MAKE) rb_all - $(MAKE) kc_all - -#target to remove all tools from cluster -rollback_all: - $(MAKE) px_rollback - $(MAKE) rb_rollback - $(MAKE) kc_rollback diff --git a/README.md b/README.md index ae17b55..ab77f81 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,36 @@ # XkOps -![XkOps Logo](./images/xkops_logo.png "XkOps Logo") - +![XkOps - Kubernetes Observability Tool](./images/xkops_logo.png "XkOps Logo") [![Lint Code Base](https://github.com/XgridInc/xkops/actions/workflows/linter.yml/badge.svg)](https://github.com/XgridInc/xkops/actions/workflows/linter.yml) [![Package and Publish Docker Image](https://github.com/XgridInc/xkops/actions/workflows/build_publish_scan.yml/badge.svg)](https://github.com/XgridInc/xkops/actions/workflows/build_publish_scan.yml) [![Shellcheck](https://github.com/XgridInc/xkops/actions/workflows/shellcheck.yml/badge.svg)](https://github.com/XgridInc/xkops/actions/workflows/shellcheck.yml) -## 💻 Overview +## 📘 Overview -Xkops is an integrated solution that combines widely-adopted open-source utilities to simplify Kubernetes management, emphasizing the enhancement of observability, reliability, security, and cost control in cluster operations which help users optimize the deployment and management of Kubernetes clusters. The platform provides a comprehensive evaluation of costs, observability, and security. -XkOps provides an effortless way for users to manage their cluster and extract meaningful insights related to observerability and cost. The platform includes the installers of top-tier open-source tools, namely Kubecost, Robusta, and Pixie, which are bundled together into a single Docker image. This Docker image is then deployed as a Pod, simplifying the installation process of these tools. -This all-in-one solution consolidates the insights obtained from these tools, making it easier for users to make informed decisions and have centralized monitoring of their Kubernetes cluster. +**XkOps** is a unified observability and management solution designed to simplify Kubernetes cluster operations, with a focus on **observability**, **reliability**, **security**, and **cost control**. By integrating powerful open-source tools like **Kubecost**, **Robusta**, and **Pixie**, XkOps provides a seamless, all-in-one experience for managing and optimizing Kubernetes environments. -Check the below video for a quick demo of XkOps. +This platform offers users an end-to-end setup for monitoring, cost analysis, and security insights directly in their Kubernetes clusters, allowing for smarter resource allocation and operational efficiency. XkOps consolidates critical information from these tools to provide a central, intuitive interface for making informed, real-time decisions. -[![XkOps Demo](./images/Demo-video.jpg)](https://youtu.be/l-psN4IDDBA) +## 📋 Key Features -## 🏗️ XkOps Architecture Diagram +### Effortless Installation +Easily deploy XkOps by visiting Artifact Hub and following the Helm chart installation instructions. It’s designed for quick setup and easy deployment, reducing the complexity of integrating multiple tools. -![XkOps Architecture Diagram](./images/arch%20diagram.png "Architecture Digram") +### Centralized Observability +Gain deep visibility into your Kubernetes cluster with integrated observability tools like Pixie, providing real-time monitoring and insights to track cluster health. + +### Cost Management +Kubecost integration delivers detailed cost analysis and recommendations, helping teams optimize their Kubernetes workloads and reduce unnecessary spend. -## 💡 Why XkOps? +### Actionable Insights +The XkOps frontend allows users to view actionable insights and directly interact with alerts and recommendations, streamlining decision-making and operational tasks. -XkOps is a unified platform that provides true observability across Kubernetes clusters while being cost-optimized, fault-tolerant, and secure. With an abundance of tools available, implementing and managing multiple data platforms can become overwhelming and lead to additional costs. +### Integrated Workflows +Benefit from pre-configured workflows that combine Kubecost, Robusta, and Pixie for a seamless Kubernetes management experience that covers cost, observability, and security. -XkOps simplifies this by offering a single platform that provides a single source of truth, making it easier for users to gain insights and manage their cluster. Achieving optimal cost efficiency, state-of-the-art security, and dependable application performance through observability is a necessity for Kubernetes clusters. -XkOps addresses this by encompassing the three core concepts of cost optimization, reliability, and security in a single platform. +## 🏗️ XkOps Architecture Diagram + +![XkOps Architecture Diagram](./images/arch%20diagram.png "Architecture Digram") ## 🛠️ Use cases @@ -41,50 +46,26 @@ XkOps can be used for several use cases, including: **Reporting:** XkOps allows users to generate reports to share with stakeholders about the cost, performance, and security of the Kubernetes cluster over time. -## ➕ Dependencies - -Please note the following requirements for using the XkOps: - -- To install the necessary dependencies, an internet connection is required. -- You will need an EKS cluster and an AWS IAM user with a minimum set of permissions listed in this [spreadsheet](https://docs.google.com/spreadsheets/d/1cuC-72oRJ7DB4HkvELpml5RLcA2clzCA7xBVd1z6fVw/edit?usp=sharing). -- The pod resources must meet the minimum requirements of 200MiB memory and 500m CPU. -- Please ensure that you use a separate XkOps namespace. - -## 📒 Getting Started - -To install XkOps, please follow these steps - -### 🔐 Secret Manager Setup - -First, set up AWS secrets manager on your AWS account: - -- Refer to this [guide](https://docs.google.com/document/d/17fhQ0zJZtJGcWtnVD8NehUbFC-x9TrMP11XjyEFi370/edit?usp=sharing) for instructions on how to set up AWS secret manager OR -- Use [this script](https://github.com/XgridInc/xkops/blob/master/secret-manager/secret-manager.sh) to automate the setup process. - ### 📥 Install XkOps -1. Clone the repository and navigate to the cloned repo: - - ```commandline - git clone https://github.com/XgridInc/xkops.git && cd xkops - ``` +### Prerequisites +Before installing XkOps, ensure the following: -2. Update values.yaml file and input your specific value for each key. -3. Install XkOps using Helm: +- **Kubernetes Cluster**: A running Kubernetes cluster (EKS, GKE, or another provider). +- **Helm:** Helm 3.x installed to deploy XkOps using Helm charts. +- **kubectl:** Installed and configured to interact with your Kubernetes cluster. - ```commandline - helm install xkops ./helm -f values.yml - ``` +For further details on environment setup, refer to the Prerequisites Documentation. -4. After successful installation, obtain the link of the XkOps frontend service to access the dashboard: +### Installation - ```commandline - kubectl get svc -n xkops - ``` +- Visit the [Artifact Hub](https://artifacthub.io/). +- Search for the XkOps Helm chart. +- Follow the installation instructions provided on the Artifact Hub page. -5. Create an unclaimed volume in your cluster and delete it using the delete button on the dashboard. You can verify the volume deletion action both from the dashboard and the cluster. +For more detailed steps and configurations, refer to the Installation Documentation. -## 🚧 Road Map +## 🗺️ Road Map To report a new feature request or to report any issues or bugs encountered while using XkOps, please feel free to [create a new issue](https://github.com/XgridInc/xkops/issues "create a new issue") on the project's GitHub repository or contact the development team via [Slack Channel](https://join.slack.com/t/xkopscommunity/shared_invite/zt-1u8xzjvvq-B52TJ2XE861v3KDvpA9UVg). The following features are currently either in progress or planned: @@ -92,9 +73,9 @@ To report a new feature request or to report any issues or bugs encountered whil - [X] Installing tools to mitigate risk. - [X] Using Robusta to monitor and troubleshoot clusters. - [X] Employing Kubecost for cost optimization. -- [X] Utilizing Pixie to monitor system performance. -- [x] Deployment using Helm charts. -- [ ] Implementation of a user interface. +- [ ] Utilizing Pixie to monitor system performance. +- [X] Deployment using Helm charts. +- [X] Implementation of a user interface. - [ ] Determining risk factors based on metrics from your Kubernetes cluster. - [ ] Extracting logs from pod using a logging solution diff --git a/SECURITY.md b/SECURITY.md index 034e848..4fd9dca 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -8,14 +8,23 @@ currently being supported with security updates. | Version | Supported | | ------- | ------------------ | | 5.1.x | :white_check_mark: | -| 5.0.x | :x: | -| 4.0.x | :white_check_mark: | | < 4.0 | :x: | ## Reporting a Vulnerability +To report a security problem in XkOps, please contact us at xkops@xgrid.co . -Use this section to tell people how to report a vulnerability. +**Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** -Tell them where to go, how often they can expect to get an update on a -reported vulnerability, what to expect if the vulnerability is accepted or -declined, etc. +Please include as much of the information listed below as you can to help us better understand and resolve the issue: +- The type of issue (e.g., buffer overflow, SQL injection, or cross-site scripting) +- Full paths of source file(s) related to the manifestation of the issue +- The location of the affected source code (tag/branch/commit or direct URL) +- Any special configuration required to reproduce the issue +- Step-by-step instructions to reproduce the issue +- Proof-of-concept or exploit code (if possible) +- Impact of the issue, including how an attacker might exploit the issue + +Our team will respond within 7 working days of your email. The team will help diagnose the severity of the issue and determine how to address the issue. Critical issues will receive immediate attention and be fixed as quickly as possible. + +## Preferred Languages +We prefer all communications to be in English. \ No newline at end of file diff --git a/charts/stable/index.yaml b/charts/stable/index.yaml deleted file mode 100644 index f8bbbb3..0000000 --- a/charts/stable/index.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -entries: - xkops: - - apiVersion: v2 - appVersion: 0.0.1 - created: "2023-06-16T16:42:09.441321744+05:00" - description: A Helm chart to deploy xkops - digest: 52baa82875323901a5a3a950571bf68b07c57006e57b3ec30a5db8fb260bb84b - name: xkops - type: application - urls: - - xkops-0.1.0.tgz - version: 0.1.0 -generated: "2023-06-16T16:42:09.440907644+05:00" diff --git a/charts/stable/xkops-0.1.0.tgz b/charts/stable/xkops-0.1.0.tgz deleted file mode 100644 index 6bfac64e34d8979fe89c6aca3b7e9e9a6df5057f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4293 zcmV;$5IXN4iwG0|00000|0w_~VMtOiV@ORlOnEsqVl!4SWK%V1T2nbTPgYhoO;>Dc zVQyr3R8em|NM&qo0PJ1cZ`(Mt@3a1jfrEo>fl8L0TaV9^H%)hgrb!T|+btF!0$QSN zZX{79sU-fg`QHyn>O$RYcau8jJ1ai44mq3|QNtgH!;9{3Xw3UZ3&e!8L}Bpi2PD^Z z-GjY7^}p-7)&JeWq5EpEx3{RMr@N#X$(3G#3yQd>jXK3Cf792!(i*{uav^ zj{wDSKs+StTDJw;f#X^){oGyBN%#L-6eO7cC;(W~{|{VuQ0@Qwhx=Xs-$vSjBN{Im znJ)yq_TIqI9qzt`-{*|@a1wcMVIf4!NB#c2%yK+xSvzn_JREWCLlXIzfmmP|$H-gY ztmG}EC;~&rh1Zhcr?lv&H~(Ypz>+2qq9sIBK*BMI1>rCw0S0{U;aC8Pz@uRt5EOYB z?ub|@F)0(r+JUbr3py2uM1a7f@e=6Fqyr?RzVaDH{r=tEor9E~4rTLxplNvj^yKLH zY;tT59amC+jsna%F#Ihc4Er!$WlPPy|1XCvYrwG$2qzK%bg+B3>RBxHj~Q*ab3FF*{#En-AAKbI{&#SkMd z?TuqZ2XGCQN8l5Jy5=i83^vTWPL(+CEuDF4fbt`o(&2GJ@W|>xMn`cZfr!c5mpqpGZ$V z%(n#dGX0=VKN!A~^l9@Yu~Orf21$q^q)8-d?@Skdl4Xjyc83Bww{?jHy5a>AixKoi z7+1K*5;D2Kk;nu!i)iD%pjUNQuR<(MZRR&QukBTKTXK@|qVF}o!m zzK$s~A6cTUv;oS*2;RBMKG+TAoBH3~&orap2+!GArixarPOL(0PkUsUe6W=>NzU?zS!wg3r?jt`W zQ6I&`zQIfW#=7|ZdVFzm{qgwg2ztF{)~Q#0gO}FMX8d)HLd5&yi_`P3ZBixy@@}#R zFhy`#;CM9bFK`g|5fj7AdSYFk z|NMD!HNO6QQV~A>G6C=y_HiT#53KQ*$@Tc?=y)=bMAs+p6`jnT93NdCUm1l)$z*bJ zes+Cz{_*%M<$Hj* z#I00|i_7!hPTn71ULT!~CzF)X`7LHlwxsN@vguT(nI%D>{&-CyFGzgJP51KvBd!VG z6y01--qg62aZ}7QCZ$Z}>v19b1cWFeGl`sX2$l}fQo25r$p@k{h#8@bh^3X&Nkp*0 zm=)S-D=`{iHP_HUl~x?VU{!@FiR6Rvriw$OVm=@@5$+m<9*t&X9-_FB<(@&w1!ZWC zZS~rki1!WRy9|_UEue}1p!T5JZshpaM1N?|7yeTN;kzb6JH_dy3jf_r#Q)hz`*XSX z+`pE{j~RDF{A{6sb@9L9?p|g7zjwI1*UkU8kscnv*A974nS!HHOU}P~U_U-umK>gL z@pX=CBg=Ytu%&kh4AFYQ65cDewgI1!yfJAaghM}gn0Y|?4DeLN=FWol( zgjDVSl~M6oWUT4`g9G=V(*O7OcDwlhR#F%L>*9aU7ypyil;TGks7ttRK^*`r++5JB zN?e5uON&qg>xM-BDCP7C#a4&~@{vGAh#{RFD2g;w2jQUi@Q@EV#Z;gin1jd1o|Q2? zI{iGkI=;+iokn%DL{$OEXLSad&PxdzXR-!yyxh0~l#*Y-&Rn(*iXzK-{N>KZyyYk;kt@!_pn*r9~|Ii&&*Z=p2 z!_NQTO6u^x!~Y+K|0s@mKOY3%=g$Ar#sP-a%iwy!pgaJCNO+4=G{r%R_pM_>26<^c zLm`=h5{p(Q0GU#1F*#d6H7eSz&zB{=%-%8}sY0h%fQQG&(F#I==Ei`P4O)geTBTgk z|MbzTOHWPJ@jqX^+v50tc(`93|99QpZv5Xy>c;ko|GyakNcS|_Tu_D`j}zzKzIn$TLiRB18g(1CW11f`WD?Prxg@sR;69WRW8kiWd_AQ>}u_X zl3LYbe|1CcQeli33bDvzLNv86_^X9`&onbLlB0>PlvVZ8juoN^%`r2{{tPiMP5~4$ z^m%|@&5C*@x=hSCAc(IgFR`uJ(|Xoxs#Mq;7PblNR`Q%}$xv3aTtS+%t6T4Cp6lpy z#)V0)X4pn&GAgWdEiaSwrTvsH{g_nq|7&~QpDttF{*S#{{O`cs@6P{jC3XIP=l}n( z^}iHMoSMt8^V@uVJmog`EC3WmG!>`anUtXhr<-JoS%d{v%n7%BeeT#!Px|!)OK{Ip ze!kR4y83v^)iea%amS;f#Uw8hW@jAV;{Xe6=_8dt>k~(xsE5=5+hv0h~*H z1kpLmvf=TuX;NOAEN3$Hk&)u$iL%8J{ADXFFv6S-M^_y93#JEqIP$uKB&$-j|2H4h ze432a{eR%@A5`~$9lGve*Z;SXI{&Zp|DN^yM|u!mLWe%${`<{&%(ZWr9_-L0mo&g7 zZ;?%r=b%I^C?o$?LJY^T^$bXXR2EADoSr&IG5LkjB<3Udqu2Xadg+UCX$WK+q0-ZC zd6tiGNF$Dgk(F;;EAnvU$CQ+EZc7BJsnl{_k(Up9MHY$X@@<(!)HnVrE(Os}sd87_ zi;WWM7k^}1=FF!`^JF`FG!l#k0cK{gZmn+#6{yHgcSzSVkmsqJPfGIpTp*ED^6)+O z$~gg@mos9@#Ei1LVxyd$UyqRet*nGb0tGSkQ6eb!kbD3q`%R*PtH)^cC!KOMhJ)_~j#SrZGLxoa;~g{=$JF-;XNkILZ<_dXNvCgUDcZo8{$TEu zr&mo{qBw5WuT)@aRw|IM56_aIUeeHU>ODePazDg^5wDsXGkU*lvw72WO9pp~+0^L1 z^L(*I_BUSqaaVe$p1QQ8U|%XsV&5$E^$f6vA+6xF0$d7c;!T;^jTX;=MrG`2z?pIj zmvw4X=Eot+<`A+5`~Dun)a|qhs$1Z%L!FmGotNwS#;a}b>}`he1l?Pdt!P)g)OO`$ zAHZJ1h*+w3OHxD6NY(7TAiqhb*rQz7bB1OpLiGam4~kH*{5NKH%;ToVOn6gc#xd`` zg^kqS5d4h1zH0Rq&eaOIl6jT-%|KQmGR?Iv*#xMUGhu}`D@Ljms7es^!YY@ruw8Xy zg(K zZ5}8H>M2t@2@~=IqxRUUJMDKO=5brE%I|=#rIhrcTUgh{XaYT-`*2*)u3s_=X8m_C7*y~7J2)72`#-jlI{&Zp|DJXK-xDtYDM0IEV-C>ViY z3D$WPMM_%V(jc$xabk-18M(!*>EKssxj@HhkbfQpOD)wX&}}gmY451qUc&3g#meT< z_}vF-8|#m3HC+x?`tnBgO0eojH*`u5TeLFC>Nt|n;6@@Mj#k_ulYfoImXwCThAU*6 zK7!+{xlLw8o%+QxD*wXx&dY|gXTnuPA+B zG%WXntS5ZL%XAJ11NoY6dxFjmxI4NKIlN3r~Y&zu~2Cgt^~^x|`!}A-(YwFsw56bzrd(T(qAw zt|-@d&Ul;pwJ4CO*mewiYbv%YP*%~raeH$;*CN|S*j7@tRgp~Jm51cF)~;)`$dqjz z$F|q3Eh^Mjfps#!6=*1=z5-Z7$ef3xt2thrG@)$v&A{~xG--`T`J05_p;uP%37X|B nYPwav|JTT*Ab8<>?7P&ZE_JC(-. Check for error messages indicating configuration or resource issues. + +How do I troubleshoot connectivity issues between XkOps components? +Inspect Kubernetes service definitions and network policies. Verify pod network connectivity using tools like kubectl exec and curl. + +Can I uninstall XkOps and reinstall it? +Yes, you can uninstall XkOps using helm uninstall and then reinstall it using the same command. + +General + +Do I need any specific network configurations for XkOps? +Ensure your Kubernetes cluster has network connectivity to required external services (e.g., container registries) and that necessary ports are open. + +Where can I find additional support resources for XkOps? +Refer to the official XkOps documentation, and community forums, or contact XkOps support for assistance. \ No newline at end of file diff --git a/documentation/Installing XkOps/Helm Parameters.md b/documentation/Installing XkOps/Helm Parameters.md new file mode 100644 index 0000000..eea56de --- /dev/null +++ b/documentation/Installing XkOps/Helm Parameters.md @@ -0,0 +1,47 @@ +This document outlines the different methods for configuring XkOps using Helm parameters. These parameters allow you to customize the deployment behavior of XkOps to suit your specific needs. + +Understanding Helm Parameters + +Helm charts offer a way to package Kubernetes applications and their configurations. XkOps utilizes a values.yaml file to store default configuration values for the deployed components. These values can be overridden during installation using Helm parameters. + +Passing Helm Parameters + +There are two primary methods for passing Helm parameters when installing XkOps: + +Using Command-Line Flags ( --set ) + +This approach allows you to directly specify individual parameters while executing the Helm install command. Here's an example: + +helm install my-xkops xkops/xkops \ + --repo https://xgridinc.github.io/xkops/charts/stable \ + --namespace xkops \ + --create-namespace \ + --set cluster.name={{CLUSTER_NAME}} \ + --set apiServer.serviceClusterIPRange={{SERVICE_CLUSTER_IP_RANGE}} + +In this example, we set the cluster name to and specify a custom service cluster IP range using the apiServer.serviceClusterIPRange parameter. + +Customizing the values.yaml file + +You can modify the default values.yaml file downloaded from the provided location (link can be added later) to customize various parameters. This approach allows for more complex configurations and avoids cluttering the install command with numerous flags. + +Here's a basic example structure of a modified values.yaml file: + +cluster: + name: {{CLUSTER_NAME}} +apiServer: + serviceClusterIPRange: {{SERVICE_CLUSTER_IP_RANGE}} + +Once the values.yaml file is customized, use the following command to install XkOps with your specific configuration: + +helm install my-xkops xkops/xkops \ + --repo https://xgridinc.github.io/xkops/charts/stable \ + --namespace xkops \ + --create-namespace \ + -f values.yaml + +Choosing the Right Method: + +Simple Configuration: For setting a few specific parameters, using command-line flags offers a concise approach. + +Complex Configuration: For extensive customization or managing multiple XkOps deployments with different configurations, modifying the values.yaml file is more efficient. \ No newline at end of file diff --git a/documentation/Installing XkOps/Requirements.md b/documentation/Installing XkOps/Requirements.md new file mode 100644 index 0000000..ebcd78d --- /dev/null +++ b/documentation/Installing XkOps/Requirements.md @@ -0,0 +1,15 @@ +This document outlines the requirements for installing XkOps on the Kubernetes cluster. + +To install XkOps, ensure that the following prerequisites are met: + +Helm 3.x +Helm 3.x must be installed on your system. Helm is a package manager for Kubernetes that simplifies the deployment and management of applications within Kubernetes clusters. You can download and install Helm 3.x from the official Helm website. + +Kubectl Command-Line Tool +Kubectl must be installed and configured on your system. Kubectl is the command-line tool for interacting with Kubernetes clusters, allowing you to deploy and manage applications within the cluster. You can download and set up kubectl by following the instructions on the Kubernetes website. + +Permissions for Kubernetes Resource Creation +Ensure you have the necessary permissions to deploy Kubernetes manifests on the cluster. This typically requires roles with sufficient privileges, such as "ClusterAdmin" or a custom role configured to allow creating, modifying, and managing resources within the cluster. + + + diff --git a/manifests/secrets.yaml b/documentation/Installing XkOps/Self Hosted Installation.md similarity index 100% rename from manifests/secrets.yaml rename to documentation/Installing XkOps/Self Hosted Installation.md diff --git a/documentation/Installing XkOps/Troubleshooting Installation Problems.md b/documentation/Installing XkOps/Troubleshooting Installation Problems.md new file mode 100644 index 0000000..fa47d1f --- /dev/null +++ b/documentation/Installing XkOps/Troubleshooting Installation Problems.md @@ -0,0 +1,29 @@ +Common Installation Issues + +Installation Failures and Errors + +Check Helm Installation and Configuration: Ensure Helm is correctly installed and configured on your system. Verify Helm version compatibility with XkOps requirements. + +Inspect Helm Logs: Review Helm installation logs for specific error messages. These can provide clues about the root cause of the failure. + +Verify Kubernetes Cluster Health: Check the status of your Kubernetes cluster using kubectl to get nodes and kubectl to get pods. Ensure there are no critical issues affecting the cluster. + +Resource Constraints: Verify that your Kubernetes cluster has sufficient resources (CPU, memory) to accommodate the XkOps deployment. Adjust resource requests and limits in the values.yaml file if necessary. + +Connectivity Issues + +Check Network Connectivity: Ensure your Kubernetes cluster can reach external services required by XkOps components. Verify network policies and firewall rules. + +Inspect Kubernetes Service Definitions: Review the service definitions for XkOps components to ensure they are configured correctly. Check for port conflicts or incorrect service types. + +Verify Pod Network Connectivity: Use kubectl exec and curl to test connectivity between XkOps pods. Check for network-related errors in pod logs. + +Component Failures + +Review Component Logs: Inspect the logs of individual XkOps components using kubectl logs to identify specific issues. + +Check Component Dependencies: Ensure that all required dependencies for XkOps components are installed and functioning correctly. + +Verify Component Configuration: Review the configuration of XkOps components to ensure they are correctly defined in the values.yaml file. + +You should be able to resolve most installation problems by addressing these common issues. If you continue to experience difficulties, refer to the XkOps documentation or seek assistance from the XkOps support team. \ No newline at end of file diff --git a/documentation/Using XkOps/Available Workflows.md b/documentation/Using XkOps/Available Workflows.md new file mode 100644 index 0000000..5a99daf --- /dev/null +++ b/documentation/Using XkOps/Available Workflows.md @@ -0,0 +1,37 @@ +XkOps offers a comprehensive suite of pre-built automation workflows, or Xworkflows, designed to optimize your Kubernetes cluster's performance and cost-efficiency. These Xworkflows leverage the capabilities of tools like Kubecost and Robusta to automate common management tasks. + +Cost Optimization Xworkflows + +Xworkflow 1: Delete Unclaimed Persistent Volumes This Xworkflow identifies unused persistent volumes (PVs) in your Kubernetes cluster by leveraging Kubecost's data analysis capabilities. It provides a clear overview of unclaimed PVs, allowing users to choose which volumes to delete selectively. Robusta seamlessly handles the deletion process, ensuring efficient reclamation of storage resources. By eliminating unnecessary storage, this Xworkflow significantly contributes to reducing overall cluster costs. + +Xworkflow 2: Underutilized Node Optimization By analyzing node utilization data from Kubecost, this Xworkflow pinpoints nodes operating below optimal capacity. It gives users actionable recommendations, such as terminating underutilized nodes or strategically relocating workloads. Implementing these suggestions can lead to substantial cost savings without compromising cluster performance. + +Xworkflow 3: Abandoned Workloads This Xworkflow identifies workloads, such as pods and deployments, exhibiting minimal or no activity. Through in-depth analysis, it highlights potential candidates for optimization, including deletion or scaling down. By eliminating idle resources, this Xworkflow helps reduce unnecessary costs and improve overall cluster efficiency. + +Resource Optimization Xworkflows + +These Xworkflows focus on optimizing resource allocation within your Kubernetes cluster by analyzing resource utilization patterns and providing actionable recommendations. + +CPU Optimization + +Xworkflow 4: Pod CPU Request Optimization This Xworkflow meticulously examines the CPU usage patterns of individual pods within your cluster. By comparing actual CPU consumption to allocated requests, it identifies pods with excessive resource requests. Users can easily adjust CPU requests using Robusta actions, resulting in optimized resource utilization and improved cost efficiency. + +Xworkflow 5: Deployment CPU Request Optimization Similar to pod optimization, this Xworkflow analyzes CPU utilization patterns of deployments. By identifying deployments with excessive CPU requests, it empowers users to make informed decisions about resizing CPU requests. This optimization helps ensure that deployment resources align with actual workload demands. + +Xworkflow 6: StatefulSet CPU Request Optimization This Xworkflow is specifically designed for StatefulSets. It analyzes CPU usage patterns of StatefulSet pods to identify potential optimization opportunities. By adjusting CPU requests, users can enhance the performance and efficiency of their StatefulSet workloads. + +Xworkflow 7: DaemonSet CPU Request Optimization Tailored for DaemonSets, this Xworkflow evaluates CPU utilization patterns across DaemonSet pods. It provides recommendations for adjusting CPU requests to match workload requirements, optimizing resource allocation, and minimizing unnecessary consumption. + +Memory Optimization + +Xworkflow 8: Pod Memory Request Optimization This Xworkflow scrutinizes memory usage patterns of individual pods to identify instances of excessive memory requests. By providing clear recommendations, it empowers users to adjust memory requests and optimize resource utilization. + +Xworkflow 9: Deployment Memory Request Optimization Focused on deployments, this Xworkflow analyzes memory consumption patterns to identify potential optimization opportunities. Users can make data-driven decisions to resize memory requests and improve overall cluster efficiency. + +Xworkflow 10: StatefulSet Memory Request Optimization This Xworkflow specializes in optimizing memory usage for StatefulSets. By analyzing memory consumption patterns of StatefulSet pods, it provides recommendations for adjusting memory requests to enhance performance and efficiency. + +Xworkflow 11: DaemonSet Memory Request Optimization Designed for DaemonSets, this Xworkflow examines memory usage patterns to identify opportunities for optimization. By adjusting memory requests, users can ensure that DaemonSets operate efficiently without excessive resource consumption. + +XkOps' pre-built Xworkflows provide a robust foundation for optimizing your Kubernetes cluster. These workflows deliver tangible benefits by automating routine tasks, reducing costs, and improving overall performance. However, XkOps also offers a flexible framework that allows you to create custom workflows tailored to your specific requirements. + +By combining pre-built and custom Xworkflows, you can achieve optimal cluster management and unlock the full potential of your Kubernetes environment. \ No newline at end of file diff --git a/documentation/Using XkOps/Creating Your Own Workflows.md b/documentation/Using XkOps/Creating Your Own Workflows.md new file mode 100644 index 0000000..4f96ed2 --- /dev/null +++ b/documentation/Using XkOps/Creating Your Own Workflows.md @@ -0,0 +1,17 @@ +This section provides detailed instructions for creating a new workflow in XkOps. Workflows are a core component of XkOps, allowing you to automate and manage specific actions within your Kubernetes environment. To ensure optimal performance and maintain system standards, it is recommended to create only those workflows that meet the requirements. For more details on these requirements, please refer to the “Workflow Creation Criteria” section. + +To create a new workflow, follow these steps: + +Create a Necessary Robusta Action + +Begin by defining the required Robusta action that aligns with your specific needs. Robusta actions are modular components within xkops, designed to execute targeted tasks in response to workflow triggers. Carefully assess your requirements and create a robust action that effectively addresses your objectives, whether it involves resource management, system monitoring, or automated scaling. + +Integrate Business Logic in the Query Pod Section + +After defining the Robusta action, move on to configuring the Query Pod section where the business logic resides. Here, a cron job will be set up to retrieve cost recommendations from Kubecost pods. Kubecost provides cost optimization insights, which can be valuable for controlling resource expenses within your Kubernetes environment. + +Within the Query Pod, refine the logic to retrieve cost recommendations specifically from the Kubecost APIs that align with your requirements. This targeted approach ensures that the workflow only pulls in relevant cost data, reducing unnecessary overhead and focusing on actionable recommendations. Be sure to customize the cron job timing to align with your monitoring and optimization intervals. + +Develop Supporting Backend APIs in Flask + +The final step involves building the backend APIs to support your Robusta action, using Flask. These APIs provide the interface needed to trigger, monitor, and manage the Robusta action, integrating seamlessly with the cost recommendations logic. It is also recommended to implement authentication, logging, and error handling for these APIs to enhance their reliability and maintain a secure audit trail of all workflow activities. \ No newline at end of file diff --git a/documentation/Using XkOps/FAQs for Workflow Creation.md b/documentation/Using XkOps/FAQs for Workflow Creation.md new file mode 100644 index 0000000..6bd7082 --- /dev/null +++ b/documentation/Using XkOps/FAQs for Workflow Creation.md @@ -0,0 +1,45 @@ +Here are some frequently asked questions about creating workflows in XkOps, specifically around using Robusta actions and integrating cost recommendations from Kubecost. + +Workflow Setup + +What is a Robusta action, and how is it used in workflows? + +A Robusta action is a modular component in XkOps that performs specific tasks triggered within workflows. Robusta actions enable workflows to execute automated responses to Kubernetes events, such as scaling, monitoring, or cost management tasks. + +How do I configure cost recommendations in the Query Pod section? + +In the Query Pod section, you can configure a cron job to retrieve cost recommendations from Kubecost. Define your business logic to fetch only the specific Kubecost API recommendations you need, ensuring streamlined and relevant cost optimization data. + +What types of Kubecost recommendations can I use? + +Kubecost offers recommendations for cost optimization, such as resizing resources or managing underutilized nodes. These recommendations help you optimize resource allocation, reducing unnecessary expenses in your Kubernetes environment. + +Implementation + +How do I add business logic for cost recommendations in a workflow? + +In the Query Pod section, add custom logic to call the relevant Kubecost APIs. This involves setting up a cron job to periodically fetch cost recommendations based on your workflow’s requirements, ensuring only the needed data is retrieved. + +What is the role of Flask in this workflow? + +Flask is used to create backend APIs that support the Robusta actions. These APIs act as an interface to trigger, and manage each action within the workflow, enhancing the control of your workflows. + +Verification and Troubleshooting + +How can I verify that my workflow is functioning correctly? + +To check if the workflow is functioning correctly, verify the status of the workflow components with kubectl logs . Ensure that the cron job in the Query Pod section is fetching cost recommendations as expected. + +What if the workflow fails to retrieve cost recommendations? + +If the workflow fails to retrieve cost recommendations, check the API configurations in the Query Pod section and verify network connectivity to the Kubecost service. You can also review the logs for any error messages related to API requests + +General + +Can I customize the cron job timing for fetching cost recommendations? + +Yes, you can adjust the cron job timing in the Query Pod section to align with your cost monitoring schedule. Setting appropriate intervals will help you balance timely insights with cluster performance. + +Where can I find additional support for workflow creation in xkops? + +Refer to the official xkops documentation, community forums, or contact xkops support for additional help with workflow creation, Robusta actions, and Kubecost integration. \ No newline at end of file diff --git a/documentation/Using XkOps/Understanding Workflows.md b/documentation/Using XkOps/Understanding Workflows.md new file mode 100644 index 0000000..da96c2b --- /dev/null +++ b/documentation/Using XkOps/Understanding Workflows.md @@ -0,0 +1,38 @@ +XkOps workflows, or Xworkflows, are automated sequences of actions that help you manage your Kubernetes cluster more efficiently. They combine the power of tools like Kubecost, Robusta, and Pixie to handle common tasks without manual intervention. + +How Xworkflows Work + +Think of an Xworkflow as a recipe for managing your Kubernetes cluster. It starts by gathering information, then decides what to do based on that information, and finally takes action. + +Data Collection: XkOps taps into tools like Kubecost to understand your spending patterns or Pixie to analyze network traffic. This data provides valuable insights into your cluster's health and performance. + +Decision Making: Based on the collected data, the Xworkflow determines the necessary steps. For instance, if Kubecost identifies unused resources, the workflow might decide to reclaim them to save costs. + +Action Execution: The final stage involves taking action. Robusta, our automation engine, carries out these actions, such as deleting unused resources or scaling resources based on demand. + +A Real-World Example: Cost Optimization + +Let's say you're concerned about rising Kubernetes costs. An Xworkflow can help by: + +Identifying Cost Drivers: Kubecost analyzes your cluster's resource usage and cost breakdown. + +Recommending Optimizations: The workflow suggests actions like rightsizing instances or reclaiming unused resources. + +Automating Savings: Robusta implements the recommended changes, reducing your cloud bill without compromising performance. + +By automating these steps, you can significantly reduce your Kubernetes expenses without constant monitoring. + +Benefits of Using Xworkflows + +Save Time and Money: Automate repetitive tasks, freeing up your team to focus on strategic initiatives. XkOps can help you identify cost-saving opportunities and implement them automatically. + +Improve Efficiency: Optimize resource utilization by proactively addressing issues like over-provisioning and underutilization. + +Enhance Cluster Health: Gain valuable insights into your cluster's performance and take corrective actions to prevent problems. + +Simplify Complex Operations: Break down complex tasks into manageable workflows, reducing errors and improving reliability. + +By leveraging the power of Xworkflows, you can transform your Kubernetes management from a reactive to a proactive approach. + + + diff --git a/documentation/Using XkOps/Using Workflows.md b/documentation/Using XkOps/Using Workflows.md new file mode 100644 index 0000000..04e1048 --- /dev/null +++ b/documentation/Using XkOps/Using Workflows.md @@ -0,0 +1,39 @@ +The XkOps Workflows section provides a user-friendly interface to manage and execute automation tasks within your Kubernetes cluster. + +Accessing Your Workflows + +To access the available Xworkflows, navigate to the Workflows section within the XkOps dashboard. Here, you will find a comprehensive list of all the Xworkflows available for your cluster. Each workflow is accompanied by a brief description to provide an initial understanding of its purpose. + +Workflow Details + +To gain deeper insights into a specific Xworkflow, click on its name. This will open a detailed view that includes: + +Workflow Name: A clear and concise description of the workflow's objective. + +Data Source: Specifies the underlying data source used by the workflow (e.g., Kubecost, Pixie) and how this data is utilized to inform the workflow's actions. + +Action: Outlines the specific actions performed by the workflow based on the collected data, including a step-by-step breakdown of the process if applicable. + +Description: Provides a comprehensive explanation of the workflow's functionality, expected outcomes, and potential benefits. This section should also highlight any prerequisites or limitations associated with the workflow. + +Workflow Diagram: A visual representation of the workflow's logic and steps, aiding in understanding the process flow. + +Interacting with Xworkflows + +To initiate a workflow, follow these steps: + +Select the Workflow: Choose the desired Xworkflow from the list presented in the Workflows section. + +Review Workflow Details: Carefully examine the workflow's description, data source, and actions to understand its purpose and potential impact. + +Access Workflow Options: Most workflows will present you with specific options based on their functionality. For example, the "Delete Unclaimed Persistent Volumes" workflow might display a list of unclaimed PVs with details like size, creation time, and storage class. You can filter or sort this list to identify specific PVs for deletion. + +Configure Workflow Parameters (if applicable): Some workflows may allow you to customize parameters or thresholds to fine-tune their behavior. For instance, you might be able to specify a minimum PV age for deletion or a CPU utilization threshold for resizing pods. + +Execute Action: Select the desired action from the available options. This could involve deleting a PV, resizing a pod, or applying other relevant actions. + +Confirm Action: Review the selected action and its potential consequences before proceeding. + +Monitor Workflow Execution: Once initiated, the workflow will execute in the background. You can monitor its progress and view the results upon completion. A detailed execution log can provide insights into the workflow's steps and any encountered issues. + +By following these steps and leveraging the detailed information provided within the Workflows section, you can effectively utilize XkOps to optimize your Kubernetes cluster and achieve the desired outcomes. \ No newline at end of file diff --git a/documentation/Using XkOps/Workdlow's Architecture.md b/documentation/Using XkOps/Workdlow's Architecture.md new file mode 100644 index 0000000..56cc805 --- /dev/null +++ b/documentation/Using XkOps/Workdlow's Architecture.md @@ -0,0 +1,8 @@ +General Workflow’s Architecture + +XkOps integrates with Kubecost to obtain real-time cost details, which inform cost optimization actions within your Kubernetes cluster. By polling Kubecost endpoints, XkOps retrieves the latest costing information, ensuring that workflows are based on up-to-date data. + +Once the latest cost information is gathered, XkOps triggers a Robusta action to manage resources in alignment with cost management goals. This process ensures that any necessary adjustments are automated and informed by accurate, current financial insights. + +Depending on the user’s configuration, xkops may update or delete specific resources to optimize costs. These actions are performed as per user-defined criteria, offering flexible options to streamline and manage cluster expenses effectively. + diff --git a/documentation/Using XkOps/Workflow Creation Criteria.md b/documentation/Using XkOps/Workflow Creation Criteria.md new file mode 100644 index 0000000..a17993a --- /dev/null +++ b/documentation/Using XkOps/Workflow Creation Criteria.md @@ -0,0 +1,19 @@ +At XkOps, we maintain rigorous standards to ensure that the workflows we offer deliver exceptional value. To be considered for inclusion in the XkOps platform, a workflow must meet the following evaluation criteria: + +Cost-Effectiveness: Demonstrates a clear path to reducing costs without compromising performance or reliability. + +Repeatability: Designed to be executed multiple times with consistent results. + +Implementation Clarity: Includes a well-defined plan outlining roles, responsibilities, and execution steps. + +Comprehensive Documentation: Supported by detailed documentation, including workflow diagrams. + +Frictionless Operation: Requires no additional permissions beyond those already granted to XkOps. + +Minimal Impact: Proven to avoid disrupting critical workloads on the cluster. + +Proven Effectiveness: Successfully tested and validated to achieve desired outcomes. + +Each workflow undergoes a thorough assessment against these standards. If a workflow meets these requirements, it is considered for inclusion in the XkOps platform. Otherwise, it undergoes refinement and retesting until it aligns with our quality benchmarks. + +This meticulous evaluation process ensures that the Xworkflows available to our users are not only efficient but also safe and reliable. \ No newline at end of file diff --git a/helm/.helmignore b/helm/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/helm/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/Chart.yaml b/helm/Chart.yaml index f1114c7..804a45f 100644 --- a/helm/Chart.yaml +++ b/helm/Chart.yaml @@ -1,42 +1,24 @@ -# Copyright (c) 2023, Xgrid Inc, https://xgrid.co - -# 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. - ---- apiVersion: v2 name: xkops -description: A Helm chart to deploy xkops +description: A Helm chart for Kubernetes # A chart can be either an 'application' or a 'library' chart. # -# Application charts are a collection of templates that can be packaged into -# versioned archives to be deployed. +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. # -# Library charts provide useful utilities or functions for the chart developer. -# They're included asa dependency of application charts to inject those -# utilities and functions into the rendering pipeline. Library charts do not -# define any templates and therefore cannot be deployed. +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. type: application -# This is the chart version. This version number should be incremented each -# time you make changes to the chart and its templates, including the app -# version. Versions are expected to follow Semantic -# Versioning (https://semver.org/) +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) version: 0.1.0 -# This is the version number of the application being deployed. -# This version number should be incremented each time you make changes -# to the application. Versions are not expected to follow Semantic Versioning. -# They should reflect the version the application is using. +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "0.0.1" +appVersion: "1.16.0" diff --git a/helm/templates/_helpers.tpl b/helm/templates/_helpers.tpl deleted file mode 100644 index ac42b75..0000000 --- a/helm/templates/_helpers.tpl +++ /dev/null @@ -1,9 +0,0 @@ -{{ define "xkops.configfile" -}} - -active_workflows: - -{{- if .Values.customWorkflows }} -{{ toYaml .Values.customWorkflows | indent 2 }} -{{- end }} - -{{ end }} diff --git a/helm/templates/clusterrole-xkops-helmfile.yaml b/helm/templates/clusterrole-xkops-helmfile.yaml new file mode 100644 index 0000000..1467be7 --- /dev/null +++ b/helm/templates/clusterrole-xkops-helmfile.yaml @@ -0,0 +1,134 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: helmfile-installation-manager +rules: + # Permissions to create and list namespaces + - apiGroups: [""] + resources: ["namespaces"] + verbs: ["create", "get", "list"] + + # Permissions to list secrets + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "list", "create"] + + # Permissions to manage CustomResourceDefinitions (CRDs) + - apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["create", "get", "list", "watch"] + + # Permissions to manage ServiceAccounts in all namespaces + - apiGroups: [""] + resources: ["serviceaccounts"] + verbs: ["get", "list", "watch", "create"] + + # Permissions to manage ConfigMaps in the 'robusta' namespace + - apiGroups: [""] + resources: ["configmaps"] + verbs: ["get", "list", "watch", "create"] # Adjust verbs as necessary (e.g., add "create" if needed) + + # Permissions to access ClusterRoles + - apiGroups: ["rbac.authorization.k8s.io"] + resources: ["clusterroles"] + verbs: ["get", "list"] # Allow reading cluster roles + + # Permissions to manage ClusterRoleBindings + - apiGroups: ["rbac.authorization.k8s.io"] + resources: ["clusterrolebindings"] + verbs: ["get", "list", "watch"] # Add permissions to manage clusterrolebindings + + # Permissions to manage Services in all namespaces + - apiGroups: [""] + resources: ["services"] + verbs: ["get", "list", "watch", "create", "delete"] # Added permissions for services + + # Permissions to manage Deployments in all namespaces + - apiGroups: ["apps"] + resources: ["deployments"] + verbs: ["get", "list", "watch", "create", "delete"] # Added permissions for deployments + + # Permissions to manage ServiceMonitors in all namespaces + - apiGroups: ["monitoring.coreos.com"] + resources: ["servicemonitors"] + verbs: ["get", "list", "watch", "create", "delete"] # Added permissions for ServiceMonitors + + # Permissions to manage OperatorGroups in all namespaces + - apiGroups: ["operators.coreos.com"] + resources: ["operatorgroups"] + verbs: ["get", "list", "watch", "create", "delete"] + + # Permissions to list and get StatefulSets in all namespaces + - apiGroups: ["apps"] + resources: ["statefulsets"] + verbs: ["get", "list"] + + # Permissions to get Roles in the pixie namespace + - apiGroups: ["rbac.authorization.k8s.io"] + resources: ["roles"] + verbs: ["get"] + resourceNames: ["pl-deleter-role"] # Specific Role in the pixie namespace + + # Permissions to get RoleBindings in the pixie namespace + - apiGroups: ["rbac.authorization.k8s.io"] + resources: ["rolebindings"] + verbs: ["get"] + resourceNames: ["pl-deleter-binding"] # Specific RoleBinding in the pixie namespace + + # Permissions to get CatalogSources in the px-operator namespace + - apiGroups: ["operators.coreos.com"] + resources: ["catalogsources"] + verbs: ["get"] + resourceNames: ["pixie-operator-index"] # Specific CatalogSource in the px-operator namespace + + # Permissions to get Subscriptions in the px-operator namespace + - apiGroups: ["operators.coreos.com"] + resources: ["subscriptions"] + verbs: ["get"] + resourceNames: ["pixie-operator-subscription"] # Specific Subscription in the px-operator namespace + + # Permissions to get Viziers in the pixie namespace + - apiGroups: ["px.dev"] + resources: ["viziers"] + verbs: ["get"] + resourceNames: ["pixie"] # Specific Vizier in the pixie namespace + +# Permissions to create, get, list, and delete ClusterRoles at the cluster scope + - apiGroups: ["rbac.authorization.k8s.io"] + resources: ["clusterroles", "clusterrolebindings"] + verbs: ["*"] # Grant all permissions on clusterroles and clusterrolebindings + +# Permissions to create Roles in all namespaces + - apiGroups: ["rbac.authorization.k8s.io"] + resources: ["roles"] + verbs: ["create"] # Allow creating Roles in all namespaces + +# Kubecost: Permissions for managing PersistentVolumes (Kubecost specific) + - apiGroups: [""] + resources: ["persistentvolumes"] + verbs: ["get", "list", "watch"] + +# Kubecost: Permissions for managing PersistentVolumeClaims in the kubecost namespace + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: ["get", "list", "watch", "create", "delete"] + +# Kubecost: Permissions for managing Roles in the kubecost namespace + - apiGroups: ["rbac.authorization.k8s.io"] + resources: ["roles"] + verbs: ["get", "list", "watch", "create", "delete"] + +# Kubecost: Permissions for managing RoleBindings in the kubecost namespace + - apiGroups: ["rbac.authorization.k8s.io"] + resources: ["rolebindings"] + verbs: ["get", "list", "watch", "create", "delete"] + +# Kubecost: Permissions for updating ConfigMaps and managing Pods/Log in the kubecost namespace + - apiGroups: [""] + resources: ["configmaps"] + verbs: ["update"] + +# Kubecost: Permissions for getting, listing, and watching Pods/Log in the kubecost namespace + - apiGroups: [""] + resources: ["pods/log"] + verbs: ["get", "list", "watch"] \ No newline at end of file diff --git a/helm/templates/clusterrolebinding-xkops-helmfile.yaml b/helm/templates/clusterrolebinding-xkops-helmfile.yaml new file mode 100644 index 0000000..c943f60 --- /dev/null +++ b/helm/templates/clusterrolebinding-xkops-helmfile.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: helmfile-installation-manager-binding +subjects: + - kind: ServiceAccount + name: default # Replace with your specific service account name + namespace: default # Adjust as needed +roleRef: + kind: ClusterRole + name: helmfile-installation-manager + apiGroup: rbac.authorization.k8s.io diff --git a/helm/templates/configmap-xkops-helmfile.yaml b/helm/templates/configmap-xkops-helmfile.yaml new file mode 100644 index 0000000..b5208ae --- /dev/null +++ b/helm/templates/configmap-xkops-helmfile.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: helmfile-values-config +data: + slack_api_key: {{ .Values.robusta.slack_api_key | quote }} # Pulls the Slack API key + slack_channel_name: {{ .Values.robusta.slack_channel_name | quote }} # Pulls the Slack channel name + cluster_name: {{ .Values.robusta.cluster_name | quote }} # Pulls the cluster name diff --git a/helm/templates/configmap-xkops-mongodb.yaml b/helm/templates/configmap-xkops-mongodb.yaml new file mode 100644 index 0000000..2b6c993 --- /dev/null +++ b/helm/templates/configmap-xkops-mongodb.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: mongodb-config +data: + mongoInitDbDatabase: k8sData diff --git a/helm/templates/configmap-xkops-query.yaml b/helm/templates/configmap-xkops-query.yaml new file mode 100644 index 0000000..5d4cd2c --- /dev/null +++ b/helm/templates/configmap-xkops-query.yaml @@ -0,0 +1,30 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: query-config +data: + values.yaml: | + workflows: + - dataSource: kubecost + name: nodeScrapeWorkflow + robustaPlaybook: nil + scrapeData: nodes + description: "Sample description" + + - dataSource: kubecost + name: unclaimedVolumeScrapeWorkflow + robustaPlaybook: xyz + scrapeData: unclaimedvolume + description: "Sample description" + + - dataSource: kubecost + name: sizingCPUandRAMWorkflow + robustaPlaybook: resourceOptimizationPlaybook + scrapeData: containerResourceSizing + description: "Analyzes and recommends optimal CPU and RAM sizing for containers" + + - dataSource: kubecost + name: abandonedWorkloadsScrapeWorkflow + robustaPlaybook: abandonedWorkloadPlaybook + scrapeData: abandonedWorkloads + description: "Fetches and processes data related to abandoned workloads" diff --git a/helm/templates/configmaps.yaml b/helm/templates/configmaps.yaml deleted file mode 100644 index a5d3535..0000000 --- a/helm/templates/configmaps.yaml +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright (c) 2023, Xgrid Inc, https://xgrid.co - -# 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. - -# Configmap to be used by xkops deployment scripts ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: xkops-config - namespace: "{{ .Values.namespace.NAMESPACE_NAME }}" -data: - CLUSTER_NAME: "{{ .Values.CLUSTER_NAME }}" - REGION: "{{ .Values.REGION }}" - SLACK_CHANNEL_NAME: "{{ .Values.SLACK_CHANNEL_NAME }}" diff --git a/helm/templates/dashboard-service.yaml b/helm/templates/dashboard-service.yaml deleted file mode 100644 index e716d84..0000000 --- a/helm/templates/dashboard-service.yaml +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright (c) 2023, Xgrid Inc, https://xgrid.co - -# 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. ---- -apiVersion: v1 -kind: Service -metadata: - name: xkops-dashboard - namespace: xkops -spec: - type: LoadBalancer - selector: - app: xkops-dashboard - ports: - - name: "{{ .Values.dashboard_service_ports.name }}" - port: 80 - targetPort: 3000 diff --git a/helm/templates/dashboard.yaml b/helm/templates/dashboard.yaml deleted file mode 100644 index b627cb1..0000000 --- a/helm/templates/dashboard.yaml +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright (c) 2023, Xgrid Inc, https://xgrid.co - -# 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. ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: xkops-dashboard - namespace: xkops -spec: - replicas: 1 - selector: - matchLabels: - app: xkops-dashboard - template: - metadata: - labels: - app: xkops-dashboard - spec: - containers: - - name: xkops-dashboard - image: "{{ .Values.dashboard_image.repository }}:{{ .Values.dashboard_image.tag }}" - ports: - - containerPort: 3000 diff --git a/helm/templates/deployment -xkops-query.yaml b/helm/templates/deployment -xkops-query.yaml new file mode 100644 index 0000000..7c71ae9 --- /dev/null +++ b/helm/templates/deployment -xkops-query.yaml @@ -0,0 +1,49 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: xkops-query-pod + labels: + app: xkops-query-pod +spec: + replicas: 1 + selector: + matchLabels: + app: xkops-query-pod + template: + metadata: + labels: + app: xkops-query-pod + spec: + containers: + - name: xkops-query-container + image: hamzaarshad10/querypodpy:1.7 # Replace with your actual image + ports: + - containerPort: 8080 # Replace with your actual port if needed + env: + - name: MONGODB_HOSTNAME + value: "mongodb-0.mongodb-headless.default.svc.cluster.local" # Replace with your actual MongoDB service name or endpoint + - name: MONGODB_PORT + value: "27017" # Default MongoDB port + - name: MONGODB_USERNAME + valueFrom: + secretKeyRef: + name: mongodb-secrets + key: mongodb-root-username + - name: MONGODB_PASSWORD + valueFrom: + secretKeyRef: + name: mongodb-secrets + key: mongodb-root-password + - name: MONGODB_DATABASE + valueFrom: + configMapKeyRef: + name: mongodb-config + key: mongoInitDbDatabase #values.yaml + volumeMounts: + - name: config-volume + mountPath: /app/config/ + #subPath: values.yaml + volumes: + - name: config-volume + configMap: + name: query-config \ No newline at end of file diff --git a/helm/templates/deployment-xkops-backend.yaml b/helm/templates/deployment-xkops-backend.yaml new file mode 100644 index 0000000..495411a --- /dev/null +++ b/helm/templates/deployment-xkops-backend.yaml @@ -0,0 +1,41 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: xkops-backend + labels: + app: xkops-backend +spec: + replicas: 1 + selector: + matchLabels: + app: xkops-backend + template: + metadata: + labels: + app: xkops-backend + spec: + containers: + - name: query-container + image: hamzaarshad10/querybackend:1.6.2 # Replace with your actual image + ports: + - containerPort: 5000 # Replace with your actual port if needed + env: + - name: MONGODB_HOSTNAME + value: "mongodb-0.mongodb-headless.default.svc.cluster.local" # Replace with your actual MongoDB service name or endpoint + - name: MONGODB_PORT + value: "27017" # Default MongoDB port + - name: MONGODB_USERNAME + valueFrom: + secretKeyRef: + name: mongodb-secrets + key: mongodb-root-username + - name: MONGODB_PASSWORD + valueFrom: + secretKeyRef: + name: mongodb-secrets + key: mongodb-root-password + - name: MONGODB_DATABASE + valueFrom: + configMapKeyRef: + name: mongodb-config + key: mongoInitDbDatabase #values.yaml \ No newline at end of file diff --git a/helm/templates/deployment-xkops-frontend.yaml b/helm/templates/deployment-xkops-frontend.yaml new file mode 100644 index 0000000..d59a7da --- /dev/null +++ b/helm/templates/deployment-xkops-frontend.yaml @@ -0,0 +1,25 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: xkops-frontend + labels: + app: xkops-frontend +spec: + replicas: 1 + selector: + matchLabels: + app: xkops-frontend + template: + metadata: + labels: + app: xkops-frontend + spec: + containers: + - name: xkops-frontend + image: hamzaarshad10/queryfrontend:1.1.5.14 # Replace with your actual image name + ports: + - containerPort: 3000 + env: + - name: REACT_APP_BACKEND_URL + value: "http://xkops-backend-svc:5000" + #value: "http://xkops-backend-svc.default.svc.cluster.local:5000" # Replace with your actual backend URL diff --git a/helm/templates/job-xkops-helmfile.yaml b/helm/templates/job-xkops-helmfile.yaml new file mode 100644 index 0000000..9142c42 --- /dev/null +++ b/helm/templates/job-xkops-helmfile.yaml @@ -0,0 +1,19 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: helmfile-job +spec: + template: + spec: + containers: + - name: helmfile-container + image: murtazashah46/helmfile:latest + command: ["bash", "scripts/install-helmfile.sh"] + volumeMounts: + - name: config-volume + mountPath: /config # Mount the ConfigMap at /config + restartPolicy: Never + volumes: + - name: config-volume + configMap: + name: helmfile-values-config # Reference the ConfigMap diff --git a/helm/templates/manifests.yaml b/helm/templates/manifests.yaml deleted file mode 100644 index ebff1df..0000000 --- a/helm/templates/manifests.yaml +++ /dev/null @@ -1,62 +0,0 @@ -# Copyright (c) 2023, Xgrid Inc, https://xgrid.co - -# 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. - -# yamllint disable-file -{{- range .Values.manifests }} -{{- if eq (printf "%d" .priority) "1" }} -{{ include .name }} -{{- end }} -{{- end }} - -{{- range .Values.manifests }} -{{- if eq (printf "%d" .priority) "2" }} -{{ include .name }} -{{- end }} -{{- end }} - -{{- range .Values.manifests }} -{{- if eq (printf "%d" .priority) "3" }} -{{ include .name }} -{{- end }} -{{- end }} - -{{- range .Values.manifests }} -{{- if eq (printf "%d" .priority) "4" }} -{{ include .name }} -{{- end }} -{{- end }} - -{{- range .Values.manifests }} -{{- if eq (printf "%d" .priority) "5" }} -{{ include .name }} -{{- end }} -{{- end }} - -{{- range .Values.manifests }} -{{- if eq (printf "%d" .priority) "6" }} -{{ include .name }} -{{- end }} -{{- end }} - -{{- range .Values.manifests }} -{{- if eq (printf "%d" .priority) "7" }} -{{ include .name }} -{{- end }} -{{- end }} - -{{- range .Values.manifests }} -{{- if eq (printf "%d" .priority) "8" }} -{{ include .name }} -{{- end }} -{{- end }} diff --git a/helm/templates/persistentvolume-xkops-mongodb.yaml b/helm/templates/persistentvolume-xkops-mongodb.yaml new file mode 100644 index 0000000..dd56f1e --- /dev/null +++ b/helm/templates/persistentvolume-xkops-mongodb.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: PersistentVolume +metadata: + name: mongodb-pv +spec: + capacity: + storage: 1Gi + accessModes: + - ReadWriteOnce + persistentVolumeReclaimPolicy: Retain + storageClassName: manual + hostPath: + path: /mnt/data/mongodb # Path on the node where data will be stored \ No newline at end of file diff --git a/helm/templates/persistentvolumeclaim-xkops-mongodb.yaml b/helm/templates/persistentvolumeclaim-xkops-mongodb.yaml new file mode 100644 index 0000000..cd90e16 --- /dev/null +++ b/helm/templates/persistentvolumeclaim-xkops-mongodb.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: mongodb-pvc +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi + storageClassName: manual diff --git a/helm/templates/role-xkops-deployment-editor.yaml b/helm/templates/role-xkops-deployment-editor.yaml new file mode 100644 index 0000000..4d6068f --- /dev/null +++ b/helm/templates/role-xkops-deployment-editor.yaml @@ -0,0 +1,9 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: deployment-editor + namespace: default +rules: + - apiGroups: ["apps"] + resources: ["deployments"] + verbs: ["get", "list", "watch", "update"] diff --git a/helm/templates/rolebinding-xkops-robusta-runner.yaml b/helm/templates/rolebinding-xkops-robusta-runner.yaml new file mode 100644 index 0000000..d962a0e --- /dev/null +++ b/helm/templates/rolebinding-xkops-robusta-runner.yaml @@ -0,0 +1,13 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: robusta-runner-binding + namespace: default +subjects: + - kind: ServiceAccount + name: robusta-runner-service-account + namespace: default +roleRef: + kind: Role + name: deployment-editor + apiGroup: rbac.authorization.k8s.io \ No newline at end of file diff --git a/helm/templates/secret-xkops-mongodb.yaml b/helm/templates/secret-xkops-mongodb.yaml new file mode 100644 index 0000000..b4b3d48 --- /dev/null +++ b/helm/templates/secret-xkops-mongodb.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Secret +metadata: + name: mongodb-secrets +type: Opaque +data: + mongodb-root-username: YWRtaW4= # base64 for 'admin' + mongodb-root-password: YWRtaW4xMjM= # base64 for 'admin123' \ No newline at end of file diff --git a/helm/templates/secretproviderclass.yaml b/helm/templates/secretproviderclass.yaml deleted file mode 100644 index beae553..0000000 --- a/helm/templates/secretproviderclass.yaml +++ /dev/null @@ -1,59 +0,0 @@ -# Copyright (c) 2023, Xgrid Inc, https://xgrid.co - -# 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. - ---- -apiVersion: secrets-store.csi.x-k8s.io/v1 -kind: SecretProviderClass -metadata: - name: xkops-secret-provider # name of secret provider class - namespace: xkops -spec: - provider: aws - secretObjects: - - secretName: xkops-secret # pragma: allowlist secret - type: Opaque - data: - - objectName: AWS_ACCESS_KEY_ID - key: AWS_ACCESS_KEY_ID - - objectName: AWS_SECRET_ACCESS_KEY - key: AWS_SECRET_ACCESS_KEY - - objectName: AWS_SESSION_TOKEN - key: AWS_SESSION_TOKEN - - objectName: ROBUSTA_UI_API_KEY - key: ROBUSTA_UI_API_KEY - - objectName: SLACK_API_KEY - key: SLACK_API_KEY - - objectName: PX_API_KEY - key: PX_API_KEY - - objectName: PX_DEPLOY_KEY - key: PX_DEPLOY_KEY - parameters: - objects: | - - objectName: "xkops-secret" - objectType: "secretsmanager" - jmesPath: - - path: "AWS_ACCESS_KEY_ID" - objectAlias: "AWS_ACCESS_KEY_ID" - - path: "AWS_SECRET_ACCESS_KEY" - objectAlias: "AWS_SECRET_ACCESS_KEY" - - path: "AWS_SESSION_TOKEN" - objectAlias: "AWS_SESSION_TOKEN" - - path: "SLACK_API_KEY" - objectAlias: "SLACK_API_KEY" - - path: "ROBUSTA_UI_API_KEY" - objectAlias: "ROBUSTA_UI_API_KEY" - - path: "PX_API_KEY" - objectAlias: "PX_API_KEY" - - path: "PX_DEPLOY_KEY" - objectAlias: "PX_DEPLOY_KEY" diff --git a/helm/templates/service-xkops-backend.yaml b/helm/templates/service-xkops-backend.yaml new file mode 100644 index 0000000..95101fb --- /dev/null +++ b/helm/templates/service-xkops-backend.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: xkops-backend-svc + labels: + app: xkops-backend-svc +spec: + selector: + app: xkops-backend # This must match the label in your deployment + ports: + - port: 5000 # The port that the service will expose + targetPort: 5000 # The port that the container listens on + type: ClusterIP # Change to LoadBalancer if you need external access diff --git a/helm/templates/service-xkops-mongodb.yaml b/helm/templates/service-xkops-mongodb.yaml new file mode 100644 index 0000000..63e65e2 --- /dev/null +++ b/helm/templates/service-xkops-mongodb.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: mongodb-headless + labels: + app: mongodb +spec: + clusterIP: None + ports: + - port: 27017 + name: mongodb + selector: + app: mongodb \ No newline at end of file diff --git a/helm/templates/statefulset-xkops-mongodb.yaml b/helm/templates/statefulset-xkops-mongodb.yaml new file mode 100644 index 0000000..eacab18 --- /dev/null +++ b/helm/templates/statefulset-xkops-mongodb.yaml @@ -0,0 +1,47 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: mongodb +spec: + serviceName: "mongodb-headless" + replicas: 1 + selector: + matchLabels: + app: mongodb + template: + metadata: + labels: + app: mongodb + spec: + containers: + - name: mongodb + image: mongo:latest + ports: + - containerPort: 27017 + volumeMounts: + - name: mongodb-data + mountPath: /data/db + env: + - name: MONGO_INITDB_ROOT_USERNAME + valueFrom: + secretKeyRef: + name: mongodb-secrets + key: mongodb-root-username + - name: MONGO_INITDB_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: mongodb-secrets + key: mongodb-root-password + - name: MONGO_INITDB_DATABASE + valueFrom: + configMapKeyRef: + name: mongodb-config + key: mongoInitDbDatabase + volumeClaimTemplates: + - metadata: + name: mongodb-data + spec: + accessModes: [ "ReadWriteOnce" ] + resources: + requests: + storage: 1Gi diff --git a/helm/templates/storage-class.yaml b/helm/templates/storage-class.yaml deleted file mode 100644 index 033f24d..0000000 --- a/helm/templates/storage-class.yaml +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright (c) 2023, Xgrid Inc, https://xgrid.co - -# 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. ---- -apiVersion: storage.k8s.io/v1 -kind: StorageClass -metadata: - name: ebs-sc - annotations: - storageclass.kubernetes.io/is-default-class: "true" -provisioner: ebs.csi.aws.com -reclaimPolicy: Delete -volumeBindingMode: WaitForFirstConsumer diff --git a/helm/templates/workflows-config.yaml b/helm/templates/workflows-config.yaml deleted file mode 100644 index c944de9..0000000 --- a/helm/templates/workflows-config.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: xkops-workflows-config-secret - # namespace: {{ .Release.Namespace }} - namespace: xkops -type: Opaque -data: - custom_workflows.yaml: |- - {{ include "xkops.configfile" . | b64enc }} - diff --git a/helm/templates/xkops-clusterrole.yaml b/helm/templates/xkops-clusterrole.yaml deleted file mode 100644 index 0a0f14f..0000000 --- a/helm/templates/xkops-clusterrole.yaml +++ /dev/null @@ -1,211 +0,0 @@ -# Copyright (c) 2023, Xgrid Inc, https://xgrid.co - -# 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. - -# Cluster roles for XkOps. ---- -kind: ClusterRole -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: xkops-clusterrole -rules: - - apiGroups: [""] - resources: - - configmaps - - daemonsets - - deployments - - endpoints - - events - - limitranges - - namespaces - - ingresses - - nodes - - persistentvolumeclaims - - persistentvolumes - - pods - - replicasets - - replicationcontrollers - - resourcequotas - - services - - serviceaccounts - - pods/status - - pods/exec - - pods/log - - pods/portforward - - secrets - - horizontalpodautoscalers.autoscaling - - cronjobs.batch - - jobs.batch - - events.events.k8s.io - - daemonsets.extensions - - deployments.extensions - - replicasets.extensions - - poddisruptionbudgets.policy - - storageclasses.storage.k8s.io - - daemonsets.apps - - deployments.apps - - replicasets.apps - - statefulsets.apps - - configmaps - - endpoints - - nodes/metrics - - nodes/proxy - - nodes - - pods - - services - verbs: - - get - - list - - watch - - create - - patch - - update - - delete - - apiGroups: ["rbac.authorization.k8s.io"] - resources: - - clusterroles - - clusterrolebindings - - configmaps - - daemonsets - - deployments - - events - - namespaces - - nodes - - roles - - rolebindings - - persistentvolumes - - pods - - replicasets - - replicationcontrollers - - services - - serviceaccounts - - persistentvolumeclaims - - pods/status - - pods/exec - - pods/log - - endpoints - - limitranges - - resourcequotas - - horizontalpodautoscalers.autoscaling - - cronjobs.batch - - jobs.batch - - events.events.k8s.io - - daemonsets.extensions - - deployments.extensions - - replicasets.extensions - - poddisruptionbudgets.policy - - storageclasses.storage.k8s.io - - daemonsets.apps - - deployments.apps - - replicasets.apps - - statefulsets.apps - verbs: - - get - - list - - watch - - create - - patch - - update - - delete - - apiGroups: ["autoscaling"] - resources: - - horizontalpodautoscalers - verbs: - - get - - list - - watch - - patch - - update - - apiGroups: ["extensions"] - resources: - - podsecuritypolicies - resourceNames: - - kubecost-grafana - - kubecost-cost-analyzer-psp - verbs: - - use - - apiGroups: ["apps", "extensions"] - resources: - - daemonsets - - deployments - - deployments/scale - - ingresses - - replicasets - - replicasets/scale - - replicationcontrollers/scale - - statefulsets - - ingresses/status - verbs: - - get - - list - - watch - - create - - patch - - update - - delete - - apiGroups: ["batch"] - resources: - - cronjobs - - jobs - verbs: - - get - - list - - watch - - create - - patch - - update - - delete - - apiGroups: ["events.k8s.io"] - resources: - - events - verbs: - - get - - list - - watch - - apiGroups: ["policy"] - resources: - - poddisruptionbudgets - - podsecuritypolicies - verbs: - - get - - list - - watch - - patch - - create - - apiGroups: ["apiregistration.k8s.io"] - resources: - - apiservices - verbs: - - get - - list - - apiGroups: ["networking.k8s.io"] - resources: - - ingresses - - networkpolicies - verbs: - - get - - list - - watch - - - apiGroups: ["storage.k8s.io"] - resources: - - storageclasses - verbs: - - get - - list - - watch - - patch - - nonResourceURLs: - - /metrics - verbs: - - get diff --git a/helm/templates/xkops-clusterrolebindings.yaml b/helm/templates/xkops-clusterrolebindings.yaml deleted file mode 100644 index 027e040..0000000 --- a/helm/templates/xkops-clusterrolebindings.yaml +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright (c) 2023, Xgrid Inc, https://xgrid.co - -# 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. - -# Cluster roles for XkOps. - -# Cluster role binding which binds the default service account -# under the xkops namespace with all-resource-role cluster role ---- -kind: ClusterRoleBinding -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: xkops-clusterrole-binding -subjects: - - kind: ServiceAccount #Subject: default service account under xkops namespace - name: xkops-secret-sa - namespace: xkops - - kind: ServiceAccount # kubecost-grafana service account - # under kubecost namespace - name: kubecost-grafana - namespace: kubecost - - kind: ServiceAccount # kubecost-prometheus-server service account - # under kubecost namespace - name: kubecost-prometheus-server - namespace: kubecost - - kind: ServiceAccount # kubecost-kube-state-metrics service account - # under kubecost namespace - name: kubecost-kube-state-metrics - namespace: kubecost -roleRef: - kind: ClusterRole # Cluster role reference to be used for binding the subject - name: xkops-clusterrole - apiGroup: rbac.authorization.k8s.io diff --git a/helm/templates/xkops-deployment.yaml b/helm/templates/xkops-deployment.yaml deleted file mode 100644 index e7e9e39..0000000 --- a/helm/templates/xkops-deployment.yaml +++ /dev/null @@ -1,133 +0,0 @@ -# Copyright (c) 2023, Xgrid Inc, https://xgrid.co - -# 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. ---- - -apiVersion: apps/v1 -kind: Deployment -metadata: - name: xkops-deployment - namespace: "{{ .Values.namespace.namespace_name }}" -spec: - replicas: 1 - selector: - matchLabels: - app: xkops - template: - metadata: - labels: - app: xkops - spec: - serviceAccountName: xkops-secret-sa - volumes: - - name: workflows-config-secret - secret: - secretName: xkops-workflows-config-secret - optional: true - - name: xkops-secret-vol - csi: - driver: secrets-store.csi.k8s.io - readOnly: true - volumeAttributes: - secretProviderClass: "{{ .Values.secret_provider_class }}" # pragma: allowlist secret - - name: "{{ .Values.xkops_volumes.name }}" - hostPath: - path: "{{ .Values.xkops_volumes.hostPath.path }}" - initContainers: - - name: terraform-init - image: "{{ .Values.xkops_init_container.image.repository }}:{{ .Values.xkops_init_container.image.tag }}" - imagePullPolicy: "{{ .Values.xkops_init_container.image.pullPolicy }}" - command: ["terraform", "apply", "-auto-approve"] - # command: ["sleep", "3600"] - volumeMounts: - - name: xkops-secret-vol - mountPath: "/mnt/xkops-secret" - readOnly: true - env: - - name: AWS_ACCESS_KEY_ID - valueFrom: - secretKeyRef: - name: xkops-secret - key: AWS_ACCESS_KEY_ID - - name: AWS_SECRET_ACCESS_KEY - valueFrom: - secretKeyRef: - name: xkops-secret - key: AWS_SECRET_ACCESS_KEY - - name: AWS_SESSION_TOKEN - valueFrom: - secretKeyRef: - name: xkops-secret - key: AWS_SESSION_TOKEN - - name: TF_VAR_CLUSTER_NAME - valueFrom: - configMapKeyRef: - name: xkops-config - key: CLUSTER_NAME - - name: TF_VAR_REGION - valueFrom: - configMapKeyRef: - name: xkops-config - key: REGION - containers: - - name: xkops - image: "{{ .Values.xkops_image.repository }}:{{ .Values.xkops_image.tag }}" - imagePullPolicy: "{{ .Values.xkops_image.pullPolicy }}" - # command: ["sleep", "3650d"] - volumeMounts: - - name: "{{ .Values.xkops_volumeMounts.name }}" - mountPath: "{{ .Values.xkops_volumeMounts.mountPath }}" - - name: workflows-config-secret - mountPath: /etc/xkops/config - - name: xkops-secret-vol - mountPath: "/mnt/xkops-secret" - readOnly: true - env: - - name: PX_API_KEY - valueFrom: - secretKeyRef: - name: xkops-secret - key: PX_API_KEY - - name: PX_DEPLOY_KEY - valueFrom: - secretKeyRef: - name: xkops-secret - key: PX_DEPLOY_KEY - - name: SLACK_API_KEY - valueFrom: - secretKeyRef: - name: xkops-secret - key: SLACK_API_KEY - - name: SLACK_CHANNEL_NAME - valueFrom: - configMapKeyRef: - name: xkops-config - key: SLACK_CHANNEL_NAME - - name: ROBUSTA_UI_API_KEY - valueFrom: - secretKeyRef: - name: xkops-secret - key: ROBUSTA_UI_API_KEY - - name: CLUSTER_NAME - valueFrom: - configMapKeyRef: - name: xkops-config - key: CLUSTER_NAME - resources: - limits: - cpu: "{{ .Values.xkops_resources.limits.cpu }}" - memory: "{{ .Values.xkops_resources.limits.memory }}" - requests: - cpu: "{{ .Values.xkops_resources.requests.cpu }}" - memory: "{{ .Values.xkops_resources.requests.memory }}" - restartPolicy: "{{ .Values.xkops_restartPolicy }}" diff --git a/helm/values.yaml b/helm/values.yaml index e967d0e..d66e870 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -1,121 +1,8 @@ -# Copyright (c) 2023, Xgrid Inc, https://xgrid.co - -# 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. - ---- -# Default values for xkops and dashboard. -# This is a YAML-formatted file. -# Declare variables to be passed into your templates. -xkops_init_container: - image: - repository: xkops/xkops_stable - tag: terraform - pullPolicy: Always - -namespace: - namespace_name: xkops - -# Set the container image details for xkops -xkops_image: - repository: xkops/xkops_stable - tag: xkops - pullPolicy: Always - -# Set the container image details for dashboard -dashboard_image: - repository: xkops/xkops_stable - tag: dashboard - pullPolicy: IfNotPresent - -# Define the resource requirements and limits for the container -xkops_resources: - limits: - cpu: "500m" - memory: "200Mi" - requests: - cpu: "100m" - memory: "128Mi" - -# Set up the volume mount for xkops -xkops_volumeMounts: - name: log-volume - mountPath: /tmp - -# Set up the persistent volume for xkops -xkops_volumes: - name: log-volume - hostPath: - path: /mnt/logs - -# Set the restart policy for the container -xkops_restartPolicy: Always - -dashboard_service_ports: - name: http - port: 80 - targetPort: 3000 -# Set up tolerations and affinity, if necessary -tolerations: [] -customWorkflows: [] - - -affinity: {} - -secret_provider_class: xkops-secret-provider # pragma: allowlist secret - -# provide your pixie API key (https://docs.px.dev/reference/admin/api-keys) -PX_API_KEY: "" -# provide your pixie deploy key -# (https://docs.px.dev/reference/admin/deploy-keys) -PX_DEPLOY_KEY: "" -# provide your Slack API key -# (https://slack.com/help/articles/215770388-Create-and-regenerate-API-tokens) -SLACK_API_KEY: "" -# provide your Slack channel name -SLACK_CHANNEL_NAME: "" -# Generate robusta API key using robusta-CLI -ROBUSTA_UI_API_KEY: "" -# AWS credentisl -AWS_ACCESS_KEY_ID: "" -AWS_SECRET_ACCESS_KEY: "" -AWS_SESSION_TOKEN: "" -# Region where cluster deployed -REGION: "" -# secret provider class for extracting secrets -SECRET_PROVIDER_CLASS: "" -# Override the name and fullname (including the release name) for the chart -nameOverride: "" -fullnameOverride: "" - -# List all manifests to apply in order of priority -manifests: - - name: xkops-clusterrole.yaml - priority: 1 - - name: xkops-clusterrolebindings.yaml - priority: 2 - - name: secretproviderclass.yaml - priority: 3 - - name: namespace.yaml - priority: 4 - - name: configmaps.yaml - priority: 5 - - name: storage-class.yaml - priority: 5 - - name: workflows-config.yaml - priority: 6 - - name: xkops-deployment.yaml - priority: 7 - - name: dashboard.yaml - priority: 8 - - name: dashboard-service.yaml - priority: 9 +# values.yaml +# This file contains default values for the Helm chart. +# Replace these placeholder values with actual values when deploying. + +robusta: + slack_api_key: "" # Add your Slack API key here (e.g., xoxb-xxxx-xxxx-xxxx) + slack_channel_name: "" # Specify the Slack channel name for notifications (e.g., my-channel) + cluster_name: "" # Name of the Kubernetes cluster (e.g., production-cluster) \ No newline at end of file diff --git a/images/arch diagram.png b/images/arch diagram.png index db2fb0d8f517060ebe2ceeebf2a4ceba1930cf98..e3d0c3751d56b390c17c7d98b747a28b22b50588 100644 GIT binary patch literal 50020 zcmdSAV|ZoF+vpi|2OW28bZi?P+qP}nwrzJO9dpOFZQC}^?&tZxXXbimzRj6A*VKod zm7Pkhs#WEK;Xp1gcLwPzF>iXfH6Tq0KciM^2h}K_~Ixat^@^a zUQkA1z~3>Qgw>oBZB3k9^&N~sOl@qfjcFVW9gK}_9L;QAcm23{~Q6UZ7m5irs-G851jNfXFPDqR=^V1j@UfQSq6E4gKz zt^0Z@KI&h7rbhV{>V4-RtZ0u%1yqZ)Y}3y4X#cTYqIKA2*o)UbBeDF$`btW)mbo-; zT_a)JUEJCo$5NB1Z9wJ6FY0A0*2u7bn;0HSl=D}if7{+mejfjS zu5U#61OL-!FFF7M>EAYt{S}Vr-H;3OxVOFi20l50~d#>`jpVEw3UmUE3x2 zzN%UnV0v_3^ga$VN0q2Ij)N`y7|P)Y?WhQ%DQ|wUG~u$Hv7c`s%-keZlKoR0wc@y3 z&J_s>Q2Ed7<1H0Q`4x@*9uks;TwQ0|TQe)+KxZbE5ZBmOqCZd#2F%OgR}J!9MOkj}yFqjJW}M3ahmpe+ zWVen)<~X(UVlVTsG61UAqwm7bO;eUn&!-AhY*jdt?L3pX20q;POmxzlG$yu+&g3#C z4X!loa&m7X*0uogn|qD2w|zGgPpGzxA!+Zb&*|;zou$J%ej@yHreK7sr4jzSyv^q2T{)s=`h7m*!Es9H#X&#SlkXws854 z+%P^x!B$TmTw5#C*BBG6s>P`rTD{=PcX%vUtNl#L03|##vRbkx8B@=wgitW{395?F zqeRrXq5x*!-e`OC1~LW~G2)i&%5hKySr#Og#GqWOfezMz2e!t#cNY9>W4Jy>MBmB~ zR@bzc?A2qpZpp_I^4}XO0#qJQHYa+o*tqq4bAo2VT*t1x%%pEZ2CK^*OcK3opbAgW zH1}I-b3%Kk7j@+cG2b^7q_*-I-w>X~+wq2j8GeYn);g29UGRRA10`RJ@>EUrC-5{7 zcPcowXA`*VLGQ0RC_+df%QB`)v0q&8RD(4`^FsMYL!Wdcmspo3c10lRgp2iyuhk@E z@JI*gJj^~U6|V(xU5#VIlbE>KW6OW%i=1ts5;qY3nhnsH4h=N2jo4c?=ibDKQM#!7 zaYIyHm=e?*YTGQ)Z>$rs=p3IV&p+QspqwmThW2tz2fvd8f$ot`+`u7V8}NLPN!aFy6_QHZb8DA2@C>0v9tAwK0H}Wsw}Uh#U=+H_HDTzkQ()#ZbQU*I~`duNL1?+%$n@ms2b)e(UVJ5#p=rM z^i=`9QI#?gEq3wM0Sptya9 z_HWA`^d3&=k%QBnvL$X!K0a`d+5dU%|i;2(pl zJ0sl&{BLh<+Uj<#))P1z=qy!)#pR<8ZMbl`b^tFOt3vaEG{h#JcBF=V9--)$QSlk) z-Pbm0EZB)Co4aIB=OzM<-<{4_8UFk{aDoGIt4bEDjSy>#tked`K2C_>*ouUeb&3L? zfxukBUlm*_t-?md#?pJk*;XeFTOMxiyyfdawH@kkiLAtZQ0lCV>K{##G)CFO(pKi_ zo|ZvoarJDqB=ht79j<<9(i#+0Z}8JEa*7f%`cdaCy~A4vJiZuJiMtKLv_57_HRD$m zEMlg7P~nt6jUgDgANZg@y+0%}@#bakz0#M??JljRgr~%0H{FJAKYv@{G=n@hcNqU- zmZyUi5H3{NV0)Rm+2+Q;mHvI95`o-#>D$QUB^WM-1&HeovtCwvkdnpe=BciaoY;hh zhNLPBlT$^&6!C`!_5ry5n`vWS8WI6}Y4}hIvk!|QB2qk6SHieSouM`~&~C)b8}H1e zeif@a&vX9}yJYiHFgK8WG0VU;J~dq2ic1FClij%gJbd7dkDft>jdi$TPHtzT0!zOjh zHI+eOLu$2Z)JIC>jJ5GTd@(p2Jxr6_y{?_F+N4rxUBHGhFSB=Z-Yi%QENtu=HEO9W_IMV{g(&9+p-KwTaq7<}I#MoI->6F)@X4{^HRff;h57GD zfU>`0XXCcZ!+-`zphklUMZo8!(qmr^*lt|4DQs$Tjj|+nIIi4#-r&wLb{v%Vxq2LY z>JP;Xsg;uhiJ8=R?%mAkxqM%cmdg3n#VH6<`!bViW@~%+ zbiG?zf{ubh&&zdU5d+cj!=6$)!husCr!Mb%Xukd-}$_9|G8penbWgB{OS8hG^QhC&%)>nEf-i8}|+$v^7WMuNGSbMCkMzRkEneVMB1XZMj3jl|@Xxd=qDd`FRA7LN4POEMDi9y4a(X z5n8ogS(mXHf?l7tv9->NAbg)!Jv10Hl;nQm3$x(@tT6bwLp$V+wMR`hJKX@q>fZ>E zNF0NI*V{@1lwbg{VyTSr{mH!7({?DeW@D5TX>SB7)j*2|k*e@zKL1DUEqUa}Q)*lD z{SZkoEJ){R&Gl|cf?}>UTrNrGc!HF%+GB4o^~nxcn)3sqtoV)lP7cOa1p%-)?|YM|IH6G6~AE)slo(5}kFvBXvs^ z!|Lx0i*A#;?3biws$ZK4EoTJ@bV-UYLTBHU6l_rbN`EC1S#97M$A|6PnHxgj^66WE z;y^>9zo4xxUAbx%-swr0mb|*&>_nkb#dF+zMJT%ybEhoE>r3dZBei{k7qhsluYup=sRV&O=n%`+($wIelXu6C)y09_yLUf6 zj^=RWWl2vBQbv^$nQRGd?R7O(!M%u=MR3fFCY%!Atr1pzUPk&prcC(T(T;AamTXIY z2SLA|t(o@~&dt%==93;2-Zyv1cefq3S^|L9qy#Q4CGK_cm%Te_!V%L{g%hW3b;JVi z&0!V=p;Xl^ha?5gQNgX{ZD&UDf>Ej~cJ1aY+O~OVrxMT^CtmH~LG=hr6^crLq?tFu z&ZLTb6Ak|!5<)}fY?iTZ8cP*c5bGZ@H(Q1f)Tm?QFP-RwMnS(f;{v3TNJ#eUv)ZQl z4qt;$a=r3#77Op^Qj&MzDvHaBcK%f7IRao7YbPRc%)tkDIKf<95Koyw6Z=9mfK%LY zXt5jD7I?yCYBKZ9N`d4_nsC_Gt`1t*6OKUp*N`vzb+X^M-ofJ@G?9LCc5*!5o{f5m zd4BfO5&$)f#H%``W4~ZXB0Mhk4mxDzWgOse98uX4?u3nHC(|XI%9t|@_g4u`M`{i( zOLb+;m1kE6S_*aK?&j3^h7g-|Byvy=Y$DhUw&gGHK~YTuI!`X_>h|wN+n0n5ghJJ` zlI$DOt_6V}o@rZ(3}f#|C%O~``Hpn!mzTz;3V{{21NOd!=_{=`Uv$n3+-u(dSfsZa zIIXJ-?`I#*EuIwB&WJC@U>umevPisJs~j!IceU%&;YQ2Ku{1?4jQ6^Jrn75qe)$YE zPPMy|8Uj(-YDWv8N>H+0mUJ!1T$C{!x`6({Uba+6%WkEQPs3IYb~mFxgvZJufB3Gf~3}j+A*Gl!E-F%P>ZFz%tQruNeJ8JkXWRsQiP(WQ`ki0<6vhZsOMmRHj$Bfio`GtIW@`bcue0BlSL9!H8fD`^P(m}Sy zXTUwaPM5au{MhXM*~Xg8_pNo-0!%*TN3FB2bYiw9#Erwkf-!;yCW!*-pL!xTHrG2K z7d#9GuhKi&+RbRn`#Odc)P*NLhyCBsF_VExHlE>OV459-<>4EtCaHsBw^0Z8J)c7W z{{}Z2{_aY6*jh70D8a+TvTNTm67P?3uH&tE!W}*?nZ=y7!`#F{(e;Da4NN^8S7tuS z%-eN~ZSfCcIJxU2D6*9$j5orYj>R*V zV4@XCFoGc*x0U+K@2>#ZugI|_5k)PmJolq8jTTSGkYwy$ zR$-bYTA`ERr5yG$Y<9eOB{j2>ThMg4Rmuup4j-7d(P;Cr-tY$~|DZ?iNrU0V5Dnh-*4=f~fnJ-*?hNC!H-+&R6>6rBTpcKOK04J;5ur)&Ab zC6E4iT4_gL!!?XSpKO(1d7P7HyB*^YFR&%OqJA)#a(xQ!_#w6~J#xsz;6&eiANEx4v)$Mn@SD}hEW&89v2gAkd?6ON^jW%V~D zr)OHtUI<9#vSgT!(cK>ge9BR&sSXO>m)BY%BGaei4`kC|ShTGNO;WqT^999R^M^Hn zXIaJ8pJS3H^%{`NGoA_G_qtfOoqnB;+e1k6&+F;!1QFJx;M{vH!NYf|g0vCm)8A{_ zf?LmDK!p2A?#{*cY^q;(>pSg5Y=hFjOfPad9$tEDhF?5vWgF_{9uC$~2^cl+^S_*v zTBJ54_H?mrO1gSdUHjy*iJ0l5(%4^3JWjuRP}w6BNWTtX3~=MeG@VdX*%0c3sc=oC z_b)N(frZTXaWnSl#-{V1;2q^}QxUFgaxMQhNH~fhVa9=)xk@(33)(T~A>NoAm$kMj zbab1BS@b1_=z#Pp(t$=|eOFq4hmLvS#?R44T6x&-AY*Gl>YRm9x$^Wb!bh~|oQ zViufGB(_*1z4fE(`bf27=j#`OZ-QUk=HP^fMG<$n-Yw$L?&*3md3g$EEHjbHJ8w#l zJ>L&wB*L&eSjBa|hYyeG*4)(Pz~ruOJ}=vIGm-huSi~Z@;KNldDAdsZJi5S$jRQ=#i9fI>CqPoS`)iOr%ynGl+&>TK|#@eULNE zHz(lc>gQLN&Kq&XJ1ymwLGr5YcOFqv<1Xv6;X9UzsxNMI%Qcm3fH8q zw8LIDo(#)&hTWcfdHFXVcHDFpW(3~sg`zNKnwGW zDQt0CV3?Y&JD69DA|q9HB4J#o%GN(TObaWrzh|Bk^&{|^uFdPnwpyK3K}~PBbTEM! z-nH(C6@T;j>~l0JV795tcfB(bAJ02-zUeD@>)6OdN1DkvUdxYz* zipmC))@74GbBBgdk)!bi(NWapL+S{{tv~IBXt*y z?3zgWtrOIhtS9dQU)5`v=z(mp$+S{Ty5N@_p03nSL&8T-TZ5gTTTQyO!8+y-g7Jfi zc>MC)4A6s(M?9g&C{rwMx0PQOXS#tK`*BmNb~KLGkSC8Ijs-Tgx?z3Uv= z24X8EToEwJo4J{OvVAqh@FDtAg@G}`GCzL5the1a#p2SPeq6F&1Hn0K4ve=J|ICoX zAU4l}oIRnvR`+v>y)ln0B*ktC0uFu+tj{a0BmG!`KF*eJSp%gk1z#`-s3HmX?&TOk z)Rt2+AOrl6dR?WEYK={7cS395Rd%FgAAH@&gPB#$8 zC8ZaVlD9nSz31|2=p;$`X9K>sox+|hd<86QDxv(l`ef_oTO%ZzuC z=PJ|$9^Qtf3I|O%=!=K+i^nVd*#M@XJtoG&bjT6dW7CRRPAbt`T4n-Q+n1d4vCaOR zur>OR zO;Y+z#}9&lLEed(_ej|VzMv=)t`1vUV_R8-^(v(p&iZ}h8qVLIg`qiaL0e;ybhHjM zazK~V0rkDi8Edc()5m8=5FQ?nFca56&(1Fg84@sguv00X`@{YX{QDr^)>51}D4Gpr z^FRx2eacpjc9i|31Q8W17XEKCmxb&%%u5`EG}GuCy^`6|PJHo1T4wUvRJ~k0f`lcY zP>1sI@Ce~CItbd|m&ihXJw^$CH?i?}i8}5TbXqP21$wTwe@rdgLUV8%t|a0@nkxJJ z5#vvl(Vj9ZZ(n7r)$+>DNK@WF#!Gp9x7wj2zh1oe81Z_Je(9k1o_YQnW3X`+KY48% zRqc%we=YDgd{e%2w`5okMW+5-G*R(J(SOfpb9>#Q?QqSDzVt%b4wRxh-ZlTAe(yXU zzGE0>j1NyiTAA1RffE~Qp1wYjP+UO_8X-w8P9pvQUKDN}Zf`PpVq%JY{t7B(plLuq z7zKEu*CqnDeB9YJ-I1D%diA?pThBNoFbq#CghItpz)DvUVb zL+{^M>`ug1Enr|u$(jUwbgyxVd8^{5yt}f#dGXx4nTCdGxBdu; z07lkb`Q1pmnaydKxeYV6@Y!td51QYaR~f?T_#FM%KFs~{nQj98V<&a-@;=p40!2k~ z$WjRsiM5W6hf}s2ZI9WvjZxijgqN&XC9?LZ-FaDe=o6k%JZn)_g<#M$IcQ!*bXlJT zIErQaCNgt2YQNYTrxks;46_$((+KZ{xUz&GaB>>m4B|I97@7G!H?}@ccga@EruDem zc!nLmd?fS=4yrk>_Ik0WncRo^PQ(QIPM$AlQsh3Y&TT%Bkg6rYj4okZ_F9H#9V__e z_Jx<2fJ=_bvmDXlH!3_$%ywq{O_4go+N_*L5 znX=*@AozNI>-3bZv<>-uF@=17b)CFCHMtRj;Zu9Y^x1E;Y*JorpuV?2?Z5@))G6=F z5ue(7oHjtH@v%m{bPCLA>iy&1mU97V*w=A>5YbVTW=>}33A6F!Vqv1?R!QJp%}hG8B1i)skF-|h3YoJtibzMOz(X)-^*FlqA+TWRC0pKqUyBaWl* zB(`HxDKa`1BDVVQsZ-vdJ=s>qGcG?ClHCe#k8jCG8)$cilwAgysa$Uy4AISsx7rN9pLViFf9=H;D}gt8x`rYI@46gg zB?l-I_L3!p&$iKu@7HRDMjbRlVDbGjc&BoHGFf4O1t0Wg`Ll^a=oX91$dJNL2~|988J&YRAC?+8U7Mf>D*2wVImJ zlctyb$oiYZ72^7!Z+G`Dr2G9t4ZW$;T$Fa-Ao<#v`?D*-sr)MbH^o2r6ApJ6YCbr7j&CN&+CA!G zecj3W23c1{(v45Ap}ST7yQAjPiwLB)C9Y6TY+T!1OZ%dLop8bljpwn96SC&YV8&pk z80_T5^0dFVb<~AXz`div91rTabAgvCs!u_E`|UA8?9hB3uvofBNuELf)w3JL$LWsj z?86OT5cazGcjq`h+QuLv(8YE(Nc@BEzn{OUNhH#$q%{9ErvO;sD1={CyL z>KdV2PB+l{Soe92jlG`H#Xq>JiPvPsVP_snr1S2RO*ll*cfXZJJHfAIGA~dFk)>?s zl9@T~=HTXZ0*2-f=ECUeC3sgI_8x1t)r99k+ zsA9&Um4LsCYw1gY_?<2P-53;%$zd}R9n$VH9BDQ8)h6jH)S5Yni&v!g(he>$qu+e^ zU^;P!COGTplU)Ve1TB$+HYIiS_0YKP-X#3H?Lu)A{kjJ>39?P$lEom?^DPdqeVQ)^ zZ`VVht-?>x&+%(^1mA~`n#x9CD))SXi*)8nR5(b7*R5>7r}*2aG@s^&zTf7%bl_IN zZ>bEc?WuVFoX^o8Z@aR5B~_nqU}PFJZ^MO6JLC%_FQwCoCxvDi?@FC8z~J>r{9eB& z_|>3#g`yVW)3*hoZffG#tVc$kuxPfI?K#B%e9mJ}gtbM;yB!?kVb-}9n8<*&D?2iG zkx>c!?Q&KsBFR`SOI1KO4_=yz2rxOO{u_ZclYU}-o-gp_RhZi_!u}Tv&>5(k#okpp z*4cG+WUfexw(~i^xy$rb@!23hlQ9z+#v_A-h;iWqKeaoslP5UZ*YmD+@tvMW?2HWm z7MLcFij{@!zH)ze%x#FlVE%F-tv^BCU^L41s71%UEJ1zV>wCcS;;Ao?i4)Y}Jn`*; zDC%PmvzPF3Y7xQZwRmGIlbAJBe5@@0hPI2@v5HU4F+UVjHqnHgmR|8Lopo(CWKpVA z7LrJGrfBjTx6lB>KW?ae_`Qn7dFLR3APz3`@i2~27_~6+w{+HR6H>KrDD*(HC`gL( zQ+pri)TUzkWDOPfHc3%*fx+TTGTMWAd4WX@%lQTOf(uGDpn|AF#PsGD1{y)sNilp& zlLG;kCR}Bj5-#xfc6ar1hVEyoyR79}k4)}I;~+`;h40RJy?bq_|NLMcm8@Rje*$26 zF~NCkd{g{%1OF{*h%)Qw0_ys5jjl4?LK19i&v4LsUQgzwG|7_ps9>QNx3MfR4q?}U z*0(e#;?`JHjTyH#C-Nr42iF@Yo|bK0e_2)H(IRdo346c{-w9@r zaB*HZq@6JMgT!I~Zy_5K9GQIU0GiGIHYjtTg_4M9F}kFLG=)}sWbU`(twv=(*eHHm zFyta$*J;MsUN{!j<`AWe@U6@&Tba)gNm zenr2hCzQzxbi0ylI%=A_;H{##1v+8bLvLJibe}9Rr}L}1#YX3zaZPx;VmXoUk$M}29XDwLOb>_`fJgR&gEa=;==s?n z`OS0C=;I+*y3JL_;D#86&&$%!R&~{VJ2cd7|Fr2AFnRKD9c*EA zzX6(^s-Kc=gy?ER&bEI8#$UjIkqD?)++L1LYcwjjAJ-hc4w!PcS2sUcMPqP=HJ^2x z4T*q-oSPnBK=LK8MqeLv+<1@fdoKf?E_<>vC|BpxRa6dWs3tki+2qZok2R5VGTUvP zQiwU5?3O0NhQMSu?B_C{GNE9H`Y05Sx`2^((y3uo2=n>r3Wd5#IWTmCYcNBM$KG(0 zjS{XTe)k!2b6mA`>$dd69StSO(-Sb ziq;AkB@$$wI(Bn{G6ve!jSF>|b_1lAciBkMyC&i+}f_ z|M?bAJ{~#ncnWb$BYD-SA+mQ*0RI(^E$VM66X=SsT@Ad3YwbzY9>I$Zp3uIzpL=HXw zw@O^mCIY5yrBh@`Vhk*7jlC!zkM|*hqC11DRNYNzrz5f=Y%nYlW9{EbMBDx)hW1*n zN_}5S(@NYa^?sF?W7jASd8JuU>HYpZXiKFca^R7R{I|~6r?w|Ca~T>%#+n)3-Ob4-bQk<&K>NCg1_M&X-t*y)0v4~=9at&q3 zj`hm33_W!HN0{b1zEKM_5EnZYLXBCtkQhK3XeE19_nYR3}mICxv$j^kfxq z@H{@5QDj0QifXu@_Qv2ex^$#1fBcVZ#As+ro;~@qOi?m7zMGu>!CJu&QvlRN&k6EJ zh^};O52%_{J5Pq$_XkVgZfC9w?!7awS&{)bgw7r9rLbYuvV(N2N*p{A7RXYj+90jS zAiC5?U(n0N`S$ZoI`I+2UmnQB*D;A|D-#S9gjBznFPR%;vV$-MFn40~EvrYKx(9AW zF>C)O9(KZnt76-t!U1@Na?D&)8hRl>kP{IZO4Izis-;u{eY##pn{RfEMj$(rb5j>O zvV+(__@)qZ@sy>$?KICr9po_E|5gA(Y`QTY{8AMxi3hO@+qT$x^J`=7h0&?Z)!KRU zm`Bqrn(I!wVd#d+$zsNw)$#<8{6Y3EihV&nFWQ8gO}j!4w>Ev9Acu2=6oX^S61(I4 z*sw54opqyT0k&pkmLvUe^2$^NPp#=j| zj{NwCx974{{Ni%J#}*Er$`AKa3$lBHHVrPM4mK9IE6*r{zl9I(M@lY{SN^VZDnn09 zIM7;S{loh(@{ZZQ8Z{+;7{#POU@z5wyUvw$6W;scH>BS_z5m|R% z9vo3a?`|@*T3?VEUqb`aY~10ogm#?sA-5`RoP55gp~lXCqaO&4Nd<0kX`U+Zhyz8R z{7$|OMI%Q|klNXyK-@qwi8i8IG3pSH3s=(sfPEvCuz)JvH?xOVk+u4*`;532`lQim zwxOKraT&LG!i?-@;g`0gPD?rqGjB$LzPhDNeMBpFb0R~*t3%x^SH`c+xIQnkv5aW( zh|6o_Hy5US4ror7!;`q9GYq_ABL2mT?^!ZjF`nFMw$5>5$pTiU-w$ynzm_vNJU_tT z(*aQxS^j^eIg8%TmrG(V*aHg<@QcN9R^mWuy1D{B7gY_fG)qDxpVD{`7D|GEf%d}x`u zTV>=*Xj{E(1H)gHG2zcJ9$dp3_9(1b={R`o%uLHn059Vi@Nz=wksv3`P&xN^YP9{D%d74jOn(G>xa?|NjiA`Fq>{bpY!BiPZW3<-pHDwaaH}s{DHy|2G_N z6yPB=^4CfnucgLGv4cczl(q+9I=kgT(rmqHeXv3wt8)ULbv)C#ZN8TUQZbOfH36F1 z2Ze^VWTDTm2VB}oe9IDyjz01=WResuyG9(qgn*V?YKVARYDc81fP*C0HA7{fmhOKL z;i2dsxIf+$qH;#nOWC_#-2NKS;|)~8IP;PMPAP|?=W8hA?0e>O3;_9LgqjTi7-Ewd z%so^QJ_+p1p3jyh6uk#SX}_cED(opx{3~AZmRboZU-7_31*qMbfA(w9N$TKsjBjqr zoNFO$d{tJWiJbDTCF%Zf5%8hg_`2^lIs)C6)kQ3RS_@Io`eTk3E|Eam${BUvag;)L ztN3kN(o9>A{g+os@CibOnY(4svkAUA4*NxO(0VP}ZssBJvyYAL*rUd8?X6-jevbVP z%9Bkg&^VvE9EYaRo7Zx++Om|JWkOCS@eDM#U%sZzf4pguCV!LVfWGY3b8E%80{_Qx z6q-BNvO9jGCZr4ixM_DRSG#>Q|4uhQGqcP;+Do{im?~!)FR6!n`t7gmg@r{;KoE5T@zVU~ox;5fx!XknpoadP2&f=>kJ{?{=rwW^nD?Uv)vlOI)^<0Xxo?gPn zqUg0D64pgq&^jY>y|v7idlxt{G~aR*FlTZ8h*8}c91#rHx5MI8X)*Y&g&utit5Bt~ zJ-}g6b8Tv2<|=yh)B?^3y)Xth#n=fxD5)Qj;yT+nS01t-=!;$4+v1mX&n%^}Ve1OFO7z8+ z4*8Rmg46Ra!{CzSn<<%$dKqpBh+0}AyEos!xZahh{}4AXfu>JHo|O;n|Ak8aHp2-U zlz5rk_YDDE4-ilN=^-kkiDx=HMi7w12h;|qvE_+{D$Xt|vaPTvIbeR-AFZ*~_$dAs5lk9e~bOY{WPVGG0@9h}4uaJJs-tBUXbzl7E z4p=#v_5`4)N_(IOdGKbq3VMIgruFH5o4%P4<}{~ItfzyS=H(JPU;jZ9N7`5zS&&|N zD-cqLVRMr6I9u~4M~(Awor`N?fuxj&O)nmV^vMBu6NSr*EBLBMi5QnL)9 zClWUx8g)yR8c}T|1L-0)7KMA*gQ%JNn|J;D_M8`ip+R$C&|tX4ZTn(-T`HAZ{cD{s4W37YX-Pk{mO+mN_IUzWzZ|scr;Itv0};#&#r_O7 zSqMaPgK{c64vFg|Wkiogw97NZY)moU)8f0~rQeK`Z$8~OW9&2tp7!Go@+ai|uXZrk zAH>@yBtj4M@|tJ_ZI_gIP=B`s=qv#ANy4odulZ1lTK+^1H8SN^7l+YKYVV~?+#tF& zCOWlbuT<3|Zq`5mv&Vhows!Y7#h#7_fBlz7$!tc}0! zJ8HR6YKG}5+=lC?V@Tql2C1lSJA5%a=P_)3x7R59hswMTg{rE|+nYACY3He+rl2aG zJI?nqPb)e3UdFoja4mqoJB&VJ&!Zu0txmC_d@bwgCoD8!(1A@_+Z%F-M`pCc%u^fE zA=*^SHZ8q4ir2`CM6`m?HY;|7$_1D0d3+MJ<;f?418!K~BT6y3HX|m;DR%a5^U$%$ z|9%HwaG8Xt)%5+co-%{$$2VJHA>t~aBewQy!@77}LwsKfd^3>uJ9c4~M@nN&$cn31 zJ;_6DQyu8)--JZP|3nT-&rDX8+)vxyItw&@`LvXi3W&U4by8hEo2)$RBswKV*cic!AS zLCKHNSen3N=|u@do^9l=7g!_ke)WLb*`j_R5Y?YU(jPA^_#4`Tl!8j#&bF0DhP=`$ zCH#lV+-2R`6RrW&IX<=Pf=?Du;sDdHUGAacvL`nI5aiviod~b$vr>O?N|_X%_Jpm1 zwXdw{v_P9KSF;oix^xH$y~g4t&$H6c?$;VomY%B4gA|XnbW!$WVODh0pEscjlAOW` z*{d&`E4BDl?6DCkOIzajkAj40@@xV|Ue#g|9gm+rU*T85`1h>aO(+qH$+X3xg;H+f zoT?sgQHp)Z-afj0XO6#W@yOu0IZP{d3?p6|+Mrz~LM1E9WM?^x_-UTVV}|shXI8c~ zs%JXKEB&sx+lG#l$Gi;Jv*l6=GBLz{;3aqOk70wN(}uE(hg)l@DN1URo~N&yjxW-L zVx0sz1qWbdIPw~!F|qCylKgaheu2C#2*v{`e$+^}IicmSeXbM5=yfq$kFJ}S=ci)< z<|fBs{~QeKSbe%BDnuHj!kwI#hd&diE%W#`(>eJ*C_BE|@C&4h0H@4ouku{mUg0j) z^M)jF6*$1q5dfObr4i1VDgLmZQ-6s{59(3Ll26L)dW)&BPJWf;aK!A6c5 zZsvG)bT*aLny#gX?aak0SnnaXQmw2s)PLJuI2gQ0cKoF9uxohU0ev?f$nbRVy&WGD z-b~qH6g(*lRYE~_D}#FESmv>O9lMj)kDYEYcB>xUz_3iIBuoj};joc#X_(FdW29JN z8xTVXh4~LG7&3GgJNq$kwUtzuliEGdKa&93Vd;Lm zVasBkj%L7#q@3{Lg>Y5fl59e9AN&^}w5&9@wvC$Vq14y`L@E?+7G3_ifo;3>cIw>V zTpKpbc!*|*a_7xby(?MoI(M0Au(rGi;F_0l)0DMInGaq~CupRJU){ykU)jL*Ibj>= zZw?zuHd;e3=%5zqWnz#j&umdW-Qv)_P!0v$JGH*Mop-epV&RL)U()uokcp+<-aXHC z{0c$*Ol^OSwY+IV|NQ*+rLAy^l*(n;L^W~5P`V`7ko~q^{7^;kevJN=r>CZL#PDIR z*~dix;g216uEoJ2L#Rt>@Wmeq&xMF}b%f@5{w7Yl(ginO$3Sb1qmmyL8ity#VyLNV z=HZCVoxVXqeqnsyh;Dp%@!b2?9-Va`od@mqTQ&hcN;?jF2~7o9?^>pRLVM|-)l*UQ zZ7EMOw-&|CTSFt1RfZgPT{T`(Gn!OjQkX_6O?7OX>9VitGe2;SgG?--e=`b3)tPzm3P4MWI(Y z=&K`xRv-U9AAg?%7Akwib)jVrWMu*cDC-5tFD}%am{7}Gq z34~pXcrZf|4pf=>Wy}Y=xkB)l>*;6>ERfw8j%w$a}_J>(oqiW*SGvW!t z-AIO~h3z@g1!2(rbJOo;z+o)2nDzREh;uy&``ifclMxRS^O1qe0^R}mfx+;%4{(w9 zbPA&UB@o#%i=Sy0-GH`$lJMF+sX5Uaj?|G|baC`+#il}%Mw=;6It~{)n(1k}9N^HW zpeX{P==A1%V*H|zWlh}Yj}jcdoHcij<| z2dPzEmEX!9f(KeKfNN4t!JYh0me2NHgK7b_^C|<%v5aP=f(<}W-oA|dDFm;ia8#|w zpjk~rUVLG8@R(Ga``aHt+v7a!lilDHiGv1s$-aGNtA zH4z@_jGV+ZwsjwT3v)qDGHKIo(4d!| zEO|UZaCWay{X2f3^wzHOUi;it5Q+h>9!RvdBh4OWWB6Y^eE+7)pa}THsDSiF5oX<4 zJ_|L2G1e{btL{iQ^jEP!Zybslred(JF}VkSzRpw^b0Irf3BNPSX^=|&*$n0aU6bR$w%Da6cV=F>h9C$H%rle0 z`d8`3qS*YJ$Vej#f1l{`3B;dCmf4jVv1ew!j)x9Q1Qo>=(x2NChV^*n#-mdB!kJiM zN4Lwh!_ht$oL*H)da~!ftpRi?JLV1w^D`3Vok&$3-zG!cY!UH09t6sCmj!|GTv92O zxmoDYgu_S=o|mWwhf5LCWxWViWpR!zQ1nH1w#88YivAx9n=-f?e50&%wEX&>MxoUoZtOicjP;@;S)$8ETBG(Cat!>xl;z z8=!K+lOU+MQP?|a1rDPGiyp)2sFvr+rYg^+9XD*Mc{J2o9SNQ%k0qgVPS~y~wrd)~ zOCSCD_TDex9{c`_&W)beY-m;Qc&)GqNYX0Ia1-l}vU-vJh#}bPLZ+&iBX6his9S5;Iekl83B4%!*2$rQ% ztSMs+3Z9b!3r*;QZLG6t6RgL~?{EsnCWOz9vL+weG8^3 z`grebKcN^l%(Ro5f}pa$A>G(^XXg(#7+|V=B@obLm)b=JsMXTc4EEkT<-j5H;u2-$ z55^6N@(+Xb5E2cqP8$yS*)`kRU=(-SD9Qn4w$D=gCJQn(j&=@@sJ1C6%We6<&73#^ zN`)Nr+u?i>Zs15o-w5PS=AY}xcn-KSS&rGwtjsz_IT29#^hfvas*)vm-t2R(SqWa4 zd0z|i#;8ht1mQm}QT00e1ug`YfoeU<6BQLzDu+A$()aTfyb<`ZX?n9atfZtAB|jY0 z>@qaRD-cFiwB=J7y;A&52$-z0+tyTz^5ldytLB?c}l68X5eQa)AatkNKzi)5~2j=8-c6>xOt1GnC2nW;3GW>8kRScFyvoC zxxt3yrd)$wM;DQZ=g`5SZRv{5Y~kTr%WF)38hL`2podcV2N1&aY$Hy&dgs&Yw})?6 zsdTp(;cBM-3ABSr9)%(ISPG|aorWmDjD-_d4%L9L&E1>3l5^YCITlUF??E)5`h0Cz%C0@{Z?peF}Am(ifhlW^MmvP_P0>|GE7H^8j!9Sv@jt9 zerOtUTqFpY;_|Jm%Yyr2EDa(M{gcL5xdT8o{B2#buOps&EL*<^&(XMADFtHkH;pLU z^z`(kq@=5ENc^jh*UQB8^x@$|G7irNY7!C>BNLMtXQW^Z$G^5Y3d*=6^}8j{MyUoy zV|QhPeiP)?c~r$T9*3NuwPf40c~9DW$fwwu^VPnFfnav%Fx}eE>wx6BtgvDxjS7sT zFF!*C_Wn@Ya;Pz56_=O8M2iF#kW;Hw`xT1D-t6Lkrs2PBL5AbOPMe*@5R;I6jTT`d zh;@?not@XqIjvI0OqC+}$C#6Fj&DcMlTW2~Kc# zcbDKE+&#FvyZf7y@7}xq|GoRN7Flr4Oixcwch#<4yL!MVWK#T9OY>V=Qcq8<6%`dF zq@|^|%NoYVon~} zZZj&_G)ERI$3H_Lci`XXVjmV8Q}dk9Ebd{&!7nMms&`H>SKl&_3LQiKQ_l&JGjaSP`C#^>6 z;^x}aR7*oLJ{L7|C2;gGk5}XKjaz?JZ#@Hb*Q&Dr=(o<8lf2p$kzSgk;8t?su>3m9 zxtJ83RQj9ag&M)KhPA~y%l=;Kn=c zcR!1z(A*Zllx4K1^s2=LG%A z`5W-GqF5u0zHbi(ymAk)V&J`D?kB4Cbf3^xwL>h`KW&I&u-RxjMW43KRcsR@kZz15 zvjX!zXU1Zo#uS-Wt%3|4K6~!yV1GX=JKKlnw2>55A{gdl%y3o-O-TtwAb(Fws70Md zt;Wd@0@MpDK0dym>!Qz%+Zh;_2vHL3Qvf>k$2JG8YQUH}I6KECB#22$N*YiFprECL ze6k^(@Tez+1r{>AMzDW$`^MpZ@Mup#35{lh^-gs^f#Kuz!APb6VdAi;zCN*1!I#bh zhfABcTbpHvX&DG#L_c?qo`&AxkBmq1OPg2B?`2^CU-h2I2Z$8vPJ!|LlFl7(zcq#t#ibI3rpoB3SXunp&&>A){qScQN!x^h{6>r z^nPCTVMYVM`7U)%CmO)!1tj*Yu*?I-$mvnK=|Eh-!MN$hJf%G#m&q^V==f>v<rRh=~csS^E=1!-^Abpmy&?k4CxvM zV-t-VDPx-0^{Xha_H)W6ABj-}G&A}~ZelUNTEhU8twEPpg4P@Y#H|P4-X0=EIAVrl z-M1Uvhlg=*S!-(sf!B-3g?cMo;1+9Z*-jg`kZLvPv*m;5biLEpo_9I$c%0q`b$70| zxpR3vbDo@>{LJcFf56sTckWrY8uU&b6oLVw2#q{^T7PWhKKCbyKw03fA+G?rSwg;a zqTOD)v)!}|$K=AoATWg=W-7e`=8ksGH@cHXO$c1p!GXIN1Jr}APbUqa%~_JU9SX~f-lKk zrIqbcoOru6nJ>pcdCYb^d5l`hk1Mr2Y>Op%TJ?VgdeZ+qePA#rS69c7z3)X{pE}-f ze=vF;Nb_EeumbnpLCnLG3YrPjN(Eq5u2&{V1iY~C*LGZr$e9?OIRpNQ=HAFAfOrdS z1ennee+v0UrKI2w4h{gve=$T`X}djuI+n)O&FK9g{iDYy95mFfXKi;M`jIo%ncEjX zd$#a|4P#3nX`jh`?X*s~oo^u-kj6;5Xl|H)1TLc^Sxgma8ttQ&=IWBHf& z{j{?IknDQ~uJeg5_zRjL_CipvW>G^>+-&A4K9M6@G;(R)%f_xi%(Wsd&E{(8Ur;A& zM`%NQSzRFkx_bj6IKWf)KpL_nC$SjA{{vGdr>FmVS{_bv_|63LD301Y4o%|D&Vov~;-s%S~# zvZruf^W| z{gV^GDdGMAW-##0{=tDHBu=e{6-g#)Iu!q~k9$UP{oSHLWiI$3!Dw%SRi{Dyx|U8r z#~J1GfR7YBe)UN`RQ?}){4{m{f*aXycPgmC!rSr~w0nzHU#;eM(W|~Y>o)x4Xx-+N zD%R4KSbayyUK%@<-Z|eMfk|^V+!(*sn`fwan4!RIcSqacz5xS$q*}jP zqiSTRyQTC@D;kp~k*C^t>9L|NCLuxyM*Ky&*=n1AL71#tbP^svUXDYe5*ox4gki(6 z1;ZVbH>~0uk-##W0CvUu&e>ZWfRzG|laco?XWrwlFHc=^1TlN65(Mv}Q&QlW^nCkWY=T^vh*xWf+v%H3yMy5#i>zR`G?E|$ zjS3TUJdsL}3AU0t5?V>~U7Y=i7ljw*Xqd}vcE4uDF9y(~msgM^rOh}YXS6Yt)VnfG z3-|ir!j%{8C3VE_?%5Z4H^v1b36}baAl~n(D!y5ej;4+%dsn7_p}1tn{X#-^e=cX6 zb=_Z{lb{6C$8lme(A^jFR>#^eyiDDxTI^?jV@{bnD%JqD2BZ4yl%RxpyUsH;R81Q0 zQut}=T+9xZIP7dvxTGLa>1o^v{dem?xT`Zn_y=>8Iq(NBEP$QxgT(3aw*wpnNQ=Do zda{OuZO_p2IVL4#XvKMLbh9_~VR?_gA21lOkncd7li+bX1Sjga{R|0#M;Fz9dwpqr zdwnu8HMPIqp9B{n1{@HujI)6$1%P=sH*Nszo3TmB$;st3KnMbGG$#lIgMh7JelH_k z{OE~4y576X>R^9Hhdo10^f zsj!wEa!@C&9NC?Y#OY|%GOGdEzDdGFRmb}l%-}%tMJCm7mv-rhjjxYgjT@z~$&cW~G=i~Ip6 zANP8)*zN&~2Jgq8E0B~m&UBrRFp0^oKPuJ!0ctK#tOWDuvBB(SzivWclf>5>*!9Xj zyn0=jEm+`?=pEJ7Q1P0)Q12al6=S5HW^2S{A@4kRgqp%HeqU<^)Pijr?{c_U{x6zO z7_Aq%boBIy<*KPJ5dwWnN78&^9=sH>a1BeN^;SzseD3UbYu*BDueS|o-JN27yR*Z1 zcWlCcoQ(`H03dae0?YP%-UiXSe1X{}*=FkOjSYtJs+uf2=}<`LO;IuMbk;Gx6)7+_ zEhi8zTbK*XX8-~{-W*lBsq>$X9-6H@N6g6Fu3G`l|IeR4`k9R6qw#t8he?vL6wJm$ zaSFq4c2j+OlGuaeIVB|?r}mko*zZ{Y$mVrhlUWV;#8tN=dOLf2bwJELX%<&jMh93b zLeFc|hBYtu9EJ!OL0}=Fp-G96M1!|sZ@zUYiRo$9UyiM+ zkz1X@$G9cuP|~p9zDm}4(T2+~T>m)JN6-Ol1NRg!qx^wSghOG=>ub)P^-1Agi?8Cu z6}{Yf`aTXp)J9mKJc|C6z7hH@QLAdSQallSBZBGrCLT7^;s}>pNSDcgNYtjwOnGb# z)wwoi^OM)*-2+uh_U{WRyFIqP$iDw4?DY^lD*yydedM&%ZcWLDy@?!phI9^2PE#u@ z=|!wLll%Q%p48^tJUo?;Tw7~$TlAcl-!JjP>sLCShdZjQme>KWR9sw~-Q1i~>HTu# zaiqD{WyYq%3jr8+QiG9qh~eH`W0r)FoPQqp(^Ckr4_m|_-W*Rk+d0;H3yhc>8 z<>KEHetu)|%8diK3JVJh1G@p>z&y#=59XUbf%k9j9vC5!JPrmw%#sqYO3g=pDaJu{ z+QPG^0H#C0$4>G<@RJaBGnQPp+)nv9pVcbN4Itg1Yp&rDzh5hw>Tx4~XACs7u53 zcx<`u1n`N4KT?c{LBVdhS4YeSXGlOkGj1y^ZsgxG!-TAcIi|duY#+qBHW``LZsge8 z@~8^t7XS1hMzkoV`8ATuvN1Lyd=j=+dsvL6@jF-Lcsh@`xOl$`as`<;VVDuo)^4gz zL3uenDoS}t$>!SI%bM%D4@}>^_g4U(M3g?Bcs3u2czN$a3Ru0NQGW95*wYdo$BOLJDmYl@5ieSLP^PopPxTmae_g(vGh!P;H3w#Z;q4&w@uI_?V+!)=mbhD zz*f@aA7*QP`y@63kdz$go9ZXoE)kKJd(!7+b_Q;>Xe(jwXmrl?dgd~}C!}5$9uBB$ zhq$j{O6gE#yTl_5r@)$pbE>K-=D^5mHGbPw!H8pbwN0UAHs-mYa}Z*Bw}@v~X7z{Q zefP-lzBbdeNOA6uO?CEb6I!Uq^4vewAI?1E{|Ko0zig0v+hsDtxOPI0Pc5@;eY4#` zFji)8QoJ|AA4~BFqtrMTw`wfZ?Zagd>-~&W7G^XarTcQI_H)pJSamKRV9=tYnfufL zwi7K#tZigg+pgD>OPJ8{yko7r(7wnk$J1d2=a-@b5H-GjILRYzyYn@#D+#3hau-$J z@$hSyq{91R+#Fy&a&mkD7t`{Y1QlTP+}biumKv^h7`08CvtE{CF~=CHuaw7|s}&ly=W zUq7Ym7a$m!%98|In4Fo3#})>Vmz1-!vq7pXR;v5@a+Ce_^)=ZP5XH+5r2be(fZ}w< zn$n8(PNtO$OZ&YGIJSg|$Fx*^=xT?LNKzlHnZv0ClkRz6B7*H`4ud>7Y* zNt-#1SM)YeAKu&!lkyBB5IFR!m@_~Q>Md6$P1if2zX9Q7$p}Nl`U~dv8Y1p}-7yV;#+3MqV@FN~+zMs&Mz zp=^MNrSsf?^r+87wgmy_43UnReZ+(nhqX z#O}t#BoPYdPDaJV_?DFXd?278>-D*#=Bcx!IoECMUZeUsQQIj!>=|!}wvA$dZe(n% zpsp@Sgu=-CQ>3(gOCy4L$ zu5dNG>}9SEGMfiW76q!(<9?;X|WUz7WjC}#>N$gr(|=?5a!pfis>9uNu! z0p7QlyQ}HU*SMC`=FoH==gT00XCHw1n4X#GmGinpYroEC{C-s4kTYop3ajYclUwtQ zTc7*{t>fW0U&!ezfUi$8F%^(Qm z;O#F~p=njie(%^RsH*k>u@}JN(5e4|j2-Mjbf?> ztn?;L84UmGB@EQGqq>iXTT_|OBn}of2oIhhoXH-2=COIy5Dh!HtE!;{$)+JD;UeqcuT`%S_XSAE< zs>)Lz+T{!MQ`jrXoD-;>M(DY=%PXYvkiU)fURELJAeOa~1DSL+ooCvPag@!WFpKcp zt9pS8@E5dG&m9U{Fx}P%_I4+wG1tz(Q1!UzQI3MO0I}Q+1di2|`PK3RW9h7mkm2Ig zrii|pbO)zC0w3*sDgl~2qkbsM00or3yKh{GYKq`^CE2RFA$JMTB`}e~lg(6j{gw2e zx7=6}X?c|;MM?eFpHrU!Tnu8m0A8)*Y31zxx==ud4F~W&4+wza$pa|))X~0n&nMWR zZr=0X5XXyk2pwzVpx_DcHw-Qn02S#2@e&^gi48cxzC?cTkBbOr;}d;T!DJL8liU{G zsjc*1x`*kp3l91UBrcq87G$|0+^JFt3+$p1&MHZNlKp%N0q)_kbdcw?x5l+7evE{z z+;L+SdN{+1*n$1?(y5&mCHrQ3Wz6p%SIrAkilL^D@tDts%cekbQH*}#T)#FHtJ@o| z>V$PD{HHf#^TN=O+&m*!N2Srox@BfY?7hmdYA=)CTj^fJv{%KJVO(RT*DeeEp3mFp ze5T8`Ab9}Ne*^*1&xM3}`c?#7st5(gT2juvZU~SW4hHHc(s_s8_yq?S@9fGjzX9R` zdo{%?idtTE!9fOr8RT90XFKei|IObBbVa3D95U0>^wi9URQB~l#el=__L8Hot_D{g zbQJ>&ykjGfp{PVkQWB1yo*wX@*;Md=>lb)?82^&VpKhAznV|c;iPif0c=Va1x;ZZ| z&;RXt{B3)XGSg_LMCIFBdpa8Bqb3{ZgzrsStxpwY=w$v$KVIju2Cdy^<_#O!&@y}t zGm--nV;1XA5dS=$aQ=Ey(ymH>4NSxnr;a~D7}Q+vF>zGHIh$FU6yEuTN4LOkx16u+ zNr^li61Z`NOV=G{IiqSZhh4NR-y2o@3$Jl6+1$PI+VkjeM3aerB!53AuG^=Ap zbTHDQSj$2*aXFTBQLwu0N6A)oq7Q!l_Si4I>cs(dc`5C++x1HJ{pEJ7+~rNBcRB#J zL(#FY?136&fKO;@YT~^dAoTz!Mx)!~g=M#+a?m7z^b>MATOG}Odp2EMS^^yXwu(10 zkY_J>NX3P2KRt0LOJ$`B|IH>qcy-E+^F)HUkzNP6yR#A2U>IasXz6HM1pc-xb*O8V z=Ng-Qc&35|_Kq=55%adHe@*JjUN*(TBZ~^l<_@_NV6f_X#fRlj&axsG1DPrUxb{v$ zj%{*WHNa1oU`__AV&Ae5wgMIp|C^G7cufQL*s8BKJHt(vRK*^bdTG{S)iV%9t$QB| z2YTiDsF^L#66Uqn;o9kY@#@LrdaD(5Im`OZ*|?6nch;B)M&lRg_ezM^ERNWiGg^2x zga9vR4HM$h(M5+=sA~yjJ@p44TZoO`VS}B>N2U6sNvww^D)2DcVv`>6_y1OdX zeF>2u>$0qD>)RfT;LupQ;ZyZ$6PB1GCzQFcl04e-0Qc$M(O7R*xuAY4{N2~F! z3q|;Zn8aOrVDOfr-qE0UW;M|!(Dylwssbt((=3U3he85AOLR(^WYQYlO2c3Yu{iP7 z^gLOTBctKO!3HwsaId3De-R`8Q?l0vw^aLH&r?|t-&(Ej>W?W@iRL&!nIRdk#JSwT z_@RK+fJniHd}W}&pBZ9b;4HhfPsEjtU038a#X`$*EKd<&Z#!ZvUQ}^Xwf4c+x-nbd zPpt|%DiPFKX@2h)&;RU!zC>Gu$k3fj$!0rJk+Kmu5>Cweenq%H28?)WG<0^T@T?8CKft`QKb zZHfxq#FOd+j5%wsVQ?5`WeK^(9X_HQz(Q1KAavHVb5}BTB;eQG?wlLzRq1H*+4Ud2gB6~UxMsXy_W1?oA)!MZj_S){U+X;NF)K=Sf9H5)59JVIzzKT z28!Bb5fqN~w5KobA2$982g-V=7Ve~qlStHz`~4|O(uXY=q1|N{=}pI{CgcaIaslqN z9N~I~Bf|uo-(jYn#PQO@Md})(zm`@SS>cNoKK9hjOKi*IJ9=Ra@6uFv>^c!TwsbXO z9>ASdfFyP(9W!W$Qz zde(I(w?-$orvMh^s-^O{;}XjIPsn|G_$n=ndZ(h7xMie*(+E1eaAj*is1I+?wEpYWbBQK+TcBlrzpC@;#ORP1iOydk_bc zA!hCe_Yr*GbL+fIEY+>}F4WmJ52xZSZ*Oa%A4$!$J!61B&Izq9+timTHz47^kiHeQRa zyCSrj&L6JXiMz==f8NYK?jm4#xuI%uBp0iH>yEJ^HdP@l5Ut3Dz=s>W`)C(LRDl%k zmi(f!wPL|IBS28GUnv;EKy&GMTsJGT=d(`tD6;x=8>c zOG(Lf(CuxA+M$McD66VxIeuzZV;Hcl7RxiSOu>*wQx-#;$zd55BHFiBluLSPyt|lyaApb<25? zP-I-J&PW6sUAkhTMSGm~nqg08qkZSd?|f^cXLs0qBRE0k)`A8!7(F*Ot=`BHi$+m{ zr)>wf#0{pFh**d$U>*mttRTh#AaS0`#yyWO;Xrv`*zYQ7r-2Rp@4Wjlpgsb*dS2|C zY&0;|LXQ|ZGxXOSB(01K3AP?m@x#IKU(DK*hF``%PZoerte}z4&vdp#v;N8SiWQl> znk$;j-m$Sxb6R!!+bnT~`YUpOGbE71E2)smA||!^Fs_byF;Shq2Zj5t&9sPfA6MATw3B;zmboOmSX*I;Ga}P;JS?p z1hqh>L+zS(vA^eyzQ_2fj?S1GI7K^N5aM@Utpg3c?#4vW+11bi$Ai*baSvqlAc;8^ z5|f#7Ro<$E*X-WC?nklfgJuP~Y7{lOXR&*WRj|m0)Dv16EVX9H~7Q9@x#`Ey}%TBc%=QvR~YR;xAug06voEJ zh5{eAVWS3#0vcb#3(q>Uwdx2E{XE&PO!!k>S;5@p2=tE*F!?9mr=gg}qzu`;(4Br7 z^pD)U1Ssxos-eclYJxM4F5m%0_uAM|%A~N(iZ)SbOLw9J4^XW?>BbD3ZfbgTDcyVH zBvN+TFGTbKl9ghu2i@v1I8z$necd7D|HpJ}q)2GM1E0zj3O-tMc_U~w0dzEBWDjW} zM?XC>dO@`b%sAWjHH^m?{u%b>MqZu;eJqsN!5`Li zpPB4(&Tyd7EfJuIg?r5Vo5B7*KeVx*6o=1ZmwTy(l^CSAS% zqf~;5FqO{F$KGz!+wkA*|>ADO|x;yDk)m5F`>q$vyi8 zfe6(1h;aeE1?AWlj*2{yT2t+q8Ie(iI+<1^U(xDZ&LF7_jtanNv6kks!LOZoi9WPxU^$+|I+bA~;)ILwYYM;5QP`;j8m=&(`hg0v z&VU9k;E$ScnjD26`rm;8{_6>j{$~#U?SohtNb&fu@BaG|_`4uLx*TAI)~PAzvFo|d zi*GAVi9i22G5eVx&R%typhXku{cLcb>5wBmoh3P)HF?cfXg-nm@oSU4{3a@IC5pkj zpMMmhiUDocjX;5hEkk7%HScUdixg-bCCYnK%-z7WZqK*X4tz)kI>P{{TKtoK`|=#Z zR#J60bwKO(ljGSIrTjV>t?nzF9Zg8k;GGum;wpPvmN0DySu9t4%R=ohFf3Z3v2YG9 z4}~Tt=)8g_rob)_wezM-PxnOjria?FgP+qisB+Aas5mlpzc{%un2T`C25vr#5)d03 zbivEFk@~t92N_40lsJ>WHsQnB*D%Psgq%QhN9^9BS*aL%wnaBbkj4TI!7C!I<7!fL7yjXL|m&iDuP8Iy&zuXL{LFUg?!%6SC7`Y;JX9G!QLQt|EGcmu zR({DTuXb3uDKSlvUilzDQ*ZT9a)DrMVGkT=AyKYttWv(VoaWe}LvW<3 zQrDtF-!fg~hQrPHwd@flykzUA;R|l_U6JVY4Zth%?WNz%PczJ5k3Y3(%-FZ&;HaG_ zsC>#(Wn?aiKpeFIn$A{}R(21}*`0GJv2d=@xOJ^b>7aLt7)xN8VeZbI%~YAGH`OZf za%*qC8GvmcClv&$^-#m__u6YzN9XzvBm9-ZEgpn(Ej-KARa8_EXXuo|GZ__dMM_GX z8j9<5Kb1yMY5?6-*XCF%!TjW}ta75PnQ2$t7oa&)e=1gIRG@zIr><9agHcU;diikn zPFaQTYr!Jx@nLg;tcnWH%wr{v%FdW$wcF*l^5$ z{eiU3djxf6%_{Vly%AjX^tzu+uG?U&G*z4vq*v@dtEki|tz03NT0CpCEf$wR96i1q zDL)-MTG-z)IeOlK&WtItH+0=AohekVyUI5%`97&yClelD{#tEpvCxj-4Fi;Y4c^rP zHGl+&!e8X+Kh%0}7K$QLi?I%iyDSOom@ghT3hj(@5A(Z}nSj zJ~}}OM!NqD9cZU`Rsah~6NO5u`!O4KlK^YgIee1$t;?|z;U(*;p6!!i=pG+E;W&x? zH?`X2z88v1%q0b|7Z2nB*7*7MT*N;x+AaxwQ#3V+3bvtzai2H(y zA{AS}yOklJ{gAZlVb)GC{!}!el|FgQCtz$$KxmLYNNYnnKf&Tz{l;>N7SKhe`r4r& zw@!}S$KiR;y-cTR1S#0m7Uf|{+FsDkJ+@DBb_JiM2fT#JuF>Eo&!8_2gXGWHPX4f6 zv;dhxlF|C#vr) zx0Xn@Tk(EhAwpPfz&?h(24=-DYvnQtQ@wX<%l1|Rl^DgDRRQmXxKL9(GP}XP_TvbA z8e8hEo0*&jDKsTqY9g*hFm6&R@|P5~}2C1c3;& zf48T|WXvHy6qIOck>iAUHf#0O)HM06na{rHkf(`!Q~cKl1M_!(_%`zGoPWsDPY@6p zI3Zu{YL`{0#W0d8@EhxJ-Qdy`-Rpqg?x21Pol|&?gK>ujTtF{b~q=KEiyL zjgqb88QDheuT#<)=_^!;Vl9U==-wr_?pyPV7W)?w-l?1%)t9d}*gLpRe9iBJ9j>wH z=4WT;hyXaTPnI^1k#nN_%!Kx69-#ab`Tba8Ew?=UFyE8j2(AO3nHd`>RSJ=BZ>L~~ zc?VxW$2WEp{^BT-6<%N9IB&y}bp|_yDZmbhw%8;-+Xx=OQjEG!h!q*?fjL|wDxKnD zT;p%NUOGa;-OPk;oaCdQ)G4&LME7}y-_P=VY|reBpS^*fB|n;fw4k*Tw-`jW&f8!3 z9j{vf0L|}00G$28sgG0t#Kbt%oz&{;L{MQ%KVw7HxXWW+UiwpJe&#gSK_5Jx9(BTZ z3RTiV8G?#)uf@!>xWFc+pUH8_lj=(AtN2ZUcBgg2RBI`}Q!GkZu7%o~)sNZ0mBapq zVZxTpHkCgFhex{1-J%(*;xZl|>YNC12^WKHa_DR*{UxfrbHiE_fJjmt3~Emp%LiY@ zGZsVayhC2<0>KNUhPqQ|BgI~N{YE@CbK`I0yfwC8DMu1?Sab#KAl8;50ue~t5SvyM z+3cxL^cNy|4l4(C=_gtArGfiv7A;S{i*A4PggkP4+vU9^unDlX zjVx#c*iI0Y&)Bzg%K=VHq%r> z7|mrIx2B{@@WNe=*}th$?o5)D%<+i5DE{A!hv!O2IJz~J$QvnEg1RicE_*Bo>vX9c^m==g=#UDJ8{Td5mg*EQ=iYyiBvLKVdFRe4t3 zmS;O;hMfUR@|*YVsQ~xT5JQD%;vn%oGsBh$~m&*9rqcQdN5PxyC`T9vN2`Q z&BTIP)&<+W$(|LJ|M(DZJaC$heW^G~IdU5h*5B=Yx_V%G>L*rR)fCk+LH&fxXgIEo z3SWq+_lJ>bN0fRK9zW3?YI|bf3MQfEP8+<>!A=5!=iW*yWh4kTKf&rEiP z;*XaaB0ZmOtL=8AT7WA~Ng7WO9f-i9q^O9x>V#Ev76kSbxK+^6<`Dtv-)s1U%5dak zm+ENpXkqv+DD*Eh1)EC>n}Wv~$>?+?*(Mv`OcF2@zt;Qjmira1p&CETg;2l8Qp9F3 zxuvJS1|!R*gBvZ_ZMUFlXNjFE4j5@AOekyBI9O%Fl_M~CKK1wo&gx<(_*vYwB9?4gHDSQ(Lt z1SiS!jht(z;?44nJ)z9sT*dIYIAdva|?9ms2@BW7$n4V~(psvE{w5P0| zv|U@DQMh>~9ov$tuwFtt^|}8%Gt05C0Edc&r%GarZLZ+EK9!Ka?nwFiDL3e`G-N=a zBW}psdP~nnmNrftcqY~UFkv%k1Ve%$r}B^_7YJ0DV}bL>C4? z{X?qD!5LWX#h+>TdDjW5 z8O;=obvAwuAs<;y-pldbpaT-$e({^9K z8x{$?$9RZ18AyTe2^})YBo%1@OL8Crr>i90`vQnMT*wjaL%-ghvm|JtWv}-^8tp3B zY2y$3y0QbCR{dKGKvqF-dczSy-H18x{zFEG?<3>y45R@@glT(2>Rw#;;7skY5bzz>0MEzVH9HIjN6+z%&AWjblMdk0LY2 zwgVhNOdNv~a1`)qUR-sqP)wfwz#tL1q+&C0>Hqr*phkfsr9SSN{XKmK4s<$TXUPyn zWTB}+JqLONv;efr&Gqa_+ZGGz!QZx-fm0-7*yuhn4&`r(U*y)`EOW&A9_93V#~8s3 z7mge4gq}IdBTx1(26xO6i!ZA7k1#i(f1%H_wIB{qk}htnj;?5TULa&J+I0-?iRss^ zkCO#mSu*BYp(ppx8dm<3A1`HP3=9uo%;qKc0-46DEzYmcl)qsS{#@2OO$VxCWF2ji!~zKa!9BW!py(xO-d?t zge+NbokaFSRQ~8xy!o6XZh*kF=CV z+#baL;nVI7fwNi41k6J4!~^B&N|fT@-{%Ef9(SK4VNLL8Wy5C!-gwwt{hX-Dd2T{y z_4Cmie0L~q@gG_54Q(EsBC~JbJa&!)xH<`fq%?%_aX~94AG~C8Gt3cGn%H5}uwXU2b29C_#ti3rc#M^qk7`Kb? z#aNm$>7VQAQ)3U16UgD!m2B*i}XGVD}@!2wg{eT?hpwD)ER4RKFgZ(h79EcO~ zJq9fM!I9U|bP|S^eNyTu(4$M*?`%0Sxxclkm(D9I{vJ_3;=8|t)OQvM(4_2Eyhwws z?o+8f!CtPaOO6kR8|tvDqRmR3z0vl2(m0R^q)LercVVd&U~PP9qAFjy1^e#iEchDm z0Co+$b5JwJ2Zbb7WaG1$2J?j@y#pE82Zv=fnUxyxApEna;K|n7l-=SPx`&Z!q+m4d zIoZ>IfWY@SACCVutD*MFvdlF<%_O2vr9-|0jIepm?xr6E`sLu7j`+@;K@a<#qKi93 zG+0$_LKhSUdL;sCb5(E`^@1QlO(jj?YaeV4;Q%FDVMwZ}a?NJ5nA9nKHk&JXye^#l zoYt`4jtX4{bL{akA5(P*Mk1jh#XtshMcS%V6y#5%sPi%l)wR0wbFfg6Cm$nUK?RJP zyM(lR4jCz*|KLNM3_ukzn|tbbdA}+>^>mPZI%U^<_N~xKquDY#5GzRbU#s*VQ;|0< zPY{~(?G@1pmV`1tYT0fP|t4uHP?=ZYqiq&A%R|67XxSe$>>DjBu$`+wWE z|F@yoQOozy`oG$tkA4eUicgSs~D)r#1xj zu{#Q602P}-&9c4cau5cYLC{CQ5q}LSaN_pF|FBpf8z_0O3Hk^)D2@eW2LE&7KlbyV z7CC{oWKn;b9;w@z6F)pWY)6h4>bXIEvUF2)_A!bh&rbehC9^^eLc9&EW>I-cLchXO zDj5r%h0v-`t(zPAT@DL8QYwpb_yg4aWC>qhUXU_@OPlKhH&f`_j$Lh~Pg2tb2?oH< z7GE<>zIan)ybtbWDalF*RIpfz9_F7$HDT)SJZvZe)XI!@Flv(VsS9i$gaHtK7v(-Mo&uGy@3`(MokY4|A8_} zm^^=OdP;!w*9Wt>iVR2;A{zq&pD%ir_-YI%S-wqD*%&oCz=4PfLqTs3>Pqx?E zQ)VVr4~I31J&O=OC|Fcl5_@>_q~n*DB~6Gr^#9)ZAXk>ft|RJTPpQe?pvamXpJ0l1 z;Qj)PEe<>AqH$5;Du%R(R}i%Vt^NafT9<`t@>eclkSMxy!cW&UCH$-tXDAx-l9Pd$ zIeyx(J_i^hsbDBXY}A>n89FW%;i@DfvC(5mm^iZzld&VY&tuTm zufppmqUNynAYUhi1x!0`6-eRF73xCWvFQ#rk&||Y!S$A=N>{pYZqC;+UmLAMdOt#KVs8> zPh2_RBU(Mgq3@ukDac*~LgVP*&EtBTkV_}9Uu7UDYJCMvpw!hPXB5$`8ASP@kGA$& z>gcge1ELl!HVH2=nB<-rwyYwTL(WmA^x)VK5vgUCG|ATbR+(0X$EFpGfRe~}1>g-_ zaatOH9~VkKU=WOf^w>2?ISoqTG5w(NN7@&x2~9fLSq<$I76jdHCy${PS3}ee9MRX$ zGukTKoh?C#TszAi&Is{p{?r45h5MHABN-FIBybxKq14T8%mLY)M$oXBDhke~_eO0R z>5#>E?5OS8@q8Tbd99mBG8b)W%Q^fbJkL!nf-C&8GMNB;V`Y5JqZCJW00}B7i)@}7 z1XYZHpO@Qs(&q1Zicg$>LF`=%R$?xWf5cbeb#&*zP!oCy)Cl>2|3_Q9cL)*AH+D!} zYf4n+8}kZse0ONL*24`>&IM8Suim)3nF;RF(1J>spM0vL@eLek($KIOy0W7$v`HMO z25YH<#}OY$v;mR()aRoUQEE@Cm}`9_BO}g-ifi_Na1*zAsjQJncc?=1UTsyrF|+tB zR}Vo&0lQAGl6HFSny!?lA^JOTBH1mO;e9%roSM?~f(LCjUginyD;?=36I0>4W`+>2 z+Fl^E&Qtp98z0YEd<3a?>@QdANrl8Uc>?TdV2$6Q?J9gSWeSw2pKd13T(A#?1?!Y9 zDN19hvL1S(-;q*)!obS#p9Aa;KbyF`R7_qZ1u%7a_!W8%!bO;{d27iT1u~CE{hl7} z;0PDv<-A^h=XOgs&EmPO5_Ek=qTj*UKL1EAIaNFW!X)WnLci>ZCb3|koAKS6TNYRw z897XmFF2XNvl35FAjWaN4%MZM^|?Z>0*dwX*ibS_kspA4JjgpVc)0ec(7LK01vVma z@E{*0w3Ylle`lg-Yjy1z5i)&ihER>{WZ^Pv41c2lNks!c-EMLtC^#|w{zH zJ)kAH;@}-odqqZp48(A$S5dwe&@E~Zm<((&;z4ZSm^RkSily_Ap_l$ZOP!+p<)H2) zZiJcw_XjVHO(prgguicbzA`_7!EIRQYrxM?*Yh!yaQpw$-d6`z^>%HmAQB=eA<`|< zBHazrDj+G1bT>$McT1~CqjYzdNJw{g$2s4<_4mv(?>q1J{Pq6x&3t?2j1K4Qz3*IW z-D_RzT4?l1uZ0ddidKe#56%e5fYJA*4@pd#XPi$(^pJ!9b=>J_?Uz(Fr<<55|@hN^-Y|z~O_MjJ?0LZtF|>_rNQE)zJezW=3ka>dP{u zXV+WmwgWQRykt>0`A37UmW@=%?ZI|ognyELTn6YvmI~UgZ#-2*_^{g9vp0R4_*COw)DF34e;iIvt;;mxT^bf|$ zDh|v|%kO9+q|$%=MH?FkSO55n-rt!{z*Ba%K(qOXPh>KuCS^ME+t?5&HqmIHJ<~&c zu+|0$k~9Rla?)ami7Tz#e%pG3)4Mx0--RCwXG!QtsqQBPay4a{_X=TqRhj1$NcC&@=itu1QVq;^rQz5l58Q^|P-u`BmCLtv~Vfi_5ilPL%f+g=e@qM$Py}vill5 za1B21KxrpWvU9d+0+#4P(mA45DiE)Yj7 z_@8pah4t+4`CZ75Ff4<*Hu)>r2Q$Q&S{9mZy$>l9(88=pHJJ`NcoJqNJL*rB@N@`1 z7CH2%&0P)lJvc^@|7`GP;u(e2HPNSaw<)SCY$}3>d_T^Icb`IMpR13MGNOG$Qo+nj z>$uOhl^KkHIsfz3DPv)e#%t-37G^ka_KRY|nWp^UA1KsuYxn7rN%D__Y4oJYNb=qf zEM&)fG@Puh;Pj&1vB?(vkk1}X7OhLBEPsZ*kH8eh!b^~M7@Q$Gq3(HVn{Csbsx9WJ zfy2lzR35n2mEE5H=OjfM1CE_MJ(}N+Q-ePIaY*H+= zr+Lz&Jqgj#-a21$bN-yk1)|z(C9+l4mA>nec(1MHBnZp#sYn`1-@Tu850$wn|5AM8 zVjVT1Xfu29NH2kpgw|2#eg~&SBv)fpE9kv^y7Z~OZ<{x0RdsGkt;Vxt4^w*oMdhy; zk1nHud*KCSP@mjwH+88@AHBr7hXa&WV{m}+11DbjFIoru@e>swkxp6us&B%sRj@oIaw)NTzJcAWxD zstdf;c(lvG@)G)EEgmPoo8}FGm7<`l@V+ydBzQj;b|*@*V+NBke8jtkPuraQNhHgD z3>hZbP55}br{?LNSt1`k^|b0>D>s59B8@5=^NdWTg|$&s1!aFKE9NUi5&spd{roNz z8S1>L-gpK=e%BWeO;O}DYaU@5w!gjUy#T%M*9_X|%YP0Q9@4XRv`g&zOhF2hQ zcoeUe#44ZTZo6CXI${sMv$fI3X19Sh%zbK`l|#gy0dHq_-Hyli?v`Vuqp=u7*`rop zZ52w>#LjqL@2;$(Bj$lT=s&DSJq9s=)Q4x_vCO71bcl&i2@8bxr^o$#?(kp|ngLtA1yx`4pSu z?sNxuXm1j$K9qM25&QKeFzN%Mp-mNKjnR26+Jyy2{e8NtS00)`+OQj; zcZC1_DV!~)hfY?cOZzR$nKiZlvK@Wnrl85qJEq5xwOo11CMuM2v#W6mytevLN8InT zBRO!IO4FoRWZLxkg?x&ypV2p1a@deXC6kq1TX>PsCaPrMf1 zye~Z(Xa|UsSZ!CN?f@4%dUJO)WzA-O>Zp2xeJ=|*!QfCS;$=B&5EcD?WVxB;;E|@w zCu26WXfbliD#J{_n&b6dX1FZhe>h#Ma{jU=(*5P;F3*$HjhfuF(W(Jb?E2iagIc5% zM_i@jX140OxSJcy3a_Vns|{~fuqc!>B*$7sp6&7K@pSc5d?3;8%&4Qlm1oSo*j5T@ z)R7#olivd8q5v?Q*0-k_N>HKTB;Sb@`aHiWLBg(9S+I`C__PM)bHrySZns*Eo= zTSVYIb$3O@v(NWi(r;0R5w;^zGs7tZDZaPTuyK?vJ-O!(MRwfSlZgwdd2)ikYNTw+ z+({;1o1FqKoUU`ZgMXqOk^2o)>1t32;^yLTXSOa;DMzBr|$ux=>J;uJ(FQ^}R zq_)^WSFJsOj#!2A^#Jl%S)+%w27?6-g%}N$NzI{53eC4q-wR_0(H(G$zUWOYBg8J< zH_3At(tD?m{+v(m!gVO;&Fi92MY!T)okkaq?}>=PGgG}P^L*Jki?a!sJMJ+zyD+BHyVAqT<+dL%b65fR#z*%&HQ6KK_5)HPl6t{X+ob-tOV-V{s0 z%c?9%*sx>uQ4sA(Cr?t8kv8HODPp|`AD7!a6&FHlIMfs|CiVPa?atJou`Gni=DR|Y z%trNFy{wXWusHkdmS)2%k{Z#MU8cwDvu|+*!7grv9@2}sQJJAo{>rt)@(U1fkW5(| zeEdAMEJ=+;WIWyB=b$ZS1C{B<=I-w5xT?$BaRnVMKA!w)3pV@GyPeK*6e zbKK1?DT(BqbM^&n8?pRuXQhC6?rd+i52R^I(Zo{4NiAa)xWaA8JdHF{mwz2xHVvvI1VN7ajUzflcYiBr7 z7sy%TUQe^*0#!N*c^%DDK&YV&a!=sUE6U_jNsQIv-Sc{uZ;Kj5NW<2UdgZ`K^)gXe z3Ga^2$^*B}*}JcvI)89?W3Ji%(XV495Px1mbF^WOH?J(eJsa(m`HC$`_7k(?4mBdJ z1fBHH(b|`Zdqc0WYB57#KZWc)IDSai?=_FiPU9}nHUT5*VaRxnd+BgF# zLOx~sU2Kj!YJe(_3-GByx?^f;3h~}uiSQ@lKv1Zws|Vim4+{$uoe(781FwOYWX^Vf zcR>S8gxZCKkwg_3GNS486(JZednjELxir-9z3IsT1m28f9Xf-pX4-w`c5W6 zk-Z@OG5cJeU(agX`s?8fF^uRMm%s!}O*Ooa{VaK7`akDLDeY&Ub^DDDvu%+`E4s>J zwQHyyeK(RJb&c}Jxwqoqwn9=tA{`?iAx6;xSdS(vO>qFREL5wN4}NoOK3AU{j6QB2rG<$)u+Gz)jjXXjL!4Z(~;TP zfj;J@{!G{jt+wkXwdeKk`6CdVT2V}bhZr2lz;ma>o>HnJaGQ=cUgFwiRAw;{bWpKN9y;uDpWd@v+%{Ty_nAQ_Ogo|Av}0=(2B-a8P~aeJXWBv4v_;&7i$;rCRPEF$--2YB+z&s6Yv#cfy{(oW`5IY%792n z{6xqF&TEEPJ+AD*BuOYKjVOuY*;m3~o;*A}7rRzoeNYA}NG}~Ag&jN)05U`ckN`lb zalO60qgwd^+)%&>xHty}(|P}-$%CIj(YcxhAG9Db3Mf$(qO~MWQjzKc-ap@^CB2Z) z(3-8Ug?F5RT2*iIY!n9MZ-B9k-s`%`OV-7O8>F!A+r8cN{Ts5eS0)At zlhAj|w1Sr?0|Nu@r-eF(fPHYrZiqk4ZLemN$!d~&`&-2YD4yOA(R$qh>35e+@N@VP zlpv<*IAz!mDsTWow6&p3!EXVXbRIVoD5Y-v=$<*Mm0{)X?c9S7R6>J>(Z04iV&yIhB?7(f=aW}o<(9K@Z$iD(q% z6q>7ZFZemc18Tt5130NJJD5FNvc*Fo0XnKRXUd3)Yz=~#$NO;D{taj}RkRd~VJ~iR z&DxU;FW9AhVgY&YKTkUbE~ikyRuLOUDwqLCEXwsdLY%aug*Q3ClHY5*wp=>o0?FPl zLBiW_t0w+gLsMkPXAKUXw#3B5?y%#`b$@*APk`1I${+;`kbTy0lM7M#IeCo6yWWE;qM_~Kr%?JOaQb1M_}>s&gPjQ149t%2OGrXI-zMNSKBv?QfknH z4VpA!pO{77{AY-idv6XSy;eZG44@ydSWLeFY&TCP(Z{kAbQ=K82T>=$Bx zo&GXkKrS`&EcFKBK~rHwBIIlb;YZh{U|JIs6DcP`nJkdKo8i0^485oI?|Wo!4*~^O zfT%72biuQEboaNmwyHs9^(A1jO4?KY_oK_p*+53N&hX@9x6gevh+4!+`U=z4)m5!j z_ilK2I7rKH`UHp*W~ohs{^_HwxVZSumTJ>db|@1CKnYlR9;em=1+mE_+qDQ%!WRHv zBn`9-?MMFnF8lLmI%ul@*&(enj=wGbNLP^7Z?oK~q@^{mHjp}|l@ReBkiWcERb{uF z;er?xK_>E$Mb&J=Uik||;3WVHmM04moSp~1q@^~?5^U`3^b8EkvN~7T5PJ+zL$XdT z1>XTg<*=JF@*t1dBN%iK{A#M&hEnjN1*3W+J9JblBY^JjVAFPhM3##|r^YshlKNI? zF#w7o6`&bA0GWx)MpmFRVAUAYS~6XPtQ0_bqqjFx6ZV871d@l)Mujvlpm3Rg4-%!p zc|}yys1^-c2KKImta^xm8AZ3n=RRcCj2EgN1BsRf@tC49ReuDzx&KHfa3I=ba_SE4 zT84xjw7UTp-w{~qHk)~gY)*TcB<=^oK%FP=67$ z0Zg!3wKX|lj2-gWvx8_AOvmz-UcVNTkdR0z6mU2_KOY+zX#tz6?H^$&_Z)8-G>Q+b zes{1t98Qtbt^Wp|`^u3xEKz3vCu;t~FloEl(L|t^&TA7l3kj zQ6=*V7wl<3S7SFKj?b#szRAvZ1I)5|pb8*e3YeGB?Z}=nB`>I z7kQP&^=CZ6O34J6|K&8NIhQaJexC4>6!I6;0`_W zsdE1!jVeeH3taXnfa>iAur(`KfY(K!mu_zyJ>orY`^1v5w7=~kr^G~tb}CRkjz7aBD~hdt#!`b_>5guQm=n=+wXZg31b z1RscU89Nx!?QCapfOCEY?6Xh6G}fwfgqVoO&`N`(E;t-%yX|s-O*~m;MGA<2 zNpNUh>9+czXj-@22jmOvfC>hT9~~VX8T(j?9S_GPtqP8Nro)j+1c6A zR$JO2u8ZT);qu%e&H9&_mu=%22(E#@W zL>vH&fu}s@Abp7dYzIKJ4h}@NorK0vk0JZh(eWkVK40r+AEKhAU03{s^$@9NV1ODI z6&ke8NYWu-k+;>XXUJMw?)5nT&c*pF+(u2wIXM$&W;7v!w>{^Cb2VUuD#G7A9yZ)8~*k77Why&~*Oa zAN;~Ttl5fnlztF`-+)LQ@q9X;H9UNMR3k9g$q^2Opr>cW$NEn+g^uDjP!bK`6EI=`E5n$Jig zNE`U4T8^?GmNOUS&^oAIFZ^O67i91SCrjYKjU{?!boQMgnB>{lz*^ag9)g)newxIa z?5Ur8B(lciJm~)@ukUZt(ALuq2Y1&ad}`KnPn4%{Y&Tu&H#dadtKi5TVz$I-Z?^Sh z*s>SZotGM)lhjU3C?!vct%fm}PT3p;-8m-NG923D-b@5`Ohl8)Rd%zl=SD?e;{Yy4`MqY7L-4Un8FP}rDOW?31Bc~3f_(-xJMp0L^k_j z+eNVQ-h~7na}JTg8}-tlyu6X`w&{uuN1Y@CA{$_si6@<=2{7zr?7EU4~hv zU_9cK`Y}@aGSek-C5C`6cU^M)_4jz?`4MSjRR)e0*RPh5kM<(5XO*PehBC4pcG>Qo z|3bGX;;Zo43iSE;!y?N=i}BKoXoE~>!DHy$^N21}LuP05m;S?Tso_%-9AXvV>2W`) zlHk$)M}=v5aj$J;Vw_;i%xxz{QVDHN^lp08ClfS}$%zeRN_J#1Z=t-Kkf6Vr774j` zN4R%gQtF#k4g-ooKr(+p(4FQfauM!X`>rfwQKGUK)ZXKdLP=lyjB)z;B94gz7eLh7 zb@}AcuL)ZRftqEz9^zQp#c(aLVhyPc{_-dEhBG@P15kkg9eri+xiVUUUkbecq_vy^z z?jfS5A%~vpZeE8Ome|V}8O%bLx3d6)ndfv)a#lIsti}Gtb*2o5$K{{brd)Zmb{#4l z>a#zHkc;o9c-C~Ukkstcd##`lbxC;yLb7x7pe{EItF3SxjJ3vfgJ|zGbJy#oz*b)| zry<=3t%1O#VSPekLCu$Q62!6+w5`n*`BnB_Ws!woafS33UC8@OwfQsd?v9(rwa8I( z^RCqOqo)lghNyn)LjjTrIOwX1o@dc+_u{m%vyQvM0YGT!h2Rrv7D|x|ezs^uC9F#e zz|JMaCeap|O%+25bH5Xu@o1~0=+lq7nCZe+aD+8OJ2k6CkwYhjhb9V8NN)YH{v0fJ zd{HFB#GTC!VBL!-P5ZL94z$d<6jqOS?fT|N)T63CMO=lydHwit%pIRG{4r};6ikov zV~?zqY3^e+w(n63xfIXV5^$z(z+e{erzWWI7*XHx)(u)xfAiv-y2OfNG)hWIH}y3t z8P2^Hmgo}BgM~Lp=sYfL09C9qWdL*}`M;2f?)%hvvJw(VUB{0pmzndQ&ZDenAXK?8 z9SoK15)shY>=6c5&B7iy-+W)@(Snn563;s*RKmaJ9|UNoOk1u^HXs+|FW>GLj-~#E z-K5uMH$-vieHPQPU2q(pL%_T}?kU;!oqjvCGi<(5Thj(lt8{1ap%N7}`G-o=62eYpD{tTl9`0swSSTcGA-RQ3{-d9X9N21f%>8g~U7 zP<;XiuDrf~OfGq$F=@XC-jQUB%iW8uii532dsXU90w^>mlMLM0EWqZ4j*8H20%v8B zcddb&DN2e;t=67izcWnQE*?C?Y+d8vCUtmTaCnMrGY&#H(dR*IHURQD3H*kDr48^<=$CPLL-d8=AtACcseEr$fRY$J#PQ~HSyGVRD65~c#svi7l0%o^Tb}{ zX<8vU01yFR3c&OS6#}rBd=M1vFd4}fjReZd-nDyUf8Xh2ELD2zUx;cPo~KxJKt4!4 zfWwJ9Z}tn9p{O3cp@{tV-BsLd|hR%dlHUk4BL_}*LLHUdjm2OOg{t9K<@i*g6yG< z%>7HJE~+u($C7864li$w!}QE$DY=_&2r1_#(G5yVblG@|FY&I2o=E_c{Rfbn81e&2{E^IFWJuaNu56|$#s4+&9z$N`*Z&E zAY-zkmu^#ocfVpg+tqL1n~0niI66%?u)0B~bUekr*q^uV?9=GsvUVN}2t&9v*maMG z?L6)7=9)5uMhF@5PcMFL$_z7zfkh`j?Iv_{KJI=sM|X>Dt2BQ}j2+A)Lt@H?|ln;Uh-uMZI!ZHR09u*gvBIp`Y7Ef}*Lq(%WDQQzyoT zWoCmDn_Bk#zZ2&=&bHyf2>M-zZ|Pp`!*3p3F{LZ4ovCg#d`nSnav|j{ghgN66uKOj zz*3?cO3LQ&m!jrt zQOTpWOc;X8^=S<2)_T0{DvpgBWVf|U2srogHzkGeW}l=5y~EaeA^1z!2hoBvp6&QI zF_nC~0~m$~Utnv#NHZ`f^)ioY$h>*zmj7zK2#v`AUhNL1lwJFRzy5n7RtY@0!t;tb z?5fR66n>WsK_Vje-0Qi~P=RcZ%NCrH)$qx+-O&4af0dw2Uhn2kbi|B(vh7s@LaPRc zn^OY`wX#EcJO#unJu$D!v@LEdy~yv%*Z$2Yt~jGPz!Lh_|0Ul+9`l#TSzM~=l%nj& z!hGeST=?@0jh^xztrz_m`DaV1PdVAYRq&WP>jSFz`bcY z*-gd`<8zE>HkkMZze_2BZT{7GX9(v1O;s<<{dULz1WJ-E;PMMKk7AaOCW&ASXCB#6 zVIFQM0%u+X(aty$5i4P8_{QzE7n8p8G{ct~XSiP86HTu7-2`r39wAD2RUg>`jG#+Y zK9Sv*x`qq_puUeslW_r{lI*q6X0~fB_8pt)MlE5R>W{4eE|{Pty^N*xaMr9aS?jfw zr{`T@S=v@hW(Ovu8mT;GBJ&!3^>yOm83vVpPDs~6lLq8QY%l%eEL+I&BC?(Q1h%4wHK33FZOWmT4*pQQQEZv$TI8GQ`o~7_x zi1U9v__26h?+(?=aXUkG^81S-s0FH}?z=6j(Vw;#8R+>hrYU7j=3Lvi>uf#+_(*rW zhS?DvI7dZb`~ z7tD~_JG==RDv6U_ibh?Olk-@x*UxS{s9YIJ^sgP9%9p1#Hm{eSN;JZZJ7Fh*H!dT} zjvkj@Y-hoeT+0exx;sPg@JN1l{A7OS5zgbn9dvvIQcvoT;WXP;VkMp{vO_WS(22z1 zQX4dl+LWY*nv|5TrP3nrlt9}r7QaQ-*BHMw@-%OKCq|%^a>NQ3|GdAE>vpSGT@{f) z>`Uv#Dr2m~zpJIOQOzE?md9Ws`^cW?zgyigpF(T5Rhu1@e%M=N-6oKoauz=!codzC z&MdK+G1i)p(BYS%C44F>{NeYH;kjQzxpncyzHKxa7TNx`&*pM`$;!TD1UO9LDxis$ z`^G0S7o3NTLx9*@Z@srV8gjCv!(&3H;jGUxCqZ-N-^vA-UkrK=lfgCso`Wds3rM?~ z{?Oz3Ib>SySk1whyQMre7-mi=o{X?>V`70;L<5aKIHX|n}lUb5Iz3PqEe#Zx z%EmEnjx0bh$9Sv+@AxqDvU!cBZTHicGL$9Y(?3_PFAICZTn%$X7jJSr-?y@fK6ZX2 zeJ6G{w}WHE{yp2rXt_O6={r=jO&`O4eK>o0_2uGQm}%tmJwXg4ZnTQSL{()RDg6%o zQ(VTa`L6D6gLiiPa>HJBS&Xr153am^X{t+mT-~#cVIpaBS^N=%F#fY(fM!>NG<1K# z%z&4etCPn%?8maRF2dRR%fqePmt5~;r0rh1glgWIG-5PrK{t$hdr#UW?q_1@3e6p& zWJ1#mqSS-sf~7H3O^nqTBLr+aHI?s)<5fCK>=_$01LcWZ%YwIN*jQLtmtTJ72#Xm^ zBA*KOCw51P;*+drYF?c4jrqq@BWbf?v86KcTP=RT&oU`4=+RqX`MAcW?FKWH*>cyt zyMIU%rgOK`9Th&-wl#sKc98l+YDoW7qcB;JusLA)_b&1CX8+{-!O{r6Ugk)SsSm6I zNmKD3w%OQZjvxh0_>u~85TVgsXo-7OW18gCMA;lB_;Mk1T`s%S3z!F>gLFq5n5jy| z1Ig@`s=jgs9f9GTmg`NGe$P3weQ77SfHub5FS(TgI{i-rMuCxGZv_!k=sfd2whnC_ zs(<|DdQ<$gSs`{J63glE`H$+plbTX4nC zNW3t*d7O1z@yInjb|X10WNJdX%IulT`<$HKGV?o!Yw3Hc2&n@n5t-dJZlw8E=GbRj=?E3no*`S|sh@kgX~xzHPC5$c*xo9>Qx_I@ zxo<{d{m9(7zh`+I{qa-Fuf|#u9I_eAOAjUr4_ps5o%ha{j(J%g5=Dp|xbovy9XlUB zc@$QHrhes)KDD2yq@*p}5!jdPk&HZtW!yEt*hC;w`QkhfN42@y>waIlIPSM1ZnzJQ zz8;e}M;F&yC)mhOu@MTQIgDfVqmCEax8LCG^lq%j?jPT`y`Mbzq3uoz>wL0RqKAd? zXHbNNeS-kU>DDfFV=nUcL?AaWoSv^{Xf*wby|w^E?eQZI-;*ch91QfgPg~=51zN>k zINn7uQ)T@iN$-(^J`G=!?!19HEWx)0O@`E8?VmCY&Nh5xR}fGjqff3L;>5Aby()M- zsid&_yJn@)kP_aZ8M8rp!>h&gu-VIic)OS1*~9aO7p>aN=DnA`RH*Q@ob)8ZjkR6d zFSZcDT~Ze}!QW!5?GM>v%?;*a2Nng*KV7-!^ekcYmEQ8DzQ}x_kcOiggMOfVCd?Xm z>v@yNrKDU-WV}ivlj<$R-a*uNkdZ09_{w&{^wd+qgXu`SKX3DN+}s4ovezg>sWSi# zom!*M`EoOn{@AB$+rNxS)8=DUN^AJ|!ufKG#2J6`QzpPjDG3+2JZJt?I*ox>>Ttmv z@Z6gwr5dq#R8OReb!5!O*#2IPVV?$I>qgr3<%em3^kU=*OTX} zx@XtL^|?ZXm!^(8j=X0?Z0bi}e5o~L)LFZbCc7^K-7j{vUdav_v}lx`o&L_vHO4kA?-rsO?bBY1vtN6Gr~524X)kzap5*+s@V8!w4ltg2Rr!EBxRQ{(9Aa}6F_Po|6J-!1m< z>#=yI>o{%BM#|iJI)iH4=n{S(t{GhRIm&_1a3!DHrZ#c3(i#1}V3qI{UwJBOcUrW9 z`W=&R`7|uyl|8DkH^D(cL4Kk1xTspYvp*;(Y|)cn(G;&JoC5zm*8v}{VHWfIr?6CG zo4vU}ln6&%Q;Te;nbVVSLdl;s6=LbfLM)R_9+mOh#%xAS7=6Jd!93epZoLn8t{t&- zuE-NqXs;3*`SwA)U=hSy(BY}XU+=}Q)Fm!l=rS`HiPsW*_PS=;<0(yRYwBBM(|U^3 zz|gQKZg4QNzkiUwuUhPvA{-L*P&B{sPQn;%Sy=kVF9oUFFH0!%_b{_IOzxpbJCk0z zWpA4WF)-&1u}1tVP+gcjc=@9Zj^M5R%3FKTP*7?=mtGBC#WZSVd{AK!1rLkesghAY z(jF9w?G8FnRldlGtj7qj>?Wh2pb$;9Ovl6`?DVlY)Mb0mt0sOD?E0;%skO1h=R!d+ zo7V7&w;1jEUb4bY265Ok<__0yNM>zH*{Mw}p3WEzgVMUSE7Lef#L^1I?`xbm?rTs? zrH?4A%HDr1sN8+DiS=AmOrjE0L{iewAlhVGdGfia2x2IXW)cm!GhhNOegL<#Se zher}G4^!g|zV6TBBE&E;AU*WB$Ab=g>cqZ#nY{^6L{_<0FBQXKOJ??jr(q!{RH?#Hp(FTmBraxv z?7HZW52{u&fsz#~YikmD$|l$NrLu1XQ$;^CD8(B-cZ9Rj->2On@HOh9~`-W=+hX{B08(E+`OH4k!xJn6c0i}K|xtr zUsq8r$cufDQ(Mb(bbKr=CG{yU&wqOQ-P@vfk2lK=kVlJfz(}@(z%iwkPf$=dbzVM5 zap2uAb-%O)K}Sj8Y68_ss6gPVi^T0=;rNqHY{_q+QlP-q24zS{$Yhh3paS0^A_$m4 z0mx`3xd0Xwx?Cn53JTxk9s?1tuZFoJ4$N%z_kD#0U>EuVmn%2`q2dDS7DHu{l0ZF+ z<*vv`dzZF3fJ&gFr+*Ko9fB`i9`=%jjvSDx5;uCUD zPftTX-~Lqu(d@6R7=ThMHK4v{Pj7E4OPEw5*NRJnc5h$bf|Dz|R?4T%7f z5DBm((NR&Do7jd)O2cFnY~7~K)RfW1Zqd<&_4R4QypD?6+JnVfb=EbS5Txbg#D1+m z+4uKurvaOIZND%BE-DoJ&)|3a{Pn9Ipb-akWTDKZXy8vdgU=(V9$<|5_NwSp} z3o+4B6AeK(Y5>0o5R2#!TZx`L0eFv3Wo6Nwof7gHe8GStnw~xwiZKI#A2v3&l$2ES zqO~po(&6D@dsi0@K7Q9{rJSJf@D9r={feoe;9zndo|KIZlkDv5ppcL&uPSQ*th&0o z+SuOC0rhu5IZpsZI=sHUa2Ehw*x1?0&B^fv#diQr^`UEnGv7~oFMtKO=O8U6{F(%; z8xu3L9Iy)jl51>c1{7VwfWpiGB?m?2!9oI&?gy=)wl?l_JVpvS-&h|ctY~3*1SY~@ z``OU$?+b^{Tn<~fpsG+lc*u_*F9Ar(w`;j&`}7gw9RLS{>b#AQ`wy*ya(PghYcNls zXe59x`m~U>=vpsK8dVThOoesmwdj@Uet#DXx<^BUR5vScZvGr_Y)~*US&|L2JVXFR za{;;>436&3WLX#yX9TFdwrtHsaUmV7N&H_n0}y1j67p5Jzz&73nl(t!|GA_l>AE8>52ztSv3;Sh)6h637upe6eh|u@Z4B6m* zgD5nAk!Yqr4_XP?{DtyA-3y7YKNpqx|8ArH-*|8;9ATmLJ8LqzGv^&}NsG&i6^rQl F{2zuSFf{-G literal 114219 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 is fetch both response that is in STDOUT and STDERR for more info read this: https://askubuntu.com/questions/812952/cant-capture-output-into-variable-in-bash - -response=$(helm uninstall xkops 2>&1) -# possible responses: -# release "xkops" uninstalled -# Error: uninstall: Release not loaded: xkops: release: not found - -if [[ $response == *"uninstalled"* ]]; then - echo "Successfully removed XkOps from your cluster." -elif [[ $response == *'release: not found'* ]]; then - echo "XkOps has already been removed." -else - echo "$response" - echo "Failed to remove XkOps from your cluster. Exiting..." - exit 1 -fi - -response=$(helm repo remove secrets-store-csi-driver 2>&1) -# possible responses: -# secrets-store-csi-driver" has been removed from your repositories -# Error: no repo named "secrets-store-csi-driver" found - -if [[ $response == *"has been removed from your repositories"* ]]; then - echo "Successfully removed secrets-store-csi-driver Helm repository." -elif [[ $response == *'Error: no repo named'* ]]; then - echo "secrets-store-csi-driver Helm repository has already been removed." -else - echo "$response" - echo "Failed to remove secrets-store-csi-driver Helm repository. Exiting..." - exit 1 -fi - -response=$(helm repo remove aws-secrets-manager 2>&1) -# possible responses: -# aws-secrets-manager" has been removed from your repositories -# Error: no repo named "aws-secrets-manager" found - -if [[ $response == *"has been removed from your repositories"* ]]; then - echo "Successfully removed aws-secrets-manager Helm repository." -elif [[ $response == *'Error: no repo named'* ]]; then - echo "aws-secrets-manager Helm repository has already been removed." -else - echo "$response" - echo "Failed to remove aws-secrets-manager Helm repository. Exiting..." - exit 1 -fi - -response=$(helm uninstall -n kube-system csi-secrets-store 2>&1) -# possible responses: -# release "csi-secrets-store" uninstalled -# Error: uninstall: Release not loaded: csi-secrets-store: release: not found - -if [[ $response == *"uninstalled"* ]]; then - echo "Successfully removed csi-secrets-store Helm chart." -elif [[ $response == *'release: not found'* ]]; then - echo "csi-secrets-store Helm chart has already been removed." -else - echo "$response" - echo "Failed to remove csi-secrets-store Helm chart. Exiting..." - exit 1 -fi - -response=$(helm uninstall -n kube-system secrets-provider-aws 2>&1) -# possible responses: -# release "secrets-provider-aws" uninstalled -# Error: uninstall: Release not loaded: secrets-provider-aws: release: not found - -if [[ $response == *"uninstalled"* ]]; then - echo "Successfully removed secrets-provider-aws Helm chart." -elif [[ $response == *'release: not found'* ]]; then - echo "secrets-provider-aws Helm chart has already been removed." -else - echo "$response" - echo "Failed to remove secrets-provider-aws Helm chart. Exiting..." - exit 1 -fi - -response=$(kubectl delete sa/xkops-secret-sa --namespace xkops 2>&1) -# possible responses: -# serviceaccount "xkops-secret-sa" deleted -# Error from server (NotFound): serviceaccounts "xkops-secret-sa" not found - -if [[ $response == *"deleted"* ]]; then - echo "Successfully deleted xkops-secret-sa service account from the cluster." -elif [[ $response == *"Error from server (NotFound)"* ]]; then - echo "xkops-secret-sa service account has already been removed from the cluster." -else - echo "$response" - echo "Failed to remove xkops-secret-sa service account from the cluster. Exiting..." - exit 1 -fi - -response=$(kubectl delete deployment xkops-deployment -n xkops 2>&1) -# possible responses: -# deployment.apps "xkops-deployment" deleted -# Error from server (NotFound): deployments.apps "xkops-deployment" not found - -if [[ $response == *"deleted"* ]]; then - echo "Successfully removed xkops-deployment deployment from your cluster." -elif [[ $response == *'Error from server (NotFound): deployments.apps'* ]]; then - echo "xkops-deployment deployment has already been removed from your cluster." -else - echo "$response" - echo "Failed to remove xkops-deployment deployment from your cluster. Exiting..." - exit 1 -fi - -response=$(kubectl delete deployment xkops-dashboard -n xkops 2>&1) -# possible responses: -# deployment.apps "xkops-dashboard" deleted -# Error from server (NotFound): deployments.apps "xkops-dashboard" not found - -if [[ $response == *"deleted"* ]]; then - echo "Successfully removed xkops-dashboard deployment from your cluster." -elif [[ $response == *'Error from server (NotFound): deployments.apps'* ]]; then - echo "xkops-dashboard deployment has already been removed from your cluster." -else - echo "$response" - echo "Failed to remove xkops-dashboard deployment from your cluster. Exiting..." - exit 1 -fi - -response=$(kubectl delete pods -n xkops --all --grace-period 0 --force 2>&1) -# possible responses: -# pod "pod-name" force deleted <--- for all pods -# No resources found - -if [[ $response == *"deleted"* ]]; then - echo "Successfully removed all xkops pods from your cluster." -elif [[ $response == *"No resources found"* ]]; then - echo "All xkops pods has already been removed from your cluster." -else - echo "$response" - echo "Failed to remove all xkops pods from your cluster. Exiting..." - exit 1 -fi - -response=$(kubectl delete namespace xkops 2>&1) -# possible responses: -# namespace "xkops" deleted -# Error from server (NotFound): namespaces "xkops" not found - -if [[ $response == *"deleted"* ]]; then - echo "Successfully removed xkops namespace from your cluster." -elif [[ $response == *'Error from server (NotFound): namespaces'* ]]; then - echo "xkops namespace has already been removed from your cluster." -else - echo "$response" - echo "Failed to remove xkops namespace from your cluster. Exiting..." - exit 1 -fi - -response=$(kubectl delete deployment robusta-forwarder -n robusta 2>&1) -# possible responses: -# deployment.apps "robusta-forwarder" deleted -# Error from server (NotFound): deployments.apps "robusta-forwarder" not found - -if [[ $response == *"deleted"* ]]; then - echo "Successfully removed robusta-forwarder deployment from your cluster." -elif [[ $response == *'Error from server (NotFound): deployments.apps'* ]]; then - echo "robusta-forwarder deployment has already been removed from your cluster." -else - echo "$response" - echo "Failed to remove robusta-forwarder deployment from your cluster. Exiting..." - exit 1 -fi - -response=$(kubectl delete deployment robusta-runner -n robusta 2>&1) -# possible responses: -# deployment.apps "robusta-runner" deleted -# Error from server (NotFound): deployments.apps "robusta-runner" not found - -if [[ $response == *"deleted"* ]]; then - echo "Successfully removed robusta-runner deployment from your cluster." -elif [[ $response == *'Error from server (NotFound): deployments.apps'* ]]; then - echo "robusta-runner deployment has already been removed from your cluster." -else - echo "$response" - echo "Failed to remove robusta-runner deployment from your cluster. Exiting..." - exit 1 -fi - -response=$(kubectl delete namespace robusta 2>&1) -# possible responses: -# namespace "robusta" deleted -# Error from server (NotFound): namespaces "robusta" not found - -if [[ $response == *"deleted"* ]]; then - echo "Successfully removed robusta namespace from your cluster." -elif [[ $response == *'Error from server (NotFound): namespaces'* ]]; then - echo "robusta namespace has already been removed from your cluster." -else - echo "$response" - echo "Failed to remove robusta namespace from your cluster. Exiting..." - exit 1 -fi - -response=$(kubectl delete deployment kubecost-cost-analyzer -n kubecost 2>&1) -# possible responses: -# deployment.apps "kubecost-cost-analyzer" deleted -# Error from server (NotFound): deployments.apps "kubecost-cost-analyzer" not found - -if [[ $response == *"deleted"* ]]; then - echo "Successfully removed kubecost-cost-analyzer deployment from your cluster." -elif [[ $response == *'Error from server (NotFound): deployments.apps'* ]]; then - echo "kubecost-cost-analyzer deployment has already been removed from your cluster." -else - echo "$response" - echo "Failed to remove kubecost-cost-analyzer deployment from your cluster. Exiting..." - exit 1 -fi - -response=$(kubectl delete deployment kubecost-grafana -n kubecost 2>&1) -# possible responses: -# deployment.apps "kubecost-grafana" deleted -# Error from server (NotFound): deployments.apps "kubecost-grafana" not found - -if [[ $response == *"deleted"* ]]; then - echo "Successfully removed kubecost-grafana deployment from your cluster." -elif [[ $response == *'Error from server (NotFound): deployments.apps'* ]]; then - echo "kubecost-grafana deployment has already been removed from your cluster." -else - echo "$response" - echo "Failed to remove kubecost-grafana deployment from your cluster. Exiting..." - exit 1 -fi -response=$(kubectl delete deployment kubecost-kube-state-metrics -n kubecost 2>&1) -# possible responses: -# deployment.apps "kubecost-kube-state-metrics" deleted -# Error from server (NotFound): deployments.apps "kubecost-kube-state-metrics" not found - -if [[ $response == *"deleted"* ]]; then - echo "Successfully removed kubecost-kube-state-metrics deployment from your cluster." -elif [[ $response == *'Error from server (NotFound): deployments.apps'* ]]; then - echo "kubecost-kube-state-metrics deployment has already been removed from your cluster." -else - echo "$response" - echo "Failed to remove kubecost-kube-state-metrics deployment from your cluster. Exiting..." - exit 1 -fi - -response=$(kubectl delete namespace kubecost 2>&1) -# possible responses: -# namespace "kubecost" deleted -# Error from server (NotFound): namespaces "kubecost" not found - -if [[ $response == *"deleted"* ]]; then - echo "Successfully removed kubecost namespace from your cluster." -elif [[ $response == *'Error from server (NotFound): namespaces'* ]]; then - echo "kubecost namespace has already been removed from your cluster." -else - echo "$response" - echo "Failed to remove kubecost namespace from your cluster. Exiting..." - exit 1 -fi - -#https://awscli.amazonaws.com/v2/documentation/api/latest/reference/cloudformation/describe-stacks.html -response=$(aws cloudformation describe-stacks --stack-name eksctl-xkops-cluster-addon-iamserviceaccount-xkops-xkops-secret-sa 2>&1) -# possible responses: -# The output shows the stack information but we only need to check the stack name -# "StackName": "eksctl-xkops-cluster-addon-iamserviceaccount-xkops-xkops-secret-sa" -# An error occurred (ValidationError) when calling the DescribeStacks operation: Stack with id eksctl-xkops-cluster-addon-iamserviceaccount-xkops-xkops-secret-sa does not exist - -if [[ $response == *'"StackName": "eksctl-xkops-cluster-addon-iamserviceaccount-xkops-xkops-secret-sa"'* ]]; then - # Refernce : https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-cli-deleting-stack.html - aws cloudformation delete-stack --stack-name eksctl-xkops-cluster-addon-iamserviceaccount-xkops-xkops-secret-sa - echo "Successfully deleted xkops-secret-sa stack." -elif [[ $response == *"does not exist"* ]]; then - echo "xkops-secret-sa stack has already been removed." -else - echo "$response" - echo "Failed to remove xkops-secret-sa stack. Exiting..." - exit 1 -fi - -# Refernce : https://docs.aws.amazon.com/eks/latest/userguide/managing-add-ons.html -response=$(eksctl delete addon --cluster xkops-cluster --name aws-ebs-csi-driver 2>&1) -# possible responses: -# deleted addon: aws-ebs-csi-driver - -if [[ $response == *"deleted addon:"* ]]; then - echo "Successfully deleted aws-ebs-csi-driver addon." -elif [[ $response == *"does not exist"* ]]; then - echo "aws-ebs-csi-driver addon has already been removed." -else - echo "$response" - echo "Failed to remove aws-ebs-csi-driver addon. Exiting..." - exit 1 -fi - - -# https://awscli.amazonaws.com/v2/documentation/api/latest/reference/iam/list-attached-role-policies.html -response=$(aws iam list-attached-role-policies --role-name XkOps-EBS-iam-role 2>&1) -# possible responses: -# The response shows the detailes of the policy, we only need the PolicyName to confirm it exists. -# "PolicyName": "AmazonEBSCSIDriverPolicy" -# "AttachedPolicies": [] -# An error occurred (NoSuchEntity) when calling the ListAttachedRolePolicies operation: The role with name XkOps-EBS-iam-role cannot be found. - -if [[ $response == *'"PolicyName": "AmazonEBSCSIDriverPolicy"'* ]]; then - # Refernce : https://awscli.amazonaws.com/v2/documentation/api/latest/reference/iam/detach-role-policy.html - aws iam detach-role-policy --role-name XkOps-EBS-iam-role --policy-arn arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy - response2=$(aws iam list-attached-role-policies --role-name XkOps-EBS-iam-role 2>&1) - if [[ $response2 == *'"AttachedPolicies": []'* ]]; then - echo "Successfully detached the AmazonEBSCSIDriverPolicy." - fi -elif [[ $response == *'"AttachedPolicies": []'* ]]; then - echo "AmazonEBSCSIDriverPolicy has already been detached." -elif [[ $response == *"The role with name XkOps-EBS-iam-role cannot be found"* ]]; then - echo "AmazonEBSCSIDriverPolicy has already been detached." -else - echo "$response" - echo "Failed to detach the AmazonEBSCSIDriverPolicy. Exiting..." - exit 1 -fi - - -#https://awscli.amazonaws.com/v2/documentation/api/latest/reference/iam/get-role.html -response=$(aws iam get-role --role-name XkOps-EBS-iam-role 2>&1) -# possible responses: -# The response shows details of the role, we only need the RoleName to confirm it exists. -# "RoleName": "XkOps-EBS-iam-role" -# An error occurred (NoSuchEntity) when calling the GetRole operation: The role with name XkOps-EBS-iam-role cannot be found. -# An error occurred (DeleteConflict) when calling the DeleteRole operation: Cannot delete entity, must detach all policies first. - -if [[ $response == *'"RoleName": "XkOps-EBS-iam-role"'* ]]; then - # Refernce : https://awscli.amazonaws.com/v2/documentation/api/latest/reference/iam/delete-role.html - aws iam delete-role --role-name XkOps-EBS-iam-role - response2=$(aws iam get-role --role-name XkOps-EBS-iam-role 2>&1) - if [[ $response2 == *"cannot be found."* ]]; then - echo "Successfully deleted the XkOps-EBS-iam-role." - fi -elif [[ $response == *"cannot be found."* ]]; then - echo "XkOps-EBS-iam-role has already been removed." -elif [[ $response == *"must detach all policies first."* ]]; then - echo "Error in detaching the policies associated with the role. Cannot delete role without detaching associated policies." - exit 1 -else - echo "$response" - echo "Failed to remove XkOps-EBS-iam-role. Exiting..." - exit 1 -fi - - diff --git a/secret-manager/secret-manager.sh b/secret-manager/secret-manager.sh deleted file mode 100755 index ba95d6a..0000000 --- a/secret-manager/secret-manager.sh +++ /dev/null @@ -1,252 +0,0 @@ -#!/bin/bash - -# Copyright (c) 2023, Xgrid Inc, https://xgrid.co - -# 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. - -# at the end of commands you would see 2>&1 is fetch both response that is in STDOUT and STDERR for more info read this: https://askubuntu.com/questions/812952/cant-capture-output-into-variable-in-bash - -response=$(helm repo add secrets-store-csi-driver https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts 2>&1) - -# possible responses: -# Helm repo added: "secrets-store-csi-driver" has been added to your repositories -# Helm repo already added: "secrets-store-csi-driver" already exists with the same configuration, skipping - -if [[ $response == *"has been added to your repositories"* ]]; then - echo "Successfully added secrets-store-csi-driver Helm repository." -elif [[ $response == *"already exists with the same configuration, skipping"* ]]; then - echo "secrets-store-csi-driver Helm repository have already been added." -else - echo "$response" - echo "Failed to add secrets-store-csi-driver Helm repository. Exiting..." - exit 1 -fi - -response=$(helm install -n kube-system csi-secrets-store secrets-store-csi-driver/secrets-store-csi-driver --set enableSecretRotation=true --set syncSecret.enabled=true --version 1.2.4 2>&1) - -# possible responses: -# Kubernetes cluster unreachable: Error: INSTALLATION FAILED: Kubernetes cluster unreachable: the server has asked for the client to provide credentials -# Already installed: Error: INSTALLATION FAILED: cannot re-use a name that is still in use -# Successfull Install:- -# --------------- -# NAME: csi-secrets-store -# LAST DEPLOYED: Thu May 18 17:16:49 2023 -# NAMESPACE: kube-system -# STATUS: deployed -# REVISION: 1 -# TEST SUITE: None -# NOTES: -# The Secrets Store CSI Driver is getting deployed to your cluster. - -# To verify that Secrets Store CSI Driver has started, run: - -# kubectl --namespace=kube-system get pods -l "app=secrets-store-csi-driver" - -# Now you can follow these steps https://secrets-store-csi-driver.sigs.k8s.io/getting-started/usage.html -# to create a SecretProviderClass resource, and a deployment using the SecretProviderClass. -# --------------- - -if [[ $response == *"STATUS: deployed"* ]]; then - echo "Successfully installed Secrets Store CSI Driver." -elif [[ $response == *"Kubernetes cluster unreachable"* ]]; then - echo "Kubernetes cluster unreachable. Exiting..." - exit 1 -elif [[ $response == *"cannot re-use a name that is still in use"* ]]; then - echo "Secrets Store CSI Driver already installed." -else - echo "$response" - echo "Failed to install Secrets Store CSI Driver. Exiting..." - exit 1 -fi - -response=$(helm repo add aws-secrets-manager https://aws.github.io/secrets-store-csi-driver-provider-aws 2>&1) - -# possible responses: -# Helm repo added: "aws-secrets-manager" has been added to your repositories -# Helm repo already added: "aws-secrets-manager" already exists with the same configuration, skipping - -if [[ $response == *"has been added to your repositories"* ]]; then - echo "Successfully added aws-secrets-manager Helm repository." -elif [[ $response == *"already exists with the same configuration, skipping"* ]]; then - echo "aws-secrets-manager Helm repository have already been added." -else - echo "$response" - echo "Failed to add aws-secrets-manager Helm repository. Exiting..." - exit 1 -fi - -response=$(helm install -n kube-system secrets-provider-aws aws-secrets-manager/secrets-store-csi-driver-provider-aws 2>&1) - -# possible responses: -# Kubernetes cluster unreachable: Error: INSTALLATION FAILED: Kubernetes cluster unreachable: the server has asked for the client to provide credentials -# Already installed: Error: INSTALLATION FAILED: cannot re-use a name that is still in use -# Successfull Install:- -# --------------- -# NAME: secrets-provider-aws -# LAST DEPLOYED: Thu May 18 18:30:50 2023 -# NAMESPACE: kube-system -# STATUS: deployed -# REVISION: 1 -# TEST SUITE: None -# --------------- - -if [[ $response == *"STATUS: deployed"* ]]; then - echo "Successfully installed AWS Secrets Manager CSI Driver." -elif [[ $response == *"Kubernetes cluster unreachable"* ]]; then - echo "Kubernetes cluster unreachable. Exiting..." - exit 1 -elif [[ $response == *"cannot re-use a name that is still in use"* ]]; then - echo "AWS Secrets Manager CSI Driver already installed." -else - echo "$response" - echo "Failed to install AWS Secrets Manager CSI Driver. Exiting..." - exit 1 -fi - -# Prompt the user to enter the values for the variables -echo "Enter the EKS cluster region: " -read -r region - -# Create the secret in the AWS Secrets Manager -response=$(aws --region "$region" secretsmanager create-secret --name xkops-secret --secret-string '{"AWS_ACCESS_KEY_ID":"", "AWS_SECRET_ACCESS_KEY":"","AWS_SESSION_TOKEN":"","ROBUSTA_UI_API_KEY":"","SLACK_API_KEY":"","PX_API_KEY":"","PX_DEPLOY_KEY":""}' 2>&1) - -# possible responses: -# Secret created: -# { -# "ARN": "arn:aws:secretsmanager:ap-southeast-1:[AWS-ACCOUNT-NUMBER]:secret:test1-Se57FI", -# "Name": "test1", -# "VersionId": "a42fc663-c76f-44de-bce8-bd43da62bba0" -# } -# Secret already created: An error occurred (ResourceExistsException) when calling the CreateSecret operation: The operation failed because the secret xkops-secret already exists. -# Secret scheduled for deletion: An error occurred (InvalidRequestException) when calling the CreateSecret operation: You can't create this secret because a secret with this name is already scheduled for deletion. - -if [[ $response == *"\"ARN\":"* ]]; then - echo "xkops-secret created in AWS Secret Manager." -elif [[ $response == *"already exists"* ]]; then - echo "xkops-secret already exists in AWS Secret Manager." -elif [[ $response == *"scheduled for deletion."* ]]; then - echo "xkops-secret scheduled for deletion in AWS Secret Manager. Exiting..." - exit 1 -else - echo "$response" - echo "Failed to create xkops-secret in AWS Secrets Manager. Exiting..." - exit 1 -fi - -#Fetch ARN of xkops-secret -response=$(aws --region "$region" secretsmanager describe-secret --secret-id xkops-secret --query ARN --output text 2>&1) - -# possible responses: -# Secret ARN fetched: arn:aws:secretsmanager:ap-southeast-1:[AWS-ACCOUNT-NUMBER]:secret:xkops-secret-LlOZro -# Secret doesn't exist: An error occurred (ResourceNotFoundException) when calling the DescribeSecret operation: Secrets Manager can't find the specified secret. -# Invalid Session Token/Access Key: An error occurred (UnrecognizedClientException) when calling the DescribeSecret operation: The security token included in the request is invalid -# Invalid Secret Access Key: An error occurred (InvalidSignatureException) when calling the DescribeSecret operation: The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details. - -if [[ $response == *"arn:"* ]]; then - echo "xkops-secret ARN fetched." - secret_arn=$response -elif [[ $response == *"can't find the specified secret"* ]]; then - echo "ARN can't be fetched because secret does no exist. Exiting..." - exit 1 -elif [[ $response == *"security token included in the request is invalid"* ]]; then - echo "Invalid Session Token or Access Key ID. Exiting..." - exit 1 -elif [[ $response == *"Check your AWS Secret Access Key and signing method"* ]]; then - echo "Invalid Secret Access Key. Exiting..." - exit 1 -else - echo "$response" - echo "Failed to fetch ARN. Exiting..." - exit 1 -fi - -# Create a policy for the secret -response=$(aws --region "$region" --query Policy.Arn --output text iam create-policy --policy-name xkops-secret-policy --policy-document '{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret", "sts:AssumeRoleWithWebIdentity"], "Resource": ["'"$secret_arn"'"] } ] }' 2>&1) -# possible responses: -# Policy created: arn:aws:iam::[AWS-ACCOUNT-NUMBER]:policy/xkops-secret-policy -# Policy already exist: An error occurred (EntityAlreadyExists) when calling the CreatePolicy operation: A policy called xkops-secret-policy already exists. Duplicate names are not allowed. - -if [[ $response == *"arn:"* ]]; then - echo "xkops-secret-policy is created." -elif [[ $response == *"Duplicate names are not allowed"* ]]; then - echo "A policy named xkops-secret-policy is already created." -else - echo "$response" - echo "Failed to create policy \"xkops-secret-policy\". Exiting..." - exit 1 -fi - -#Fetch ARN of policy created -response=$(aws iam list-policies --scope Local --query "Policies[?PolicyName=='xkops-secret-policy'].Arn" --output text 2>&1) -# possible responses: -# Policy ARN fetched: arn:aws:iam::[AWS-ACCOUNT-NUMBER]:policy/xkops-secret-policy - -if [[ $response == *"arn:"* ]]; then - echo "xkops-secret-policy ARN fetched." - policy_arn=$response -elif [[ $response == *"security token included in the request is invalid"* ]]; then - echo "Invalid Session Token or Access Key ID. Exiting..." - exit 1 -elif [[ $response == *"Check your AWS Secret Access Key and signing method"* ]]; then - echo "Invalid Secret Access Key. Exiting..." - exit 1 -else - echo "$response" - echo "Failed to fetch ARN. Exiting..." - exit 1 -fi - - -# Prompt the user to enter the values for the variables -echo "Enter the EKS cluster name: " -read -r cluster_name - -# Associate the IAM OIDC provider with the EKS cluster -response=$(eksctl utils associate-iam-oidc-provider --region="$region" --cluster="$cluster_name" --approve) - -# possible responses: -# [ℹ] IAM Open ID Connect provider is already associated with cluster "xkops-cluster" in "ap-southeast-1" -# will create IAM Open ID Connect provider for cluster "xkops-cluster" in "ap-southeast-1" [✔] created IAM Open ID Connect provider for cluster "xkops-cluster" in "ap-southeast-1" - - - -if [[ $response == *"is already associated with cluster"* ]]; then - echo "IAM Open ID Connect provider already associated with cluster" - -elif [[ $response == *"created IAM Open ID Connect provider for cluster"* ]]; then - echo "IAM Open ID Connect provider associated with cluster" -else - echo "$response" - echo "Failed to create IAM Open ID Connect provider. Exiting..." - exit 1 -fi - - -# Create an IAM service account for the secret -response=$(eksctl create iamserviceaccount --name xkops-secret-sa --namespace xkops --region="$region" --cluster "$cluster_name" --attach-policy-arn "$policy_arn" --approve --override-existing-serviceaccounts) - - - -#possible responses -# created serviceaccount "xkops/xkops-secret-sa" -# 1 existing iamserviceaccount(s) (xkops/xkops-secret-sa) will be excluded - -if [[ $response == *"created serviceaccount"* ]]; then - echo "Serviceaccount created in xkops namespace" -elif [[ $response == *"existing iamserviceaccount(s)"* ]]; then - echo "Serviceaccount stack already exists" -else - echo "$response" - echo "Failed to create service account. Exiting..." - exit 1 -fi diff --git a/src/checker/README.md b/src/checker/README.md deleted file mode 100644 index bb440b5..0000000 --- a/src/checker/README.md +++ /dev/null @@ -1,104 +0,0 @@ - -# Kubecost Checker - -This bash script is used to check the presence of Kubecost in a Kubernetes cluster. It verifies the configuration of `kubectl`, and then uses `kubectl` or `curl` to check the existence of Kubecost namespace, deployment, and image in the cluster. - -## Components of Kubecost Checker - -This script consists of several functions that are called sequentially to check the presence of Kubecost in the Kubernetes cluster. Each function performs a specific check and provides feedback on the results. - -### `check_kubectl()` - -This function checks if `kubectl` is configured or not. If `kubectl` is configured, it calls the `kubectl_kc_checker` function. Otherwise, it calls the `curl_kc_checker` function. - -### `kubectl_kc_checker()` - -This function checks the presence of Kubecost namespace, deployment, and image in the Kubernetes cluster using `kubectl` utility. It calls the following functions sequentially: - -- `kubectl_kcNS_checker()`: This function checks if the Kubecost namespace is found in the cluster. -- `kubectl_kcDeploy_checker()`: This function checks if the Kubecost deployment is found in the cluster. -- `kubectl_kcImage_checker()`: This function checks the correctness of the Kubecost deployment image. - -### `kubectl_kcNS_checker()` - -This function uses `kubectl` to find the Kubecost namespace in the cluster. If the namespace is found, it displays a success message. Otherwise, it displays an error message and exits the script with status code 0. - -### `kubectl_kcDeploy_checker()` - -This function uses `kubectl` to find the Kubecost deployment in the cluster. If the deployment is found, it displays a success message. Otherwise, it displays an error message and exits the script with status code 0. - -### `kubectl_kcImage_checker()` - -This function uses `kubectl` to get the image used by the Kubecost deployment in the cluster. It checks if the image matches the expected image. If the image is correct, it displays a success message. Otherwise, it displays an error message and exits the script with status code 0. - -### `curl_kc_checker()` - -This function is called when `kubectl` is not configured. It uses `curl` to access the Kubernetes API server and retrieve the list of deployments. It then checks the correctness of the Kubecost deployment name and image in the retrieved data. If Kubecost is found, it displays a success message. Otherwise, it displays an error message and exits the script with status code 0. - -# Robusta Checker - -This Bash script is used to check if the Robusta deployment is installed in your Kubernetes cluster. It performs checks by querying the cluster using `kubectl` or `curl` commands to verify the presence of expected deployments and images. - -## Components of Robusta Checker - -The script has two main functions: - -1. `kubectl_rb_checker()`: This function checks for the presence of Robusta deployments and images using `kubectl` commands. -2. `curl_rb_checker()`: This function checks for the presence of Robusta deployments and images using `curl` commands. - -### `kubectl_rb_checker()` - -This function performs the following steps: - -1. Sets the list of expected deployment names and image names to check. -2. Gets the list of namespaces in the cluster using `kubectl` command. -3. Iterates through each namespace. -4. Gets the list of deployments in each namespace. -5. Iterates through each deployment in each namespace. -6. Iterates through each expected deployment name and image name. -7. Checks if the deployment name and image name match the expected values. -8. If the deployment and image are found, it gets the status of pods in the deployment. -9. Checks if any of the pods are not in the "Running" state. -10. Prints an error message if the deployment or image is not found or the pods are not in the "Running" state. -11. Exits with error code 1 if Robusta is found in the cluster. - -### `curl_rb_checker()` - -This function performs the following steps: - -1. Gets the list of namespaces in the cluster using `curl` command. -2. Extracts the names of the namespaces from the JSON response. -3. Iterates through the namespaces. -4. Checks if the "robusta-runner" deployment exists in the namespace using `curl` command. -5. Extracts the image for the "robusta-runner" deployment from the JSON response. -6. Checks if the image matches the expected image name. -7. Gets the list of pods for the "robusta-runner" deployment using `curl` command. -8. Checks if any of the pods are not in the "Running" state. -9. Sets the `runner_found` flag if the "robusta-runner" deployment and image are found. -10. Performs similar checks for the "robusta-forwarder" deployment and sets the `forwarder_found` flag if found. -11. Prints an error message if the deployment or image is not found or the pods are not in the "Running" state. -12. Exits with error code 1 if Robusta is found in the cluster. - -# Pixie Checker - -This Bash script, named `px_checker.sh`, is used to detect the presence of Pixie in a Kubernetes cluster. It performs a series of checks to verify the existence of Pixie namespaces and deployments using `kubectl` and `curl` commands. If any of the Pixie components are not found, the script exits with an appropriate exit code. - -## Components of Pixie Checker - -The `px_checker.sh` script consists of the following functions: - -### `check_permissions()` - -This function checks whether the service account running the script has the necessary permissions to list deployments in the cluster. - -### `kubectl_pxNS_checker()` - -This function checks for the existence of Pixie namespaces using the `kubectl` command. If `kubectl` is unable to reach the Kubernetes API server, it falls back to using `curl` to directly query the API server. If any of the Pixie namespaces (`$OLMNS`, `$PXOPNS`, `$PLNS`) are not found, the script exits with a non-zero exit code, indicating that Pixie was not detected in the cluster. - -### `curl_pxNS_checker()` - -This function uses `curl` to directly query the Kubernetes API server for the existence of Pixie namespaces. It iterates through an array of namespaces (`$OLMNS`, `$PXOPNS`, `$PLNS`) and checks if each namespace is present. If any of the Pixie namespaces are not found, the script exits with a non-zero exit code, indicating that Pixie was not detected in the cluster. - -### `kubectl_pxDeploy_checker()` - -This function checks for the existence of Pixie deployments in the Pixie namespaces using the `kubectl` command. It retrieves the list of deployments in the `$PLNS` and `$OLMNS` namespaces, and checks if the expected deployments (`$PL_KELVIN`, `$PL_CLOUD_CONNECTOR`, `$PL_VIZIER_QUERY_BROKER`, `$OLM_CATALOG_OPERATOR`, `$OLM_OPERATOR`) are present. If any of the deployments are not found, the script logs an information message and continues to the next deployment. If all expected deployments are found, the script exits with a non-zero exit code, indicating that Pixie was detected in the cluster. diff --git a/src/checker/kc-checker.sh b/src/checker/kc-checker.sh deleted file mode 100755 index 93362c9..0000000 --- a/src/checker/kc-checker.sh +++ /dev/null @@ -1,150 +0,0 @@ -#!/bin/bash - -# Copyright (c) 2023, Xgrid Inc, https://xgrid.co - -# 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. - -# Skip this check -# shellcheck source=/dev/null -source /src/config/kc-config.sh -source /src/commons/common-functions.sh - -# Function that starts basic execution of the checker script -print_prompt() { - log "${CYAN}[INFO]" "[CHECKER]" "Detecting Kubecost in the Kubernetes cluster.${CC}" -} - -# This function checks if kubectl is configured or not. -check_kubectl() { - # Checking Kubecost In Kubernetes Cluster - log "${CYAN}[INFO]" "[CHECKER]" "Checking if kubectl is configured.${CC}" - - if command -v kubectl &>/dev/null; then - log "${GREEN}[INFO]" "[CHECKER]" "kubectl configurations are obtained successfully.${CC}" - kubectl_kc_checker # Function call to below defined function. - else - log "${RED}[ERROR]" "[CHECKER]" "kubectl is not configured. Using Curl Instead.${CC}" - curl_kc_checker # Function call to below defined functions. - fi -} - -# This function calls three functions which checks namespace, deployment and image of kubecost using kubectl utility. -kubectl_kc_checker() { - log "${CYAN}[INFO]" "[CHECKER]" "Checking if kubecost namespace is found in the cluster.${CC}" - kubectl_kc_ns_checker - - log "${CYAN}[INFO]" "[CHECKER]" "Checking if kubecost deployment is found in the cluster.${CC}" - kubectl_kc_deployment_checker - - log "${CYAN}[INFO]" "[CHECKER]" "Checking correctness of kubecost deployment image.${CC}" - kubectl_kc_image_checker -} - -# Function finds kubecost namespace that we are looking for using kubectl tool. -kubectl_kc_ns_checker() { - returnedNamespace=$(kubectl get ns --no-headers 2>&1) - - # Check the exit code of the last command (kubectl). - errorCode=$? - if [ $errorCode -ne 0 ]; then - log "${RED}[ERROR]" "[CHECKER]" "Error occurred while checking for kubecost namespace.${CC}" - exit 1 # Exit with a non-zero code to indicate failure. - fi - - returnedNamespace=$(echo "$returnedNamespace" | grep -i kubecost | awk '{print $1}') - - if [ "$returnedNamespace" == "${KC_NAMESPACE[0]}" ]; then - log "${GREEN}[INFO]" "[CHECKER]" "Namespace $returnedNamespace found in the cluster.${CC}" - else - log "${RED}[INFO]" "[CHECKER]" "Namespace kubecost not found in the cluster.${CC}" - exit 0 - fi -} - -# This function checks whether kubecost deployment is present in the cluster or not. -kubectl_kc_deployment_checker() { - kubecostDeploy=$(kubectl -n "${KC_NAMESPACE[@]}" get deploy --no-headers 2>&1 | grep -i "${EXPECTED_KC_DEPLOY}" | awk '{print $1}') - # Check the exit code of the last command (kubectl). - errorCode=$? - if [ $errorCode -ne 0 ]; then - log "${RED}[ERROR]" "[CHECKER]" "Error occurred while checking for kubecost deployment.${CC}" - exit 1 # Exit the script with a non-zero code to indicate failure. - fi - - if [ "$kubecostDeploy" == "${EXPECTED_KC_DEPLOY}" ]; then - log "${GREEN}[INFO]" "[CHECKER]" "Kubecost deployment found in cluster.${CC}" - else - log "${RED}[INFO]" "[CHECKER]" "Unable to find kubecost Deployment in cluster.${CC}" - exit 0 - fi -} - -# This function checks whether image used by kubecost deployment is correct or not. -kubectl_kc_image_checker() { - kubecostImage=("$(kubectl -n "${KC_NAMESPACE[@]}" get deployment "${EXPECTED_KC_DEPLOY}" -o=jsonpath='{$.spec.template.spec.containers[:1].image}')") - # Check the exit code of the last command (kubectl). - errorCode=$? - if [ $errorCode -ne 0 ]; then - log "${RED}[ERROR]" "[CHECKER]" "Error occurred while checking for kubecost image.${CC}" - exit 1 # Exit the script with a non-zero code to indicate failure. - fi - - image="${kubecostImage:0:27}" - if [[ $image == "${KC_IMAGE}" ]]; then - log "${GREEN}[INFO]" "[CHECKER]" "Image[$image] found in Deployment [$EXPECTED_KC_DEPLOY] ${GREEN}is correct.${CC}" - exit 1 - else - log "${RED}[ERROR]" "[CHECKER]" "Unable to find required image in Deployment${GREEN}[$EXPECTED_KC_DEPLOY].${CC}" - exit 0 - fi -} - -# In the absence of kubectl this function will be called and curl will be used to achieved required checks. -curl_kc_checker() { - # CURLING API-SERVER. (Getting deployment list and filtering using field selector) - # Using the pod's environment variables, default serviceaccount's certificate, and default serviceaccount's token to access the cluster. - deploymentsList=$(curl --silent "$KUBERNETES_API_SERVER_URL"/apis/apps/v1/deployments \ - --cacert "$CA_CERT_PATH" \ - --header "${HEADERS[@]}") - # Check the exit code of the last command (kubectl). - errorCode=$? - if [ $errorCode -ne 0 ]; then - log "${RED}[ERROR]" "[CHECKER]" "Error occurred while curling the Kubernetes API server.${CC}" - exit 1 # Exit the script with a non-zero code to indicate failure. - fi - - # Extracting Name and Image from Kubecost Deployment. - kcDeployment=$(echo "$deploymentsList" | grep -o '"name": "[^"]*' | head -n1) - kcDeployImage=$(echo "$deploymentsList" | grep -o '"image": "[^"]*') - - # Checking the correctness of name in Kubecost Deployment. - if [[ $kcDeployment == *"$KC_DEPLOYMENT"* ]]; then - - # Checking the correctness of image in Kubecost Deployment. - if ! [[ $kcDeployImage == *"$KC_IMAGE"* ]]; then - log "${RED}[ERROR]" "[CHECKER]" "Kubecost not found.${CC}" - exit 0 - fi - else - log "${RED}[ERROR]" "[CHECKER]" "Kubecost not found.${CC}" - exit 0 - fi - - log "${GREEN}[INFO]" "[CHECKER]" "Kubecost Exists In Your Cluster.${CC}" - exit 1 -} - -# Calling the above Defined functions. -print_prompt -check_permissions -check_kubectl diff --git a/src/checker/px-checker.sh b/src/checker/px-checker.sh deleted file mode 100644 index a159590..0000000 --- a/src/checker/px-checker.sh +++ /dev/null @@ -1,164 +0,0 @@ -#!/bin/bash - -# Copyright (c) 2023, Xgrid Inc, https://xgrid.co - -# 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. - - -source /src/config/px-config.sh -source /src/config/config.sh -source /src/commons/common-functions.sh - -print_prompt() { - - # px_checker.sh: Script to detect Pixie in a K8s cluster. - log "${CYAN}[INFO]" "[CHECKER]" "Starting checks to detect Pixie in Kubernetes cluster.${CC}" - log "${CYAN}[INFO]" "[CHECKER]" "Current Context: $currentCtx${CC}" -} - -#check whether the service account has permision to list deployments. -check_permissions - -# CLUSTER BASED CHECKS -# 1.0 Pixie namesapces check using kubectl. -# If kubectl exists on the system this function uses kubectl to find the Pixie relevant namespaces. -# If any of the Pixie namespaces is not found, it returns with a non-zero exit code. -kubectl_px_ns_checker() { - if ! kubectl get pods &>/dev/null; then - log "${CYAN}[INFO]" "[CHECKER]" "kubectl exists, but can not reach kube_api server. Using curl instead.${CC}" - curl_px_ns_checker - else - olmCheck=$(kubectl get ns 2>&1 | grep "$OLMNS" | awk '{print $1}') - errorCode=$? - if [ $errorCode -ne 0 ]; then - log "${RED}[ERROR]" "[CHECKER]" "Error occurred while checking for $OLMNS namespace.${CC}" - exit 1 # Exit the script with a non-zero code to indicate failure. - fi - - plCheck=$(kubectl get ns 2>&1 | grep "$PLNS" | awk '{print $1}') - errorCode=$? - if [ $errorCode -ne 0 ]; then - log "${RED}[ERROR]" "[CHECKER]" "Error occurred while checking for $PLNS namespace.${CC}" - exit 1 # Exit the script with a non-zero code to indicate failure. - fi - - pxOpCheck=$(kubectl get ns 2>&1 | grep "$PXOPNS" | awk '{print $1}') - errorCode=$? - if [ $errorCode -ne 0 ]; then - log "${RED}[ERROR]" "[CHECKER]" "Error occurred while checking for $PXOPNS namespace.${CC}" - exit 1 # Exit the script with a non-zero code to indicate failure. - fi - - if [[ "$olmCheck" == "${OLMNS}" && "$plCheck" == "${PLNS}" && "$pxOpCheck" == "${PXOPNS}" ]]; then - log "${GREEN}[PASSED]" "[CHECKER]" "Pixie namespaces found.${CC}" - else - log "${RED}[ERROR]" "[CHECKER]" "Pixie namespaces not found. ${CC}" - log "${RED}[ERROR]" "[CHECKER]" "Pixie not found in the cluster${CC}" - exit 0 # Exit the script with a non-zero code to indicate failure. - fi - fi -} - -# 1.1- Pixie namesapces check using curl. -# If kubectl doesn't exist on the system this function uses curl to directly talk to kube_api server -# to find the namespaces. If any of the Pixie namespaces is not found, it returns with a non-zero exit code. -curl_px_ns_checker() { - - # these namespaces are pixie's specific and all the pixie's components are - # deployed in those namespaces. - declare -a arr=("$OLMNS" "$PXOPNS" "$PLNS") - - for namespace in "${arr[@]}"; do - ns=$(ns=$(curl --silent "$KUBERNETES_API_SERVER_URL/api/v1/namespaces" \ - --cacert "$CA_CERT_PATH" \ - --header "${HEADERS[@]}" | - grep -i "$namespace" | grep name | head -2 | grep -oP '(?<="name": ")[^"]*')) - errorCode=$? - if [ $errorCode -ne 0 ]; then - log "${RED}[ERROR]" "[CHECKER]" "Error occurred while checking for $namespace namespace.${CC}" - exit 1 # Exit the script with a non-zero code to indicate failure. - fi - - if [[ $ns != "$namespace" ]]; then - log "${RED}[ERROR]" "[CHECKER]" "$namespace namespace not found.${CC}" - log "${RED}[ERROR]" "[CHECKER]" "Pixie not found in the cluster${CC}" - exit 0 - else - log "${GREEN}[PASSED]" "[CHECKER]" "$namespace namespace found.${CC}" - exit 1 - fi - done -} - -# 2.1- Pixie deployments check using kubectl. -# This function tries to check for Pixie deployments in the Pixie namespaces. -# If deployments are not found the script exits with a non-zero status code. -kubectl_px_deploy_checker() { - - mapfile -t plDeploy < <(kubectl get deploy -n "$PLNS" --no-headers 2>checker.log | awk '{print $1}') - errorCode=$? - if [ $errorCode -ne 0 ]; then - log "${RED}[ERROR]" "[CHECKER]" "Error occurred while checking for $PLNS deployments.${CC}" - exit 1 # Exit the script with a non-zero code to indicate failure. - fi - - mapfile -t olmDeploy < <(kubectl get deploy -n "$OLMNS" --no-headers 2>checker.log | awk '{print $1}') - errorCode=$? - if [ $errorCode -ne 0 ]; then - log "${RED}[ERROR]" "[CHECKER]" "Error occurred while checking for $OLMNS deployments.${CC}" - exit 1 # Exit the script with a non-zero code to indicate failure. - fi - - if [[ ${#plDeploy[*]} -ne 0 && ${#olmDeploy[*]} -ne 0 ]]; then - for dp in "${plDeploy[@]}"; do - ns="$PLNS" - if [[ $dp == "$PL_KELVIN" || $dp == "$PL_CLOUD_CONNECTOR" || $dp == "$PL_VIZIER_QUERY_BROKER" ]]; then - log "${GREEN}[PASSED]" "[CHECKER]" "$dp deployment found in $ns namespace.${CC}" - else - log "${CYAN}[INFO]" "[CHECKER]" "$dp deployment not found in $ns namespace.${CC}" - fi - done - - for dp in "${olmDeploy[@]}"; do - ns="$OLMNS" - if [[ $dp == "$OLM_CATALOG_OPERATOR" || $dp == "$OLM_OPERATOR" ]]; then - log "${CYAN}[PASSED]" "[CHECKER]" "$dp deployment found in $ns namespace.${CC}" - exit 1 - else - log "${CYAN}[INFO]" "[CHECKER]" "$dp deployment not found in $ns namespace.${CC}" - log "${CYAN}[INFO]" "[CHECKER]" "Pixie not found in the cluster${CC}" - exit 0 - fi - done - else - log "${CYAN}[INFO]" "[CHECKER]" "Pixie not found in the cluster${CC}" - exit 0 - fi -} - -if command -v kubectl &>/dev/null; then - currentCtx=$(kubectl config current-context) - print_prompt - check_permissions - log "${CYAN}[INFO]" "[CHECKER]" "Checking for Pixie namespaces in the current cluster.${CC}" - kubectl_px_ns_checker - log "${CYAN}[INFO]" "[CHECKER]" "Checking for Pixie Deployments in the current cluster.${CC}" - kubectl_px_deploy_checker -else - currentCtx=$(kubectl config current-context) - print_prompt - check_permissions - log "${CYAN}[INFO]" "[CHECKER]" "Checking for Pixie namespaces in the current cluster.${CC}" - log "${RED}[ERROR]" "[CHECKER]" "kubectl not found. Using curl instead.${CC}" - curl_px_ns_checker -fi diff --git a/src/checker/rb-checker.sh b/src/checker/rb-checker.sh deleted file mode 100644 index 2325e6c..0000000 --- a/src/checker/rb-checker.sh +++ /dev/null @@ -1,181 +0,0 @@ -#!/bin/bash - -# Copyright (c) 2023, Xgrid Inc, https://xgrid.co - -# 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. - -# Skip this check -# shellcheck source=/dev/null -source /src/config/rb-config.sh -source /src/commons/common-functions.sh - -# Function that starts basic execution of the checker script -print_prompt() { - log "${CYAN}[INFO]" "[CHECKER]" "Detecting robusta in the Kubernetes cluster.${CC}" -} - -# This function checks if kubectl is configured or not. -check_kubectl() { - # Checking robusta In Kubernetes Cluster - log "${CYAN}[INFO]" "[CHECKER]" "Checking if kubectl is configured.${CC}" - - if command -v kubectl &>/dev/null; then - log "${GREEN}[INFO]" "[CHECKER]" "kubectl configurations are obtained successfully.${CC}" - kubectl_rb_checker # Function call to below defined function. - else - log "${RED}[ERROR]" "[CHECKER]" "kubectl is not configured. Using Curl Instead.${CC}" - curl_rb_checker # Function call to below defined functions. - fi -} - -# This function calls three functions which checks namespace, deployment and image of robusta using kubectl utility. -kubectl_rb_checker() { - log "${CYAN}[INFO]" "[CHECKER]" "Checking if robusta namespace is found in the cluster.${CC}" - kubectl_rb_ns_checker - - log "${CYAN}[INFO]" "[CHECKER]" "Checking if robusta deployment is found in the cluster.${CC}" - kubectl_rb_deployment_checker - - log "${CYAN}[INFO]" "[CHECKER]" "Checking correctness of robusta deployment image.${CC}" - kubectl_rb_image_checker -} - -# Function finds robusta namespace that we are looking for using kubectl tool. -kubectl_rb_ns_checker() { - returnedNamespace=$(kubectl get ns --no-headers 2>&1) - - # Check the exit code of the last command (kubectl). - errorCode=$? - if [ $errorCode -ne 0 ]; then - log "${RED}[ERROR]" "[CHECKER]" "Error occurred while checking for robusta namespace.${CC}" - exit 1 # Exit with a non-zero code to indicate failure. - fi - - returnedNamespace=$(echo "$returnedNamespace" | grep -i robusta | awk '{print $1}') - - if [ "$returnedNamespace" == "${RB_NAMESPACE[0]}" ]; then - log "${GREEN}[INFO]" "[CHECKER]" "Namespace $returnedNamespace found in the cluster.${CC}" - else - log "${RED}[INFO]" "[CHECKER]" "Namespace robusta not found in the cluster.${CC}" - exit 0 - fi -} - -# This function checks whether robusta deployment is present in the cluster or not. -kubectl_rb_deployment_checker() { - runnerDeploy=$(kubectl -n "${RB_NAMESPACE[@]}" get deploy --no-headers 2>&1 | grep -i "${EXPECTED_RUNNER_NAME}" | awk '{print $1}') - forwarderDeploy=$(kubectl -n "${RB_NAMESPACE[@]}" get deploy --no-headers 2>&1 | grep -i "${EXPECTED_FORWARDER_NAME}" | awk '{print $1}') - - # Check the exit code of the last command (kubectl). - errorCode=$? - if [ $errorCode -ne 0 ]; then - log "${RED}[ERROR]" "[CHECKER]" "Error occurred while checking for robusta deployment.${CC}" - exit 1 # Exit the script with a non-zero code to indicate failure. - fi - - if [[ "$runnerDeploy" == "${EXPECTED_RUNNER_NAME}" && "$forwarderDeploy" == "${EXPECTED_FORWARDER_NAME}" ]]; then - log "${GREEN}[INFO]" "[CHECKER]" "robusta deployment found in cluster.${CC}" - else - log "${RED}[INFO]" "[CHECKER]" "Unable to find robusta Deployment in cluster.${CC}" - exit 0 - fi -} - -# This function checks whether image used by robusta deployment is correct or not. -kubectl_rb_image_checker() { - runnerImage=("$(kubectl -n "${RB_NAMESPACE[@]}" get deployment "${EXPECTED_RUNNER_NAME}" -o=jsonpath='{$.spec.template.spec.containers[:1].image}')") - forwarderImage=("$(kubectl -n "${RB_NAMESPACE[@]}" get deployment "${EXPECTED_FORWARDER_NAME}" -o=jsonpath='{$.spec.template.spec.containers[:1].image}')") - # Check the exit code of the last command (kubectl). - errorCode=$? - if [ $errorCode -ne 0 ]; then - log "${RED}[ERROR]" "[CHECKER]" "Error occurred while checking for robusta image.${CC}" - exit 1 # Exit the script with a non-zero code to indicate failure. - fi - - if [[ "${runnerImage[0]}" == *"$EXPECTED_RUNNER_IMAGE"* && "${forwarderImage[0]}" == *"$EXPECTED_FORWARDER_IMAGE"* ]]; then - log "${GREEN}[INFO]" "[CHECKER]" "Images found in Deployments ${GREEN}are correct.${CC}" - exit 1 - else - log "${RED}[ERROR]" "[CHECKER]" "Unable to find required image in Deployment${GREEN}.${CC}" - exit 0 - fi -} - -curl_rb_checker() { - - # Get a list of all namespaces in the cluster - mapfile -t namespaces < <(curl -s -k --cacert "$CA_CERT_PATH" -H "${HEADERS[@]}" "$KUBERNETES_API_SERVER_URL/api/v1/namespaces") - - runnerFound=false - forwarderFound=false - - # Extract the names of the namespaces from the JSON response - mapfile -t namespaces < <(echo "${namespaces[@]}" | sed -n 's/.*"name": "\(.*\)",.*/\1/p') - - # Iterate through the namespaces - for ns in "${namespaces[@]}"; do - # Check if the "robusta-runner" deployment exists in the namespace - runner=$(curl -s -k --cacert "$CA_CERT_PATH" -H "${HEADERS[@]}" "$KUBERNETES_API_SERVER_URL/apis/apps/v1/namespaces/$ns/deployments/$EXPECTED_RUNNER_NAME") - if [ "$runner" != "Not Found" ]; then - # Extract the image for the "robusta-runner" deployment from the JSON response - runnerImage[0]=$(echo "$runner" | sed -n 's/.*"image": "\(.*\)",.*/\1/p') - if [[ "${runnerImage[0]}" == *"$EXPECTED_RUNNER_IMAGE"* ]]; then - - # Get the list of pods for the "robusta-runner" deployment - pods=$(curl -s -k --cacert "$CA_CERT_PATH" -H "${HEADERS[@]}" "$KUBERNETES_API_SERVER_URL/api/v1/namespaces/$ns/pods?labelSelector=app%3Drobusta-runner") - - # Iterate through the pods - while read -r pod; do - # Extract the status of the pod from the JSON response - status=$(echo "$pod" | grep '"phase": "Running"') - if [ -n "$status" ]; then - runnerFound=true - fi - done <<<"$pods" - fi - fi - # Check if the "robusta-forwarder" deployment exists in the namespace - forwarder=$(curl -s -k --cacert "$CA_CERT_PATH" -H "${HEADERS[@]}" "$KUBERNETES_API_SERVER_URL/apis/apps/v1/namespaces/$ns/deployments/$EXPECTED_FORWARDER_NAME") - if [ "$forwarder" != "Not Found" ]; then - # Extract the image for the "robusta-forwarder" deployment from the JSON response - forwarderImage[0]=$(echo "$forwarder" | sed -n 's/.*"image": "\(.*\)",.*/\1/p') - if [[ "${forwarderImage[0]}" == *"$EXPECTED_FORWARDER_IMAGE"* ]]; then - - # Get the list of pods for the "robusta-forwarder" deployment - pods=$(curl -s -k --cacert "$CA_CERT_PATH" -H "${HEADERS[@]}" -H "Content-Type: application/json" "$KUBERNETES_API_SERVER_URL/api/v1/namespaces/$ns/pods?labelSelector=app%3Drobusta-forwarder") - - # Iterate through the pods - while read -r pod; do - # Extract the status of the pod from the JSON response - status=$(echo "$pod" | grep '"phase": "Running"') - if [ -n "$status" ]; then - forwarderFound=true - - fi - done <<<"$pods" - fi - fi - done - - if [[ "$runnerFound" == true ]] && [[ "$forwarderFound" == true ]]; then - log "${GREEN}[INFO]" "[CHECKER]" "Robusta Exists in your cluster.${CC}" - exit 1 - else - log "${RED}[ERROR]" "[CHECKER]" "Robusta not found.${CC}" - fi -} - -# Calling the above Defined functions. -print_prompt -check_permissions -check_kubectl diff --git a/src/commons/README.md b/src/commons/README.md deleted file mode 100644 index e8ea1df..0000000 --- a/src/commons/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# Common Functions - -This bash script contains a collection of common functions that can be used by other bash scripts. The functions provided in this script perform various tasks such as logging messages, checking if Helm is installed in the cluster, installing Helm if it is not installed, checking permissions for a service account, and verifying the status of pods in a namespace. - -## Functions - -### log() - -This function is used to log messages with a timestamp, log level, function name, and message. The logged messages are saved in the `/tmp/app.log` file. The parameters for this function are: - -- `level`: Type of log level, which can be ERROR, PASSED, or INFO. -- `function`: Name of the script or function from which the log is coming. -- `message`: Message to be logged. - -### log_test() - -Similar to the `log()` function, this function is used to log messages with a timestamp, log level, function name, and message. However, the logged messages are saved in the `/tmp/app_test.log` file instead of `/tmp/app.log`. - -### helm_checker() - -This function checks if Helm is installed in the cluster. If Helm is not installed, it calls the `helm_installer()` function to download and install Helm. - -### helm_installer() - -This function downloads the Helm 3 binary using `curl` and installs it on the system. It also checks if Helm is installed successfully and logs the result. - -### check_permissions() - -This function checks if the service account has permissions to list deployments in the Kubernetes cluster. It uses `curl` to send a request to the Kubernetes API server and checks the response for any Forbidden error messages. If the service account does not have the necessary permissions, the script terminates with an error message. - -### pod_status_verifier() - -This function verifies the status of pods in the specified namespaces. It takes a list of namespaces as input and checks if the namespaces exist. If a namespace exists, it retrieves the list of pods in that namespace and iterates over each pod to check its status. If a pod is not in the Running or Completed state, an error message is logged. diff --git a/src/commons/common-functions.sh b/src/commons/common-functions.sh deleted file mode 100644 index efe9621..0000000 --- a/src/commons/common-functions.sh +++ /dev/null @@ -1,220 +0,0 @@ -#!/bin/bash - -# Copyright (c) 2023, Xgrid Inc, https://xgrid.co - -# 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. - - -log() { - - # Parameters - # :var level: Type of level = ERROR PASSED INFO. - # :var function: From which script the log is coming. - # :var message: Message to log - - #This functions saves the messages passed to it in /tmp/app.log path. - - local level=$1 - local function=$2 - local message=$3 - local timestamp - timestamp=$(date +"%Y-%m-%d %H:%M:%S") - echo -e "[$timestamp] $level $function $message" | tee -a /tmp/app.log -} - -log_test() { - - # Parameters - # :var level: Type of level = ERROR PASSED INFO. - # :var function: From which script the log is coming. - # :var message: Message to log - - #This functions saves the messages passed to it in /tmp/app_test.log path. - - local level=$1 - local function=$2 - local message=$3 - local timestamp - timestamp=$(date +"%Y-%m-%d %H:%M:%S") - echo -e "[$timestamp] $level $function $message" | tee -a /tmp/app_test.log -} - - -helm_checker() { - - # Parameters - # :None - - # Function to check if helm is installed in the cluster - - log "${CYAN}[INFO]" "[PRE-FLIGHT]" "Checking if helm is configured.${CC}" - if command -v helm &>/dev/null; then - log "${GREEN}[INFO]" "[PRE-FLIGHT]" "Helm configurations obtained successfully.${CC}" - exit 0 - else - log "${RED}[ERROR]" "[PRE-FLIGHT]" "Helm is not installed. Installing it now.${CC}" - helm_installer # Function call to install helm - fi -} - -helm_installer() { - - # Parameters - # :None - - # This function downloads helm 3 binary using curl and installs it. - - log "${CYAN}[INFO]" "[PRE-FLIGHT]" "Downloading and installing Helm.${CC}" - - if curl -s -O https://get.helm.sh/helm-v3.10.0-linux-amd64.tar.gz >/dev/null && - tar -zxvf helm-v3.10.0-linux-amd64.tar.gz >/dev/null && - cp ./linux-amd64/helm /usr/local/bin/; then - if command -v helm &>/dev/null; then - log "${GREEN}[INFO]" "[PRE-FLIGHT]" "Helm has been installed successfully.${CC}" - return 0 - else - log "${RED}[ERROR]" "[PRE-FLIGHT]" "Helm is not installed. Exiting...${CC}" - return 1 - fi - else - log "${RED}[ERROR]" "[PRE-FLIGHT]" "Failed to download and install Helm. Exiting...${CC}" - return 1 - fi -} -check_permissions() { - - # Parameters - # :None - - # This function checks if service account has permission to list deployments. - # If the service account has no permissions then the script will terminate. - - forbiddenError="Forbidden" - - log "${CYAN}[INFO]" "[CHECKER]" "Checking if service account has permissions to list deployments.${CC}" - - deployments=$(curl --silent "$KUBERNETES_API_SERVER_URL/apis/apps/v1/deployments" \ - --cacert "$CA_CERT_PATH" \ - --header "${HEADERS[@]}") - errorCode=$? - if [ $errorCode -ne 0 ]; then - log "${RED}[ERROR]" "[CHECKER]" "Failed to get deployment permissions. Exiting...${CC}" - exit 1 - fi - - # Extract the "reason" field from the response. - reason=$(echo "$deployments" | grep -o '"reason": "[^"]*') - - # Check if the "reason" field contains the word "Forbidden" - if [[ $reason == *"$forbiddenError"* ]]; then - log "${RED}[ERROR]" "[CHECKER]" "Forbidden, cannot list deployments. Exiting.${CC}" - log "${CYAN}[INFO]" "[CHECKER]" "Create clusterrole and clusterrole binding with enough permissions.${CC}" - exit 1 - else - log "${GREEN}[INFO]" "[CHECKER]" "Service account has permissions to list deployments.${CC}" - fi -} - -pod_status_verifier() { - - # Parameters - # :var namespaces: (list) | A list of namespaces - - #This function check if the namespace exists - #If it exists it checks whether all the pod in the namespace is running or not. - - log_test "${CYAN}[INFO]" "[TEST]" "Verifying the status of pods related to the tool. ${CC}" - - namespaces=("${@}") - for namespace in "${namespaces[@]}"; do - - if kubectl get namespace "$namespace" &>/dev/null; then - - # Get a list of pods in the namespace - pods=$(kubectl get pods -n "$namespace" -o jsonpath='{.items[*].metadata.name}') - - # Iterate over pods in the namespace to verify their status - for pod in $pods; do - podStatus=$(kubectl get pod "$pod" -n "$namespace" -o jsonpath='{.status.phase}') - - # If pod status is not Running or Completed, tool is not deployed successfully. - if [[ "$podStatus" != "Running" && "$podStatus" != "Succeeded" ]]; then - - log_test "${RED}[ERROR]" "[TEST]" "$pod pod in $namespace namespace is not in Running state.${CC}" - else - log_test "${GREEN}[PASSED]" "[TEST]" "$pod pod in $namespace namespace is in Running state.${CC}" - fi - done - else - log_test "${RED}[ERROR]" "[TEST]" "Namespace $namespace does not exist.${CC}" - fi - - done -} - -get_eks_cluster_name() { - - # Parameters - # :None - - # this function fetches the eks cluster name and returns it - - - # Get the current context of the kubeconfig - currentContext=$(kubectl config current-context) - - # Get the cluster name associated with the current context - clusterName=$(kubectl config get-contexts "$currentContext" | awk '{print $3}' | grep -Eo 'arn:aws:eks:[a-zA-Z0-9-]*:[0-9]*:[cluster/[a-zA-Z0-9-]*|@[a-zA-Z0-9-]*') - if [[ -z $clusterName ]]; then - log "${RED}[ERROR]" "[CHECKER]" "Failed to get EKS cluster name. Exiting.${CC}" - exit 1 - else - export clusterName - rm -rf root/.kube/config - # Print the cluster name - return "$clusterName" - fi -} - -# The wrapper_function ensures that the deployment and pods in a namespace are fully operational by waiting for them to be in a running state. -validate_healthy_deployment() { - while true; do - dc=$(kubectl get deployments -n "$1" -o jsonpath='{.items[*].metadata.name}' | cut -d'%' -f1 | wc -w) - - # Check to see if atleast one deployment can be found in the namespace. - if [ "$dc" -gt 0 ]; then - pods=$(kubectl get pods -n "$1" -o jsonpath='{.items[*].metadata.name}' | cut -d'%' -f1) - for pod in $pods; do - while true; do - status=$(kubectl get pods "$pod" -n "$1" -o jsonpath='{.status.phase}') - - # Check if Pods Phase reached running or succeeded. - if [[ $status == "Succeeded" || $status == "Running" ]]; then - log "Pod $pod reached the desired status: $status" 1> /dev/null - break - else - log "Waiting for pod $pod to reach the desired status: $status" 1> /dev/null - fi - done - done - break - else - log "Waiting for deployments to become available." 1> /dev/null - fi -done -} - -unpatch_default_storageclass() { - #unpatching default gp2 storage class - kubectl patch storageclass gp2 -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}' -} diff --git a/src/config/config.sh b/src/config/config.sh deleted file mode 100644 index bb99331..0000000 --- a/src/config/config.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/bash - -# Copyright (c) 2023, Xgrid Inc, https://xgrid.co - -# 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. - -#COLOR CODES -export CYAN='\033[0;36m' # Level = INFO -export RED='\033[0;31m' # Level = ERROR -export GREEN='\033[0;32m' # Level = PASSED -export CC="\033[0m" - -# Get the Kubernetes API server URL and CA certificate path -export KUBERNETES_API_SERVER_URL="https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT}" -export CA_CERT_PATH="/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" - -# Read the service account token -SERVICE_ACCOUNT_TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) - -# Set the HTTP headers -export HEADERS=("Authorization: Bearer $SERVICE_ACCOUNT_TOKEN") -export CLUSTER_NAME=$CLUSTER_NAME - diff --git a/src/config/kc-config.sh b/src/config/kc-config.sh deleted file mode 100755 index 110cc6c..0000000 --- a/src/config/kc-config.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash - -# Copyright (c) 2023, Xgrid Inc, https://xgrid.co - -# 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. - -source /src/config/config.sh - -# Kubecost deployment name and image name -export KC_IMAGE="gcr.io/kubecost1/cost-model" -export KC_DEPLOYMENT="kubecost" -export KC_NAMESPACE=("kubecost") -export EXPECTED_KC_DEPLOY="kubecost-cost-analyzer" -export KC_UI_SVC="kubecost-ui-service" diff --git a/src/config/px-config.sh b/src/config/px-config.sh deleted file mode 100644 index d98c483..0000000 --- a/src/config/px-config.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/bash - -# Copyright (c) 2023, Xgrid Inc, https://xgrid.co - -# 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. - -#pixie namespace -export OLMNS="olm" -export PXOPNS="px-operator" -export PLNS="pl" -export PX_NAMESPACES=("olm" "pl" "px-operator") -export PX_TEST_NS=xkops-testing - -#pl deployments -export PL_KELVIN="kelvin" -export PL_CLOUD_CONNECTOR="vizier-cloud-connector" -export PL_VIZIER_QUERY_BROKER="vizier-query-broker" - -#olm deployments -export OLM_CATALOG_OPERATOR="catalog-operator" -export OLM_OPERATOR="olm-operator" - -#pixie pods -export TEST_POD=xkops-test-pod - -#pixie keys -export PX_API_KEY=$PX_API_KEY -export PX_DEPLOY_KEY=$PX_DEPLOY_KEY - - diff --git a/src/config/rb-config.sh b/src/config/rb-config.sh deleted file mode 100644 index f92915c..0000000 --- a/src/config/rb-config.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash - -# Copyright (c) 2023, Xgrid Inc, https://xgrid.co - -# 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. - -source /src/config/config.sh - -export RB_NAMESPACE=("robusta") - -#rb-preflight path -export RB_PREFLIGHT_SCRIPT_PATH=/src/pre-flight/rb-pre-flight/rb-preflight.sh -#rb-installer path -export RB_INSTALLER_SCRIPT_PATH=/src/installer/rb-installer.sh - -#rb-preflight directory path -export PREFLIGHT_DIR_PATH=/src/pre-flight/rb-pre-flight -export PLAYBOOK_DIR_PATH=/src/installer/rb-actions -# Set expected image names -export EXPECTED_RUNNER_IMAGE="us-central1-docker.pkg.dev/genuine-flight-317411/devel/robusta-runner" -export EXPECTED_FORWARDER_IMAGE="us-central1-docker.pkg.dev/genuine-flight-317411/devel/kubewatch" - -#Set expected values for runner and forwarder deployments -export EXPECTED_RUNNER_NAME="robusta-runner" -export EXPECTED_FORWARDER_NAME="robusta-forwarder" -export NAMESPACE="robusta" - -#TODO: Give option to upload generated_values.yaml file through UI -export HELM_VALUES=generated_values.yaml #File required to installed robusta - -# TODO: To be taken from user from UI -# read -rp 'Enter cluster name: ' cluster_name -# read -rp 'Enter slack api key: ' slack_api_key -# read -rp 'Enter slack channel name: ' slack_channel_name - -#These values are coming from environment variables set on container - -export SLACK_API_KEY=$SLACK_API_KEY -export SLACK_CHANNEL_NAME=$SLACK_CHANNEL_NAME -export ROBUSTA_UI_API_KEY=$ROBUSTA_UI_API_KEY diff --git a/src/installer/README.md b/src/installer/README.md deleted file mode 100644 index 46a194d..0000000 --- a/src/installer/README.md +++ /dev/null @@ -1,61 +0,0 @@ -# Installers - -## Kubecost Installer - -This script is used to install Kubecost, a tool for analyzing and managing Kubernetes costs, in a Kubernetes cluster. The script is written in Bash and automates the installation process using Helm, a package manager for Kubernetes. - -### Kubecost Installation Steps - -The script performs the following steps to install Kubecost: - -1. Sources the `kc-config.sh` and `common-functions.sh` files from the `/src` directory. These files contain configuration settings and common functions used by the script. - -2. Defines a function called `kc_installer()` which is the main installer function for Kubecost. This function checks if Kubecost is already deployed in the cluster by querying the Kubernetes deployment for the `kubecost-cost-analyzer` deployment. If the deployment is found, the function logs an info message and exits with a status code of 0. Otherwise, the function logs an error message and proceeds with the installation. - -3. Inside the `kc_installer()` function, the script adds the Kubecost Helm repository using the `helm repo add` command, and then installs Kubecost using the `helm install` command. The Helm chart used for installation is `kubecost/cost-analyzer` and the release name is set to `kubecost` in the `kubecost` namespace. - -4. After the installation, the function waits for the `kubecost prometheus-server` pod to be in a `Ready` condition using the `kubectl wait` command with a timeout of 1 hour. - -5. Once the `kubecost prometheus-server` pod is ready, the function calls another function called `create_kc_service()` to create a Kubernetes service for Kubecost. This service exposes the `kubecost-cost-analyzer` deployment as a LoadBalancer service on port 80, targeting port 9090. - -6. The `create_kc_service()` function uses the `kubectl expose` command to create the service. - -7. Finally, the script calls the `kc_installer()` function to start the installation process. - -## Robusta Installer - -This Bash script installs Robusta in a Kubernetes cluster using Helm. Robusta is an open-source tool used for remediation and mitigation of security vulnerabilities in Kubernetes clusters. - -### Robusta Installation Steps - -1. Import configuration variables and common functions using the `source` command. -2. Print a prompt to the user using the `print_prompt()` function to initiate the installation of Robusta in the cluster. -3. Install Robusta using Helm by running the `rb_installer()` function, which performs the following tasks: - - Adds the Robusta Helm repository using `helm repo add` command. - - Updates the Helm repository using `helm repo update` command. - - Installs Robusta chart using `helm install` command with custom values from `generated_values.yaml` file and creates a Kubernetes namespace called `robusta`. - - Waits for the `robusta-runner` and `robusta-forwarder` deployments to be available in the `robusta` namespace using `kubectl wait` command with a timeout of 1 hour. - - Calls the `watch_runner_logs()` function to watch the logs of `robusta-runner` pod and waits for the actions to be loaded. - - Logs a success message when Robusta is successfully installed. -4. Loads Robusta custom remediation actions by running the `load_playbook_actions()` function, which performs the following tasks: - - Logs a message to indicate the loading of playbook actions. - - Pushes the playbook actions from the specified playbook directory using `robusta playbooks push` command with the `robusta` namespace. - - Logs a success message when playbook actions are loaded. -5. Exits the script with a status code of 0. - -## Pixie Installer - -This Bash script installs Pixie, a tool used for monitoring and observability in a containerized environment using Helm. Helm is a package manager for Kubernetes that allows for easy deployment of applications as Helm charts. - -### Pixie Installation Steps - -The main installation process is done through the `px_installer()` function in the script. This function performs the following steps: - -1. Installs the Pixie binary on the container by running the `install.sh` script with the `bash` command and passing "y" as an input to automatically confirm the installation. The `&>/dev/null` redirects the output to null, so there is no console output. -2. Sets up the Helm repository for Pixie by adding the `pixie-operator` repository and updating the Helm repository. -3. Installs Pixie using Helm by running the `helm install` command with the `pixie-operator/pixie-operator-chart` chart and passing the `PX_DEPLOY_KEY` and `clusterName` as parameters. The `--namespace` flag specifies the namespace for the installation and `--create-namespace` creates the namespace if it doesn't exist. -4. Validates the deployment of Pixie components in three namespaces (`OLMNS`, `PXOPNS`, and `PLNS`) using the `validate_healthy_deployment()` function. -5. Checks the status of the Pixie Vizier using a while loop and the `px get viziers` command with the `jq` tool to parse the JSON output. It waits until the Vizier status is "1", indicating that Pixie has been deployed successfully. -6. Logs a success message and exits with status code 0. - -Note: The specific values for `PX_DEPLOY_KEY` and `clusterName` may need to be adjusted based on the environment and requirements of the installation. diff --git a/src/installer/kc-installer.sh b/src/installer/kc-installer.sh deleted file mode 100755 index 038b955..0000000 --- a/src/installer/kc-installer.sh +++ /dev/null @@ -1,66 +0,0 @@ -#!/bin/bash - -# Copyright (c) 2023, Xgrid Inc, https://xgrid.co - -# 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. - -source /src/config/kc-config.sh -source /src/commons/common-functions.sh - -# Installer function for Kubecost -kc_installer() { - - # This function checks if Kubecost is already installed in the cluster. If it is not installed, it installs Kubecost. - - kubecostDeploy=$(kubectl -n "${KC_NAMESPACE[@]}" get deploy --no-headers 2>&1 | grep -i "${EXPECTED_KC_DEPLOY}" | awk '{print $1}') - - if [ "$kubecostDeploy" == "${EXPECTED_KC_DEPLOY}" ]; then - log "${GREEN}[INFO]" "[INSTALLER]" "Kubecost already found in the cluster.${CC}" - exit 0 - else - log "${RED}[ERROR]" "[INSTALLER]" "Unable to find Kubecost Deployment in the cluster. Installing Kubecost now...${CC}" - - # Add Kubecost Helm repository and install Kubecost - if ! helm repo add kubecost https://kubecost.github.io/cost-analyzer/ &>/dev/null; then - log "${RED}[ERROR]" "[INSTALLER]" "Failed to add Kubecost Helm repository. Exiting.${CC}" - exit 1 - fi - - if ! helm install kubecost kubecost/cost-analyzer -n "${KC_NAMESPACE[@]}" --create-namespace &>/dev/null; then - log "${RED}[ERROR]" "[INSTALLER]" "Failed to install Kubecost. Exiting.${CC}" - exit 1 - fi - - # Wait for Kubecost Prometheus server and cost analyzer pod to be ready - if ! kubectl -n "${KC_NAMESPACE[@]}" wait pod --for=condition=Ready -l component=server -l app=cost-analyzer --timeout=1h &>/dev/null; then - log "${RED}[ERROR]" "[INSTALLER]" "Failed to wait for Kubecost pods to be ready. Exiting.${CC}" - exit 1 - fi - - create_kc_service - fi -} -create_kc_service() { - - # This function creates a service for Kubecost UI. - - if ! kubectl -n "${KC_NAMESPACE[@]}" expose deployment "${EXPECTED_KC_DEPLOY}" --port=80 --target-port=9090 --name="${KC_UI_SVC}" --type=LoadBalancer &>/dev/null; then - log "${RED}[ERROR]" "[INSTALLER]" "Failed to create Kubecost UI service. Exiting.${CC}" - exit 1 - else - log "${GREEN}[INFO]" "[INSTALLER]" "Kubecost UI service created successfully.${CC}" - fi - -} -# Calling functions defined above -kc_installer diff --git a/src/installer/px-installer.sh b/src/installer/px-installer.sh deleted file mode 100644 index 28477f2..0000000 --- a/src/installer/px-installer.sh +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env bash - -# Copyright (c) 2023, Xgrid Inc, https://xgrid.co - -# 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. - -source /src/config/px-config.sh -source /src/config/config.sh -source /src/commons/common-functions.sh - -# Installing pixie on a containerized environment using Helm. -px_installer() { - - # This function installs Pixie on the cluster using Helm. - - # Install Pixie binary on the container - log "${CYAN}[INFO]" "[INSTALLER]" "Downloading Pixie binary on the container.${CC}" - if ! echo "y" | bash -c "$(curl -fsSL https://withpixie.ai/install.sh)" &>/dev/null; then - log "${RED}[ERROR]" "[INSTALLER]" "Failed to download Pixie binary. Exiting.${CC}" - exit 1 - else - log "${GREEN}[INFO]" "[INSTALLER]" "Pixie binary downloaded successfully.${CC}" - fi - - # Deploy Pixie using Helm - log "${CYAN}[INFO]" "[INSTALLER]" "Deploying Pixie using Helm.${CC}" - if ! helm repo add pixie-operator https://pixie-operator-charts.storage.googleapis.com &>/dev/null; then - log "${RED}[ERROR]" "[INSTALLER]" "Failed to add Pixie Helm repository. Exiting.${CC}" - exit 1 - fi - - if ! helm repo update &>/dev/null; then - log "${RED}[ERROR]" "[INSTALLER]" "Failed to update Pixie Helm repository. Exiting.${CC}" - exit 1 - fi - - if ! helm install pixie pixie-operator/pixie-operator-chart --set deployKey="$PX_DEPLOY_KEY" --set clusterName=xgrid-website-migration --namespace pl --create-namespace &>/dev/null; then - log "${RED}[ERROR]" "[INSTALLER]" "Failed to deploy Pixie using Helm. Exiting.${CC}" - exit 1 - fi - - # Wait for Pixie deployments to become available - log "${CYAN}[INFO]" "[INSTALLER]" "Waiting for Pixie deployments to become available.${CC}" - if ! validate_healthy_deployment "$OLMNS"; then - log "${RED}[ERROR]" "[INSTALLER]" "Failed to wait for OLM namespace deployment to become available. Exiting.${CC}" - exit 1 - fi - - if ! validate_healthy_deployment "$PXOPNS"; then - log "${RED}[ERROR]" "[INSTALLER]" "Failed to wait for Pixie operator namespace deployment to become available. Exiting.${CC}" - exit 1 - fi - - if ! validate_healthy_deployment "$PLNS"; then - log "${RED}[ERROR]" "[INSTALLER]" "Failed to wait for Pixie namespace deployment to become available. Exiting.${CC}" - exit 1 - fi - - # Check for vizier status - vizierStatus=0 - while [ "$vizierStatus" -ne 1 ]; do - if px get viziers -o json | jq -c '. | select(.ClusterName == "xgrid-website-migration") | .Status' &>/dev/null; then - vizierStatus=1 - fi - done - - log "${GREEN}[PASSED]" "[INSTALLER]" "Pixie has been deployed to your cluster.${CC}" - exit 0 -} - -px_installer diff --git a/src/installer/rb-actions/my_playbook_repo/delete_persistent_volume.py b/src/installer/rb-actions/my_playbook_repo/delete_persistent_volume.py deleted file mode 100644 index eda406c..0000000 --- a/src/installer/rb-actions/my_playbook_repo/delete_persistent_volume.py +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright (c) 2023, Xgrid Inc, https://xgrid.co - -# 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. - -""" -Module for deleting a persistent volume -""" -import logging - -from robusta.api import ( - Finding, - FindingSource, - FindingType, - MarkdownBlock, - PersistentVolumeEvent, - action, -) - - -@action -def delete_persistent_volume(event: PersistentVolumeEvent): - """ - Deletes a persistent volume - """ - # Check if the persistent volume is present - if not event.get_persistentvolume(): - # Log an error message if the volume is not found - logging.error("Failed to get the persistent volume for deletion") - return - # Get the persistent volume - persistentVolume = event.get_persistentvolume() - # Get the name of the persistent volume - pvName = persistentVolume.metadata.name - persistentVolume.delete() - - # Create a Finding object to store and send the details of the deleted volume - functionName = "delete_persistent_volume" - finding = Finding( - title="Persistent volume deleted", - source=FindingSource.MANUAL, - aggregation_key=functionName, - finding_type=FindingType.REPORT, - failure=False, - ) - # Add a MarkdownBlock to the finding object to display the deleted volume name - finding.add_enrichment( - [ - MarkdownBlock(f"{pvName} deleted."), - ] - ) - event.add_finding(finding) diff --git a/src/installer/rb-actions/pyproject.toml b/src/installer/rb-actions/pyproject.toml deleted file mode 100644 index 55e7efe..0000000 --- a/src/installer/rb-actions/pyproject.toml +++ /dev/null @@ -1,15 +0,0 @@ -[tool.poetry] -name = "my_playbook_repo" -version = "0.0.1" -description = "" -authors = ["xkops "] - -# if playbook requires additional dependencies -[tool.poetry.dependencies] - -[tool.poetry.dev-dependencies] -robusta-cli = "^0.8.9" - -[build-system] -requires = ["poetry-core>=1.0.0"] -build-backend = "poetry.core.masonry.api" diff --git a/src/installer/rb-installer.sh b/src/installer/rb-installer.sh deleted file mode 100644 index 1f4efcd..0000000 --- a/src/installer/rb-installer.sh +++ /dev/null @@ -1,101 +0,0 @@ -#!/bin/bash - -# Copyright (c) 2023, Xgrid Inc, https://xgrid.co - -# 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. - -# This script installs Robusta in a Kubernetes cluster using Helm. - -# Import configuration variables and common functions. -source /src/config/rb-config.sh -source /src/commons/common-functions.sh - -# Print a prompt to the user. -print_prompt() { - - - log "${CYAN}[INFO]" "[INSTALLER]" "Initiating installation of Robusta in your cluster.${CC}" -} - -# Install Robusta using Helm. -rb_installer() { - - # This function installs Robusta using Helm. - - # Installing Robusta using Helm - if ! helm repo add robusta https://robusta-charts.storage.googleapis.com &>/dev/null; then - log "${RED}[ERROR]" "[INSTALLER]" "Failed to add Robusta Helm repository. Exiting.${CC}" - exit 1 - fi - - if ! helm repo update &>/dev/null; then - log "${RED}[ERROR]" "[INSTALLER]" "Failed to update Robusta Helm repository. Exiting.${CC}" - exit 1 - fi - - if ! helm install robusta robusta/robusta -f "$PREFLIGHT_DIR_PATH/generated_values.yaml" -n robusta --create-namespace &>/dev/null; then - log "${RED}[ERROR]" "[INSTALLER]" "Failed to install Robusta using Helm. Exiting.${CC}" - exit 1 - fi - - # Waiting for Robusta deployments to become available - if ! kubectl -n robusta wait deployment robusta-runner robusta-forwarder --for=condition=Available --timeout=1h &>/dev/null; then - log "${RED}[ERROR]" "[INSTALLER]" "Failed to wait for Robusta deployments to become available. Exiting.${CC}" - exit 1 - fi - - # Watching runner logs - watch_runner_logs - -} - -#Load robusta custom remediation actions -load_playbook_actions() { - - # This function loads the playbook actions. - - log "${CYAN}[INFO]" "[INSTALLER]" "Loading playbook actions.${CC}" - - # Pushing our playbook action - if ! robusta playbooks push "$PLAYBOOK_DIR_PATH" --namespace="${RB_NAMESPACE[0]}" >/dev/null; then - log "${RED}[ERROR]" "[INSTALLER]" "Failed to load playbook actions. Exiting.${CC}" - exit 1 - fi - - log "${CYAN}[INFO]" "[INSTALLER]" "Playbook actions loaded.${CC}" - exit 0 -} - -watch_runner_logs() { - - # This function watches logs of robusta-runner and checks if all the actions are loaded. - # TODO: Add timeout if actions aren't loaded. - - if ! kubectl -n robusta logs -f -l app=robusta-runner | while read -r line; do - echo "$line" | grep -E ".*Serving Flask app 'robusta.runner.web'.*" &>/dev/null - result=$? - if [ $result -eq 0 ]; then - pkill -f "kubectl -n robusta logs -f -l app=robusta-runner" - break - fi - done; then - log "${RED}[ERROR]" "[INSTALLER]" "Failed to watch runner logs. Exiting.${CC}" - exit 1 - fi - -} - -# Run the script. -print_prompt -rb_installer -load_playbook_actions diff --git a/src/pre-flight/README.md b/src/pre-flight/README.md deleted file mode 100644 index abf9df7..0000000 --- a/src/pre-flight/README.md +++ /dev/null @@ -1,52 +0,0 @@ -# Preflights - -## Kubecost Preflight - -This Bash code is used to initiate pre-flight checks for Kubecost installation to a K8s cluster. The code checks if kubectl is installed in the cluster, installs kubectl if not configured, and proceeds to execute other pre-flight checks. - -### Kubecost Preflight Functions - -#### `kubectl_checker()` - -This function is defined to check if kubectl is installed in the K8s cluster or not. If kubectl is not configured, the function calls the `kubectl_installer()` function to install kubectl in the K8s cluster. - -#### `kubectl_installer()` - -This function is defined to install kubectl in the K8s cluster. It downloads the latest release of kubectl, installs it, creates a new directory for the binary, and adds kubectl to the PATH. - -## Robusta Preflight - -This script is a bash script that performs pre-flight checks for installing Robusta in a cluster. It checks for a generated_values.yaml file and generates it if it does not exist. The script also checks if Robusta CLI is installed and installs it if it is not. It ends with a check for the default storage class of the cluster and unpatches it if it has been patched before. - -### Robusta Preflight Functions - -#### `check_values_file` - -This function checks for the existence of a generated_values.yaml file in the pre-flight directory. If the file exists, the function logs a message that the file was found. If the file does not exist, the function calls the `rb_cli_checker` function to check if Robusta CLI is installed. If it is installed, the function generates the `generated_values.yaml` file using the `generate_values_file` function. If it is not installed, the function logs an error message and exits the script. - -#### `rb_cli_checker` - -This function checks if Robusta CLI is installed by verifying the presence of the `robusta` command. If it is installed, the function logs an informational message that Robusta CLI is installed along with its version. If it is not installed, the function calls the `rb_cli_installer` function to install it. If the installation is successful, the function logs an informational message that Robusta CLI is installed along with its version. If it is not successful, the function logs an error message and exits the script. - -#### `rb_cli_installer` - -This function installs Robusta CLI by first checking if pip3 is installed. If it is installed, the function installs Robusta CLI using pip3. If pip3 is not installed, the function installs it and then installs Robusta CLI using pip3. If the installation is successful, the function returns. If it is not successful, the function logs an error message and exits the script. - -#### `generate_values_file` - -This function generates a `generated_values.yaml` file in the pre-flight directory using the `robusta gen-config` command. The function takes a filename as a parameter, which is used as the output path of the generated `generated_values.yaml` file. The function also enables the persistent volume in the `generated_values.yaml` file by adding the line `playbooksPersistentVolume: true` to the file if it is not already present. - -## Pixie Preflight - -This Bash script is designed to perform pre-flight checks for installing the Pixie tool on a Kubernetes cluster. It includes several functions and uses various commands to perform checks for Kubernetes version, Linux kernel version, CPU architecture, and available memory. - -### Pixie Preflight Functions - -#### `preFlight_checks` - -The `preFlight_checks()` function contains several checks to ensure that the Kubernetes cluster meets the requirements for Pixie installation. These checks are as follows: - -1. Kubernetes Version Check: The function checks the version of Kubernetes installed on the cluster. It does this by sending a request to the Kubernetes API server and parsing the response to extract the version. If the version is less than 1.21, the function exits with an error message. -2. Linux Kernel Version Check: The function checks the version of the Linux kernel installed on the node. It does this by using the `uname` command to get the kernel version and comparing it to the required version of 4.14+. If the version is less than 4.14, the function exits with an error message. -3. CPU Check: The function checks the architecture of the CPU. It does this by using the `lscpu` command to get the architecture and comparing it to the required architecture of x86_64. If the architecture is not x86_64, the function exits with an error message. -4. Memory Check: The function checks the amount of free memory available on the node. It does this by using the `free` command to get the amount of free memory and comparing it to the required amount of 1Gi per node. If the available memory is less than 1Gi, the function exits with an error message. diff --git a/src/pre-flight/kc-pre-flight/kc-preflight.sh b/src/pre-flight/kc-pre-flight/kc-preflight.sh deleted file mode 100755 index 240849d..0000000 --- a/src/pre-flight/kc-pre-flight/kc-preflight.sh +++ /dev/null @@ -1,66 +0,0 @@ -#!/bin/bash - -# Copyright (c) 2023, Xgrid Inc, https://xgrid.co - -# 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. - -source /src/config/kc-config.sh -source /src/commons/common-functions.sh - -print_prompt() { - log "${CYAN}[INFO]" "[PRE-FLIGHT]" "Initiating pre-flight checks for Kubecost installation to your K8s cluster.${CC}" -} - -# Function to check if kubectl is installed in the cluster or not - -kubectl_checker() { - log "${CYAN}[INFO]" "[PRE-FLIGHT]" "Checking if kubectl is configured.${CC}" - if command -v kubectl &>/dev/null; then - log "${GREEN}[INFO]" "kubectl configurations are obtained successfully.${CC}" - else - log "${RED}[ERROR]" "[PRE-FLIGHT]" "kubectl is not configured. Installing now.${CC}" - kubectl_installer # Function call to install kubectl - fi -} - -# Function to install kubectl in the cluster -kubectl_installer() { - - # This function downloads latest release of kubectl and installs. - - log "${CYAN}[INFO]" "[PRE-FLIGHT]" "Installing kubectl.${CC}" - - # Downloads latest release of kubectl and installs - if ! curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"; then - log "${RED}[ERROR]" "[PRE-FLIGHT]" "Failed to download kubectl. Exiting.${CC}" - exit 1 - fi - - if ! chmod +x ./kubectl; then - log "${RED}[ERROR]" "[PRE-FLIGHT]" "Failed to make kubectl executable. Exiting.${CC}" - exit 1 - fi - - if ! mkdir -p "$HOME"/bin && cp ./kubectl "$HOME"/bin/kubectl && export PATH=$PATH:$HOME/bin; then - log "${RED}[ERROR]" "[PRE-FLIGHT]" "Failed to install kubectl. Exiting.${CC}" - exit 1 - fi - - log "${GREEN}[INFO]" "[PRE-FLIGHT]" "kubectl is installed successfully.${CC}" -} - -# Calling functions defined above -print_prompt -kubectl_checker -helm_checker -unpatch_default_storageclass diff --git a/src/pre-flight/px-pre-flight/px-preflight.sh b/src/pre-flight/px-pre-flight/px-preflight.sh deleted file mode 100644 index 8d993df..0000000 --- a/src/pre-flight/px-pre-flight/px-preflight.sh +++ /dev/null @@ -1,68 +0,0 @@ -#!/bin/bash - -# Copyright (c) 2023, Xgrid Inc, https://xgrid.co - -# 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. - -source /src/config/config.sh -source /src/commons/common-functions.sh - -print_prompt() { - log "${CYAN}[INFO]" "[PRE-FLIGHT]" "Initiating pre-flight checks for Pixie installation to your K8s cluster.${CC}" -} - -preFlight_checks() { - # 1. Kubernetes Version Check. Supports 1.21+ - version=$(curl -s "$KUBERNETES_API_SERVER_URL/version" \ - --cacert "$CA_CERT_PATH" \ - --header "${HEADERS[@]}" | - grep -oP '(?<="gitVersion": ")[^"]*') - mapfile -t kVers < <(curl -s "$KUBERNETES_API_SERVER_URL/version" \ - --cacert "$CA_CERT_PATH" \ - --header "${HEADERS[@]}" | - grep -oP '(?<="gitVersion": ")[^"]*' | tr -d 'v' | tr '.' '\n') - - if [[ "${kVers[0]}" -lt 1 && "${kVers[1]}" -le 21 ]]; then - log "${RED}[ERROR]" "[PRE-FLIGHT]" "Unsupported K8s Version: $version. Pixie supports Kubernetes version 1.21+.${CC}" - exit 1 - fi - - # 2. Linux Kernel Version Check. Supports 4.14+ - majorVer=$(awk -F . '{print $1}' <<<"$(uname -r)") - minorVer=$(awk -F . '{print $2}' <<<"$(uname -r)") - - if [[ $majorVer -lt 4 && $minorVer -le 14 ]]; then - log "${RED}[ERROR]" "[PRE-FLIGHT]" "Unsupported Linux Kernel Version: $(uname -r).Pixie supports Linux Kernel version 4.14+.${CC}" - exit 1 - fi - - #3. CPU check. Supports x86_64. - cpuArch="$(lscpu | grep Architecture | awk '{print $2}')" - reqArch="x86_64" - if [[ "$cpuArch" != "$reqArch" ]]; then - log "${RED}[ERROR]" "[PRE-FLIGHT]" "Unsupported Architecture: $cpuArch.Pixie only supports x86_64 architecture.${CC}" - exit 1 - fi - - #4. Memory Check. Requires 1Gi per node. - freeMem=$(free -g | awk '{print $4}' | head -2 | tail -1) - if [ "$freeMem " -lt 0 ]; then - log "${RED}[ERROR]" "[PRE-FLIGHT]" "Not enough memory on the node. Pixie requires minimum 1 Gib memory per node.${CC}" - exit 1 - fi - log "${GREEN}[PASSED]" "[PRE-FLIGHT]" "Pre-flights checks passed. Initiating installation...${CC}" -} - -print_prompt -preFlight_checks -helm_checker diff --git a/src/pre-flight/rb-pre-flight/rb-preflight.sh b/src/pre-flight/rb-pre-flight/rb-preflight.sh deleted file mode 100644 index 8e1f287..0000000 --- a/src/pre-flight/rb-pre-flight/rb-preflight.sh +++ /dev/null @@ -1,124 +0,0 @@ -#!/bin/bash - -# Copyright (c) 2023, Xgrid Inc, https://xgrid.co - -# 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. - -source /src/config/rb-config.sh -source /src/commons/common-functions.sh - -print_prompt() { - log "${CYAN}[INFO]" "[PRE-FLIGHT]" "Initiating pre-flight checks for Robusta installation in your cluster.${CC}" -} - -check_values_file() { - - # This function checks if a generated_values.yaml file exists in the pre-flight directory. If the file does not exist, the function generates the file using rb_cli_checker and generate_values_file. - - log "${CYAN}[INFO]" "[PRE-FLIGHT]" "Searching for generated_values.yaml files.${CC}" - log "${CYAN}[INFO]" "[PRE-FLIGHT]" "Pre-flight Directory: $PREFLIGHT_DIR_PATH${CC}." - - # Check if file exists in current directory (generated_values.yaml) - if [[ -f "$PREFLIGHT_DIR_PATH/$HELM_VALUES" ]]; then - log "${GREEN}[INFO]" "[PRE-FLIGHT]" "$HELM_VALUES found at $PREFLIGHT_DIR_PATH.${CC}" - else - # File not found - # Check robusta-tool installation - # Generate generated_values.yaml file - log "${CYAN}[INFO]" "[PRE-FLIGHT]" "$HELM_VALUES not found in $PREFLIGHT_DIR_PATH. Generating...${CC}" - if ! rb_cli_checker; then - log "${RED}[ERROR]" "[PRE-FLIGHT]" "Robusta CLI check failed. Exiting.${CC}" - exit 1 - fi - if ! generate_values_file "$HELM_VALUES"; then - log "${RED}[ERROR]" "[PRE-FLIGHT]" "Failed to generate $HELM_VALUES. Exiting.${CC}" - exit 1 - fi - if [[ -f "$PREFLIGHT_DIR_PATH/$HELM_VALUES" ]]; then - log "${GREEN}[INFO]" "[PRE-FLIGHT]" "generated_values.yaml generated at $PREFLIGHT_DIR_PATH${CC}." - else - log "${RED}[ERROR]" "[PRE-FLIGHT]" "generated_values.yaml not generated at $PREFLIGHT_DIR_PATH.${CC} Exiting...${CC}" - log "${CYAN}[INFO]" "[PRE-FLIGHT]" "Use Robusta CLI to create generated_values.yaml file at $PREFLIGHT_DIR_PATH.${CC}" - exit 1 - fi - fi -} - -rb_cli_checker() { - - # This function checks if the Robusta CLI is installed and configured properly. If the Robusta CLI is not installed, the function will install it using rb_cli_installer. - - log "${CYAN}[INFO]" "[PRE-FLIGHT]" "Checking if Robusta-CLI is configured.${CC}" - - # Check if robusta-cli tool is present - if command -v robusta &>/dev/null; then - log "${GREEN}[INFO]" "[PRE-FLIGHT]" "Robusta CLI is installed. $(robusta version).${CC}" - return 0 - else - # Install robusta-cli if not present - log "${CYAN}[INFO]" "[PRE-FLIGHT]" "Robusta CLI not found. Installing...${CC}" - if ! rb_cli_installer; then - log "${RED}[ERROR]" "[PRE-FLIGHT]" "Failed to install Robusta CLI. Exiting...${CC}" - log "${CYAN}[INFO]" "[PRE-FLIGHT]" "Install Robusta CLI.${CC}" - exit 1 - fi - if command -v robusta &>/dev/null; then - log "${GREEN}[INFO]" "[PRE-FLIGHT]" "Robusta CLI is installed. $(robusta version).${CC}" - return 0 - else - log "${RED}[ERROR]" "[PRE-FLIGHT]" "Robusta CLI failed to install. Exiting...${CC}" - log "${CYAN}[INFO]" "[PRE-FLIGHT]" "Install Robusta CLI.${CC}" - exit 1 - fi - fi -} - -rb_cli_installer() { - #This functions installs pip and robusta-cli. Pip is required to install robusta-cli - if command -v pip3 &>/dev/null; then - pip3 install -U robusta-cli --no-cache &> /dev/null - return - else - log "${CYAN}[INFO]" "[PRE-FLIGHT]" "Python3-pip not found. Installing...${CC}" - chmod 1777 /tmp - apt update - apt-get install -y python3-pip &> /dev/null - if command -v pip3 &>/dev/null; then - pip3 install -U robusta-cli --no-cache &> /dev/null - return - else - log "${RED}[ERROR]" "[PRE-FLIGHT]" "pip3 failed to install. Exiting...${CC}" - log "${CYAN}[INFO]" "[PRE-FLIGHT]" "Install pip3.${CC}" - exit 1 - fi - fi -} - -generate_values_file() { - # Param $1 is filename - # Used as output path of generated_values.yaml filepath/filename (filepath is coming from $PATH) - - log "${CYAN}[INFO]" "[PRE-FLIGHT]" "Generating generated_values.yaml file.${CC}" - - printf '%s\n' n n n y y n | robusta gen-config --slack-api-key "$SLACK_API_KEY" --slack-channel "$SLACK_CHANNEL_NAME" --cluster-name "$CLUSTER_NAME" --output-path "$PREFLIGHT_DIR_PATH/$1" > /dev/null - # enabling persistent volume in robusta configuration file - if ! grep -q 'playbooksPersistentVolume: true' "$PREFLIGHT_DIR_PATH/$HELM_VALUES"; then - sed -i '4i\playbooksPersistentVolume: true' "$PREFLIGHT_DIR_PATH/$HELM_VALUES" - fi -} - -print_prompt -check_values_file -helm_checker -unpatch_default_storageclass -exit 0 diff --git a/src/remediation/unclaimed-volumes/unclaimed-volume-tests/unclaimed-volume-test.sh b/src/remediation/unclaimed-volumes/unclaimed-volume-tests/unclaimed-volume-test.sh deleted file mode 100755 index 25b32b8..0000000 --- a/src/remediation/unclaimed-volumes/unclaimed-volume-tests/unclaimed-volume-test.sh +++ /dev/null @@ -1,77 +0,0 @@ -#!/bin/bash - -# Copyright (c) 2023, Xgrid Inc, https://xgrid.co - -# 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. - -unclaimedPv="" - -# This function get_unclaimed_volume is used to retrieve the list of unclaimed Persistent Volumes in a Kubernetes cluster using the Kubecost API -get_unclaimed_volume() { - # Define the API endpoint of Kubecost for Persistent volumes - endpoint="http://kubecost-cost-analyzer.kubecost.svc.cluster.local:9003/allPersistentVolumes" - - # Make a GET request to the API using curl - response=$(curl -s "$endpoint") - - # Parse the response to get the list of all volumes - pvNames=$(echo "$response" | jq '.items[].metadata.name') - - # Gets statuses of all available volumes - pvStatuses=$(echo "$response" | jq '.items[].status.phase') - - # Iterate over the list of statuses and get the name of those pv's whose status is available - - for i in $(echo "$pvStatuses" | grep -n "Available"); do - lineNum=$(echo "$i" | cut -d: -f1) - name=$(echo "$pvNames" | sed -n "${lineNum}p") - unclaimedPv=${name//\"/} - done - -} - -# This function trigger_robusta_action is used to trigger a delete action on an unclaimed Persistent Volume in a Kubernetes cluster using the Robusta API -trigger_robusta_action() { - # Trigger robusta action to delete unclaimed volume - result=$(curl -X POST http://robusta-runner.robusta.svc.cluster.local/api/trigger -H 'Content-Type: application/json' -d '{"action_name": "delete_persistent_volume", "action_params": {"name": "'"$unclaimedPv"'"}}' 2>/dev/null | head -n 1) - - # Check the response of the delete action if the response is success, it means pv is deleted successfully. - # We use this check to confirm if volume is deleted successfully - if echo "$result" | grep -q '"success":true'; then - echo "Unclaimed PV $unclaimedPv deleted successfully" - else - echo "PV not deleted" - exit 1 - fi -} - -# Verify the deletion of PV through Kubecost API -# This function verifies the deleted volume. It stores already retireved volume in a separate variable and then gets pvs data through get_unclaimed_volume function -verify_deletion() { - # We store already retrieved pv name in a variable to compare later - oldPv=$unclaimedPv - unclaimedPv="" - # Get unclaimed volume from kubecost - get_unclaimed_volume - - if [[ "$oldPv" == "$unclaimedPv" ]]; then - echo "Volume $oldPv is not deleted" - exit 1 - else - echo "PV deleted and confirmed" - fi -} - -get_unclaimed_volume -trigger_robusta_action -verify_deletion diff --git a/src/remediation/unclaimed-volumes/unclaimed-volume-tests/unclaimed-volume.yaml b/src/remediation/unclaimed-volumes/unclaimed-volume-tests/unclaimed-volume.yaml deleted file mode 100644 index 85623fd..0000000 --- a/src/remediation/unclaimed-volumes/unclaimed-volume-tests/unclaimed-volume.yaml +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (c) 2023, Xgrid Inc, https://xgrid.co - -# 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. - ---- -apiVersion: v1 -kind: PersistentVolume -metadata: - name: new-pv-volume3 - labels: - type: local -spec: - storageClassName: local-storage - capacity: - storage: 10Gi - accessModes: - - ReadWriteOnce - hostPath: - path: "/mnt/data" - diff --git a/src/rollback/kc-rollback.sh b/src/rollback/kc-rollback.sh deleted file mode 100644 index bb39753..0000000 --- a/src/rollback/kc-rollback.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/bash - -# Copyright (c) 2023, Xgrid Inc, https://xgrid.co - -# 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. - -source /src/commons/common-functions.sh -source /src/config/config.sh -source /src/config/kc-config.sh - -print_prompt() { - log "${CYAN}[INFO]" "[ROLLBACK]" "Initiating rollback of Kubecost in your cluster.${CC}" -} - -#Rollback function for kubecost -kc_rollback() { - - # This function uninstalls and deletes the kubecost helm release and namespace using helm uninstall and kubectl delete commands, respectively. - - - if kubectl delete namespace "${KC_NAMESPACE[@]}" &>/dev/null; then - log "${GREEN}[PASSED]" "[ROLLBACK]" "Kubecost namespace has been deleted from your cluster.${CC}" - else - log "${RED}[ERROR]" "[ROLLBACK]" "Failed to delete Kubecost namespace from your cluster. Exiting...${CC}" - exit 1 - fi - log "${GREEN}[PASSED]" "[ROLLBACK]" "Kubecost has been deleted from your cluster${CC}" -} - -print_prompt -kc_rollback diff --git a/src/rollback/px-rollback.sh b/src/rollback/px-rollback.sh deleted file mode 100644 index 534591d..0000000 --- a/src/rollback/px-rollback.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash - -# Copyright (c) 2023, Xgrid Inc, https://xgrid.co - -# 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. - -source /src/commons/common-functions.sh -source /src/config/config.sh -source /src/config/px-config.sh - -#waits for 5 minutes and then call rollback functions. -pixie_rollback() { - - # This function uninstalls and deletes the pixie helm release and namespace using helm uninstall and kubectl delete commands, respectively. - - if helm uninstall pixie -n "${PLNS}" &>/dev/null; then - log "${GREEN}[PASSED]" "[ROLLBACK]" "Pixie has been uninstalled from your cluster.${CC}" - else - log "${RED}[ERROR]" "[ROLLBACK]" "Failed to uninstall Pixie from your cluster. Exiting...${CC}" - exit 1 - fi - - if kubectl delete namespace "${PLNS}" &>/dev/null; then - log "${GREEN}[PASSED]" "[ROLLBACK]" "Pixie namespace has been deleted from your cluster.${CC}" - else - log "${RED}[ERROR]" "[ROLLBACK]" "Failed to delete Pixie namespace from your cluster. Exiting...${CC}" - exit 1 - fi - log "${GREEN}[INFO]" "[ROLLBACK]" "Pixie has been deleted from your cluster${CC}" -} - -pixie_rollback diff --git a/src/rollback/rb-rollback.sh b/src/rollback/rb-rollback.sh deleted file mode 100644 index 18a8bb1..0000000 --- a/src/rollback/rb-rollback.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/bin/bash - -# Copyright (c) 2023, Xgrid Inc, https://xgrid.co - -# 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. - -source /src/config/config.sh -source /src/commons/common-functions.sh -source /src/config/rb-config.sh - -print_prompt() { - log "${CYAN}[INFO]" "[ROLLBACK]" "Initiating rollback of Robusta in your cluster.${CC}" -} -rb_rollback() { - - # This function uninstalls and deletes the robusta helm release and namespace using helm uninstall and kubectl delete commands, respectively. If Helm is not installed, the function logs an error message and exits. - - if command -v helm &>/dev/null; then - #Uninstall robusta using helm - if helm uninstall robusta -n "${RB_NAMESPACE[@]}" > /dev/null; then - log "${GREEN}[PASSED]" "[ROLLBACK]" "Robusta has been uninstalled from your cluster.${CC}" - else - log "${RED}[ERROR]" "[ROLLBACK]" "Failed to uninstall Robusta from your cluster. Exiting...${CC}" - exit 1 - fi - - if kubectl delete namespace "${RB_NAMESPACE[@]}" > /dev/null; then - log "${GREEN}[PASSED]" "[ROLLBACK]" "Robusta namespace has been deleted from your cluster.${CC}" - else - log "${RED}[ERROR]" "[ROLLBACK]" "Failed to delete Robusta namespace from your cluster. Exiting...${CC}" - exit 1 - fi - log "${GREEN}[PASSED]" "[ROLLBACK]" "Robusta has been deleted from your cluster${CC}" - else - # If Helm is not installed, print an error message and exit. - log "${RED}[ERROR]" "[ROLLBACK]" "Helm is not installed. Exiting...${CC}" - log "${CYAN}[INFO]" "[ROLLBACK]" "Install Helm.${CC}" - exit 1 - fi -} - -print_prompt -rb_rollback diff --git a/src/test/kc-test.sh b/src/test/kc-test.sh deleted file mode 100755 index 63d8102..0000000 --- a/src/test/kc-test.sh +++ /dev/null @@ -1,83 +0,0 @@ -#!/bin/bash - -# Copyright (c) 2023, Xgrid Inc, https://xgrid.co - -# 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. - -source /src/commons/common-functions.sh -source /src/config/config.sh -source /src/config/kc-config.sh - -print_prompt() { - log_test "${CYAN}[INFO]" "[TEST]" "Initiating test plan for Kubecost.${CC}" - -} - -# This Function checks whether kubectl_cost plugin is installed or not -# This plugin is used to get cost data from the cluster. -check_kubectl_cost_plugin() { - - # Check if kubectl_cost plugin is installed - if command -v kubectl-cost &>/dev/null; then - log_test "${GREEN}[PASSED]" "[TEST]" "kubectl cost plugin already installed.${CC}" - else - log_test "${RED}[FAILED]" "[TEST]" "Kubecost cost plugin isn't installed in the cluster.${CC}" - log_test "${CYAN}[INFO]" "[TEST]" "Installing kubectl cost plugin...${CC}" - install_kubectl_cost - fi - # Checks if kubectl_cost plugin can retrieve cost data using kubecost-cost-analyzer service. - log_test "${CYAN}[INFO]" "[TEST]" "Verifying kubecost installation using kubectl cost plugin...${CC}" - get_cost - -} - -# Function to print UI links for all three tools to access their respective dashboards -#TODO: To be removed from here. -print_UI_links() { - log_test "${CYAN}[INFO]" "[TEST]" "Robusta UI: ${GREEN}https://platform.robusta.dev/${CC}" - log_test "${CYAN}[INFO]" "[TEST]" "Pixie UI: ${GREEN}https://work.withpixie.ai/live/clusters/xgrid-website-migration${CC}" - KC_LOADBALANCER=$(kubectl get svc kubecost-ui-service -n "${KC_NAMESPACE[@]}" -o jsonpath='{.status.loadBalancer.ingress[0].hostname}') - log_test "${CYAN}[INFO]" "[TEST]" "Kubecost UI:${GREEN} $KC_LOADBALANCER ${CC}" -} - -# This Function uses kubecost-cost-analyzer service to get cost data from the cluster. -# If the data is retrieved successfully, it means kubecost is installed in the cluster and can access cluster resources. -get_cost() { - - _=$(kubectl cost namespace --show-all-resources 2>&1) - if kubectl_exit_code=$? && [ $kubectl_exit_code -eq 0 ]; then - # Command succeeded it means kubecost is installed successfully - log_test "${GREEN}[PASSED]" "[TEST]" "Kubecost is installed successfully on your cluster.${CC}" - print_UI_links - exit 0 - else - # command failed which means kubecost is not installed successfully - log_test "${RED}[FAILED]" "[TEST]" "Kubecost is not installed on your cluster.${CC}" - exit 1 - fi -} - -# Function to install kubectl_cost plugin in the cluster -install_kubectl_cost() { - os=$(uname | tr '[:upper:]' '[:lower:]') && - arch=$(uname -m | tr '[:upper:]' '[:lower:]' | sed -e s/x86_64/amd64/) && - curl -s -L https://github.com/kubecost/kubectl-cost/releases/latest/download/kubectl-cost-"$os"-"$arch".tar.gz | tar xz -C /tmp && - chmod +x /tmp/kubectl-cost && - mv /tmp/kubectl-cost /usr/local/bin/kubectl-cost - log_test "${GREEN}[PASSED]" "[TEST]" "kubectl cost plugin installed successfully${CC}" -} - -print_prompt -# Kubecost namespace is passed to the function. -pod_status_verifier "${KC_NAMESPACE[@]}" -check_kubectl_cost_plugin diff --git a/src/test/px-test.sh b/src/test/px-test.sh deleted file mode 100644 index bc85524..0000000 --- a/src/test/px-test.sh +++ /dev/null @@ -1,89 +0,0 @@ -#!/bin/bash - -# Copyright (c) 2023, Xgrid Inc, https://xgrid.co - -# 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. - -source /src/commons/common-functions.sh -source /src/config/config.sh -source /src/config/px-config.sh - -print_prompt() { - log_test "${CYAN}[INFO]" "[TEST]" "Initiating test plan for Pixie. ${CC}" - -} - -# This function checks the status of the Pixie Vizier component. The healthy status is defined by status "CS_HEALTHY" -check_vizier() { - - log_test "${CYAN}[INFO]" "[TEST]" "Pixie Check Vizier. ${CC}" - - # Authentication of pixie CLI - px auth login --api_key "$PX_API_KEY" &>/dev/null - - #TODO: [noman-xg] check specifically against the name of the cluster instead of tailing. - # Get status of Vizier. - vizierStatus=$(px get viziers -o json | jq -c '. | select(.ClusterName == "${CLUSTER_NAME}") | .Status') - - # check if Vizier is in healthy state or not. - if [ "$vizierStatus" -ne 1 ]; then - log_test "${RED}[FAILED]" "[TEST]" "Pixie Vizier is not in healthy state.${CC}" - else - log_test "${GREEN}[PASSED]" "[TEST]" "Pixie Vizier is in healthy state.${CC}" - - fi -} - -# This function creates a test pod in the test namespace and then queries for the test pod using Pixie binary pxl script. -# The result of the query returns true/false which determines whether Pixie is functional inside the cluster or not. -px_demo_action() { - - log_test "${CYAN}[INFO]" "[TEST]" "Pixie Demo Action. ${CC}" - podName="$PX_TEST_NS/$TEST_POD" - - #check whether the test namespace exists already or not - if ! kubectl get namespace "$PX_TEST_NS" &>/dev/null; then - # Create a namespace for testing. - kubectl create namespace "$PX_TEST_NS" &>/dev/null - fi - - #check whether the test-pod in test namespace exists or not - if ! kubectl get pod "$podName" -n "$PX_TEST_NS" &>/dev/null; then - # Create a pod in the test namespace - kubectl create -f /src/manifests/test-pod.yaml &>/dev/null - fi - - #import kubeconfig -- dependancy to run px run command. - aws eks update-kubeconfig --region ap-southeast-1 --name xgrid-website-migration &>/dev/null - - # Execute a Pxl script to get pods info in a certain namespace. If it returns true, it means Pixie is successfully deployed and actively monitoring the cluster. - podFound=$(px run px/pods -o json -- --namespace "$PX_TEST_NS" | jq --arg pod_name "$podName" 'select(._tableName_ == "Pods List") and select(.pod == $podName)') - - if [[ $podFound == "true" ]]; then - - log_test "${GREEN}[PASSED]" "[TEST]" "Successfully queried for test pod using Pixie. ${CC}" - else - log_test "${RED}[FAILED]" "[TEST]" "Could not query for test pod using Pixie. ${CC}" - - fi - - #clean up - rm -rf root/.kube/config &>/dev/null - kubectl delete namespace "$PX_TEST_NS" &>/dev/null -} - -print_prompt -# An array of Pixie namespaces is passed to the function. -pod_status_verifier "${PX_NAMESPACES[@]}" -check_vizier -px_demo_action diff --git a/src/test/rb-test.sh b/src/test/rb-test.sh deleted file mode 100755 index fa38787..0000000 --- a/src/test/rb-test.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/bash - -# Copyright (c) 2023, Xgrid Inc, https://xgrid.co - -# 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. - -source /src/commons/common-functions.sh -source /src/config/config.sh -source /src/config/rb-config.sh - -print_prompt() { - log_test "${CYAN}[INFO]" "[TEST]" "Initiating test plan for Robusta. ${CC}" - -} - -check_robusta_pod_status() { - # checks if the pods in all the robusta namespaces are runnning or not. - pod_status_verifier "${RB_NAMESPACE[@]}" -} - -check_robusta_actions() { - node=$(kubectl get nodes -o jsonpath='{.items[0].metadata.name}') - results=$(robusta playbooks trigger node_running_pods_enricher name="$node" --namespace "${RB_NAMESPACE[0]}") - substring='"success":true' - if echo "$results" | grep -q "$substring"; then - log_test "${GREEN}[PASSED]" "[TEST]" "Robusta actions are working.${CC}" - log "${GREEN}[INFO]" "[INSTALLER]" "Robusta successfully installed.${CC}" - - exit 0 - else - log_test "${RED}[ERROR]" "[TEST]" "Robusta actions are not working.${CC}" - exit 1 - fi - -} - -print_prompt -check_robusta_pod_status -check_robusta_actions diff --git a/src/testing/ebs-provisioning-test.py b/src/testing/ebs-provisioning-test.py deleted file mode 100644 index 8952ff0..0000000 --- a/src/testing/ebs-provisioning-test.py +++ /dev/null @@ -1,91 +0,0 @@ -# Copyright (c) 2023, Xgrid Inc, https://xgrid.co - -# 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. - -import json -import subprocess - -import boto3 - - -def test_iam_role_creation(): - """ - Test if the IAM role is created by Terraform. - - The test uses Boto3 to check if the IAM role with the given name exists. If the IAM role exists, - the test passes. If the IAM role does not exist, the test fails. - - Raises: - AssertionError: If the IAM role with the given name does not exist. - """ - iamClient = boto3.client("iam") - roleName = "XkOps-EBS-iam-role" - try: - roleResponse = iamClient.get_role(RoleName=roleName) - assert roleResponse["Role"]["RoleName"] == roleName - except iamClient.exceptions.NoSuchEntityException: - assert False, f"IAM role {roleName} does not exist" - - -def test_iam_policy_attachment(): - """ - Test if the IAM policy is attached to the IAM role created by Terraform. - - The test uses Boto3 to check if the IAM role with the given name has the expected policy attached. - If the IAM role has the expected policy attached, the test passes. If the IAM role does not have - the expected policy attached, the test fails. - - Raises: - AssertionError: If the IAM role does not have the expected policy attached. - """ - iamClient = boto3.client("iam") - roleName = "XkOps-EBS-iam-role" - policyArn = "arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy" - attachedPoliciesResponse = iamClient.list_attached_role_policies( - RoleName=roleName - ) - for policy in attachedPoliciesResponse["AttachedPolicies"]: - if policy["PolicyArn"] == policyArn: - break - else: - assert False, f"IAM role {roleName} does not have the expected policy attached" - - -def test_addon_addition(): - """ - Test whether the Amazon EBS CSI Driver add-on is added to an existing EKS cluster. - - This test checks whether the specified add-on is present in the cluster using the AWS CLI. - The test fails if the add-on is not present or if there is an error in the AWS CLI command. - - Assumes that the add-on has been successfully added to the cluster using Terraform. - - Raises: - AssertionError: If the add-on is not present in the cluster or if there is an error in the AWS CLI command. - - """ - clusterName = "xkops-cluster-2" - addonName = "aws-ebs-csi-driver" - region = "ap-southeast-1" - try: - addonInfo = subprocess.check_output( - [ - f"aws eks describe-addon --addon-name {addonName} --cluster-name {clusterName} --region {region}" - ], - shell=True, - text=True, - ) - addonInfo = json.loads(addonInfo) - assert addonInfo["addon"]["addonName"] == addonName - except subprocess.CalledProcessError: - assert False, f"Addon {addonName} does not exist" diff --git a/terraform-scripts/Dockerfile b/terraform-scripts/Dockerfile deleted file mode 100644 index b5a662c..0000000 --- a/terraform-scripts/Dockerfile +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright (c) 2023, Xgrid Inc, https://xgrid.co - -# 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. - -# Use an official Terraform image as the base -FROM hashicorp/terraform:light - -# Set working directory -WORKDIR /app - -# Copy the Terraform script into the container -COPY ebs-provisioning.tf . - -# Initialize Terraform and download the required provider -RUN terraform init diff --git a/terraform-scripts/README b/terraform-scripts/README deleted file mode 100644 index bccce42..0000000 --- a/terraform-scripts/README +++ /dev/null @@ -1,42 +0,0 @@ -# Overview - -This Terraform code is used to specify the version of the AWS provider that will be used, to create and manage EC2 instances in an AWS account. It also creates an IAM role with a trust relationship set as ebs-csi-controller-sa, attaches the AmazonEBSCSIDriverPolicy to the IAM role and adds Amazon EBS CSI Driver add-on in the existing EKS cluster provided. - -## Dependencies - -This code is written in Terraform and requires version 0.12.0 or higher. The AWS provider version 4.0 or higher is required to interact with AWS resources. - -## Usage - -To use this code, you need to pass the following input values via Helm: - -- `REGION`: The region value is picked from the environment. -- `CLUSTER_NAME`: The cluster name on which the Terraform scripts will be run. - -The Terraform code can be executed by running `terraform apply`. - -## Code Details - -### Terraform block - -The Terraform block specifies the required Terraform version and required providers. The required version of Terraform is 0.12.0 or higher. The required providers for this code are `aws` and `tls`. - -### Provider block - -The `aws` provider block is used to interact with AWS resources. The `region` parameter is specified using the value passed in via Helm. - -### Data blocks - -The `data` blocks are used to query AWS for information about the existing resources. The `aws_eks_cluster` data block queries the EKS cluster for information about the cluster with the provided name. The `aws_iam_openid_connect_provider` data block queries the EKS cluster for the OpenID Connect Provider URL. - -### Locals block - -The `locals` block defines a local variable `eks_oidc_provider` which is set to the `data.aws_iam_openid_connect_provider.eks` value. - -### Resource blocks - -The `aws_iam_role` resource block creates an IAM Role with the name "XkOps-EBS-iam-role". The trust relationship is set as `ebs-csi-controller-sa`. The `assume_role_policy` is defined using a `jsonencode` function to create the required JSON structure. - -The `aws_iam_role_policy_attachment` resource block attaches the AmazonEBSCSIDriverPolicy to the IAM Role created above. - -The `aws_eks_addon` resource block adds Amazon EBS CSI Driver add-on in the existing EKS cluster provided above. The `addon_name`, `cluster_name` and `service_account_role_arn` parameters are specified using the values from input variables and previously created resources. diff --git a/terraform-scripts/ebs-provisioning.tf b/terraform-scripts/ebs-provisioning.tf deleted file mode 100644 index 8371c2c..0000000 --- a/terraform-scripts/ebs-provisioning.tf +++ /dev/null @@ -1,90 +0,0 @@ -# Copyright (c) 2023, Xgrid Inc, https://xgrid.co - -# 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. - -# This code is written in Terraform and is used to specify the version of the AWS provider that will be used. The source of the provider is "hashicorp/aws" and the version must be 4.0 or higher. -terraform { - required_version = ">= 0.12.0" - required_providers { - aws = { - source = "hashicorp/aws" - version = "~> 4.0" - } - tls = { - source = "hashicorp/tls" - version = "3.1.0" - } - } -} - -# The AWS provider is used to interact with resources in an AWS account, such as creating and managing EC2 instances. -# var is used here to pick the region value from environment which will be feeded through input values in helm. -variable "REGION" { - type = string -} -variable "CLUSTER_NAME" { - type = string -} -provider "aws" { - region = var.REGION -} - -# Cluster name for terraform scripts to be run on -data "aws_eks_cluster" "Eks_cluster" { - name=var.CLUSTER_NAME -} - -data "aws_iam_openid_connect_provider" "eks" { - url = data.aws_eks_cluster.Eks_cluster.identity[0].oidc[0].issuer -} - -locals { - eks_oidc_provider = data.aws_iam_openid_connect_provider.eks -} - -# Creates IAM Role with trust relationship set as ebs-csi-controller-sa -resource "aws_iam_role" "eks_cluster" { - name = "XkOps-EBS-iam-role" - # Adds trust relationship in the role with ebs-csi-controller service account - assume_role_policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Action = "sts:AssumeRoleWithWebIdentity" - Effect = "Allow" - Principal = { - Federated = local.eks_oidc_provider.arn - } - Condition = { - StringEquals = { - "${replace(data.aws_eks_cluster.Eks_cluster.identity[0].oidc[0].issuer, "https://", "")}:aud" = "sts.amazonaws.com", - "${replace(data.aws_eks_cluster.Eks_cluster.identity[0].oidc[0].issuer, "https://", "")}:sub" = "system:serviceaccount:kube-system:ebs-csi-controller-sa" - } - } - } - ] - }) -} - -# Attaches AmazonEBSCSIDriverPolicy to above created IAM Role -resource "aws_iam_role_policy_attachment" "eks_cluster_ebs_policy_attachment" { - policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy" - role = aws_iam_role.eks_cluster.name -} - -# Adds Amazon EBS CSI Driver add-on in the existing EKS cluster provided above -resource "aws_eks_addon" "ebs_csi" { - addon_name = "aws-ebs-csi-driver" - cluster_name = data.aws_eks_cluster.Eks_cluster.name - service_account_role_arn = aws_iam_role.eks_cluster.arn -} diff --git a/terraform-scripts/user-creation.tf b/terraform-scripts/user-creation.tf deleted file mode 100644 index 4ad83b6..0000000 --- a/terraform-scripts/user-creation.tf +++ /dev/null @@ -1,78 +0,0 @@ -# Copyright (c) 2023, Xgrid Inc, https://xgrid.co - -# 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. - -# Declare the AWS provider with the region -provider "aws" { - region = "ap-southeast-1" # Change this to the region you prefer -} - -# Create an AWS IAM user named "XkOps_user" -#checkov:skip=CKV_AWS_273 -resource "aws_iam_user" "XkOps_user" { - name = "XkOps_user" -} - -# Create an IAM user policy named "XkOps-user-policy" and attach it to the "XkOps_user" user -#checkov:CKV_AWS_40 -resource "aws_iam_user_policy" "XkOps_user_policy" { - name = "XkOps-user-policy" - user = aws_iam_user.XkOps_user.name - - # Define the policy in JSON format - policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Effect = "Allow" - Action = [ - # List of AWS actions that are allowed by this policy - "cloudformation:ListStacks", - "cloudformation:CreateStack", - "cloudformation:GetStackPolicy", - "cloudformation:DescribeStacks", - "cloudformation:ListStackResources", - "iam:CreatePolicy", - "iam:GetOpenIDConnectProvider", - "iam:CreateOpenIDConnectProvider", - "iam:TagOpenIDConnectProvider", - "iam:DetachRolePolicy", - "iam:AttachRolePolicy", - "iam:GetRole", - "iam:DeleteRole", - "iam:ListOpenIDConnectProviders", - "iam:ListRolePolicies", - "iam:ListAttachedRolePolicies", - "iam:PassRole", - "ec2:CreateVolume", - "ec2:AttachVolume", - "ec2:DescribeVolumes", - "eks:DescribeCluster", - "eks:ListClusters", - "eks:AccessKubernetesApi", - "eks:TagResource", - "eks:CreateAddon", - "eks:ListAddons", - "eks:DescribeAddon", - "eks:DescribeAddonConfiguration", - "eks:DescribeAddonVersions", - "secretsmanager:CreateSecret", - "secretsmanager:DescribeSecret", - "secretsmanager:ListSecrets", - "secretsmanager:PutSecretValue" - ] - Resource = "*" # Allow actions on any AWS resource - } - ] - }) -} From 4b8f092f2c165d72b0d358ff72e57af77c3cf7c3 Mon Sep 17 00:00:00 2001 From: Syed Murtaza Hassan Date: Thu, 14 Nov 2024 03:42:09 +0500 Subject: [PATCH 2/2] Updated documentation formatting and images --- documentation/About XkOps/FAQs.md | 35 ++++---- documentation/About XkOps/Overview.md | 65 ++++++++------- documentation/About XkOps/Tools we use.md | 14 ++-- documentation/About XkOps/XkOps Roadmap.md | 24 ++---- documentation/HOME.md | 16 ++-- .../Installing XkOps/FAQs for Installation.md | 78 +++++++++++------- .../Installing XkOps/Helm Parameters.md | 20 +++-- .../Installing XkOps/Requirements.md | 9 +- .../Self Hosted Installation.md | 65 +++++++++++++++ .../Troubleshooting Installation Problems.md | 28 +++---- .../Using XkOps/Available Workflows.md | 54 ++++++++---- .../Creating Your Own Workflows.md | 8 +- .../Using XkOps/FAQs for Workflow Creation.md | 26 +++--- .../Using XkOps/Understanding Workflows.md | 24 +++--- documentation/Using XkOps/Using Workflows.md | 30 +++---- .../Using XkOps/Workdlow's Architecture.md | 8 -- .../Using XkOps/Workflow Creation Criteria.md | 14 ++-- .../Using XkOps/Workflow's Architecture.md | 12 +++ images/Demo-video.jpg | Bin 85723 -> 0 bytes images/general workflow architecture.png | Bin 0 -> 40186 bytes 20 files changed, 318 insertions(+), 212 deletions(-) delete mode 100644 documentation/Using XkOps/Workdlow's Architecture.md create mode 100644 documentation/Using XkOps/Workflow's Architecture.md delete mode 100644 images/Demo-video.jpg create mode 100644 images/general workflow architecture.png diff --git a/documentation/About XkOps/FAQs.md b/documentation/About XkOps/FAQs.md index 791fa16..10f5cdd 100644 --- a/documentation/About XkOps/FAQs.md +++ b/documentation/About XkOps/FAQs.md @@ -1,30 +1,31 @@ -General -What is XkOps +# General -Who created XkOps +- What is XkOps -What license is XkOps released under? +- Who created XkOps -Which Kubernetes environments are supported? +- What license is XkOps released under? -Can I completely self-host XkOps? +- Which Kubernetes environments are supported? -Installation -What are the requirements for installing XkOps? +- Can I completely self-host XkOps? -How do I set up a Kubernetes cluster for XkOps? +# Installation +- What are the requirements for installing XkOps? -How do I install the required tools for XkOps? +- How do I set up a Kubernetes cluster for XkOps? -How do I install XkOps? +- How do I install the required tools for XkOps? -Workflows -What are X-Workflows? +- How do I install XkOps? -How do the X-Workflows work? +# Workflows +- What are X-Workflows? -What types of workflows are present in XkOps? +- How do the X-Workflows work? -What are the query sources for XkOps? +- What types of workflows are present in XkOps? -How will I get notifications for any issue? \ No newline at end of file +- What are the query sources for XkOps? + +- How will I get notifications for any issue? \ No newline at end of file diff --git a/documentation/About XkOps/Overview.md b/documentation/About XkOps/Overview.md index 0305846..0ed0b06 100644 --- a/documentation/About XkOps/Overview.md +++ b/documentation/About XkOps/Overview.md @@ -12,68 +12,69 @@ XkOps streamlines the deployment and management of essential observability tools Beyond installation, XkOps empowers users to create powerful workflows by combining the capabilities of integrated tools. For instance, Kubecost’s cost optimization recommendations can be automatically implemented using Robusta actions. XkOps also offers pre-built workflows for common tasks like reclaiming unused persistent volumes, gathering node information, and addressing abandoned workloads. To facilitate data-driven insights, XkOps utilizes MongoDB as a centralized repository for storing recommendations, actions, and other relevant metrics. Users can further extend XkOps' functionality by defining custom workflows using values.yaml file. -XkOps Architecture +# XkOps Architecture -XkOps Architecture +![XkOps Architecture Diagram](../../images/arch%20diagram.png "Architecture Digram") -Pod for Querying Kubecost -A Kubernetes pod is deployed to periodically query Kubecost, which is a tool used for monitoring and optimizing Kubernetes costs. +## Pod for Querying Kubecost -The pod is configured to run based on a cron expression, which defines the schedule for querying Kubecost (e.g., every hour, daily, etc.). +- A Kubernetes pod is deployed to periodically query Kubecost, which is a tool used for monitoring and optimizing Kubernetes costs. -The pod's role is to fetch relevant cost data from Kubecost based on specific workflows. Workflows could be related to monitoring the cost of certain resources or workloads in the Kubernetes cluster. +- The pod is configured to run based on a cron expression, which defines the schedule for querying Kubecost (e.g., every hour, daily, etc.). -Once the data is retrieved, it is processed and stored in MongoDB, which serves as the persistent storage for cost-related data. MongoDB is ideal for this purpose due to its document-based structure, which allows flexible and efficient data storage. +- The pod's role is to fetch relevant cost data from Kubecost based on specific workflows. Workflows could be related to monitoring the cost of certain resources or workloads in the Kubernetes cluster. -The pod may also have logging mechanisms to ensure that queries and updates to MongoDB are tracked and can be monitored for future reference. +- Once the data is retrieved, it is processed and stored in MongoDB, which serves as the persistent storage for cost-related data. MongoDB is ideal for this purpose due to its document-based structure, which allows flexible and efficient data storage. -Backend API Endpoint +- The pod may also have logging mechanisms to ensure that queries and updates to MongoDB are tracked and can be monitored for future reference. -The backend is implemented using Flask, providing a lightweight yet powerful API service. One of the key API endpoints is designed to handle requests from the dashboard (the frontend). +## Backend API Endpoint -When users interact with the web dashboard (which is built using React), they may trigger specific workflows or actions related to cost analysis, system monitoring, or other operational tasks. +- The backend is implemented using Flask, providing a lightweight yet powerful API service. One of the key API endpoints is designed to handle requests from the dashboard (the frontend). -The API endpoint in the Flask backend receives these requests and processes them accordingly. The request may contain parameters, such as which workflow to execute or what cost data to retrieve. +- When users interact with the web dashboard (which is built using React), they may trigger specific workflows or actions related to cost analysis, system monitoring, or other operational tasks. -Once the API processes the request, it triggers a call to Robusta to perform the necessary action based on the input from the dashboard. +- The API endpoint in the Flask backend receives these requests and processes them accordingly. The request may contain parameters, such as which workflow to execute or what cost data to retrieve. -Interaction with Robusta +- Once the API processes the request, it triggers a call to Robusta to perform the necessary action based on the input from the dashboard. -Robusta is a Kubernetes-native automation and observability platform that can automate operational tasks. In this system, Robusta is used to perform actions based on requests from the Flask backend. +## Interaction with Robusta -When the backend API triggers a call to Robusta, it provides the necessary details (e.g., which workflow to execute, actions to perform, etc.). +- Robusta is a Kubernetes-native automation and observability platform that can automate operational tasks. In this system, Robusta is used to perform actions based on requests from the Flask backend. -Robusta responds by carrying out the specified task, such as restarting pods, scaling workloads, monitoring certain metrics, or adjusting resources based on cost optimization insights from Kubecost. +- When the backend API triggers a call to Robusta, it provides the necessary details (e.g., which workflow to execute, actions to perform, etc.). -Robusta operates seamlessly within the Kubernetes cluster, making it an ideal tool for automating complex operational workflows and ensuring that tasks are executed efficiently and reliably. +- Robusta responds by carrying out the specified task, such as restarting pods, scaling workloads, monitoring certain metrics, or adjusting resources based on cost optimization insights from Kubecost. -Updating MongoDB +- Robusta operates seamlessly within the Kubernetes cluster, making it an ideal tool for automating complex operational workflows and ensuring that tasks are executed efficiently and reliably. -After Robusta completes the action requested by the backend, it returns the results or status of the operation. +## Updating MongoDB -The backend processes the response and updates the MongoDB database accordingly. MongoDB stores the results of these workflows, which could include the outcomes of cost optimizations, resource adjustments, or other automated actions taken by Robusta. +- After Robusta completes the action requested by the backend, it returns the results or status of the operation. -By updating MongoDB, the system ensures that there is a persistent record of all actions taken, which can later be queried for auditing, reporting, or further analysis. +- The backend processes the response and updates the MongoDB database accordingly. MongoDB stores the results of these workflows, which could include the outcomes of cost optimizations, resource adjustments, or other automated actions taken by Robusta. -User Interaction via Dashboard +- By updating MongoDB, the system ensures that there is a persistent record of all actions taken, which can later be queried for auditing, reporting, or further analysis. -The web dashboard, built using React, serves as the frontend where users can interact with the system. +## User Interaction via Dashboard -From the dashboard, users can monitor cost data, trigger workflows, and view the results of actions taken by Robusta. The backend API acts as the intermediary between the dashboard and Robusta, ensuring that user requests are processed efficiently. +- The web dashboard, built using React, serves as the frontend where users can interact with the system. -The dashboard also provides visibility into the MongoDB data, allowing users to track cost trends, view action logs, and analyze the impact of changes made by Robusta. +- From the dashboard, users can monitor cost data, trigger workflows, and view the results of actions taken by Robusta. The backend API acts as the intermediary between the dashboard and Robusta, ensuring that user requests are processed efficiently. -Intended Audience +- The dashboard also provides visibility into the MongoDB data, allowing users to track cost trends, view action logs, and analyze the impact of changes made by Robusta. + +# Intended Audience XkOps mainly targets the following teams. -DevOps teams: DevOps teams are responsible for the deployment, scaling, and maintenance of containerized applications on Kubernetes. XkOps would provide these teams with the visibility and insights they need to ensure the performance and availability of the applications. +**DevOps teams:** DevOps teams are responsible for the deployment, scaling, and maintenance of containerized applications on Kubernetes. XkOps would provide these teams with the visibility and insights they need to ensure the performance and availability of the applications. -SRE teams: Site reliability engineers (SREs) are responsible for ensuring the reliability, scalability, and performance of production systems. XkOps would provide SREs with the necessary information to identify and diagnose performance issues in a Kubernetes cluster and make informed decisions about resource allocation and cost optimization. +**SRE teams:** Site reliability engineers (SREs) are responsible for ensuring the reliability, scalability, and performance of production systems. XkOps would provide SREs with the necessary information to identify and diagnose performance issues in a Kubernetes cluster and make informed decisions about resource allocation and cost optimization. -Operations teams: Operations teams are responsible for the day-to-day management and monitoring of production systems. XkOps would provide these teams with the visibility they need to proactively identify and address issues, and ensure the overall health and stability of the applications. +**Operations teams:** Operations teams are responsible for the day-to-day management and monitoring of production systems. XkOps would provide these teams with the visibility they need to proactively identify and address issues, and ensure the overall health and stability of the applications. -Cloud engineers and Architects: They are responsible for designing and maintaining cloud-based infrastructures for their organizations, XkOps could provide them with a unified view of their k8s clusters and cloud-based resources and also offer cost forecasting and optimization recommendations that could help them make informed decisions about resource allocation and cost optimization. +**Cloud engineers and Architects:** They are responsible for designing and maintaining cloud-based infrastructures for their organizations, XkOps could provide them with a unified view of their k8s clusters and cloud-based resources and also offer cost forecasting and optimization recommendations that could help them make informed decisions about resource allocation and cost optimization. -Developers: Developers who write code for containerized applications could use XkOps to understand how their applications are performing, and the infrastructure they are running on. \ No newline at end of file +**Developers:** Developers who write code for containerized applications could use XkOps to understand how their applications are performing, and the infrastructure they are running on. \ No newline at end of file diff --git a/documentation/About XkOps/Tools we use.md b/documentation/About XkOps/Tools we use.md index 968b8cd..c61291e 100644 --- a/documentation/About XkOps/Tools we use.md +++ b/documentation/About XkOps/Tools we use.md @@ -1,19 +1,19 @@ -Kubecost +# Kubecost XkOps utilizes Kubecost for cost monitoring in Kubernetes environments, leveraging the OpenCost models originally developed and open-sourced by Kubecost. These models provide visibility into current and historical Kubernetes spending and resource allocation, offering cost transparency. -Robusta +# Robusta Incorporating Robusta into XkOps boosts cluster reliability and uptime by seamlessly connecting to the Kubernetes API and monitoring Kubernetes changes. Robusta, an open-source platform designed for Kubernetes troubleshooting and multi-cluster observability, actively listens to all events in your cluster, provides valuable insights on triggers behind alerts, and concurrent occurrences, and recommends actions. Robusta empowers Kubernetes administrators to efficiently identify, troubleshoot, and mitigate issues such as crashing pods, high CPU usage, and excessive memory consumption by enhancing existing alerts or creating new ones based on changes in your Kubernetes cluster. With the seamless integration of Robusta, Kubernetes administrators can confidently address and resolve various challenges, ensuring the smooth operation of their clusters. -Pixie +# Pixie Pixie, an open-source observability platform for Kubernetes clusters. that uses eBPF, improves reliability by providing effortless cluster-wide visibility into resources, performance, and events, eliminating the need for manual instrumentation or service restarts. With its plug-and-play architecture, Pixie offers granular observability, allowing users to quickly track cluster resources, analyze cluster events, and diagnose cluster problems. Its platform-wide observability enables users to see everything in their clusters, understand how nodes, pods, containers, and applications impact each other, and receive real-time alerts for critical issues, ensuring the efficient operation of Kubernetes clusters. -Links +# Links -Kubecost +- [Kubecost](https://docs.kubecost.com/) -Robusta +- [Robusta](https://docs.robusta.dev/master/) -Pixie \ No newline at end of file +- [Pixie](https://docs.px.dev/) \ No newline at end of file diff --git a/documentation/About XkOps/XkOps Roadmap.md b/documentation/About XkOps/XkOps Roadmap.md index d92bb8e..df29fd0 100644 --- a/documentation/About XkOps/XkOps Roadmap.md +++ b/documentation/About XkOps/XkOps Roadmap.md @@ -1,33 +1,21 @@ -XkOps Roadmap +# XkOps Roadmap The vision of XkOps is to provide a comprehensive observability strategy using state-of-the-art tools to achieve cost optimization, reliability/uptime, and security for cloud and k8s environments. Xgrid offers a range of services, including a checklist for assessing CRS, single-packaged installers for implementing observability to achieve each of the CRS requirements, and an all-in-one image containing all the tools required for a complete observability strategy. -Expand Cloud Support: - +## Expand Cloud Support: At present, XkOps exclusively caters to EKS clusters. However, a key objective is to broaden the scope of XkOps to encompass multiple cloud providers and self-hosted Kubernetes clusters. -XkOps Documentation Website: - +## XkOps Documentation Website: Currently, XkOps lacks an official website providing comprehensive documentation. A key objective is to develop a website that offers users access to critical details regarding XkOps, including an overview, architecture, community, installation instructions, operational details, troubleshooting tips, developer guides, and other relevant information. -XkOps CLI Tool: - +## XkOps CLI Tool: At present, users must use distinct CLI tools to generate API keys for XkOps installation. For instance, to install Robusta via XkOps, the user must first install Robusta CLI and then generate its API keys. Similarly, for Pixie, a separate Pixie CLI must be downloaded to generate Pixie keys. The goal is to simplify this process by creating a unified XkOps CLI tool. This tool would enable users to download only one XkOps tool to generate API keys for all necessary components, eliminating the need for multiple separate tools. - - -Revamping of XkOps Dashboard: - +## Revamping of XkOps Dashboard: One of the main objectives is to revamp the XkOps Dashboard to provide a better user experience and improved functionality. The revamped dashboard is designed to streamline the process of gaining valuable insights into your Kubernetes cluster. With the new interface, users can easily view cost spend metrics, performance of their applications, performance of Kubernetes infrastructure components, and security posture, including alerts, misconfigurations, threats, and vulnerabilities SLAs.  - - -More remediation workflows: - +## More remediation workflows: Currently, XkOps provides only one workflow for the remediation of unclaimed volumes. However, as we continue to develop and improve the platform, we recognize the need to introduce more workflows that align with Kubecost’s recommendations. Our team is working hard to bring you additional workflows that will help you optimize your Kubernetes environment and reduce costs. We understand the importance of providing flexible and customizable solutions that adapt to your unique needs. - - -Ref: https://docs.pixielabs.ai/about-pixie/roadmap/ \ No newline at end of file diff --git a/documentation/HOME.md b/documentation/HOME.md index 13e74dd..98a4ee2 100644 --- a/documentation/HOME.md +++ b/documentation/HOME.md @@ -4,17 +4,17 @@ XkOps is a powerful and comprehensive solution that simplifies Kubernetes observ XkOps has multiple features designed to enhance Kubernetes observability, cost management, security, and more. These features are tailored to provide comprehensive insights and control over your Kubernetes clusters. -Features -Observability: XkOps would provide detailed visibility into the behavior and performance of the applications running in the Kubernetes cluster, including metrics, logs, and traces. This would allow teams to identify and diagnose performance issues, such as bottlenecks or errors, and respond quickly to ensure the availability and stability of the applications. +## Features -Cost management: XkOps would provide cost and usage insights, including granular breakdowns of resource usage, and the ability to identify and address over-provisioning and under-utilization in a Kubernetes cluster. It would also provide cost forecasting and optimization recommendations, allowing teams to plan for future growth and manage expenses more effectively. +**Observability:** XkOps would provide detailed visibility into the behavior and performance of the applications running in the Kubernetes cluster, including metrics, logs, and traces. This would allow teams to identify and diagnose performance issues, such as bottlenecks or errors, and respond quickly to ensure the availability and stability of the applications. -Security: XkOps would provide detailed visibility into the behavior and performance of the applications deployed in a Kubernetes cluster, which can help teams identify and address security issues, such as misconfigurations or vulnerabilities. Additionally, XkOps would provide alerts and notifications of suspicious activity, allowing teams to respond quickly to potential security threats. +**Cost management:** XkOps would provide cost and usage insights, including granular breakdowns of resource usage, and the ability to identify and address over-provisioning and under-utilization in a Kubernetes cluster. It would also provide cost forecasting and optimization recommendations, allowing teams to plan for future growth and manage expenses more effectively. -Alerting: XkOps would provide alerting capability to notify teams of issues in any Kubernetes cluster in near real-time, enabling them to quickly address problems before they become critical. +**Security:** XkOps would provide detailed visibility into the behavior and performance of the applications deployed in a Kubernetes cluster, which can help teams identify and address security issues, such as misconfigurations or vulnerabilities. Additionally, XkOps would provide alerts and notifications of suspicious activity, allowing teams to respond quickly to potential security threats. -Integration with other tools: XkOps would integrate with other observability tools such as Prometheus, Grafana, and Elasticsearch. +**Alerting:** XkOps would provide alerting capability to notify teams of issues in any Kubernetes cluster in near real-time, enabling them to quickly address problems before they become critical. -Reporting: XkOps would allow for generating reports to share with stakeholders about the Kubernetes cluster's cost, performance, and security over time. +**Integration with other tools:** XkOps would integrate with other observability tools such as Prometheus, Grafana, and Elasticsearch. + +**Reporting:** XkOps would allow for generating reports to share with stakeholders about the Kubernetes cluster's cost, performance, and security over time. -Links (To be added after other documents are completed) diff --git a/documentation/Installing XkOps/FAQs for Installation.md b/documentation/Installing XkOps/FAQs for Installation.md index 81b9d78..a237629 100644 --- a/documentation/Installing XkOps/FAQs for Installation.md +++ b/documentation/Installing XkOps/FAQs for Installation.md @@ -1,53 +1,67 @@ -Prerequisites +# Prerequisites -What are the basic requirements for installing XkOps? -To install XkOps, you'll need a Kubernetes cluster with the cluster-admin role or equivalent permissions, and Helm 3.x or later installed on your system. +- **What are the basic requirements for installing XkOps?** -Do I need a specific Kubernetes version for XkOps? -XkOps is compatible with Kubernetes versions 1.18 and above. For optimal performance and feature support, it's recommended to use the latest stable Kubernetes version. + To install XkOps, you'll need a Kubernetes cluster with the cluster-admin role or equivalent permissions, and Helm 3.x or later installed on your system. -Can I use a managed Kubernetes service for XkOps? -Yes, XkOps can be installed on the most popular managed Kubernetes services like Amazon EKS, Google Kubernetes Engine (GKE), and Azure Kubernetes Service (AKS). +- **Do I need a specific Kubernetes version for XkOps?** -Installation + XkOps is compatible with Kubernetes versions 1.18 and above. For optimal performance and feature support, it's recommended to use the latest stable Kubernetes version. -How do I add the XkOps Helm repository? -To add the XkOps Helm repository, run the following command in your terminal: +- **Can I use a managed Kubernetes service for XkOps?** -helm repo add xkops https://xgridinc.github.io/xkops/charts/stable + Yes, XkOps can be installed on the most popular managed Kubernetes services like Amazon EKS, Google Kubernetes Engine (GKE), and Azure Kubernetes Service (AKS). +# Installation -How do I install XkOps using Helm? -Install XkOps using the following Helm command: +- **How do I add the XkOps Helm repository?** -helm install my-xkops xkops/xkops -f values.yaml + To add the XkOps Helm repository, run the following command in your terminal: + + ``` + helm repo add xkops https://xgridinc.github.io/xkops/charts/stable + ``` +- **How do I install XkOps using Helm?** - Replace my-xkops with your desired release name and specify your custom values file if needed. + Install XkOps using the following Helm command: + + ``` + helm install my-xkops xkops/xkops -f values.yaml + ``` -Can I customize the XkOps installation? -Yes, you can customize the XkOps installation by modifying the values.yaml file. This allows you to adjust resource requests, image tags, and other configuration options. + Replace my-xkops with your desired release name and specify your custom values file if needed. -Verification +- **Can I customize the XkOps installation?** -How can I verify if XkOps is installed successfully? -Check the status of XkOps deployments and pods using kubectl get deployments and kubectl get pods. All components should have a status of "Running". + Yes, you can customize the XkOps installation by modifying the values.yaml file. This allows you to adjust resource requests, image tags, and other configuration options. -Troubleshooting +# Verification -What if some XkOps components are not running? -Inspect the logs of the failing components using kubectl logs . Check for error messages indicating configuration or resource issues. +- **How can I verify if XkOps is installed successfully?** -How do I troubleshoot connectivity issues between XkOps components? -Inspect Kubernetes service definitions and network policies. Verify pod network connectivity using tools like kubectl exec and curl. + Check the status of XkOps deployments and pods using kubectl get deployments and kubectl get pods. All components should have a status of "Running". -Can I uninstall XkOps and reinstall it? -Yes, you can uninstall XkOps using helm uninstall and then reinstall it using the same command. +# Troubleshooting -General +- **What if some XkOps components are not running?** -Do I need any specific network configurations for XkOps? -Ensure your Kubernetes cluster has network connectivity to required external services (e.g., container registries) and that necessary ports are open. + Inspect the logs of the failing components using kubectl logs . Check for error messages indicating configuration or resource issues. -Where can I find additional support resources for XkOps? -Refer to the official XkOps documentation, and community forums, or contact XkOps support for assistance. \ No newline at end of file +- **How do I troubleshoot connectivity issues between XkOps components?** + + Inspect Kubernetes service definitions and network policies. Verify pod network connectivity using tools like kubectl exec and curl. + +- **Can I uninstall XkOps and reinstall it?** + + Yes, you can uninstall XkOps using helm uninstall and then reinstall it using the same command. + +# General + +- **Do I need any specific network configurations for XkOps?** + + Ensure your Kubernetes cluster has network connectivity to required external services (e.g., container registries) and that necessary ports are open. + +- **Where can I find additional support resources for XkOps?** + + Refer to the official XkOps documentation, and community forums, or contact XkOps support for assistance. \ No newline at end of file diff --git a/documentation/Installing XkOps/Helm Parameters.md b/documentation/Installing XkOps/Helm Parameters.md index eea56de..d956b9e 100644 --- a/documentation/Installing XkOps/Helm Parameters.md +++ b/documentation/Installing XkOps/Helm Parameters.md @@ -1,47 +1,53 @@ This document outlines the different methods for configuring XkOps using Helm parameters. These parameters allow you to customize the deployment behavior of XkOps to suit your specific needs. -Understanding Helm Parameters +## Understanding Helm Parameters Helm charts offer a way to package Kubernetes applications and their configurations. XkOps utilizes a values.yaml file to store default configuration values for the deployed components. These values can be overridden during installation using Helm parameters. -Passing Helm Parameters +### Passing Helm Parameters There are two primary methods for passing Helm parameters when installing XkOps: -Using Command-Line Flags ( --set ) +### 1. Using Command-Line Flags ( --set ) This approach allows you to directly specify individual parameters while executing the Helm install command. Here's an example: +``` helm install my-xkops xkops/xkops \ --repo https://xgridinc.github.io/xkops/charts/stable \ --namespace xkops \ --create-namespace \ --set cluster.name={{CLUSTER_NAME}} \ --set apiServer.serviceClusterIPRange={{SERVICE_CLUSTER_IP_RANGE}} +``` In this example, we set the cluster name to and specify a custom service cluster IP range using the apiServer.serviceClusterIPRange parameter. -Customizing the values.yaml file +### 2. Customizing the values.yaml file You can modify the default values.yaml file downloaded from the provided location (link can be added later) to customize various parameters. This approach allows for more complex configurations and avoids cluttering the install command with numerous flags. Here's a basic example structure of a modified values.yaml file: +```` cluster: name: {{CLUSTER_NAME}} apiServer: serviceClusterIPRange: {{SERVICE_CLUSTER_IP_RANGE}} +```` Once the values.yaml file is customized, use the following command to install XkOps with your specific configuration: +```` helm install my-xkops xkops/xkops \ --repo https://xgridinc.github.io/xkops/charts/stable \ --namespace xkops \ --create-namespace \ -f values.yaml +```` -Choosing the Right Method: +### Choosing the Right Method: -Simple Configuration: For setting a few specific parameters, using command-line flags offers a concise approach. +**Simple Configuration:** For setting a few specific parameters, using command-line flags offers a concise approach. -Complex Configuration: For extensive customization or managing multiple XkOps deployments with different configurations, modifying the values.yaml file is more efficient. \ No newline at end of file +**Complex Configuration:** For extensive customization or managing multiple XkOps deployments with different configurations, modifying the values.yaml file is more efficient. \ No newline at end of file diff --git a/documentation/Installing XkOps/Requirements.md b/documentation/Installing XkOps/Requirements.md index ebcd78d..bc195a7 100644 --- a/documentation/Installing XkOps/Requirements.md +++ b/documentation/Installing XkOps/Requirements.md @@ -2,13 +2,16 @@ This document outlines the requirements for installing XkOps on the Kubernetes c To install XkOps, ensure that the following prerequisites are met: -Helm 3.x +**Helm 3.x** + Helm 3.x must be installed on your system. Helm is a package manager for Kubernetes that simplifies the deployment and management of applications within Kubernetes clusters. You can download and install Helm 3.x from the official Helm website. -Kubectl Command-Line Tool +**Kubectl Command-Line Tool** + Kubectl must be installed and configured on your system. Kubectl is the command-line tool for interacting with Kubernetes clusters, allowing you to deploy and manage applications within the cluster. You can download and set up kubectl by following the instructions on the Kubernetes website. -Permissions for Kubernetes Resource Creation +**Permissions for Kubernetes Resource Creation** + Ensure you have the necessary permissions to deploy Kubernetes manifests on the cluster. This typically requires roles with sufficient privileges, such as "ClusterAdmin" or a custom role configured to allow creating, modifying, and managing resources within the cluster. diff --git a/documentation/Installing XkOps/Self Hosted Installation.md b/documentation/Installing XkOps/Self Hosted Installation.md index e69de29..4decc11 100644 --- a/documentation/Installing XkOps/Self Hosted Installation.md +++ b/documentation/Installing XkOps/Self Hosted Installation.md @@ -0,0 +1,65 @@ +This document outlines the installation steps for XkOps, XkOps is a powerful and comprehensive solution that simplifies Kubernetes observability challenges. It combines popular open-source tools like Kubecost, Robusta, and Pixie into a single Helm chart. This results in improved cost control, reliability, and security in cluster operations, ultimately enhancing observability. + +**Target Audience:** This guide is intended for users familiar with Kubernetes and Helm package management. + +## Prerequisites + +**A Kubernetes Cluster:** Ensure you have a functioning Kubernetes cluster with the necessary permissions to deploy pods and resources. + +**Helm:** Helm should be installed and operational on your system. Refer to the official Helm documentation for installation instructions: Helm Docs. + +## Installation Steps + +**1. Add the XkOps Helm Repository:** + +Open your terminal and execute the following command to add the XkOps Helm repository: + +``` +helm repo add xkops https://xgridinc.github.io/xkops/charts/stable +``` + +**2. Obtain the Default values.yaml** + +A default values.yaml file containing pre-configured settings for XkOps is available at XkOps values.yaml. Download this file and customize it according to your specific requirements. + +**3. Customize the values.yaml (Optional)** + +Open the downloaded values.yaml file in a text editor. Review and modify the parameters as needed. Refer to the XkOps documentation for detailed explanations of each parameter. + +**Note:** This step is optional. If you're satisfied with the default settings, you can proceed to the next step. + +**4. Install XkOps with Helm** + +Run the following Helm command to install XkOps in your Kubernetes cluster: + +``` +helm install my-xkops xkops/xkops -f values.yaml +``` + +Replace my-xkops with your desired release name for the XkOps deployment. + +**5. Verification** + +Once the installation is complete, verify the successful deployment of XkOps components by checking the status of the pods: + +``` +kubectl get pods +``` + +This command will display a list of pods running in your cluster. Look for pods with names related to XkOps. The status of these pods should be "Running" to indicate a successful installation. + +## Additional Considerations + +- Refer to the XkOps documentation for detailed usage instructions, configuration options, and troubleshooting steps. + +- Consider using a configuration management tool like Ansible or Puppet to manage complex deployments and values.yaml configuration in large environments. + +- For security reasons, avoid storing sensitive information directly in the values.yaml file. Utilize environment variables or Kubernetes Secrets for sensitive data. + +## Next Steps + +With XkOps successfully installed, you can leverage its features to manage your Kubernetes cluster configurations. Explore the XkOps documentation for in-depth information on configuration options and usage examples. + +## Support: + +For further assistance or troubleshooting, refer to the XkOps documentation or contact the XkOps support team. \ No newline at end of file diff --git a/documentation/Installing XkOps/Troubleshooting Installation Problems.md b/documentation/Installing XkOps/Troubleshooting Installation Problems.md index fa47d1f..dac12e0 100644 --- a/documentation/Installing XkOps/Troubleshooting Installation Problems.md +++ b/documentation/Installing XkOps/Troubleshooting Installation Problems.md @@ -1,29 +1,29 @@ -Common Installation Issues +# Common Installation Issues -Installation Failures and Errors +## Installation Failures and Errors -Check Helm Installation and Configuration: Ensure Helm is correctly installed and configured on your system. Verify Helm version compatibility with XkOps requirements. +**Check Helm Installation and Configuration:** Ensure Helm is correctly installed and configured on your system. Verify Helm version compatibility with XkOps requirements. -Inspect Helm Logs: Review Helm installation logs for specific error messages. These can provide clues about the root cause of the failure. +**Inspect Helm Logs:** Review Helm installation logs for specific error messages. These can provide clues about the root cause of the failure. -Verify Kubernetes Cluster Health: Check the status of your Kubernetes cluster using kubectl to get nodes and kubectl to get pods. Ensure there are no critical issues affecting the cluster. +**Verify Kubernetes Cluster Health:** Check the status of your Kubernetes cluster using kubectl to get nodes and kubectl to get pods. Ensure there are no critical issues affecting the cluster. -Resource Constraints: Verify that your Kubernetes cluster has sufficient resources (CPU, memory) to accommodate the XkOps deployment. Adjust resource requests and limits in the values.yaml file if necessary. +**Resource Constraints:** Verify that your Kubernetes cluster has sufficient resources (CPU, memory) to accommodate the XkOps deployment. Adjust resource requests and limits in the values.yaml file if necessary. -Connectivity Issues +## Connectivity Issues -Check Network Connectivity: Ensure your Kubernetes cluster can reach external services required by XkOps components. Verify network policies and firewall rules. +**Check Network Connectivity:** Ensure your Kubernetes cluster can reach external services required by XkOps components. Verify network policies and firewall rules. -Inspect Kubernetes Service Definitions: Review the service definitions for XkOps components to ensure they are configured correctly. Check for port conflicts or incorrect service types. +**Inspect Kubernetes Service Definitions:** Review the service definitions for XkOps components to ensure they are configured correctly. Check for port conflicts or incorrect service types. -Verify Pod Network Connectivity: Use kubectl exec and curl to test connectivity between XkOps pods. Check for network-related errors in pod logs. +**Verify Pod Network Connectivity:** Use kubectl exec and curl to test connectivity between XkOps pods. Check for network-related errors in pod logs. -Component Failures +## Component Failures -Review Component Logs: Inspect the logs of individual XkOps components using kubectl logs to identify specific issues. +**Review Component Logs:** Inspect the logs of individual XkOps components using kubectl logs to identify specific issues. -Check Component Dependencies: Ensure that all required dependencies for XkOps components are installed and functioning correctly. +**Check Component Dependencies:** Ensure that all required dependencies for XkOps components are installed and functioning correctly. -Verify Component Configuration: Review the configuration of XkOps components to ensure they are correctly defined in the values.yaml file. +**Verify Component Configuration:** Review the configuration of XkOps components to ensure they are correctly defined in the values.yaml file. You should be able to resolve most installation problems by addressing these common issues. If you continue to experience difficulties, refer to the XkOps documentation or seek assistance from the XkOps support team. \ No newline at end of file diff --git a/documentation/Using XkOps/Available Workflows.md b/documentation/Using XkOps/Available Workflows.md index 5a99daf..4c0deed 100644 --- a/documentation/Using XkOps/Available Workflows.md +++ b/documentation/Using XkOps/Available Workflows.md @@ -1,36 +1,60 @@ XkOps offers a comprehensive suite of pre-built automation workflows, or Xworkflows, designed to optimize your Kubernetes cluster's performance and cost-efficiency. These Xworkflows leverage the capabilities of tools like Kubecost and Robusta to automate common management tasks. -Cost Optimization Xworkflows +## Cost Optimization Xworkflows -Xworkflow 1: Delete Unclaimed Persistent Volumes This Xworkflow identifies unused persistent volumes (PVs) in your Kubernetes cluster by leveraging Kubecost's data analysis capabilities. It provides a clear overview of unclaimed PVs, allowing users to choose which volumes to delete selectively. Robusta seamlessly handles the deletion process, ensuring efficient reclamation of storage resources. By eliminating unnecessary storage, this Xworkflow significantly contributes to reducing overall cluster costs. +**Xworkflow 1: Delete Unclaimed Persistent Volumes** -Xworkflow 2: Underutilized Node Optimization By analyzing node utilization data from Kubecost, this Xworkflow pinpoints nodes operating below optimal capacity. It gives users actionable recommendations, such as terminating underutilized nodes or strategically relocating workloads. Implementing these suggestions can lead to substantial cost savings without compromising cluster performance. + This Xworkflow identifies unused persistent volumes (PVs) in your Kubernetes cluster by leveraging Kubecost's data analysis capabilities. It provides a clear overview of unclaimed PVs, allowing users to choose which volumes to delete selectively. Robusta seamlessly handles the deletion process, ensuring efficient reclamation of storage resources. By eliminating unnecessary storage, this Xworkflow significantly contributes to reducing overall cluster costs. -Xworkflow 3: Abandoned Workloads This Xworkflow identifies workloads, such as pods and deployments, exhibiting minimal or no activity. Through in-depth analysis, it highlights potential candidates for optimization, including deletion or scaling down. By eliminating idle resources, this Xworkflow helps reduce unnecessary costs and improve overall cluster efficiency. +**Xworkflow 2: Underutilized Node Optimization** -Resource Optimization Xworkflows +By analyzing node utilization data from Kubecost, this Xworkflow pinpoints nodes operating below optimal capacity. It gives users actionable recommendations, such as terminating underutilized nodes or strategically relocating workloads. Implementing these suggestions can lead to substantial cost savings without compromising cluster performance. + +**Xworkflow 3: Abandoned Workloads** + +This Xworkflow identifies workloads, such as pods and deployments, exhibiting minimal or no activity. Through in-depth analysis, it highlights potential candidates for optimization, including deletion or scaling down. By eliminating idle resources, this Xworkflow helps reduce unnecessary costs and improve overall cluster efficiency. + +## Resource Optimization Xworkflows These Xworkflows focus on optimizing resource allocation within your Kubernetes cluster by analyzing resource utilization patterns and providing actionable recommendations. -CPU Optimization +### CPU Optimization + +**Xworkflow 4: Pod CPU Request Optimization** + +This Xworkflow meticulously examines the CPU usage patterns of individual pods within your cluster. By comparing actual CPU consumption to allocated requests, it identifies pods with excessive resource requests. Users can easily adjust CPU requests using Robusta actions, resulting in optimized resource utilization and improved cost efficiency. + +**Xworkflow 5: Deployment CPU Request Optimization** + +Similar to pod optimization, this Xworkflow analyzes CPU utilization patterns of deployments. By identifying deployments with excessive CPU requests, it empowers users to make informed decisions about resizing CPU requests. This optimization helps ensure that deployment resources align with actual workload demands. + +**Xworkflow 6: StatefulSet CPU Request Optimization** + +This Xworkflow is specifically designed for StatefulSets. It analyzes CPU usage patterns of StatefulSet pods to identify potential optimization opportunities. By adjusting CPU requests, users can enhance the performance and efficiency of their StatefulSet workloads. + +**Xworkflow 7: DaemonSet CPU Request Optimization** + +Tailored for DaemonSets, this Xworkflow evaluates CPU utilization patterns across DaemonSet pods. It provides recommendations for adjusting CPU requests to match workload requirements, optimizing resource allocation, and minimizing unnecessary consumption. + +## Memory Optimization -Xworkflow 4: Pod CPU Request Optimization This Xworkflow meticulously examines the CPU usage patterns of individual pods within your cluster. By comparing actual CPU consumption to allocated requests, it identifies pods with excessive resource requests. Users can easily adjust CPU requests using Robusta actions, resulting in optimized resource utilization and improved cost efficiency. +**Xworkflow 8: Pod Memory Request Optimization** -Xworkflow 5: Deployment CPU Request Optimization Similar to pod optimization, this Xworkflow analyzes CPU utilization patterns of deployments. By identifying deployments with excessive CPU requests, it empowers users to make informed decisions about resizing CPU requests. This optimization helps ensure that deployment resources align with actual workload demands. +This Xworkflow scrutinizes memory usage patterns of individual pods to identify instances of excessive memory requests. By providing clear recommendations, it empowers users to adjust memory requests and optimize resource utilization. -Xworkflow 6: StatefulSet CPU Request Optimization This Xworkflow is specifically designed for StatefulSets. It analyzes CPU usage patterns of StatefulSet pods to identify potential optimization opportunities. By adjusting CPU requests, users can enhance the performance and efficiency of their StatefulSet workloads. +**Xworkflow 9: Deployment Memory Request Optimization** -Xworkflow 7: DaemonSet CPU Request Optimization Tailored for DaemonSets, this Xworkflow evaluates CPU utilization patterns across DaemonSet pods. It provides recommendations for adjusting CPU requests to match workload requirements, optimizing resource allocation, and minimizing unnecessary consumption. +Focused on deployments, this Xworkflow analyzes memory consumption patterns to identify potential optimization opportunities. Users can make data-driven decisions to resize memory requests and improve overall cluster efficiency. -Memory Optimization +**Xworkflow 10: StatefulSet Memory Request Optimization** -Xworkflow 8: Pod Memory Request Optimization This Xworkflow scrutinizes memory usage patterns of individual pods to identify instances of excessive memory requests. By providing clear recommendations, it empowers users to adjust memory requests and optimize resource utilization. +This Xworkflow specializes in optimizing memory usage for StatefulSets. By analyzing memory consumption patterns of StatefulSet pods, it provides recommendations for adjusting memory requests to enhance performance and efficiency. -Xworkflow 9: Deployment Memory Request Optimization Focused on deployments, this Xworkflow analyzes memory consumption patterns to identify potential optimization opportunities. Users can make data-driven decisions to resize memory requests and improve overall cluster efficiency. +**Xworkflow 11: DaemonSet Memory Request Optimization** -Xworkflow 10: StatefulSet Memory Request Optimization This Xworkflow specializes in optimizing memory usage for StatefulSets. By analyzing memory consumption patterns of StatefulSet pods, it provides recommendations for adjusting memory requests to enhance performance and efficiency. +Designed for DaemonSets, this Xworkflow examines memory usage patterns to identify opportunities for optimization. By adjusting memory requests, users can ensure that DaemonSets operate efficiently without excessive resource consumption. -Xworkflow 11: DaemonSet Memory Request Optimization Designed for DaemonSets, this Xworkflow examines memory usage patterns to identify opportunities for optimization. By adjusting memory requests, users can ensure that DaemonSets operate efficiently without excessive resource consumption. +--- XkOps' pre-built Xworkflows provide a robust foundation for optimizing your Kubernetes cluster. These workflows deliver tangible benefits by automating routine tasks, reducing costs, and improving overall performance. However, XkOps also offers a flexible framework that allows you to create custom workflows tailored to your specific requirements. diff --git a/documentation/Using XkOps/Creating Your Own Workflows.md b/documentation/Using XkOps/Creating Your Own Workflows.md index 4f96ed2..654cedb 100644 --- a/documentation/Using XkOps/Creating Your Own Workflows.md +++ b/documentation/Using XkOps/Creating Your Own Workflows.md @@ -1,17 +1,17 @@ -This section provides detailed instructions for creating a new workflow in XkOps. Workflows are a core component of XkOps, allowing you to automate and manage specific actions within your Kubernetes environment. To ensure optimal performance and maintain system standards, it is recommended to create only those workflows that meet the requirements. For more details on these requirements, please refer to the “Workflow Creation Criteria” section. +This section provides detailed instructions for creating a new workflow in XkOps. Workflows are a core component of XkOps, allowing you to automate and manage specific actions within your Kubernetes environment. To ensure optimal performance and maintain system standards, it is recommended to create only those workflows that meet the requirements. For more details on these requirements, please refer to the **“Workflow Creation Criteria”** document. To create a new workflow, follow these steps: -Create a Necessary Robusta Action +## Create a Necessary Robusta Action Begin by defining the required Robusta action that aligns with your specific needs. Robusta actions are modular components within xkops, designed to execute targeted tasks in response to workflow triggers. Carefully assess your requirements and create a robust action that effectively addresses your objectives, whether it involves resource management, system monitoring, or automated scaling. -Integrate Business Logic in the Query Pod Section +## Integrate Business Logic in the Query Pod Section After defining the Robusta action, move on to configuring the Query Pod section where the business logic resides. Here, a cron job will be set up to retrieve cost recommendations from Kubecost pods. Kubecost provides cost optimization insights, which can be valuable for controlling resource expenses within your Kubernetes environment. Within the Query Pod, refine the logic to retrieve cost recommendations specifically from the Kubecost APIs that align with your requirements. This targeted approach ensures that the workflow only pulls in relevant cost data, reducing unnecessary overhead and focusing on actionable recommendations. Be sure to customize the cron job timing to align with your monitoring and optimization intervals. -Develop Supporting Backend APIs in Flask +## Develop Supporting Backend APIs in Flask The final step involves building the backend APIs to support your Robusta action, using Flask. These APIs provide the interface needed to trigger, monitor, and manage the Robusta action, integrating seamlessly with the cost recommendations logic. It is also recommended to implement authentication, logging, and error handling for these APIs to enhance their reliability and maintain a secure audit trail of all workflow activities. \ No newline at end of file diff --git a/documentation/Using XkOps/FAQs for Workflow Creation.md b/documentation/Using XkOps/FAQs for Workflow Creation.md index 6bd7082..e3c7c9c 100644 --- a/documentation/Using XkOps/FAQs for Workflow Creation.md +++ b/documentation/Using XkOps/FAQs for Workflow Creation.md @@ -1,45 +1,45 @@ Here are some frequently asked questions about creating workflows in XkOps, specifically around using Robusta actions and integrating cost recommendations from Kubecost. -Workflow Setup +## Workflow Setup -What is a Robusta action, and how is it used in workflows? +**What is a Robusta action, and how is it used in workflows?** A Robusta action is a modular component in XkOps that performs specific tasks triggered within workflows. Robusta actions enable workflows to execute automated responses to Kubernetes events, such as scaling, monitoring, or cost management tasks. -How do I configure cost recommendations in the Query Pod section? +**How do I configure cost recommendations in the Query Pod section?** In the Query Pod section, you can configure a cron job to retrieve cost recommendations from Kubecost. Define your business logic to fetch only the specific Kubecost API recommendations you need, ensuring streamlined and relevant cost optimization data. -What types of Kubecost recommendations can I use? +**What types of Kubecost recommendations can I use?** Kubecost offers recommendations for cost optimization, such as resizing resources or managing underutilized nodes. These recommendations help you optimize resource allocation, reducing unnecessary expenses in your Kubernetes environment. -Implementation +## Implementation -How do I add business logic for cost recommendations in a workflow? +**How do I add business logic for cost recommendations in a workflow?** In the Query Pod section, add custom logic to call the relevant Kubecost APIs. This involves setting up a cron job to periodically fetch cost recommendations based on your workflow’s requirements, ensuring only the needed data is retrieved. -What is the role of Flask in this workflow? +**What is the role of Flask in this workflow?** Flask is used to create backend APIs that support the Robusta actions. These APIs act as an interface to trigger, and manage each action within the workflow, enhancing the control of your workflows. -Verification and Troubleshooting +## Verification and Troubleshooting -How can I verify that my workflow is functioning correctly? +**How can I verify that my workflow is functioning correctly?** To check if the workflow is functioning correctly, verify the status of the workflow components with kubectl logs . Ensure that the cron job in the Query Pod section is fetching cost recommendations as expected. -What if the workflow fails to retrieve cost recommendations? +**What if the workflow fails to retrieve cost recommendations?** If the workflow fails to retrieve cost recommendations, check the API configurations in the Query Pod section and verify network connectivity to the Kubecost service. You can also review the logs for any error messages related to API requests -General +## General -Can I customize the cron job timing for fetching cost recommendations? +**Can I customize the cron job timing for fetching cost recommendations?** Yes, you can adjust the cron job timing in the Query Pod section to align with your cost monitoring schedule. Setting appropriate intervals will help you balance timely insights with cluster performance. -Where can I find additional support for workflow creation in xkops? +**Where can I find additional support for workflow creation in xkops?** Refer to the official xkops documentation, community forums, or contact xkops support for additional help with workflow creation, Robusta actions, and Kubecost integration. \ No newline at end of file diff --git a/documentation/Using XkOps/Understanding Workflows.md b/documentation/Using XkOps/Understanding Workflows.md index da96c2b..31146ac 100644 --- a/documentation/Using XkOps/Understanding Workflows.md +++ b/documentation/Using XkOps/Understanding Workflows.md @@ -1,36 +1,36 @@ XkOps workflows, or Xworkflows, are automated sequences of actions that help you manage your Kubernetes cluster more efficiently. They combine the power of tools like Kubecost, Robusta, and Pixie to handle common tasks without manual intervention. -How Xworkflows Work +## How Xworkflows Work Think of an Xworkflow as a recipe for managing your Kubernetes cluster. It starts by gathering information, then decides what to do based on that information, and finally takes action. -Data Collection: XkOps taps into tools like Kubecost to understand your spending patterns or Pixie to analyze network traffic. This data provides valuable insights into your cluster's health and performance. +**Data Collection:** XkOps taps into tools like Kubecost to understand your spending patterns or Pixie to analyze network traffic. This data provides valuable insights into your cluster's health and performance. -Decision Making: Based on the collected data, the Xworkflow determines the necessary steps. For instance, if Kubecost identifies unused resources, the workflow might decide to reclaim them to save costs. +**Decision Making:** Based on the collected data, the Xworkflow determines the necessary steps. For instance, if Kubecost identifies unused resources, the workflow might decide to reclaim them to save costs. -Action Execution: The final stage involves taking action. Robusta, our automation engine, carries out these actions, such as deleting unused resources or scaling resources based on demand. +**Action Execution:** The final stage involves taking action. Robusta, our automation engine, carries out these actions, such as deleting unused resources or scaling resources based on demand. -A Real-World Example: Cost Optimization +## A Real-World Example: Cost Optimization Let's say you're concerned about rising Kubernetes costs. An Xworkflow can help by: -Identifying Cost Drivers: Kubecost analyzes your cluster's resource usage and cost breakdown. +**Identifying Cost Drivers:** Kubecost analyzes your cluster's resource usage and cost breakdown. -Recommending Optimizations: The workflow suggests actions like rightsizing instances or reclaiming unused resources. +**Recommending Optimizations:** The workflow suggests actions like rightsizing instances or reclaiming unused resources. -Automating Savings: Robusta implements the recommended changes, reducing your cloud bill without compromising performance. +**Automating Savings:** Robusta implements the recommended changes, reducing your cloud bill without compromising performance. By automating these steps, you can significantly reduce your Kubernetes expenses without constant monitoring. -Benefits of Using Xworkflows +## Benefits of Using Xworkflows Save Time and Money: Automate repetitive tasks, freeing up your team to focus on strategic initiatives. XkOps can help you identify cost-saving opportunities and implement them automatically. -Improve Efficiency: Optimize resource utilization by proactively addressing issues like over-provisioning and underutilization. +**Improve Efficiency:** Optimize resource utilization by proactively addressing issues like over-provisioning and underutilization. -Enhance Cluster Health: Gain valuable insights into your cluster's performance and take corrective actions to prevent problems. +**Enhance Cluster Health:** Gain valuable insights into your cluster's performance and take corrective actions to prevent problems. -Simplify Complex Operations: Break down complex tasks into manageable workflows, reducing errors and improving reliability. +**Simplify Complex Operations:** Break down complex tasks into manageable workflows, reducing errors and improving reliability. By leveraging the power of Xworkflows, you can transform your Kubernetes management from a reactive to a proactive approach. diff --git a/documentation/Using XkOps/Using Workflows.md b/documentation/Using XkOps/Using Workflows.md index 04e1048..b100eb1 100644 --- a/documentation/Using XkOps/Using Workflows.md +++ b/documentation/Using XkOps/Using Workflows.md @@ -1,39 +1,39 @@ The XkOps Workflows section provides a user-friendly interface to manage and execute automation tasks within your Kubernetes cluster. -Accessing Your Workflows +## Accessing Your Workflows To access the available Xworkflows, navigate to the Workflows section within the XkOps dashboard. Here, you will find a comprehensive list of all the Xworkflows available for your cluster. Each workflow is accompanied by a brief description to provide an initial understanding of its purpose. -Workflow Details +### Workflow Details To gain deeper insights into a specific Xworkflow, click on its name. This will open a detailed view that includes: -Workflow Name: A clear and concise description of the workflow's objective. +**Workflow Name:** A clear and concise description of the workflow's objective. -Data Source: Specifies the underlying data source used by the workflow (e.g., Kubecost, Pixie) and how this data is utilized to inform the workflow's actions. +**Data Source:** Specifies the underlying data source used by the workflow (e.g., Kubecost, Pixie) and how this data is utilized to inform the workflow's actions. -Action: Outlines the specific actions performed by the workflow based on the collected data, including a step-by-step breakdown of the process if applicable. +**Action:** Outlines the specific actions performed by the workflow based on the collected data, including a step-by-step breakdown of the process if applicable. -Description: Provides a comprehensive explanation of the workflow's functionality, expected outcomes, and potential benefits. This section should also highlight any prerequisites or limitations associated with the workflow. +**Description:** Provides a comprehensive explanation of the workflow's functionality, expected outcomes, and potential benefits. This section should also highlight any prerequisites or limitations associated with the workflow. -Workflow Diagram: A visual representation of the workflow's logic and steps, aiding in understanding the process flow. +**Workflow Diagram:** A visual representation of the workflow's logic and steps, aiding in understanding the process flow. -Interacting with Xworkflows +### Interacting with Xworkflows To initiate a workflow, follow these steps: -Select the Workflow: Choose the desired Xworkflow from the list presented in the Workflows section. +**Select the Workflow:** Choose the desired Xworkflow from the list presented in the Workflows section. -Review Workflow Details: Carefully examine the workflow's description, data source, and actions to understand its purpose and potential impact. +**Review Workflow Details:** Carefully examine the workflow's description, data source, and actions to understand its purpose and potential impact. -Access Workflow Options: Most workflows will present you with specific options based on their functionality. For example, the "Delete Unclaimed Persistent Volumes" workflow might display a list of unclaimed PVs with details like size, creation time, and storage class. You can filter or sort this list to identify specific PVs for deletion. +**Access Workflow Options:** Most workflows will present you with specific options based on their functionality. For example, the "Delete Unclaimed Persistent Volumes" workflow might display a list of unclaimed PVs with details like size, creation time, and storage class. You can filter or sort this list to identify specific PVs for deletion. -Configure Workflow Parameters (if applicable): Some workflows may allow you to customize parameters or thresholds to fine-tune their behavior. For instance, you might be able to specify a minimum PV age for deletion or a CPU utilization threshold for resizing pods. +**Configure Workflow Parameters (if applicable):** Some workflows may allow you to customize parameters or thresholds to fine-tune their behavior. For instance, you might be able to specify a minimum PV age for deletion or a CPU utilization threshold for resizing pods. -Execute Action: Select the desired action from the available options. This could involve deleting a PV, resizing a pod, or applying other relevant actions. +**Execute Action:** Select the desired action from the available options. This could involve deleting a PV, resizing a pod, or applying other relevant actions. -Confirm Action: Review the selected action and its potential consequences before proceeding. +**Confirm Action:** Review the selected action and its potential consequences before proceeding. -Monitor Workflow Execution: Once initiated, the workflow will execute in the background. You can monitor its progress and view the results upon completion. A detailed execution log can provide insights into the workflow's steps and any encountered issues. +**Monitor Workflow Execution:** Once initiated, the workflow will execute in the background. You can monitor its progress and view the results upon completion. A detailed execution log can provide insights into the workflow's steps and any encountered issues. By following these steps and leveraging the detailed information provided within the Workflows section, you can effectively utilize XkOps to optimize your Kubernetes cluster and achieve the desired outcomes. \ No newline at end of file diff --git a/documentation/Using XkOps/Workdlow's Architecture.md b/documentation/Using XkOps/Workdlow's Architecture.md deleted file mode 100644 index 56cc805..0000000 --- a/documentation/Using XkOps/Workdlow's Architecture.md +++ /dev/null @@ -1,8 +0,0 @@ -General Workflow’s Architecture - -XkOps integrates with Kubecost to obtain real-time cost details, which inform cost optimization actions within your Kubernetes cluster. By polling Kubecost endpoints, XkOps retrieves the latest costing information, ensuring that workflows are based on up-to-date data. - -Once the latest cost information is gathered, XkOps triggers a Robusta action to manage resources in alignment with cost management goals. This process ensures that any necessary adjustments are automated and informed by accurate, current financial insights. - -Depending on the user’s configuration, xkops may update or delete specific resources to optimize costs. These actions are performed as per user-defined criteria, offering flexible options to streamline and manage cluster expenses effectively. - diff --git a/documentation/Using XkOps/Workflow Creation Criteria.md b/documentation/Using XkOps/Workflow Creation Criteria.md index a17993a..41e8b9f 100644 --- a/documentation/Using XkOps/Workflow Creation Criteria.md +++ b/documentation/Using XkOps/Workflow Creation Criteria.md @@ -1,18 +1,18 @@ At XkOps, we maintain rigorous standards to ensure that the workflows we offer deliver exceptional value. To be considered for inclusion in the XkOps platform, a workflow must meet the following evaluation criteria: -Cost-Effectiveness: Demonstrates a clear path to reducing costs without compromising performance or reliability. +- **Cost-Effectiveness:** Demonstrates a clear path to reducing costs without compromising performance or reliability. -Repeatability: Designed to be executed multiple times with consistent results. +- **Repeatability:** Designed to be executed multiple times with consistent results. -Implementation Clarity: Includes a well-defined plan outlining roles, responsibilities, and execution steps. +- **Implementation Clarity:** Includes a well-defined plan outlining roles, responsibilities, and execution steps. -Comprehensive Documentation: Supported by detailed documentation, including workflow diagrams. +- **Comprehensive Documentation:** Supported by detailed documentation, including workflow diagrams. -Frictionless Operation: Requires no additional permissions beyond those already granted to XkOps. +- **Frictionless Operation:** Requires no additional permissions beyond those already granted to XkOps. -Minimal Impact: Proven to avoid disrupting critical workloads on the cluster. +- **Minimal Impact:** Proven to avoid disrupting critical workloads on the cluster. -Proven Effectiveness: Successfully tested and validated to achieve desired outcomes. +- **Proven Effectiveness:** Successfully tested and validated to achieve desired outcomes. Each workflow undergoes a thorough assessment against these standards. If a workflow meets these requirements, it is considered for inclusion in the XkOps platform. Otherwise, it undergoes refinement and retesting until it aligns with our quality benchmarks. diff --git a/documentation/Using XkOps/Workflow's Architecture.md b/documentation/Using XkOps/Workflow's Architecture.md new file mode 100644 index 0000000..6181091 --- /dev/null +++ b/documentation/Using XkOps/Workflow's Architecture.md @@ -0,0 +1,12 @@ +## General Workflow’s Architecture + +

+ General Workflow Architecture Diagram +

+ +- XkOps integrates with Kubecost to obtain real-time cost details, which inform cost optimization actions within your Kubernetes cluster. By polling Kubecost endpoints, XkOps retrieves the latest costing information, ensuring that workflows are based on up-to-date data. + +- Once the latest cost information is gathered, XkOps triggers a Robusta action to manage resources in alignment with cost management goals. This process ensures that any necessary adjustments are automated and informed by accurate, current financial insights. + +- Depending on the user’s configuration, xkops may update or delete specific resources to optimize costs. These actions are performed as per user-defined criteria, offering flexible options to streamline and manage cluster expenses effectively. + diff --git a/images/Demo-video.jpg b/images/Demo-video.jpg deleted file mode 100644 index 76f0064da32f291e085587c095def02a09d523f9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 85723 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 z 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 diff --git a/images/general workflow architecture.png b/images/general workflow architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..051ce4276be3e894bdd1df04599a7ef98699ba38 GIT binary patch literal 40186 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 L#AxKI*#!O<7&M(o literal 0 HcmV?d00001