pkg/service/platform/kubernetes/k8s.go (533 lines of code) (raw):
package kubernetes
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"reflect"
"strings"
coreV1Api "k8s.io/api/core/v1"
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
appsV1Client "k8s.io/client-go/kubernetes/typed/apps/v1"
networkingV1Client "k8s.io/client-go/kubernetes/typed/networking/v1"
authV1Client "k8s.io/client-go/kubernetes/typed/rbac/v1"
"k8s.io/client-go/rest"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
cdPipeApi "github.com/epam/edp-cd-pipeline-operator/v2/pkg/apis/edp/v1"
edpCompApi "github.com/epam/edp-component-operator/pkg/apis/v1/v1"
"github.com/epam/edp-gerrit-operator/v2/pkg/service/helpers"
keycloakV1Api "github.com/epam/edp-keycloak-operator/pkg/apis/v1/v1"
jenkinsApi "github.com/epam/edp-jenkins-operator/v2/pkg/apis/v2/v1"
"github.com/epam/edp-jenkins-operator/v2/pkg/model"
jenkinsDefaultSpec "github.com/epam/edp-jenkins-operator/v2/pkg/service/jenkins/spec"
platformHelper "github.com/epam/edp-jenkins-operator/v2/pkg/service/platform/helper"
)
var log = ctrl.Log.WithName("platform")
// K8SService struct for K8S platform service.
type K8SService struct {
Scheme *runtime.Scheme
client client.Client
authClient authV1Client.RbacV1Client
networkingClient networkingV1Client.NetworkingV1Interface
appsV1Client appsV1Client.AppsV1Interface
}
// Init initializes K8SService.
func (s *K8SService) Init(config *rest.Config, scheme *runtime.Scheme, k8sClient client.Client) error {
authClient, err := authV1Client.NewForConfig(config)
if err != nil {
return fmt.Errorf("failed to init auth V1 client for K8S: %w", err)
}
s.Scheme = scheme
s.authClient = *authClient
s.client = k8sClient
ncl, err := networkingV1Client.NewForConfig(config)
if err != nil {
return fmt.Errorf("failed to init networking V1 client for K8S: %w", err)
}
s.networkingClient = ncl
appsClient, err := appsV1Client.NewForConfig(config)
if err != nil {
return fmt.Errorf("failed to init apps V1 client for K8S: %w", err)
}
s.appsV1Client = appsClient
return nil
}
// GetExternalEndpoint returns Ingress object and connection protocol from Kubernetes.
func (s *K8SService) GetExternalEndpoint(namespace, name string) (host, routeScheme, path string, err error) {
ingress, err := s.networkingClient.
Ingresses(namespace).
Get(context.TODO(), name, metav1.GetOptions{})
if err != nil {
if k8sErrors.IsNotFound(err) {
return "", "", "", fmt.Errorf("failed to find ingress %v in namespace %v", name, namespace)
}
return "", "", "", fmt.Errorf("failed to get ingress: %w", err)
}
specHost := ingress.Spec.Rules[0].Host
routeHTTPSScheme := jenkinsDefaultSpec.RouteHTTPSScheme
specPath := strings.TrimRight(ingress.Spec.Rules[0].HTTP.Paths[0].Path, platformHelper.UrlCutset)
return specHost, routeHTTPSScheme, specPath, nil
}
// AddVolumeToInitContainer adds volume to Jenkins init container.
func (s *K8SService) AddVolumeToInitContainer(instance *jenkinsApi.Jenkins, containerName string,
vol []coreV1Api.Volume, volMount []coreV1Api.VolumeMount,
) error {
if len(vol) == 0 || len(volMount) == 0 {
return nil
}
deployment, err := s.appsV1Client.Deployments(instance.Namespace).Get(context.TODO(), instance.Name, metav1.GetOptions{})
if err != nil {
return nil
}
initContainer, err := selectContainer(deployment.Spec.Template.Spec.InitContainers, containerName)
if err != nil {
return err
}
initContainer.VolumeMounts = updateVolumeMounts(initContainer.VolumeMounts, volMount)
deployment.Spec.Template.Spec.InitContainers = append(deployment.Spec.Template.Spec.InitContainers, initContainer)
volumes := deployment.Spec.Template.Spec.Volumes
volumes = updateVolumes(volumes, vol)
deployment.Spec.Template.Spec.Volumes = volumes
jsonDc, err := json.Marshal(deployment)
if err != nil {
return fmt.Errorf("failed to marshal: %w", err)
}
_, err = s.appsV1Client.
Deployments(deployment.Namespace).
Patch(context.TODO(), deployment.Name, types.StrategicMergePatchType, jsonDc, metav1.PatchOptions{})
if err != nil {
return fmt.Errorf("failed to patch deployment: %w", err)
}
return nil
}
func (s *K8SService) IsDeploymentReady(instance *jenkinsApi.Jenkins) (bool, error) {
deployment, err := s.appsV1Client.
Deployments(instance.Namespace).
Get(context.TODO(), instance.Name, metav1.GetOptions{})
if err != nil {
return false, fmt.Errorf("failed to get deployments: %w", err)
}
if deployment.Status.UpdatedReplicas == 1 && deployment.Status.AvailableReplicas == 1 {
return true, nil
}
return false, nil
}
func (s *K8SService) CreateSecret(instance *jenkinsApi.Jenkins, name string, data map[string][]byte) error {
_, err := s.getSecret(name, instance.Namespace)
if err != nil {
if k8sErrors.IsNotFound(err) {
secret := &coreV1Api.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: instance.Namespace,
Labels: platformHelper.GenerateLabels(instance.Name),
},
Data: data,
Type: "Opaque",
}
return s.createSecretInK8S(instance, secret)
}
return fmt.Errorf("failed to get Secret %v object: %w", name, err)
}
return nil
}
func (s *K8SService) getSecret(name, namespace string) (*coreV1Api.Secret, error) {
secret := &coreV1Api.Secret{}
if err := s.client.Get(
context.TODO(),
types.NamespacedName{
Namespace: namespace,
Name: name,
},
secret,
); err != nil {
return nil, fmt.Errorf("failed to get secret: %w", err)
}
return secret, nil
}
func (s *K8SService) createSecretInK8S(jenkins *jenkinsApi.Jenkins, secret *coreV1Api.Secret) error {
if err := controllerutil.SetControllerReference(jenkins, secret, s.Scheme); err != nil {
return fmt.Errorf("failed to set reference for Secret %v object: %w", secret.Name, err)
}
if err := s.client.Create(context.TODO(), secret); err != nil {
return fmt.Errorf("failed to create Secret %v object: %w", secret.Name, err)
}
log.Info(fmt.Sprintf("Secret %v has been created", secret.Name))
return nil
}
// GetSecretData return data field of Secret.
func (s *K8SService) GetSecretData(namespace, name string) (map[string][]byte, error) {
secret, err := s.getSecret(name, namespace)
if err != nil {
if k8sErrors.IsNotFound(err) {
log.Info(fmt.Sprintf("Secret %v in namespace %v not found", name, namespace))
return nil, nil
}
return nil, err
}
return secret.Data, nil
}
func (s *K8SService) CreateConfigMapWithUpdate(instance *jenkinsApi.Jenkins, name string, data map[string]string,
labels ...map[string]string,
) (isUpdated bool, err error) {
currentConfigMap, err := s.CreateConfigMap(instance, name, data, labels...)
if err != nil {
return false, fmt.Errorf("failed to create configmap: %w", err)
}
if reflect.DeepEqual(data, currentConfigMap.Data) {
return false, nil
}
currentConfigMap.Data = data
if err := s.client.Update(context.TODO(), currentConfigMap); err != nil {
return false, fmt.Errorf("failed to update config map: %w", err)
}
return true, nil
}
func (s *K8SService) CreateConfigMap(instance *jenkinsApi.Jenkins, name string, data map[string]string,
labels ...map[string]string,
) (*coreV1Api.ConfigMap, error) {
currentConfigMap, err := s.getConfigMap(name, instance.Namespace)
if err != nil {
if k8sErrors.IsNotFound(err) {
resultLabels := platformHelper.GenerateLabels(instance.Name)
if len(labels) != 0 {
resultLabels = labels[0]
}
currentConfigMap = &coreV1Api.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: instance.Namespace,
Labels: resultLabels,
},
Data: data,
}
if createMapErr := s.createConfigMapInK8S(instance, currentConfigMap); createMapErr != nil {
return nil, fmt.Errorf("failed to create config map: %w", err)
}
return currentConfigMap, nil
}
return nil, fmt.Errorf("failed to get ConfigMap %v object: %w", name, err)
}
return currentConfigMap, nil
}
func (s *K8SService) getConfigMap(name, namespace string) (*coreV1Api.ConfigMap, error) {
configMap := &coreV1Api.ConfigMap{}
if err := s.client.Get(
context.TODO(),
types.NamespacedName{
Namespace: namespace,
Name: name,
},
configMap,
); err != nil {
return nil, fmt.Errorf("failed to get ConfigMap: %w", err)
}
return configMap, nil
}
func (s *K8SService) createConfigMapInK8S(jenkins *jenkinsApi.Jenkins, cm *coreV1Api.ConfigMap) error {
if err := controllerutil.SetControllerReference(jenkins, cm, s.Scheme); err != nil {
return fmt.Errorf("failed to set reference for Config Map %v object: %w", cm.Name, err)
}
if err := s.client.Create(context.TODO(), cm); err != nil {
return fmt.Errorf("failed to create Config Map %v object: %w", cm.Name, err)
}
log.Info(fmt.Sprintf("ConfigMap %s/%s has been created", cm.Namespace, cm.Name))
return nil
}
// CreateConfigMapFromFileOrDir performs creating ConfigMap in K8S.
func (s *K8SService) CreateConfigMapFromFileOrDir(instance *jenkinsApi.Jenkins, configMapName string,
configMapKey *string, path string, _ metav1.Object, customLabels ...map[string]string,
) error {
configMapData, err := s.fillConfigMapData(path, configMapKey)
if err != nil {
return fmt.Errorf("failed to generate Config Map data for %v: %w", configMapName, err)
}
labels := platformHelper.GenerateLabels(instance.Name)
if len(customLabels) == 1 {
for key, value := range customLabels[0] {
labels[key] = value
}
}
_, err = s.CreateConfigMap(instance, configMapName, configMapData, labels)
if err != nil {
return fmt.Errorf("failed to create Config Map %v: %w", configMapName, err)
}
return nil
}
func (s *K8SService) CreateEDPComponentIfNotExist(jen *jenkinsApi.Jenkins, url, icon string) error {
c, err := s.getEDPComponent(jen.Name, jen.Namespace)
if err != nil {
if k8sErrors.IsNotFound(err) {
return s.createEDPComponent(jen, url, icon)
}
return fmt.Errorf("failed to get edp component, with parans (%v, %v): %w",
jen.Name, jen.Namespace, err)
}
log.V(1).Info("edp component already exists", "name", c.Name)
return nil
}
func (s *K8SService) getEDPComponent(name, namespace string) (*edpCompApi.EDPComponent, error) {
edpComponent := &edpCompApi.EDPComponent{}
if err := s.client.Get(
context.TODO(),
types.NamespacedName{
Namespace: namespace,
Name: name,
},
edpComponent,
); err != nil {
return nil, fmt.Errorf("failed to get EDP component: %w", err)
}
return edpComponent, nil
}
func (s *K8SService) createEDPComponent(jen *jenkinsApi.Jenkins, url, icon string) error {
edpComponent := &edpCompApi.EDPComponent{
ObjectMeta: metav1.ObjectMeta{
Name: jen.Name,
Namespace: jen.Namespace,
},
Spec: edpCompApi.EDPComponentSpec{
Type: "jenkins",
Url: url,
Icon: icon,
Visible: true,
},
}
if err := controllerutil.SetControllerReference(jen, edpComponent, s.Scheme); err != nil {
return fmt.Errorf("failed to set Controller reference: %w", err)
}
if err := s.client.Create(context.TODO(), edpComponent); err != nil {
return fmt.Errorf("failed to create EDPComponent: %w", err)
}
return nil
}
func (s *K8SService) fillConfigMapData(path string, configMapKey *string) (map[string]string, error) {
var configMapData map[string]string
pathInfo, err := os.Stat(path)
if err != nil {
return nil, fmt.Errorf("failed to open path %v: %w", path, err)
}
if pathInfo.Mode().IsDir() {
configMapData, err = s.fillConfigMapFromDir(path)
if err != nil {
return nil, fmt.Errorf("failed to generate config map data from directory %v: %w", path, err)
}
return configMapData, nil
}
configMapData, err = s.fillConfigMapFromFile(path, configMapKey)
if err != nil {
return nil, fmt.Errorf("failed to generate config map data from file %v: %w", path, err)
}
return configMapData, nil
}
func (*K8SService) fillConfigMapFromFile(path string, configMapKey *string) (map[string]string, error) {
content, err := os.ReadFile(filepath.Clean(path))
if err != nil {
return nil, fmt.Errorf("failed to read file %v: %w", path, err)
}
key := filepath.Base(path)
if configMapKey != nil {
key = *configMapKey
}
configMapData := map[string]string{
key: string(content),
}
return configMapData, nil
}
func (*K8SService) fillConfigMapFromDir(path string) (map[string]string, error) {
configMapData := make(map[string]string)
directory, err := os.ReadDir(path)
if err != nil {
return nil, fmt.Errorf("failed to open path %v: %w", path, err)
}
for _, file := range directory {
content, err := os.ReadFile(fmt.Sprintf("%v/%v", path, file.Name()))
if err != nil {
return nil, fmt.Errorf("failed to open path %v: %w", path, err)
}
configMapData[file.Name()] = string(content)
}
return configMapData, nil
}
// GetConfigMapData return data field of ConfigMap.
func (s *K8SService) GetConfigMapData(namespace, name string) (map[string]string, error) {
cm, err := s.getConfigMap(name, namespace)
if err != nil {
if k8sErrors.IsNotFound(err) {
return nil, fmt.Errorf("failed to find config map %s in namespace %s", name, namespace)
}
return nil, fmt.Errorf("failed to get ConfigMap %s object: %w", name, err)
}
return cm.Data, nil
}
func (s *K8SService) CreateKeycloakClient(kc *keycloakV1Api.KeycloakClient) error {
nsn := types.NamespacedName{
Namespace: kc.Namespace,
Name: kc.Name,
}
if err := s.client.Get(context.TODO(), nsn, kc); err != nil {
if k8sErrors.IsNotFound(err) {
if err = s.client.Create(context.TODO(), kc); err != nil {
return fmt.Errorf("failed to create Keycloak client %s/%s: %w", kc.Namespace, kc.Name, err)
}
log.Info(fmt.Sprintf("Keycloak client %s/%s created", kc.Namespace, kc.Name))
return nil
}
return fmt.Errorf("failed to create Keycloak client %s/%s: %w", kc.Namespace, kc.Name, err)
}
return nil
}
func (s *K8SService) GetKeycloakClient(name, namespace string) (keycloakV1Api.KeycloakClient, error) {
out := keycloakV1Api.KeycloakClient{}
nsn := types.NamespacedName{
Namespace: namespace,
Name: name,
}
if err := s.client.Get(context.TODO(), nsn, &out); err != nil {
return out, fmt.Errorf("failed to get KeycloakClient: %w", err)
}
return out, nil
}
func (s *K8SService) CreateJenkinsScript(namespace, name string, forceRecreate bool) (*jenkinsApi.JenkinsScript, error) {
js, err := s.getJenkinsScript(name, namespace)
if err != nil {
if k8sErrors.IsNotFound(err) {
newJenkinsScript := &jenkinsApi.JenkinsScript{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: jenkinsApi.JenkinsScriptSpec{
SourceCmName: name,
},
}
if err = s.client.Create(context.TODO(), newJenkinsScript); err != nil {
return nil, fmt.Errorf("failed to create jenkins script: %w", err)
}
return newJenkinsScript, nil
}
return nil, fmt.Errorf("failed to getJenkinsScript: %w", err)
}
if forceRecreate {
if err := s.client.Delete(context.TODO(), js); err != nil {
return nil, fmt.Errorf("failed to delete jenkins script: %w", err)
}
js = &jenkinsApi.JenkinsScript{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: jenkinsApi.JenkinsScriptSpec{
SourceCmName: name,
},
}
if err := s.client.Create(context.TODO(), js); err != nil {
return nil, fmt.Errorf("failed to create jenkins script: %w", err)
}
}
return js, nil
}
func (s *K8SService) getJenkinsScript(name, namespace string) (*jenkinsApi.JenkinsScript, error) {
js := &jenkinsApi.JenkinsScript{}
if err := s.client.Get(
context.TODO(),
types.NamespacedName{
Namespace: namespace,
Name: name,
},
js,
); err != nil {
return nil, fmt.Errorf("failed to get JenkinsScript: %w", err)
}
return js, nil
}
func selectContainer(containers []coreV1Api.Container, name string) (coreV1Api.Container, error) {
for i := 0; i < len(containers); i++ {
if containers[i].Name == name {
return containers[i], nil
}
}
return coreV1Api.Container{}, fmt.Errorf("failed to find matching container in spec")
}
func updateVolumes(existing, vol []coreV1Api.Volume) []coreV1Api.Volume {
var (
out []coreV1Api.Volume
covered []string
)
for i := 0; i < len(existing); i++ {
newer, ok := findVolume(vol, existing[i].Name)
if ok {
covered = append(covered, existing[i].Name)
out = append(out, newer)
continue
}
out = append(out, existing[i])
}
for i := 0; i < len(vol); i++ {
if helpers.IsStringInSlice(vol[i].Name, covered) {
continue
}
covered = append(covered, vol[i].Name)
out = append(out, vol[i])
}
return out
}
func updateVolumeMounts(existing, volMount []coreV1Api.VolumeMount) []coreV1Api.VolumeMount {
var (
out []coreV1Api.VolumeMount
covered []string
)
for _, v := range existing {
newer, ok := findVolumeMount(volMount, v.Name)
if ok {
covered = append(covered, v.Name)
out = append(out, newer)
continue
}
out = append(out, v)
}
for _, v := range volMount {
if helpers.IsStringInSlice(v.Name, covered) {
continue
}
covered = append(covered, v.Name)
out = append(out, v)
}
return out
}
func findVolumeMount(volMount []coreV1Api.VolumeMount, name string) (coreV1Api.VolumeMount, bool) {
for i := 0; i < len(volMount); i++ {
if volMount[i].Name == name {
return volMount[i], true
}
}
return coreV1Api.VolumeMount{}, false
}
func findVolume(vol []coreV1Api.Volume, name string) (coreV1Api.Volume, bool) {
for i := 0; i < len(vol); i++ {
if vol[i].Name == name {
return vol[i], true
}
}
return coreV1Api.Volume{}, false
}
func (*K8SService) CreateStageJSON(stage *cdPipeApi.Stage) (string, error) {
j := []model.PipelineStage{
{
Name: "deploy-helm",
StepName: "deploy-helm",
},
}
for _, ps := range stage.Spec.QualityGates {
i := model.PipelineStage{
Name: ps.QualityGateType,
StepName: ps.StepName,
}
j = append(j, i)
}
j = append(j, model.PipelineStage{Name: "promote-images", StepName: "promote-images"})
o, err := json.Marshal(j)
if err != nil {
return "", fmt.Errorf("failed to marshal: %w", err)
}
return string(o), err
}