2023-08-07 21:13:26 +02:00
// Copyright 2022 Woodpecker Authors
//
// 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.
2022-09-05 06:01:14 +02:00
package kubernetes
import (
2023-12-19 05:53:52 +02:00
"context"
2023-06-04 00:50:08 +02:00
"fmt"
2023-10-09 09:11:08 +02:00
"maps"
2022-09-05 06:01:14 +02:00
"strings"
2023-07-09 19:22:50 +02:00
"github.com/rs/zerolog/log"
2022-09-05 06:01:14 +02:00
v1 "k8s.io/api/core/v1"
2023-12-19 05:53:52 +02:00
"k8s.io/apimachinery/pkg/api/errors"
2022-09-05 06:01:14 +02:00
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2023-10-09 09:11:08 +02:00
2023-12-08 09:15:08 +02:00
"go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/common"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/types"
2022-09-05 06:01:14 +02:00
)
2023-12-19 05:53:52 +02:00
const (
StepLabel = "step"
2024-01-11 17:32:37 +02:00
podPrefix = "wp-"
2023-12-19 05:53:52 +02:00
)
2022-10-31 01:26:49 +02:00
2024-01-11 17:32:37 +02:00
func mkPod ( step * types . Step , config * config , podName , goos string ) ( * v1 . Pod , error ) {
meta := podMeta ( step , config , podName )
2023-12-19 05:53:52 +02:00
2024-01-11 17:32:37 +02:00
spec , err := podSpec ( step , config )
2023-12-19 05:53:52 +02:00
if err != nil {
return nil , err
2022-09-05 06:01:14 +02:00
}
2024-01-11 17:32:37 +02:00
container , err := podContainer ( step , podName , goos )
2023-12-19 05:53:52 +02:00
if err != nil {
return nil , err
2022-09-05 06:01:14 +02:00
}
2023-12-19 05:53:52 +02:00
spec . Containers = append ( spec . Containers , container )
2022-09-05 06:01:14 +02:00
2023-12-19 05:53:52 +02:00
pod := & v1 . Pod {
ObjectMeta : meta ,
Spec : spec ,
2022-09-05 06:01:14 +02:00
}
2023-12-19 05:53:52 +02:00
return pod , nil
}
2024-01-09 06:42:36 +02:00
func stepToPodName ( step * types . Step ) ( name string , err error ) {
if step . Type == types . StepTypeService {
return serviceName ( step )
}
return podName ( step )
}
2023-12-19 05:53:52 +02:00
func podName ( step * types . Step ) ( string , error ) {
2024-01-11 17:32:37 +02:00
return dnsName ( podPrefix + step . UUID )
2023-12-19 05:53:52 +02:00
}
2024-01-11 17:32:37 +02:00
func podMeta ( step * types . Step , config * config , podName string ) metav1 . ObjectMeta {
2023-12-19 05:53:52 +02:00
meta := metav1 . ObjectMeta {
2024-01-11 17:32:37 +02:00
Name : podName ,
Namespace : config . Namespace ,
Annotations : config . PodAnnotations ,
2023-12-19 05:53:52 +02:00
}
2024-01-11 17:32:37 +02:00
labels := make ( map [ string ] string , len ( config . PodLabels ) + 1 )
// copy to not alter the engine config
maps . Copy ( labels , config . PodLabels )
labels [ StepLabel ] = step . Name
2023-12-19 05:53:52 +02:00
meta . Labels = labels
return meta
}
2024-01-11 17:32:37 +02:00
func podSpec ( step * types . Step , config * config ) ( v1 . PodSpec , error ) {
2023-12-19 05:53:52 +02:00
var err error
spec := v1 . PodSpec {
RestartPolicy : v1 . RestartPolicyNever ,
2024-01-11 17:32:37 +02:00
ServiceAccountName : step . BackendOptions . Kubernetes . ServiceAccountName ,
ImagePullSecrets : imagePullSecretsReferences ( config . ImagePullSecretNames ) ,
HostAliases : hostAliases ( step . ExtraHosts ) ,
NodeSelector : nodeSelector ( step . BackendOptions . Kubernetes . NodeSelector , step . Environment [ "CI_SYSTEM_PLATFORM" ] ) ,
Tolerations : tolerations ( step . BackendOptions . Kubernetes . Tolerations ) ,
SecurityContext : podSecurityContext ( step . BackendOptions . Kubernetes . SecurityContext , config . SecurityContext ) ,
}
spec . Volumes , err = volumes ( step . Volumes )
2023-12-19 05:53:52 +02:00
if err != nil {
return spec , err
2022-09-05 06:01:14 +02:00
}
2023-12-19 05:53:52 +02:00
return spec , nil
}
2024-01-11 17:32:37 +02:00
func podContainer ( step * types . Step , podName , goos string ) ( v1 . Container , error ) {
2023-06-04 00:50:08 +02:00
var err error
2023-12-19 05:53:52 +02:00
container := v1 . Container {
2024-01-11 17:32:37 +02:00
Name : podName ,
Image : step . Image ,
WorkingDir : step . WorkingDir ,
2023-12-19 05:53:52 +02:00
}
2024-01-11 17:32:37 +02:00
if step . Pull {
2023-12-19 05:53:52 +02:00
container . ImagePullPolicy = v1 . PullAlways
}
2024-01-11 17:32:37 +02:00
if len ( step . Commands ) != 0 {
scriptEnv , command , args := common . GenerateContainerConf ( step . Commands , goos )
2023-12-19 05:53:52 +02:00
container . Command = command
container . Args = args
2024-01-11 17:32:37 +02:00
maps . Copy ( step . Environment , scriptEnv )
2023-12-19 05:53:52 +02:00
}
2024-01-11 17:32:37 +02:00
container . Env = mapToEnvVars ( step . Environment )
container . SecurityContext = containerSecurityContext ( step . BackendOptions . Kubernetes . SecurityContext , step . Privileged )
2023-12-19 05:53:52 +02:00
2024-01-11 17:32:37 +02:00
container . Resources , err = resourceRequirements ( step . BackendOptions . Kubernetes . Resources )
2023-12-19 05:53:52 +02:00
if err != nil {
return container , err
}
2024-01-11 17:32:37 +02:00
container . VolumeMounts , err = volumeMounts ( step . Volumes )
2023-12-19 05:53:52 +02:00
if err != nil {
return container , err
}
return container , nil
}
func volumes ( volumes [ ] string ) ( [ ] v1 . Volume , error ) {
var vols [ ] v1 . Volume
for _ , v := range volumes {
volumeName , err := volumeName ( v )
2023-06-04 00:50:08 +02:00
if err != nil {
2023-12-19 05:53:52 +02:00
return nil , err
2023-06-04 00:50:08 +02:00
}
2023-12-19 05:53:52 +02:00
vols = append ( vols , volume ( volumeName ) )
}
return vols , nil
}
func volume ( name string ) v1 . Volume {
pvcSource := v1 . PersistentVolumeClaimVolumeSource {
ClaimName : name ,
ReadOnly : false ,
}
return v1 . Volume {
Name : name ,
VolumeSource : v1 . VolumeSource {
PersistentVolumeClaim : & pvcSource ,
} ,
2023-06-04 00:50:08 +02:00
}
2023-12-19 05:53:52 +02:00
}
func volumeMounts ( volumes [ ] string ) ( [ ] v1 . VolumeMount , error ) {
var mounts [ ] v1 . VolumeMount
for _ , v := range volumes {
volumeName , err := volumeName ( v )
2023-06-04 00:50:08 +02:00
if err != nil {
2023-12-19 05:53:52 +02:00
return nil , err
2023-06-04 00:50:08 +02:00
}
2023-12-19 05:53:52 +02:00
mount := volumeMount ( volumeName , volumeMountPath ( v ) )
mounts = append ( mounts , mount )
2022-09-05 06:01:14 +02:00
}
2023-12-19 05:53:52 +02:00
return mounts , nil
}
2022-09-05 06:01:14 +02:00
2023-12-19 05:53:52 +02:00
func volumeMount ( name , path string ) v1 . VolumeMount {
return v1 . VolumeMount {
Name : name ,
MountPath : path ,
2023-06-12 16:00:59 +02:00
}
2023-12-19 05:53:52 +02:00
}
2023-06-12 16:00:59 +02:00
2023-12-19 05:53:52 +02:00
// Here is the service IPs (placed in /etc/hosts in the Pod)
2023-12-23 01:42:30 +02:00
func hostAliases ( extraHosts [ ] types . HostAlias ) [ ] v1 . HostAlias {
2023-12-19 05:53:52 +02:00
hostAliases := [ ] v1 . HostAlias { }
for _ , extraHost := range extraHosts {
hostAlias := hostAlias ( extraHost )
hostAliases = append ( hostAliases , hostAlias )
}
return hostAliases
}
2023-12-23 01:42:30 +02:00
func hostAlias ( extraHost types . HostAlias ) v1 . HostAlias {
2023-12-19 05:53:52 +02:00
return v1 . HostAlias {
2023-12-23 01:42:30 +02:00
IP : extraHost . IP ,
Hostnames : [ ] string { extraHost . Name } ,
2023-03-21 21:00:45 +02:00
}
2023-12-19 05:53:52 +02:00
}
2023-03-21 21:00:45 +02:00
2024-01-05 09:33:56 +02:00
func imagePullSecretsReferences ( imagePullSecretNames [ ] string ) [ ] v1 . LocalObjectReference {
log . Trace ( ) . Msgf ( "Using the image pull secrets: %v" , imagePullSecretNames )
secretReferences := make ( [ ] v1 . LocalObjectReference , len ( imagePullSecretNames ) )
for i , imagePullSecretName := range imagePullSecretNames {
secretReferences [ i ] = imagePullSecretsReference ( imagePullSecretName )
}
return secretReferences
}
func imagePullSecretsReference ( imagePullSecretName string ) v1 . LocalObjectReference {
return v1 . LocalObjectReference {
Name : imagePullSecretName ,
}
}
2023-12-19 05:53:52 +02:00
func resourceRequirements ( resources types . Resources ) ( v1 . ResourceRequirements , error ) {
var err error
requirements := v1 . ResourceRequirements { }
2022-12-31 02:37:09 +02:00
2023-12-19 05:53:52 +02:00
requirements . Requests , err = resourceList ( resources . Requests )
if err != nil {
return requirements , err
2023-05-18 11:21:20 +02:00
}
2023-12-19 05:53:52 +02:00
requirements . Limits , err = resourceList ( resources . Limits )
if err != nil {
return requirements , err
2023-06-12 16:00:59 +02:00
}
2023-12-19 05:53:52 +02:00
return requirements , nil
}
func resourceList ( resources map [ string ] string ) ( v1 . ResourceList , error ) {
requestResources := v1 . ResourceList { }
for key , val := range resources {
resName := v1 . ResourceName ( key )
resVal , err := resource . ParseQuantity ( val )
if err != nil {
2024-01-10 21:57:12 +02:00
return nil , fmt . Errorf ( "resource request '%s' quantity '%s': %w" , key , val , err )
2023-08-22 22:34:59 +02:00
}
2023-12-19 05:53:52 +02:00
requestResources [ resName ] = resVal
2023-08-22 22:34:59 +02:00
}
2023-12-19 05:53:52 +02:00
return requestResources , nil
}
2023-08-22 22:34:59 +02:00
2023-12-19 05:53:52 +02:00
func nodeSelector ( backendNodeSelector map [ string ] string , platform string ) map [ string ] string {
nodeSelector := make ( map [ string ] string )
2023-11-26 09:46:06 +02:00
2023-12-19 05:53:52 +02:00
if platform != "" {
arch := strings . Split ( platform , "/" ) [ 1 ]
nodeSelector [ v1 . LabelArchStable ] = arch
log . Trace ( ) . Msgf ( "Using the node selector from the Agent's platform: %v" , nodeSelector )
2022-09-05 06:01:14 +02:00
}
2023-12-19 05:53:52 +02:00
if len ( backendNodeSelector ) > 0 {
log . Trace ( ) . Msgf ( "Appending labels to the node selector from the backend options: %v" , backendNodeSelector )
maps . Copy ( nodeSelector , backendNodeSelector )
}
return nodeSelector
2022-09-05 06:01:14 +02:00
}
2023-12-19 05:53:52 +02:00
func tolerations ( backendTolerations [ ] types . Toleration ) [ ] v1 . Toleration {
var tolerations [ ] v1 . Toleration
if len ( backendTolerations ) > 0 {
log . Trace ( ) . Msgf ( "Tolerations that will be used in the backend options: %v" , backendTolerations )
for _ , backendToleration := range backendTolerations {
toleration := toleration ( backendToleration )
tolerations = append ( tolerations , toleration )
}
2022-09-05 06:01:14 +02:00
}
2023-12-19 05:53:52 +02:00
return tolerations
2022-09-05 06:01:14 +02:00
}
2023-12-19 05:53:52 +02:00
func toleration ( backendToleration types . Toleration ) v1 . Toleration {
return v1 . Toleration {
Key : backendToleration . Key ,
Operator : v1 . TolerationOperator ( backendToleration . Operator ) ,
Value : backendToleration . Value ,
Effect : v1 . TaintEffect ( backendToleration . Effect ) ,
TolerationSeconds : backendToleration . TolerationSeconds ,
2022-09-05 06:01:14 +02:00
}
}
2023-11-26 09:46:06 +02:00
func podSecurityContext ( sc * types . SecurityContext , secCtxConf SecurityContextConfig ) * v1 . PodSecurityContext {
var (
nonRoot * bool
user * int64
group * int64
fsGroup * int64
)
if sc != nil && sc . RunAsNonRoot != nil {
if * sc . RunAsNonRoot {
nonRoot = sc . RunAsNonRoot // true
}
} else if secCtxConf . RunAsNonRoot {
nonRoot = & secCtxConf . RunAsNonRoot // true
}
if sc != nil {
user = sc . RunAsUser
group = sc . RunAsGroup
fsGroup = sc . FSGroup
}
if nonRoot == nil && user == nil && group == nil && fsGroup == nil {
return nil
}
2023-12-19 05:53:52 +02:00
securityContext := & v1 . PodSecurityContext {
2023-11-26 09:46:06 +02:00
RunAsNonRoot : nonRoot ,
RunAsUser : user ,
RunAsGroup : group ,
FSGroup : fsGroup ,
}
2023-12-19 05:53:52 +02:00
log . Trace ( ) . Msgf ( "Pod security context that will be used: %v" , securityContext )
return securityContext
2023-11-26 09:46:06 +02:00
}
func containerSecurityContext ( sc * types . SecurityContext , stepPrivileged bool ) * v1 . SecurityContext {
var privileged * bool
if sc != nil && sc . Privileged != nil && * sc . Privileged {
privileged = sc . Privileged // true
} else if stepPrivileged {
privileged = & stepPrivileged // true
}
if privileged == nil {
return nil
}
2023-12-19 05:53:52 +02:00
securityContext := & v1 . SecurityContext {
2023-11-26 09:46:06 +02:00
Privileged : privileged ,
}
2023-12-19 05:53:52 +02:00
log . Trace ( ) . Msgf ( "Container security context that will be used: %v" , securityContext )
return securityContext
}
func mapToEnvVars ( m map [ string ] string ) [ ] v1 . EnvVar {
var ev [ ] v1 . EnvVar
for k , v := range m {
ev = append ( ev , v1 . EnvVar {
Name : k ,
Value : v ,
} )
}
return ev
}
func startPod ( ctx context . Context , engine * kube , step * types . Step ) ( * v1 . Pod , error ) {
podName , err := podName ( step )
if err != nil {
return nil , err
}
2024-01-11 17:32:37 +02:00
pod , err := mkPod ( step , engine . config , podName , engine . goos )
2023-12-19 05:53:52 +02:00
if err != nil {
return nil , err
}
log . Trace ( ) . Msgf ( "Creating pod: %s" , pod . Name )
return engine . client . CoreV1 ( ) . Pods ( engine . config . Namespace ) . Create ( ctx , pod , metav1 . CreateOptions { } )
}
func stopPod ( ctx context . Context , engine * kube , step * types . Step , deleteOpts metav1 . DeleteOptions ) error {
podName , err := podName ( step )
if err != nil {
return err
}
log . Trace ( ) . Str ( "name" , podName ) . Msg ( "Deleting pod" )
err = engine . client . CoreV1 ( ) . Pods ( engine . config . Namespace ) . Delete ( ctx , podName , deleteOpts )
if errors . IsNotFound ( err ) {
// Don't abort on 404 errors from k8s, they most likely mean that the pod hasn't been created yet, usually because pipeline was canceled before running all steps.
return nil
}
return err
2023-11-26 09:46:06 +02:00
}