pkg/service/platform/openshift/openshift.go (222 lines of code) (raw):
package openshift
import (
"context"
"encoding/json"
"fmt"
"os"
"strings"
appsV1client "github.com/openshift/client-go/apps/clientset/versioned/typed/apps/v1"
projectV1Client "github.com/openshift/client-go/project/clientset/versioned/typed/project/v1"
routeV1Client "github.com/openshift/client-go/route/clientset/versioned/typed/route/v1"
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"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
cdPipeApi "github.com/epam/edp-cd-pipeline-operator/v2/pkg/apis/edp/v1"
"github.com/epam/edp-gerrit-operator/v2/pkg/service/helpers"
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"
"github.com/epam/edp-jenkins-operator/v2/pkg/service/platform/kubernetes"
)
// OpenshiftService struct for Openshift platform service.
type OpenshiftService struct {
kubernetes.K8SService
appClient appsV1client.AppsV1Interface
routeClient routeV1Client.RouteV1Interface
projectClient projectV1Client.ProjectV1Interface
}
const (
deploymentTypeEnvName = "DEPLOYMENT_TYPE"
deploymentConfigsDeploymentType = "deploymentConfigs"
)
// Init initializes OpenshiftService.
func (service *OpenshiftService) Init(config *rest.Config, scheme *runtime.Scheme, k8sClient client.Client) error {
if err := service.K8SService.Init(config, scheme, k8sClient); err != nil {
return fmt.Errorf("failed to init K8S platform service: %w", err)
}
appClient, err := appsV1client.NewForConfig(config)
if err != nil {
return fmt.Errorf("failed to init apps V1 client for Openshift: %w", err)
}
routeClient, err := routeV1Client.NewForConfig(config)
if err != nil {
return fmt.Errorf("failed to init route V1 client for Openshift: %w", err)
}
pc, err := projectV1Client.NewForConfig(config)
if err != nil {
return fmt.Errorf("failed to init project client for Openshift: %w", err)
}
service.appClient = appClient
service.routeClient = routeClient
service.projectClient = pc
return nil
}
// GetExternalEndpoint returns hostname and protocol for Route.
func (service *OpenshiftService) GetExternalEndpoint(namespace, name string) (host, routeScheme, path string, err error) {
route, err := service.routeClient.
Routes(namespace).
Get(context.TODO(), name, metav1.GetOptions{})
if err != nil {
if k8sErrors.IsNotFound(err) {
return "", "", "", fmt.Errorf("failed to find route %v in namespace %v", name, namespace)
}
return "", "", "", fmt.Errorf("failed to get Routes: %w", err)
}
specHost := route.Spec.Host
routeHTTPSScheme := jenkinsDefaultSpec.RouteHTTPScheme
if route.Spec.TLS.Termination != "" {
routeHTTPSScheme = jenkinsDefaultSpec.RouteHTTPSScheme
}
specPath := strings.TrimRight(route.Spec.Path, platformHelper.UrlCutset)
return specHost, routeHTTPSScheme, specPath, nil
}
func (service *OpenshiftService) IsDeploymentReady(instance *jenkinsApi.Jenkins) (bool, error) {
if os.Getenv(deploymentTypeEnvName) == deploymentConfigsDeploymentType {
deploymentConfig, err := service.appClient.
DeploymentConfigs(instance.Namespace).
Get(context.TODO(), instance.Name, metav1.GetOptions{})
if err != nil {
return false, fmt.Errorf("failed to get DeploymentConfigs: %w", err)
}
if deploymentConfig.Status.UpdatedReplicas == 1 && deploymentConfig.Status.AvailableReplicas == 1 {
return true, nil
}
return false, nil
}
ready, err := service.K8SService.IsDeploymentReady(instance)
if err != nil {
return false, fmt.Errorf("failed to check if deployment is ready: %w", err)
}
return ready, nil
}
func (service *OpenshiftService) AddVolumeToInitContainer(instance *jenkinsApi.Jenkins,
containerName string, vol []coreV1Api.Volume, volMount []coreV1Api.VolumeMount,
) error {
if os.Getenv(deploymentTypeEnvName) == deploymentConfigsDeploymentType {
if len(vol) == 0 || len(volMount) == 0 {
return nil
}
deploymentConfig, err := service.appClient.DeploymentConfigs(instance.Namespace).
Get(context.TODO(), instance.Name, metav1.GetOptions{})
if err != nil {
return nil
}
initContainer, err := selectContainer(deploymentConfig.Spec.Template.Spec.InitContainers, containerName)
if err != nil {
return err
}
initContainer.VolumeMounts = updateVolumeMounts(initContainer.VolumeMounts, volMount)
deploymentConfig.Spec.Template.Spec.InitContainers = append(deploymentConfig.Spec.Template.Spec.InitContainers, initContainer)
volumes := deploymentConfig.Spec.Template.Spec.Volumes
volumes = updateVolumes(volumes, vol)
deploymentConfig.Spec.Template.Spec.Volumes = volumes
jsonDc, err := json.Marshal(deploymentConfig)
if err != nil {
return fmt.Errorf("failed to marshal deployment config: %w", err)
}
_, err = service.appClient.
DeploymentConfigs(deploymentConfig.Namespace).
Patch(context.TODO(), deploymentConfig.Name, types.StrategicMergePatchType, jsonDc, metav1.PatchOptions{})
if err != nil {
return fmt.Errorf("failed to patch DeploymentConfigs: %w", err)
}
return nil
}
if err := service.K8SService.AddVolumeToInitContainer(instance, containerName, vol, volMount); err != nil {
return fmt.Errorf("failed to add volume to init container: %w", err)
}
return 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 i := 0; i < len(existing); i++ {
newer, ok := findVolumeMount(volMount, 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(volMount); i++ {
if helpers.IsStringInSlice(volMount[i].Name, covered) {
continue
}
covered = append(covered, volMount[i].Name)
out = append(out, volMount[i])
}
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 (*OpenshiftService) CreateStageJSON(stage *cdPipeApi.Stage) (string, error) {
j := []model.PipelineStage{
{
Name: "deploy",
StepName: "deploy",
},
}
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 stages: %w", err)
}
return string(o), err
}