diff --git a/.classpath b/.classpath new file mode 100644 index 0000000000000000000000000000000000000000..6fc81d6140de0b393a820c398f30c39bfe3babc1 --- /dev/null +++ b/.classpath @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="UTF-8"?> +<classpath> + <classpathentry kind="src" output="target/classes" path="src/main/java"> + <attributes> + <attribute name="optional" value="true"/> + <attribute name="maven.pomderived" value="true"/> + </attributes> + </classpathentry> + <classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources"> + <attributes> + <attribute name="maven.pomderived" value="true"/> + </attributes> + </classpathentry> + <classpathentry kind="src" output="target/test-classes" path="src/test/java"> + <attributes> + <attribute name="test" value="true"/> + <attribute name="optional" value="true"/> + <attribute name="maven.pomderived" value="true"/> + </attributes> + </classpathentry> + <classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources"> + <attributes> + <attribute name="test" value="true"/> + <attribute name="maven.pomderived" value="true"/> + </attributes> + </classpathentry> + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17"> + <attributes> + <attribute name="module" value="true"/> + <attribute name="maven.pomderived" value="true"/> + </attributes> + </classpathentry> + <classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER"> + <attributes> + <attribute name="maven.pomderived" value="true"/> + </attributes> + </classpathentry> + <classpathentry kind="output" path="target/classes"/> +</classpath> diff --git a/.project b/.project new file mode 100644 index 0000000000000000000000000000000000000000..20e03945710cf25f655a8080c281170c0717c20d --- /dev/null +++ b/.project @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>org.etsi.osl.controllers.kcj</name> + <comment></comment> + <projects> + </projects> + <buildSpec> + <buildCommand> + <name>org.eclipse.jdt.core.javabuilder</name> + <arguments> + </arguments> + </buildCommand> + <buildCommand> + <name>org.eclipse.m2e.core.maven2Builder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.jdt.core.javanature</nature> + <nature>org.eclipse.m2e.core.maven2Nature</nature> + </natures> +</projectDescription> diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000000000000000000000000000000000000..7edaf303426a1671f6726fdaa17f4064d03d17f0 --- /dev/null +++ b/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding//src/main/resources/cridge_helm.yaml=UTF-8 diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000000000000000000000000000000000000..ecb498c8fac88c08a85758b8bfb8322e15944b05 --- /dev/null +++ b/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,16 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning +org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.source=17 diff --git a/.settings/org.eclipse.m2e.core.prefs b/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 0000000000000000000000000000000000000000..f897a7f1cb2389f85fe6381425d29f0a9866fb65 --- /dev/null +++ b/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..59fa5fc9fb90ae8cca268c380b5158d155a52075 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,5 @@ +FROM ibm-semeru-runtimes:open-17.0.7_7-jdk +MAINTAINER osl.etsi.org + +COPY target/org.etsi.osl.controllers.kcj-0.0.1-SNAPSHOT-exec.jar /opt/oslkcj +CMD ["java", "-jar", "/opt/oslkcj/org.etsi.osl.controllers.kcj-0.0.1-SNAPSHOT-exec.jar"] \ No newline at end of file diff --git a/README.md b/README.md index dd84a8563e3b235f3701deb370b2c82f65ac7d67..79a617d05a042614077dcb7b346f00ac53836c21 100644 --- a/README.md +++ b/README.md @@ -1,92 +1,95 @@ # org.etsi.osl.controllers.kcj - +OSL Operator for configuring an external Kubernetes Cluster to Join the OSL management cluster and enable it for deploying services and applications ## Getting started -To make it easy for you to get started with GitLab, here's a list of recommended next steps. - -Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)! +`helm install oslkcj . --set-file kubeconfig=../src/main/resources/config` -## Add your files -- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files -- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command: +The operator is as follows: ``` -cd existing_repo -git remote add origin https://labs.etsi.org/rep/osl/code/addons/org.etsi.osl.controllers.kcj.git -git branch -M main -git push -uf origin main +apiVersion: controllers.osl.etsi.org/v1alpha1 +kind: KCJResource +metadata: + name: kcjexample +spec: + clusterConfigBase64: \\kubeconfig of remote cluster in BASE64 + installArgoCDRemote: \\if true will install ARGO in remote cluster + installCridgeLocal: \\if true will install CRDIGE in local OSL management cluster via local ARGO. This CRIDGE will monitor remotely the remote cluster + installCridgeRemote: \\if true will install CRDIGE in remote cluster via remote or local ARGO. This CRIDGE will monitor the remote cluster and connect to OSL management cluster + cridgeAMQEndPoint: "tcp://osl-url:61616?jms.watchTopicAdvisories=false" \\the URL of the OSL service bus + cridgeAMQUsername: "artemis" \\the username to connect to the OSL service bus + cridgeAMQPassword: "artemis" \\the password to connect to the OSL service bus + addClusterToLocalArgoCD: false \\ if true will create service account to remote cluster and add the cluster to local ARGO in OSL management cluster ``` -## Integrate with your tools - -- [ ] [Set up project integrations](https://labs.etsi.org/rep/osl/code/addons/org.etsi.osl.controllers.kcj/-/settings/integrations) - -## Collaborate with your team - -- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/) -- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) -- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically) -- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/) -- [ ] [Automatically merge when pipeline succeeds](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html) - -## Test and Deploy - -Use the built-in continuous integration in GitLab. -- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html) -- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://docs.gitlab.com/ee/user/application_security/sast/) -- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html) -- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/) -- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html) -*** -# Editing this README +## Examples -When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template. -## Suggestions for a good README -Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information. +### Add remote kubernetes cluster to local Argo in OSL management cluster -## Name -Choose a self-explaining name for your project. - -## Description -Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors. +``` +apiVersion: controllers.osl.etsi.org/v1alpha1 +kind: KCJResource +metadata: + name: kcjexample +spec: + clusterConfigBase64: "YXBpVmVyc2lv...vPQo=" + installArgoCDRemote: false + installCridgeLocal: false + installCridgeRemote: false + cridgeAMQEndPoint: "tcp://osl-url:61616?jms.watchTopicAdvisories=false" + cridgeAMQUsername: "artemis" + cridgeAMQPassword: "artemis" + addClusterToLocalArgoCD: true +``` + +### Install CRIDGE in OSL management cluster to manage remote cluster -## Badges -On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge. +``` +apiVersion: controllers.osl.etsi.org/v1alpha1 +kind: KCJResource +metadata: + name: kcjexample +spec: + clusterConfigBase64: "YXBpVmVyc2lv...vPQo=" + installArgoCDRemote: false + installCridgeLocal: true + installCridgeRemote: false + cridgeAMQEndPoint: "tcp://osl-url:61616?jms.watchTopicAdvisories=false" + cridgeAMQUsername: "artemis" + cridgeAMQPassword: "artemis" + addClusterToLocalArgoCD: true +``` -## Visuals -Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method. -## Installation -Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection. +### Install ARGO to remote cluster and then install CRIDGE in remote cluster to manage remote cluster -## Usage -Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README. +``` +apiVersion: controllers.osl.etsi.org/v1alpha1 +kind: KCJResource +metadata: + name: kcjexample +spec: + clusterConfigBase64: "YXBpVmVyc2lv...vPQo=" + installArgoCDRemote: true + installCridgeLocal: false + installCridgeRemote: true + cridgeAMQEndPoint: "tcp://osl-url:61616?jms.watchTopicAdvisories=false" + cridgeAMQUsername: "artemis" + cridgeAMQPassword: "artemis" + addClusterToLocalArgoCD: false +``` -## Support -Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc. -## Roadmap -If you have ideas for releases in the future, it is a good idea to list them in the README. -## Contributing -State if you are open to contributions and what your requirements are for accepting them. -For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self. -You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. -## Authors and acknowledgment -Show your appreciation to those who have contributed to the project. -## License -For open source projects, say how it is licensed. -## Project status -If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. diff --git a/ci-templates/build.yml b/ci-templates/build.yml new file mode 100644 index 0000000000000000000000000000000000000000..c0ff25498f2df442bc6fe61079f7088417f50804 --- /dev/null +++ b/ci-templates/build.yml @@ -0,0 +1,24 @@ +.maven_build: + extends: .default + stage: build + image: maven:3.9.5-ibm-semeru-17-focal + script: + - mvn deploy -s ci_settings.xml -Dversion=$APP_VERSION + artifacts: + paths: + - target/ + +.docker_build: + extends: .default + stage: build + image: + name: gcr.io/kaniko-project/executor:debug + entrypoint: [""] + script: + - export DOCKER_TAG=$APP_VERSION + - | + if [ "$CI_COMMIT_REF_NAME" = "main" ]; then + echo "Pushing Docker image with tag 'latest'" + export DOCKER_TAG=latest + fi + - /kaniko/executor --context "${CI_PROJECT_DIR}" --dockerfile "${CI_PROJECT_DIR}/Dockerfile" --destination "${CI_REGISTRY_IMAGE}:$DOCKER_TAG" diff --git a/ci-templates/default.yml b/ci-templates/default.yml new file mode 100644 index 0000000000000000000000000000000000000000..e49b634243ca7c5870671034b0027e80ea233d53 --- /dev/null +++ b/ci-templates/default.yml @@ -0,0 +1,17 @@ +stages: + - .pre + - build + - test + - post + - security + +.default: + before_script: + - | + if [ "$CI_COMMIT_REF_PROTECTED" = true ] && [ -n "$CI_COMMIT_TAG" ]; then + export APP_VERSION=$CI_COMMIT_TAG + elif [ "$CI_COMMIT_REF_NAME" = "develop" ]; then + export APP_VERSION="develop" + else + export APP_VERSION=$CI_COMMIT_REF_NAME + fi diff --git a/helm/.helmignore b/helm/.helmignore new file mode 100644 index 0000000000000000000000000000000000000000..0e8a0eb36f4ca2c939201c0d54b5d82a1ea34778 --- /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 new file mode 100644 index 0000000000000000000000000000000000000000..9f93d02db537fd691f092e9f1b5ef3a7ccfa2442 --- /dev/null +++ b/helm/Chart.yaml @@ -0,0 +1,23 @@ +apiVersion: v2 +name: oslkcj +description: OSL Kubernetes Cluster Join Operator 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. +# +# 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/) +version: 0.0.1 + +# 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. +appVersion: 0.0.1 diff --git a/helm/templates/KCJResourceOperator.yaml b/helm/templates/KCJResourceOperator.yaml new file mode 100644 index 0000000000000000000000000000000000000000..d688706e0cc5b0c601e6d2b1ea7730570008cf37 --- /dev/null +++ b/helm/templates/KCJResourceOperator.yaml @@ -0,0 +1,51 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: kcjresources.controllers.osl.etsi.org # Name of the CRD +spec: + group: controllers.osl.etsi.org # Custom resource group + names: + kind: KCJResource # Name of the resource type + plural: kcjresources # Plural form of the resource + singular: kcjresource # Singular form of the resource + scope: Namespaced # Namespaced resource + versions: + - name: v1alpha1 # Version of the CRD + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + clusterConfig: + type: string + clusterConfigBase64: + type: string + installArgoCDRemote: + type: boolean + installCridgeLocal: + type: boolean + installCridgeRemote: + type: boolean + cridgeAMQEndPoint: + type: string + cridgeAMQUsername: + type: string + cridgeAMQPassword: + type: string + addClusterToLocalArgoCD: + type: boolean + status: + type: object + properties: + state: + type: string + infoInstallArgoCD: + type: string + infoInstallCridge: + type: string + infoEnableLocalArgoCD: + type: string diff --git a/helm/templates/_helpers.tpl b/helm/templates/_helpers.tpl new file mode 100644 index 0000000000000000000000000000000000000000..89f9f71fba483f569751099ef78cdfb1da8a3f4c --- /dev/null +++ b/helm/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "openslice.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "openslice.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "openslice.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "openslice.labels" -}} +helm.sh/chart: {{ include "openslice.chart" . }} +{{ include "openslice.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "openslice.selectorLabels" -}} +app.kubernetes.io/name: {{ include "openslice.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "openslice.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "openslice.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..afc3e0e53f881a7a9c3834158962d81c8a29040d --- /dev/null +++ b/helm/templates/deployment.yaml @@ -0,0 +1,51 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + namespace: {{ .Release.Namespace }} + labels: + app: {{ include "openslice.fullname" . }} + org.etsi.osl.service: oslkcj + chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}" + {{- include "openslice.labels" . | nindent 4 }} + name: {{ include "openslice.fullname" . }}-oslkcj +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + app: {{ include "openslice.fullname" . }} + org.etsi.osl.service: oslkcj + {{- include "openslice.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + app: {{ include "openslice.fullname" . }} + org.etsi.osl.service: oslkcj + {{- include "openslice.selectorLabels" . | nindent 8 }} + spec: + containers: + - image: "{{ .Values.image.oslkcj.repository }}:{{ .Values.image.oslkcj.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.oslkcj.pullPolicy | default "Always" }} + name: {{ include "openslice.fullname" . }}-oslkcj + env: + - name: SPRING_APPLICATION_JSON + value: >- + { + "logging.level.org.springframework" : "{{ .Values.spring.logLevel | default "INFO" }}", + "logging.level.org.etsi.osl.controllers.kcj" : "{{ .Values.logLevel | default "INFO" }}" + } + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumeMounts: + - name: oslkcj-kubeconfig + readOnly: true + mountPath: /root/.kube/config + subPath: config + restartPolicy: Always + volumes: + - name: oslkcj-kubeconfig + secret: + secretName: {{ include "openslice.fullname" . }}-oslkcj-kubeconfig + + \ No newline at end of file diff --git a/helm/values.yaml b/helm/values.yaml new file mode 100644 index 0000000000000000000000000000000000000000..2d2d4567af951ebc4ac5c95722f7d4bfa3461389 --- /dev/null +++ b/helm/values.yaml @@ -0,0 +1,30 @@ +# Default values for oslkcj. + +replicaCount: 1 + +image: + oslkcj: + repository: labs.etsi.org:5050/osl/code/addons/org.etsi.osl.controllers.kcj + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + tag: "latest" + +logLevel: INFO +spring: + loglevel: INFO + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + targetMemoryUtilizationPercentage: 80 + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..39c3d73d8a02c14df4ceca3e7d9ca1d15dafac7a --- /dev/null +++ b/pom.xml @@ -0,0 +1,188 @@ +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <groupId>org.etsi.osl</groupId> + <artifactId>org.etsi.osl.controllers.kcj</artifactId> + <version>0.0.1-SNAPSHOT</version> + <name>KCJ</name> + <description>OSL Operator for configuring an external Kubernetes workload Cluster to Join the OSL management cluster and enable it for deployng services and applications</description> + <organization> + <name>osl.etsi.org</name> + <url>https://osl.etsi.org</url> + </organization> + + <inceptionYear>2024</inceptionYear> + + <properties> + <java.version>17</java.version> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> + <spring.boot-version>3.2.2</spring.boot-version> + <spring.boot.fabric8-version>3.1.0</spring.boot.fabric8-version> + <fabric8.version>6.10.0</fabric8.version> + + <slf4j-api.version>1.7.5</slf4j-api.version> + <slf4j-simple.version>1.7.28</slf4j-simple.version> + <maven-license-plugin.version>2.0.0</maven-license-plugin.version> + <license.licenseName>apache_v2</license.licenseName> + </properties> + + <repositories> + <repository> + <id>gitlab-maven</id> + <url>https://labs.etsi.org/rep/api/v4/groups/260/-/packages/maven</url> + </repository> + </repositories> + <distributionManagement> + <repository> + <id>gitlab-maven</id> + <url>${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/maven</url> + </repository> + <snapshotRepository> + <id>gitlab-maven</id> + <url>${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/maven</url> + </snapshotRepository> + </distributionManagement> + + + + <dependencyManagement> + <dependencies> + <!-- Spring Boot BOM --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-dependencies</artifactId> + <version>${spring.boot-version}</version> + <type>pom</type> + <scope>import</scope> + </dependency> + <dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-starter-kubernetes-fabric8-all</artifactId> + <version>${spring.boot.fabric8-version}</version> + </dependency> + + </dependencies> + + </dependencyManagement> + <dependencies> + <!-- Spring Boot and related dependencies --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-web</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-actuator</artifactId> + </dependency> + + <!-- Add Kubernetes client dependency for Java --> + <dependency> + <groupId>io.fabric8</groupId> + <artifactId>kubernetes-client</artifactId> + <version>6.13.4</version> + </dependency> + <dependency> + <groupId>org.yaml</groupId> + <artifactId>snakeyaml</artifactId> + </dependency> + + + <dependency> + <groupId>org.projectlombok</groupId> + <artifactId>lombok</artifactId> + <scope>provided</scope> + </dependency> + </dependencies> + + + + <build> + <pluginManagement> + <plugins> + <plugin> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-maven-plugin</artifactId> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <configuration> + <source>17</source> + <target>17</target> + <annotationProcessorPaths> + <path> + <groupId>org.projectlombok</groupId> + <artifactId>lombok</artifactId> + <version>1.18.28</version> + </path> + </annotationProcessorPaths> + </configuration> + </plugin> + <plugin> + <!-- run mvn license:update-file-header to manually update all headers + everywhere --> + <groupId>org.codehaus.mojo</groupId> + <artifactId>license-maven-plugin</artifactId> + <version>${maven-license-plugin.version}</version> + <configuration> + <addJavaLicenseAfterPackage>false</addJavaLicenseAfterPackage> + <processStartTag>========================LICENSE_START=================================</processStartTag> + <processEndTag>=========================LICENSE_END==================================</processEndTag> + <excludes>*.json</excludes> + </configuration> + <executions> + <execution> + <id>generate-license-headers</id> + <goals> + <goal>update-file-header</goal> + </goals> + <phase>process-sources</phase> + <configuration> + <licenseName>${license.licenseName}</licenseName> + + </configuration> + </execution> + <execution> + <id>download-licenses</id> + <goals> + <goal>download-licenses</goal> + </goals> + </execution> + </executions> + </plugin> + </plugins> + </pluginManagement> + <plugins> + <plugin> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-maven-plugin</artifactId> + <version>${spring.boot-version}</version> + <executions> + <execution> + <goals> + <goal>repackage</goal> + </goals> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-maven-plugin</artifactId> + <version>${spring.boot-version}</version> + <configuration> + <classifier>exec</classifier> + </configuration> + </plugin> + </plugins> + + </build> + + + + + + + +</project> \ No newline at end of file diff --git a/src/main/java/org/etsi/osl/controllers/kcj/KCJControllerApp.java b/src/main/java/org/etsi/osl/controllers/kcj/KCJControllerApp.java new file mode 100644 index 0000000000000000000000000000000000000000..c54c2393df1db3747272439b64c41133276bfec4 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/kcj/KCJControllerApp.java @@ -0,0 +1,35 @@ +package org.etsi.osl.controllers.kcj; +/*- + * ========================LICENSE_START================================= + * org.etsi.osl.controllers.sylva + * %% + * Copyright (C) 2024 osl.etsi.org + * %% + * 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. + * =========================LICENSE_END================================== + */ + + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * + */ +@SpringBootApplication +public class KCJControllerApp { + + public static void main(String[] args) { + SpringApplication.run(KCJControllerApp.class, args); + } +} diff --git a/src/main/java/org/etsi/osl/controllers/kcj/KCJResource.java b/src/main/java/org/etsi/osl/controllers/kcj/KCJResource.java new file mode 100644 index 0000000000000000000000000000000000000000..3483b5169172feab44eb535caab710b7502da4b1 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/kcj/KCJResource.java @@ -0,0 +1,32 @@ +/*- + * ========================LICENSE_START================================= + * org.etsi.osl.controllers.sylva + * %% + * Copyright (C) 2024 osl.etsi.org + * %% + * 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. + * =========================LICENSE_END================================== + */ +package org.etsi.osl.controllers.kcj; + +import io.fabric8.kubernetes.api.model.Namespaced; +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.Version; + +@Version("v1alpha1") // -> CRD Version +@Group("controllers.osl.etsi.org") // -> CRD Group +public class KCJResource + extends CustomResource<KCJResourceSpec, KCJResourceStatus> + implements Namespaced { +} // -> CRD scope Namespaced diff --git a/src/main/java/org/etsi/osl/controllers/kcj/KCJResourceOperator.java b/src/main/java/org/etsi/osl/controllers/kcj/KCJResourceOperator.java new file mode 100644 index 0000000000000000000000000000000000000000..e87edcd08425325fe27f6795f373f632bb703bcf --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/kcj/KCJResourceOperator.java @@ -0,0 +1,731 @@ +/*- + * ========================LICENSE_START================================= + * org.etsi.osl.controllers.kcj + * %% + * Copyright (C) 2024 osl.etsi.org + * %% + * 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. + * =========================LICENSE_END================================== + */ +package org.etsi.osl.controllers.kcj; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.KubernetesResourceList; +import io.fabric8.kubernetes.api.model.Namespace; +import io.fabric8.kubernetes.api.model.NamespaceBuilder; +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.api.model.SecretBuilder; +import io.fabric8.kubernetes.api.model.StatusDetails; +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientBuilder; +import io.fabric8.kubernetes.client.KubernetesClientException; +import io.fabric8.kubernetes.client.dsl.MixedOperation; +import io.fabric8.kubernetes.client.dsl.Resource; +import io.fabric8.kubernetes.client.informers.ResourceEventHandler; +import io.fabric8.kubernetes.client.informers.SharedIndexInformer; + +@Service +public class KCJResourceOperator { + + private static final Logger log = LoggerFactory.getLogger(KCJResourceOperator.class); + private static final String REMOTARGO_URL = "https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml"; + private static final String ARGOCD_NAMESPACE = "argocd"; + @Autowired + private KubernetesClient localKubernetesClient; + + + + + @Autowired + public KCJResourceOperator(KubernetesClient kubernetesClient) { + this.localKubernetesClient = kubernetesClient; + // Start watching for resource events + watchResources(); + } + + // Watch for events on KCJResource + private void watchResources() { + + + + + MixedOperation<KCJResource, KubernetesResourceList<KCJResource>, Resource<KCJResource>> resourceClient = + localKubernetesClient.resources(KCJResource.class); + + SharedIndexInformer<KCJResource> sharedIndexInformer = + resourceClient.inAnyNamespace().inform(new ResourceEventHandler<KCJResource>() { + + public void onAdd(KCJResource resource) { + log.info("===== onAdd action ====="); + reconcile(resource, null, localKubernetesClient); + + } + + public void onUpdate(KCJResource oldResource, KCJResource resource) { + log.info("===== onUpdate action ====="); + reconcile(resource, oldResource, localKubernetesClient); + + } + + public void onDelete(KCJResource resource, boolean deletedFinalStateUnknown) { + //deletion is handled by reconcile due to finalizers + // handleDelete(resource, kubernetesClient); + log.info("===== onDelete action ====="); + log.info("Removed resource from system KCJResource: {}", + resource.getMetadata().getName()); + } + + + + + }); + + + log.info("SharedIndexInformer running"); + sharedIndexInformer.run(); + + + + + } + + + // Handle the deletion of the KCJResource + private void handleDelete(KCJResource resource, KubernetesClient localKubClient, KubernetesClient remoteKubClient) { + log.info("Handling deletion of KCJResource: {} in {} state", resource.getMetadata().getName(), resource.getStatus().getState()); + if ( resource.getSpec().isInstallCridgeLocal() ) { //install cridge on local cluster via local argoCD + installCRIDGELocallyviaLocalArgoCD( resource, localKubClient, remoteKubClient, "NONE", true); + } + + if ( resource.getSpec().isInstallCridgeRemote() ) { //install cridge on remote cluster via remote cridge + if ( resource.getSpec().isAddClusterToLocalArgoCD() ) { + installCRIDGERemotellyviaLocalArgoCD( resource, localKubClient, remoteKubClient, "NONE", true); + } else if ( resource.getSpec().isInstallArgoCDRemote() ) { + installCRIDGERemotellyviaRemoteArgoCD( resource, localKubClient, remoteKubClient, "NONE", true); + } + + } + + } + + + + public KCJResource reconcile(KCJResource resource, KCJResource oldResource, + KubernetesClient localKubClient) { + + log.info("reconciling"); + + + String resourceName = resource.getMetadata().getName(); + String namespace = resource.getMetadata().getNamespace(); + String targetKubeConfig = ""; + KCJResource resourceClone; + + if ( resource.getSpec().getClusterConfigBase64() != null ) { + String secretBase64 = resource.getSpec().getClusterConfigBase64(); + // Decode the Base64 string to a regular string + byte[] decodedBytes = Base64.getDecoder().decode( secretBase64 ); + targetKubeConfig = new String(decodedBytes); + }else { + targetKubeConfig = resource.getSpec().getClusterConfig(); + } + + log.info("targetKubeConfig = " + targetKubeConfig); + + KubernetesClient remoteKubClient; + // Load kubeconfig from string + try { + remoteKubClient = new KubernetesClientBuilder().withConfig(Config.fromKubeconfig(targetKubeConfig)).build(); + } catch (KubernetesClientException e) { + e.printStackTrace(); + + log.error("Failed to create Kubernetes client: " + e.getMessage()); + resourceClone = + updateResourceStatus(resource, KCJResourceState.FAILED, "FAILED", "FAILED", "FAILED", localKubClient); + return resourceClone; + } + + + + + + + // Handle resource deletion + if (resource.getMetadata().getDeletionTimestamp() != null) { + if (resource.getStatus().getState().equals(KCJResourceState.DELETE_FAILED) + || resource.getStatus().getState().equals(KCJResourceState.DELETED) ) { + log.info("The resource is in {} state. Reconcile deletion action is ignored.", + resource.getStatus().getState()); + return resource; + } + // Perform cleanup logic + handleDelete(resource, localKubClient, remoteKubClient); + + removeFinalizer(resource, localKubClient); + return null; + } + + + + if (oldResource == null) { + if (resource.getStatus() != null) { + log.info( + "Old resource is NULL. Current resource is in {} state. Reconcile action is ignored.", + resource.getStatus().getState()); + return resource; + } + } else { + + log.info("oldResource.spec = {}", oldResource.getSpec().toString()); + log.info("newresource.spec = {}", resource.getSpec().toString()); + + if (resource.getStatus() == null) { + log.info("The resource has NULL status."); + return resource; + } + if (!resource.getStatus().getState().equals(KCJResourceState.ACTIVE) + && !resource.getStatus().getState().equals(KCJResourceState.FAILED)) { + log.info("The resource is in {} state. Reconcile action is ignored.", + resource.getStatus().getState()); + return resource; + } + + + // Compare old and new resource state ( compare spec, status, etc.) + if (resource.getSpec().toString().equals(oldResource.getSpec().toString())) { + log.info("No significant changes detected, skipping reconciliation."); + return resource; // Skip reconciliation if there are no significant changes + } + } + + + + resourceClone = + updateResourceStatus(resource, KCJResourceState.IN_PROGRESS, "reconciling", "reconciling", "reconciling", localKubClient); + resourceClone = addFinalizers(resourceClone, localKubClient); + + + + + if ( resource.getSpec().isAddClusterToLocalArgoCD() ) { + resourceClone = execStrategyEnablelocalArgo(resourceClone, localKubClient, remoteKubClient); + } + + if ( resource.getSpec().isInstallArgoCDRemote() ) { + resourceClone = execStrategyInstallRemoteArgo(resourceClone, localKubClient, remoteKubClient); + } + + + if ( resource.getSpec().isInstallCridgeLocal() ) { //install cridge on local cluster via local argoCD + resourceClone = installCRIDGELocallyviaLocalArgoCD(resourceClone, localKubClient, remoteKubClient, targetKubeConfig, false); + } else if ( resource.getSpec().isInstallCridgeRemote() ) { //install cridge on remote cluster via remote cridge + + if ( resource.getSpec().isInstallArgoCDRemote() ) { + resourceClone = installCRIDGERemotellyviaRemoteArgoCD(resourceClone, localKubClient, remoteKubClient, targetKubeConfig, false); + }else if ( resource.getSpec().isAddClusterToLocalArgoCD() ) { + resourceClone = installCRIDGERemotellyviaLocalArgoCD(resourceClone, localKubClient, remoteKubClient, targetKubeConfig, false); + + } + + } + + + return resourceClone; + } + + + + + + + private KCJResource installCRIDGERemotellyviaRemoteArgoCD(KCJResource resource, KubernetesClient localKubClient, + KubernetesClient remoteKubClient, String targetKubeConfig, Boolean isDelete) { + + log.info("In installCRIDGERemotellyviaLocalArgoCD"); + Config remoteConfig = remoteKubClient.getConfiguration(); + String appName = "cridge-"+remoteConfig.getCurrentContext().getContext().getCluster(); + String activemq_brokerUrl = resource.getSpec().getCridgeAMQEndPoint() ; + String activemq_user = resource.getSpec().getCridgeAMQUsername(); + String activemq_password = resource.getSpec().getCridgeAMQPassword(); + + + String kubeconfig = targetKubeConfig ; + String destination_server = "https://kubernetes.default.svc"; + String destination_server_name = ""; + String destination_namespace = "oslcridge"; + + + String yamlToapply = prepareCRIDGEyaml(appName, activemq_brokerUrl, activemq_user, activemq_password, + destination_server, destination_server_name, destination_namespace, kubeconfig); + + // Convert the YAML string to an InputStream + InputStream yamlInputStream = new java.io.ByteArrayInputStream(yamlToapply.getBytes()); + + // Parse the YAML file into Kubernetes resources + try { + if (isDelete) { + + List<StatusDetails> response = remoteKubClient.load(yamlInputStream).delete(); + log.info("response delete=" + response.toString()); + return resource; + }else { + // Define the namespace with labels + Namespace namespace = new NamespaceBuilder() + .withNewMetadata() + .withName(destination_namespace) // Set the namespace name + .addToLabels("pod-security.kubernetes.io/enforce", "baseline") // Add labels + .addToLabels("pod-security.kubernetes.io/audit", "baseline") // Add labels + .addToLabels("pod-security.kubernetes.io/warn", "baseline") // Add labels + .endMetadata() + .build(); + + // Create the namespace + remoteKubClient.namespaces().createOrReplace(namespace); + + + List<HasMetadata> response = remoteKubClient.load(yamlInputStream).createOrReplace(); + log.info("response=" + response.toString()); + return updateResourceStatus(resource, KCJResourceState.ACTIVE, null, "CRIDGE successfully configured remotelly via Remote ArgoCD", null, localKubClient); + } + + + }catch (Exception e) { + log.info("Error in installCRIDGELocallyviaLocalArgoCD: " + e.getMessage()); + return updateResourceStatus(resource, KCJResourceState.ACTIVE, null, "Failed to configure CRIDGE remotelly via Remote ArgoCD", null, localKubClient); + } + + + } + + private KCJResource installCRIDGELocallyviaLocalArgoCD(KCJResource resource, KubernetesClient localKubClient, + KubernetesClient remoteKubClient, String targetKubeConfig, Boolean isDelete) { + + log.info("In installCRIDGELocallyviaLocalArgoCD"); + + Config remoteConfig = remoteKubClient.getConfiguration(); + String appName = "cridge-local-"+remoteConfig.getCurrentContext().getContext().getCluster(); + String activemq_brokerUrl = resource.getSpec().getCridgeAMQEndPoint() ; + String activemq_user = resource.getSpec().getCridgeAMQUsername(); + String activemq_password = resource.getSpec().getCridgeAMQPassword(); + + + String kubeconfig = targetKubeConfig ; + String destination_server = "https://kubernetes.default.svc"; + String destination_server_name = ""; + String destination_namespace = appName; + + String yamlToapply = prepareCRIDGEyaml(appName, activemq_brokerUrl, activemq_user, activemq_password, + destination_server, destination_server_name, destination_namespace, kubeconfig); + + // Convert the YAML string to an InputStream + InputStream yamlInputStream = new java.io.ByteArrayInputStream(yamlToapply.getBytes()); + + // Parse the YAML file into Kubernetes resources + try { + if (isDelete) { + + List<StatusDetails> response = localKubClient.load(yamlInputStream).delete(); + log.info("response delete=" + response.toString()); + return resource; + }else { + Namespace namespace = new NamespaceBuilder() + .withNewMetadata() + .withName(destination_namespace) // Set the namespace name + .addToLabels("pod-security.kubernetes.io/enforce", "baseline") // Add labels + .addToLabels("pod-security.kubernetes.io/audit", "baseline") // Add labels + .addToLabels("pod-security.kubernetes.io/warn", "baseline") // Add labels + .endMetadata() + .build(); + + // Create the namespace + remoteKubClient.namespaces().createOrReplace(namespace); + + List<HasMetadata> response = localKubClient.load(yamlInputStream).createOrReplace(); + log.info("response=" + response.toString()); + return updateResourceStatus(resource, KCJResourceState.ACTIVE, null, "CRIDGE successfully configured locally via Local ArgoCD", null, localKubClient); + } + + + }catch (Exception e) { + log.info("Error in installCRIDGELocallyviaLocalArgoCD: " + e.getMessage()); + return updateResourceStatus(resource, KCJResourceState.ACTIVE, null, "Failed to configure CRIDGE Locally via Local ArgoCD", null, localKubClient); + } + + } + + + private KCJResource installCRIDGERemotellyviaLocalArgoCD(KCJResource resource, + KubernetesClient localKubClient, KubernetesClient remoteKubClient, String targetKubeConfig, Boolean isDelete) { + + log.info("In installCRIDGERemotellyviaLocalArgoCD"); + + Config remoteConfig = remoteKubClient.getConfiguration(); + String appName = "cridge-remote-"+remoteConfig.getCurrentContext().getContext().getCluster(); + String activemq_brokerUrl = resource.getSpec().getCridgeAMQEndPoint() ; + String activemq_user = resource.getSpec().getCridgeAMQUsername(); + String activemq_password = resource.getSpec().getCridgeAMQPassword(); + + + String kubeconfig = targetKubeConfig ; + //String destination_server = remoteConfig.getMasterUrl(); + String destination_server = ""; + String destination_server_name = remoteConfig.getCurrentContext().getContext().getCluster(); + + log.info("destination_server_name = " + destination_server_name); + String destination_namespace = appName; + + String yamlToapply = prepareCRIDGEyaml(appName, activemq_brokerUrl, activemq_user, activemq_password, + destination_server, destination_server_name, destination_namespace, kubeconfig); + + // Convert the YAML string to an InputStream + InputStream yamlInputStream = new java.io.ByteArrayInputStream(yamlToapply.getBytes()); + + // Parse the YAML file into Kubernetes resources + try { + if (isDelete) { + + List<StatusDetails> response = localKubClient.load(yamlInputStream).delete(); + log.info("response delete=" + response.toString()); + return resource; + }else { + + Namespace namespace = new NamespaceBuilder() + .withNewMetadata() + .withName(destination_namespace) // Set the namespace name + .addToLabels("pod-security.kubernetes.io/enforce", "baseline") // Add labels + .addToLabels("pod-security.kubernetes.io/audit", "baseline") // Add labels + .addToLabels("pod-security.kubernetes.io/warn", "baseline") // Add labels + .endMetadata() + .build(); + + // Create the namespace + remoteKubClient.namespaces().createOrReplace(namespace); + List<HasMetadata> response = localKubClient.load(yamlInputStream).createOrReplace(); + log.info("response=" + response.toString()); + return updateResourceStatus(resource, KCJResourceState.ACTIVE, null, "CRIDGE successfully configured remotelly via Local ArgoCD", null, localKubClient); + } + + + }catch (Exception e) { + log.info("Error in installCRIDGELocallyviaLocalArgoCD: " + e.getMessage()); + return updateResourceStatus(resource, KCJResourceState.ACTIVE, null, "Failed to configure CRIDGE remotelly via Local ArgoCD", null, localKubClient); + + } + } + + private String prepareCRIDGEyaml(String appName, String activemq_brokerUrl, String activemq_user, String activemq_password, + String destination_server, String destination_server_name, String destination_namespace, String kubeconfig) { + String yamlToapply = + """ + apiVersion: argoproj.io/v1alpha1 + kind: Application + metadata: + finalizers: + - resources-finalizer.argocd.argoproj.io + name: %s + namespace: argocd + spec: + project: default + source: + repoURL: https://labs.etsi.org/rep/osl/code/org.etsi.osl.cridge.git + path: helm/cridge + targetRevision: helm + helm: + parameters: + - name: oscreds.activemq.brokerUrl + value: %s + - name: oscreds.activemq.user + value: %s + - name: oscreds.activemq.password + value: %s + values: | + kubeconfig: | + XXX + destination: + server: %s + name: %s + namespace: %s + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + """; + + + + + yamlToapply = String.format(yamlToapply, appName, activemq_brokerUrl, activemq_user, activemq_password, + destination_server, destination_server_name, destination_namespace); + + + // Step 2: Parse the YAML string into a Map + Yaml yaml = new Yaml(); + Map<String, Object> yamlMap = yaml.load(yamlToapply); + + // Step 3: Modify the node (e.g., change the image field) + Map<String, Object> firstContainer = (Map<String, Object>) ((Map<String, Object>) yamlMap.get("spec")).get("source") ; + firstContainer = (Map<String, Object>) ((Map<String, Object>) firstContainer.get("helm")); + String val = (String) ( firstContainer.get("values")); + + kubeconfig = kubeconfig.replace("\n", "\n "); + firstContainer.put("values", "kubeconfig: |\n " + kubeconfig); // Modify the image field + // Step 4: Convert the modified Map back to a YAML string + DumperOptions options = new DumperOptions(); + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); // Set the style to block for readability + Yaml yamlDumper = new Yaml(options); + yamlToapply = yamlDumper.dump(yamlMap); + + + log.info("yamlToapply=" + yamlToapply); + return yamlToapply; + } + + private KCJResource execStrategyEnablelocalArgo(KCJResource resource, + KubernetesClient localKubClient, + KubernetesClient remoteKubClient) { + + //STEP 1: exec to remote cluster the RBC service accounts creation + + try (InputStream inputStream = KCJResourceOperator.class.getResourceAsStream("/remoteArgoCDRBAC.yaml")) { + if (inputStream == null) { + log.error("File not found!"); + return resource; + } + // Parse the YAML file into Kubernetes resources + remoteKubClient.load(inputStream).serverSideApply() ; + + } catch (Exception e) { + e.printStackTrace(); + } + + // STEP 2 Create the Secret + Secret secret = new SecretBuilder() + .withNewMetadata() + .withName("argocd-manager-token") + .withNamespace("kube-system") + .addToAnnotations("kubernetes.io/service-account.name", "argocd-manager") + .endMetadata() + .withType("kubernetes.io/service-account-token") + .build(); + + + // Apply the Secret to the Kubernetes cluster + remoteKubClient.secrets().inNamespace("kube-system").createOrReplace(secret); + + + Secret aSecret = remoteKubClient.secrets().inNamespace("kube-system").withName("argocd-manager-token").get(); + + log.info("Secret created or replaced successfully!"); + log.info("Secret:\n{}", aSecret); + + + // Extract the Base64 encoded token from the Secret's data + String base64EncodedToken = aSecret.getData().get("token"); + String decodedToken =""; + if (base64EncodedToken != null) { + // Decode the Base64 token + byte[] decodedBytes = Base64.getDecoder().decode(base64EncodedToken); + decodedToken = new String(decodedBytes); + + // Print the decoded token + log.info("Decoded token: " + decodedToken); + } else { + log.error("Token not found in the Secret data."); + } + + // Extract the Base64 encoded token from the Secret's data + String ca = aSecret.getData().get("ca.crt"); + log.info("ca: " + ca); + + + //STEP 3 create th secret in the local cluster + + + Config remoteConfig = remoteKubClient.getConfiguration(); + + // Retrieve the server URL + String serverUrl = remoteConfig.getMasterUrl(); + + Secret localSecret = new SecretBuilder() + .withNewMetadata() + .withName( remoteConfig.getCurrentContext().getContext().getCluster() + "-secret") + .addToLabels("argocd.argoproj.io/secret-type", "cluster") + .endMetadata() + .withType("Opaque") + .addToStringData("name", remoteConfig.getCurrentContext().getContext().getCluster() ) + .addToStringData("server", serverUrl ) + .addToStringData("config", "{\n" + + " \"bearerToken\": \""+decodedToken+"\",\n" + + " \"tlsClientConfig\": {\n" + + " \"insecure\": false,\n" + + " \"caData\": \""+ ca +"\"\n" + + " }\n" + + "}") + .build(); + + // Apply the Secret to the Kubernetes cluster + localKubClient.secrets().inNamespace("argocd").createOrReplace(localSecret); + + System.out.println("Secret created or replaced successfully!"); + + + + + return updateResourceStatus(resource, KCJResourceState.ACTIVE, null, null, "Failed to configure local ArgoCD", localKubClient); + + } + + + public static String convertInputStreamToString(InputStream inputStream) throws IOException { + // Use try-with-resources to ensure InputStream is closed after usage + try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { + // Use stream to collect the lines into a single string + return reader.lines().collect(Collectors.joining("\n")); + } +} + + private KCJResource execStrategyInstallRemoteArgo(KCJResource resource, KubernetesClient localKubClient, KubernetesClient remoteKubClient) { + + + // Define the namespace + Namespace namespace = new NamespaceBuilder() + .withNewMetadata() + .withName(ARGOCD_NAMESPACE) // Replace with your desired namespace name + .endMetadata() + .build(); + + // Use .resource(namespace) and then .createOrReplace() to create or replace the namespace + remoteKubClient.resource(namespace).createOr( (operation) -> { + try{ + operation.create(); + }catch (Exception e) { + log.info("NAMESPACE EXISTS. WILL UPDATE IT: " + e.getMessage()); + } + + return operation.update(); + }) ; + + + + try { + // Download the remote YAML file + InputStream yamlStream = new URL(REMOTARGO_URL).openStream(); + String yamlString = convertInputStreamToString(yamlStream); + //System.out.println("Converted String: \n" + yamlString); + // Convert the YAML string to an InputStream + InputStream yamlInputStream = new ByteArrayInputStream(yamlString.getBytes(StandardCharsets.UTF_8)); + + // Parse the YAML file into Kubernetes resources + remoteKubClient.load(yamlInputStream).inNamespace(ARGOCD_NAMESPACE).serverSideApply() ; + + + log.info("Applied the remote YAML to namespace: " + namespace); + return updateResourceStatus(resource, KCJResourceState.ACTIVE, "ArgoCD installed remotelly", null, null, localKubClient); + + } catch (IOException e) { + e.printStackTrace(); + return updateResourceStatus(resource, KCJResourceState.FAILED, "Failed to install remotely ArgoCD", null, null, localKubClient); + } + + + + } + + /** + * @param resource + * @param i + * @param string + * @param client + * @return + */ + private KCJResource updateResourceStatus(KCJResource resource, KCJResourceState state, + String infoInstallArgoCD, + String infoInstallCridge, + String infoEnableLocalArgoCD, KubernetesClient client) { + + try { + KCJResourceStatus resStatus = resource.getStatus(); + if (resStatus == null) { + resStatus = new KCJResourceStatus(); + } + if ( state != null) + resStatus.setState(state); + if ( infoInstallArgoCD!= null) + resStatus.setInfoInstallArgoCD(infoInstallArgoCD); + if ( infoInstallCridge!= null) + resStatus.setInfoInstallCridge(infoInstallCridge); + if ( infoEnableLocalArgoCD!= null) + resStatus.setInfoEnableLocalArgoCD(infoEnableLocalArgoCD); + resource.setStatus(resStatus); + + resource = client.resource(resource).patch(); + + return resource; + } catch (Exception e) { + return null; + } + + + } + + private KCJResource addFinalizers(KCJResource resource, KubernetesClient client) { + + List<String> finalizers = resource.getMetadata().getFinalizers(); + if (finalizers == null) { + finalizers = new ArrayList<String>(); + } + finalizers.clear(); + finalizers.add("kcj.controller.osl.etsi.org"); + resource.getMetadata().setFinalizers(finalizers); + + resource = client.resource(resource).patch(); + return resource; + } + + // Remove the finalizer after cleanup + private KCJResource removeFinalizer(KCJResource resource, KubernetesClient client) { + List<String> finalizers = resource.getMetadata().getFinalizers(); + if (finalizers != null) { + finalizers.clear(); + resource.getMetadata().setFinalizers(finalizers); + + resource = client.resource(resource).patch(); + log.info("Finalizer removed from resource: " + resource.getMetadata().getName()); + return resource; + } + return resource; + } + +} diff --git a/src/main/java/org/etsi/osl/controllers/kcj/KCJResourceSpec.java b/src/main/java/org/etsi/osl/controllers/kcj/KCJResourceSpec.java new file mode 100644 index 0000000000000000000000000000000000000000..60dfcd19bbb32a2f841a369dc3dbf6a2101d634b --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/kcj/KCJResourceSpec.java @@ -0,0 +1,65 @@ +/*- + * ========================LICENSE_START================================= + * org.etsi.osl.controllers.sylva + * %% + * Copyright (C) 2024 osl.etsi.org + * %% + * 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. + * =========================LICENSE_END================================== + */ +package org.etsi.osl.controllers.kcj; + +import lombok.Data; + +@Data +public class KCJResourceSpec { + + /** + *provide kube config to connect to cluster + */ + private String clusterConfig; + /** + *provide kube config as base64 string + */ + private String clusterConfigBase64; + /** + *if true installs argo cd to remote cluster + */ + private boolean installArgoCDRemote; + /** + *if true installs OSL CRIDGE in local cluster and configures it + */ + private boolean installCridgeLocal; + /** + *if true installs OSL CRIDGE in remote cluster and configures it + */ + private boolean installCridgeRemote; + /** + * Active MQ endpoint for CRIDGE to connect + */ + private String cridgeAMQEndPoint; + /** + *Active MQ username for CRIDGE to connect + */ + private String cridgeAMQUsername; + /** + *Active MQ password for CRIDGE to connect + */ + private String cridgeAMQPassword; + /** + * if true will add the Kubernetes cluster to Argo CD using the CRD method. + * Will create service account and add cluster to local argoCD + */ + private boolean addClusterToLocalArgoCD; + +} diff --git a/src/main/java/org/etsi/osl/controllers/kcj/KCJResourceState.java b/src/main/java/org/etsi/osl/controllers/kcj/KCJResourceState.java new file mode 100644 index 0000000000000000000000000000000000000000..fc917e12a792b5321965fddeecf7fee4e006acad --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/kcj/KCJResourceState.java @@ -0,0 +1,52 @@ +/*- + * ========================LICENSE_START================================= + * org.etsi.osl.controllers.sylva + * %% + * Copyright (C) 2024 osl.etsi.org + * %% + * 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. + * =========================LICENSE_END================================== + */ +package org.etsi.osl.controllers.kcj; + + +public enum KCJResourceState { + IN_PROGRESS("IN_PROGRESS"), + ACTIVE("JOINED"), + FAILED("FAILED"), + DELETE_FAILED("DELETE_FAILED"), + DELETED("DELETED"); + + + private String value; + + KCJResourceState(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + @Override + public String toString() { + return this.getValue(); + } + + public static KCJResourceState getEnum(String value) { + for(KCJResourceState v : values()) + if(v.getValue().equalsIgnoreCase(value)) return v; + throw new IllegalArgumentException(); + } + +} diff --git a/src/main/java/org/etsi/osl/controllers/kcj/KCJResourceStatus.java b/src/main/java/org/etsi/osl/controllers/kcj/KCJResourceStatus.java new file mode 100644 index 0000000000000000000000000000000000000000..20947ee83e6274138e1d1e748eaf169b813fe7c8 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/kcj/KCJResourceStatus.java @@ -0,0 +1,31 @@ +/*- + * ========================LICENSE_START================================= + * org.etsi.osl.controllers.sylva + * %% + * Copyright (C) 2024 osl.etsi.org + * %% + * 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. + * =========================LICENSE_END================================== + */ +package org.etsi.osl.controllers.kcj; + +import lombok.Data; + +@Data +public class KCJResourceStatus { + + private KCJResourceState state; + private String infoInstallArgoCD; + private String infoInstallCridge; + private String infoEnableLocalArgoCD; +} diff --git a/src/main/java/org/etsi/osl/controllers/kcj/KubernetesClientConfig.java b/src/main/java/org/etsi/osl/controllers/kcj/KubernetesClientConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..ef4ec135d0e5fcbbc0afa6ec25c6472f9c20f964 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/kcj/KubernetesClientConfig.java @@ -0,0 +1,45 @@ +/*- + * ========================LICENSE_START================================= + * org.etsi.osl.controllers.sylva + * %% + * Copyright (C) 2024 osl.etsi.org + * %% + * 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. + * =========================LICENSE_END================================== + */ +package org.etsi.osl.controllers.kcj; + +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.ConfigBuilder; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientBuilder; +import io.fabric8.kubernetes.client.utils.HttpClientUtils; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class KubernetesClientConfig { + + @Bean + public KubernetesClient kubernetesClient() { + // Create a custom configuration that disables SSL verification + Config config = new ConfigBuilder() + .withRequestTimeout(30000) // 30 seconds + .withConnectionTimeout(30000) // 30 seconds + .withWatchReconnectInterval(5000) // Reconnect interval for watches + .build(); + + // Build the Kubernetes client with this configuration + return new KubernetesClientBuilder().withConfig(config).build(); + } +} diff --git a/src/main/resources/KCJResourceOperator.yaml b/src/main/resources/KCJResourceOperator.yaml new file mode 100644 index 0000000000000000000000000000000000000000..d688706e0cc5b0c601e6d2b1ea7730570008cf37 --- /dev/null +++ b/src/main/resources/KCJResourceOperator.yaml @@ -0,0 +1,51 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: kcjresources.controllers.osl.etsi.org # Name of the CRD +spec: + group: controllers.osl.etsi.org # Custom resource group + names: + kind: KCJResource # Name of the resource type + plural: kcjresources # Plural form of the resource + singular: kcjresource # Singular form of the resource + scope: Namespaced # Namespaced resource + versions: + - name: v1alpha1 # Version of the CRD + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + clusterConfig: + type: string + clusterConfigBase64: + type: string + installArgoCDRemote: + type: boolean + installCridgeLocal: + type: boolean + installCridgeRemote: + type: boolean + cridgeAMQEndPoint: + type: string + cridgeAMQUsername: + type: string + cridgeAMQPassword: + type: string + addClusterToLocalArgoCD: + type: boolean + status: + type: object + properties: + state: + type: string + infoInstallArgoCD: + type: string + infoInstallCridge: + type: string + infoEnableLocalArgoCD: + type: string diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000000000000000000000000000000000000..c787730c4db97c96021df9094880820dae5babb5 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,9 @@ +spring: + application: + name: kcj-osl-operator + main: + web-application-type: none + +server: + port: 0 + \ No newline at end of file diff --git a/src/main/resources/banner.txt b/src/main/resources/banner.txt new file mode 100644 index 0000000000000000000000000000000000000000..71bba67fa45692e2299e0ce85d6f8cdf1182d17f --- /dev/null +++ b/src/main/resources/banner.txt @@ -0,0 +1,16 @@ + ___ ____ _ _ + / _ \ _ __ ___ _ __ / ___|| (_) ___ ___ + | | | | '_ \ / _ \ '_ \\___ \| | |/ __/ _ \ + | |_| | |_) | __/ | | |___) | | | (_| __/ + \___/| .__/ \___|_| |_|____/|_|_|\___\___| + |_| + __ __________________ + / / __ __ / __/_ __/ __/ _/ + / _ \/ // / / _/ / / _\ \_/ / + /_.__/\_, / /___/ /_/ /___/___/ + /___/ + _ _____ _ ___ _ _ _ + | |/ / __| | | / __|___ _ _| |_ _ _ ___| | |___ _ _ + | ' < (_| || | | (__/ _ \ ' \ _| '_/ _ \ | / -_) '_| + |_|\_\___\__/ \___\___/_||_\__|_| \___/_|_\___|_| + \ No newline at end of file diff --git a/src/main/resources/remoteArgoCDRBAC.yaml b/src/main/resources/remoteArgoCDRBAC.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e892562e5c5f125df00042411a0d05b177823cd4 --- /dev/null +++ b/src/main/resources/remoteArgoCDRBAC.yaml @@ -0,0 +1,34 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: argocd-manager + namespace: kube-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: argocd-manager-role +rules: +- apiGroups: + - '*' + resources: + - '*' + verbs: + - '*' +- nonResourceURLs: + - '*' + verbs: + - '*' +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: argocd-manager-role-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: argocd-manager-role +subjects: +- kind: ServiceAccount + name: argocd-manager + namespace: kube-system \ No newline at end of file diff --git a/target/.gitignore b/target/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..840e7d3120ee3206168d49bf62df2c269c38e17e --- /dev/null +++ b/target/.gitignore @@ -0,0 +1 @@ +/classes/