2023-08-07 19:13:26 +00: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 04:01:14 +00:00
package kubernetes
import (
2023-12-19 03:53:52 +00:00
"context"
2023-06-03 22:50:08 +00:00
"fmt"
2023-10-09 07:11:08 +00:00
"maps"
2022-09-05 04:01:14 +00:00
"strings"
2023-07-09 17:22:50 +00:00
"github.com/rs/zerolog/log"
2022-09-05 04:01:14 +00:00
v1 "k8s.io/api/core/v1"
2023-12-19 03:53:52 +00:00
"k8s.io/apimachinery/pkg/api/errors"
2022-09-05 04:01:14 +00:00
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2023-10-09 07:11:08 +00:00
2023-12-08 07:15:08 +00:00
"go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/common"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/types"
2022-09-05 04:01:14 +00:00
)
2023-12-19 03:53:52 +00:00
const (
StepLabel = "step"
2024-01-11 15:32:37 +00:00
podPrefix = "wp-"
2023-12-19 03:53:52 +00:00
)
2022-10-30 23:26:49 +00:00
2024-02-08 17:39:32 +00:00
func mkPod ( step * types . Step , config * config , podName , goos string , options BackendOptions ) ( * v1 . Pod , error ) {
2024-01-26 12:42:21 +00:00
var err error
2024-02-08 17:39:32 +00:00
meta , err := podMeta ( step , config , options , podName )
2024-01-26 12:42:21 +00:00
if err != nil {
return nil , err
}
2023-12-19 03:53:52 +00:00
2024-02-08 17:39:32 +00:00
spec , err := podSpec ( step , config , options )
2023-12-19 03:53:52 +00:00
if err != nil {
return nil , err
2022-09-05 04:01:14 +00:00
}
2024-02-08 17:39:32 +00:00
container , err := podContainer ( step , podName , goos , options )
2023-12-19 03:53:52 +00:00
if err != nil {
return nil , err
2022-09-05 04:01:14 +00:00
}
2023-12-19 03:53:52 +00:00
spec . Containers = append ( spec . Containers , container )
2022-09-05 04:01:14 +00:00
2023-12-19 03:53:52 +00:00
pod := & v1 . Pod {
ObjectMeta : meta ,
Spec : spec ,
2022-09-05 04:01:14 +00:00
}
2023-12-19 03:53:52 +00:00
return pod , nil
}
2024-01-09 04:42:36 +00:00
func stepToPodName ( step * types . Step ) ( name string , err error ) {
if step . Type == types . StepTypeService {
return serviceName ( step )
}
return podName ( step )
}
2023-12-19 03:53:52 +00:00
func podName ( step * types . Step ) ( string , error ) {
2024-01-11 15:32:37 +00:00
return dnsName ( podPrefix + step . UUID )
2023-12-19 03:53:52 +00:00
}
2024-02-08 17:39:32 +00:00
func podMeta ( step * types . Step , config * config , options BackendOptions , podName string ) ( metav1 . ObjectMeta , error ) {
2024-01-26 12:42:21 +00:00
var err error
2023-12-19 03:53:52 +00:00
meta := metav1 . ObjectMeta {
2024-01-12 22:32:24 +00:00
Name : podName ,
Namespace : config . Namespace ,
2023-12-19 03:53:52 +00:00
}
2024-01-15 02:59:08 +00:00
meta . Labels = config . PodLabels
if meta . Labels == nil {
meta . Labels = make ( map [ string ] string , 1 )
}
2024-01-26 12:42:21 +00:00
meta . Labels [ StepLabel ] , err = stepLabel ( step )
if err != nil {
return meta , err
}
2023-12-19 03:53:52 +00:00
2024-01-23 06:42:47 +00:00
if step . Type == types . StepTypeService {
2024-02-17 11:30:06 +00:00
meta . Labels [ ServiceLabel ] , _ = serviceName ( step )
2024-01-23 06:42:47 +00:00
}
2024-01-15 02:59:08 +00:00
meta . Annotations = config . PodAnnotations
if meta . Annotations == nil {
meta . Annotations = make ( map [ string ] string )
}
2024-01-12 22:32:24 +00:00
2024-02-08 17:39:32 +00:00
securityContext := options . SecurityContext
2024-01-12 22:32:24 +00:00
if securityContext != nil {
key , value := apparmorAnnotation ( podName , securityContext . ApparmorProfile )
if key != nil && value != nil {
meta . Annotations [ * key ] = * value
}
}
2024-01-26 12:42:21 +00:00
return meta , nil
}
func stepLabel ( step * types . Step ) ( string , error ) {
return toDNSName ( step . Name )
2023-12-19 03:53:52 +00:00
}
2024-02-08 17:39:32 +00:00
func podSpec ( step * types . Step , config * config , options BackendOptions ) ( v1 . PodSpec , error ) {
2023-12-19 03:53:52 +00:00
var err error
spec := v1 . PodSpec {
RestartPolicy : v1 . RestartPolicyNever ,
2024-02-08 17:39:32 +00:00
ServiceAccountName : options . ServiceAccountName ,
2024-01-11 15:32:37 +00:00
ImagePullSecrets : imagePullSecretsReferences ( config . ImagePullSecretNames ) ,
HostAliases : hostAliases ( step . ExtraHosts ) ,
2024-02-08 17:39:32 +00:00
NodeSelector : nodeSelector ( options . NodeSelector , step . Environment [ "CI_SYSTEM_PLATFORM" ] ) ,
Tolerations : tolerations ( options . Tolerations ) ,
2024-03-13 21:41:13 +00:00
SecurityContext : podSecurityContext ( options . SecurityContext , config . SecurityContext , step . Privileged ) ,
2024-01-11 15:32:37 +00:00
}
spec . Volumes , err = volumes ( step . Volumes )
2023-12-19 03:53:52 +00:00
if err != nil {
return spec , err
2022-09-05 04:01:14 +00:00
}
2023-12-19 03:53:52 +00:00
return spec , nil
}
2024-02-08 17:39:32 +00:00
func podContainer ( step * types . Step , podName , goos string , options BackendOptions ) ( v1 . Container , error ) {
2023-06-03 22:50:08 +00:00
var err error
2023-12-19 03:53:52 +00:00
container := v1 . Container {
2024-03-29 08:48:28 +00:00
Name : podName ,
Image : step . Image ,
WorkingDir : step . WorkingDir ,
Ports : containerPorts ( step . Ports ) ,
SecurityContext : containerSecurityContext ( options . SecurityContext , step . Privileged ) ,
2023-12-19 03:53:52 +00:00
}
2024-01-11 15:32:37 +00:00
if step . Pull {
2023-12-19 03:53:52 +00:00
container . ImagePullPolicy = v1 . PullAlways
}
2024-01-11 15:32:37 +00:00
if len ( step . Commands ) != 0 {
scriptEnv , command , args := common . GenerateContainerConf ( step . Commands , goos )
2024-01-19 04:34:02 +00:00
if len ( step . Entrypoint ) > 0 {
command = step . Entrypoint
}
2023-12-19 03:53:52 +00:00
container . Command = command
2024-01-19 04:34:02 +00:00
container . Args = [ ] string { args }
2024-01-11 15:32:37 +00:00
maps . Copy ( step . Environment , scriptEnv )
2023-12-19 03:53:52 +00:00
}
2024-01-11 15:32:37 +00:00
container . Env = mapToEnvVars ( step . Environment )
2023-12-19 03:53:52 +00:00
2024-02-08 17:39:32 +00:00
container . Resources , err = resourceRequirements ( options . Resources )
2023-12-19 03:53:52 +00:00
if err != nil {
return container , err
}
2024-01-11 15:32:37 +00:00
container . VolumeMounts , err = volumeMounts ( step . Volumes )
2023-12-19 03:53:52 +00: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-03 22:50:08 +00:00
if err != nil {
2023-12-19 03:53:52 +00:00
return nil , err
2023-06-03 22:50:08 +00:00
}
2023-12-19 03:53:52 +00: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-03 22:50:08 +00:00
}
2023-12-19 03:53:52 +00:00
}
func volumeMounts ( volumes [ ] string ) ( [ ] v1 . VolumeMount , error ) {
var mounts [ ] v1 . VolumeMount
for _ , v := range volumes {
volumeName , err := volumeName ( v )
2023-06-03 22:50:08 +00:00
if err != nil {
2023-12-19 03:53:52 +00:00
return nil , err
2023-06-03 22:50:08 +00:00
}
2023-12-19 03:53:52 +00:00
mount := volumeMount ( volumeName , volumeMountPath ( v ) )
mounts = append ( mounts , mount )
2022-09-05 04:01:14 +00:00
}
2023-12-19 03:53:52 +00:00
return mounts , nil
}
2022-09-05 04:01:14 +00:00
2023-12-19 03:53:52 +00:00
func volumeMount ( name , path string ) v1 . VolumeMount {
return v1 . VolumeMount {
Name : name ,
MountPath : path ,
2023-06-12 14:00:59 +00:00
}
2023-12-19 03:53:52 +00:00
}
2023-06-12 14:00:59 +00:00
2024-01-12 22:57:24 +00:00
func containerPorts ( ports [ ] types . Port ) [ ] v1 . ContainerPort {
containerPorts := make ( [ ] v1 . ContainerPort , len ( ports ) )
for i , port := range ports {
containerPorts [ i ] = containerPort ( port )
}
return containerPorts
}
func containerPort ( port types . Port ) v1 . ContainerPort {
return v1 . ContainerPort {
ContainerPort : int32 ( port . Number ) ,
Protocol : v1 . Protocol ( strings . ToUpper ( port . Protocol ) ) ,
}
}
2023-12-19 03:53:52 +00:00
// Here is the service IPs (placed in /etc/hosts in the Pod)
2023-12-22 23:42:30 +00:00
func hostAliases ( extraHosts [ ] types . HostAlias ) [ ] v1 . HostAlias {
2024-01-26 12:42:21 +00:00
var hostAliases [ ] v1 . HostAlias
2023-12-19 03:53:52 +00:00
for _ , extraHost := range extraHosts {
hostAlias := hostAlias ( extraHost )
hostAliases = append ( hostAliases , hostAlias )
}
return hostAliases
}
2023-12-22 23:42:30 +00:00
func hostAlias ( extraHost types . HostAlias ) v1 . HostAlias {
2023-12-19 03:53:52 +00:00
return v1 . HostAlias {
2023-12-22 23:42:30 +00:00
IP : extraHost . IP ,
Hostnames : [ ] string { extraHost . Name } ,
2023-03-21 19:00:45 +00:00
}
2023-12-19 03:53:52 +00:00
}
2023-03-21 19:00:45 +00:00
2024-01-05 07:33:56 +00:00
func imagePullSecretsReferences ( imagePullSecretNames [ ] string ) [ ] v1 . LocalObjectReference {
2024-01-11 18:17:07 +00:00
log . Trace ( ) . Msgf ( "using the image pull secrets: %v" , imagePullSecretNames )
2024-01-05 07:33:56 +00:00
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 ,
}
}
2024-02-08 17:39:32 +00:00
func resourceRequirements ( resources Resources ) ( v1 . ResourceRequirements , error ) {
2023-12-19 03:53:52 +00:00
var err error
requirements := v1 . ResourceRequirements { }
2022-12-31 00:37:09 +00:00
2023-12-19 03:53:52 +00:00
requirements . Requests , err = resourceList ( resources . Requests )
if err != nil {
return requirements , err
2023-05-18 09:21:20 +00:00
}
2023-12-19 03:53:52 +00:00
requirements . Limits , err = resourceList ( resources . Limits )
if err != nil {
return requirements , err
2023-06-12 14:00:59 +00:00
}
2023-12-19 03:53:52 +00: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 19:57:12 +00:00
return nil , fmt . Errorf ( "resource request '%s' quantity '%s': %w" , key , val , err )
2023-08-22 20:34:59 +00:00
}
2023-12-19 03:53:52 +00:00
requestResources [ resName ] = resVal
2023-08-22 20:34:59 +00:00
}
2023-12-19 03:53:52 +00:00
return requestResources , nil
}
2023-08-22 20:34:59 +00:00
2023-12-19 03:53:52 +00:00
func nodeSelector ( backendNodeSelector map [ string ] string , platform string ) map [ string ] string {
nodeSelector := make ( map [ string ] string )
2023-11-26 07:46:06 +00:00
2023-12-19 03:53:52 +00:00
if platform != "" {
arch := strings . Split ( platform , "/" ) [ 1 ]
nodeSelector [ v1 . LabelArchStable ] = arch
2024-01-11 18:17:07 +00:00
log . Trace ( ) . Msgf ( "using the node selector from the Agent's platform: %v" , nodeSelector )
2022-09-05 04:01:14 +00:00
}
2023-12-19 03:53:52 +00:00
if len ( backendNodeSelector ) > 0 {
2024-01-11 18:17:07 +00:00
log . Trace ( ) . Msgf ( "appending labels to the node selector from the backend options: %v" , backendNodeSelector )
2023-12-19 03:53:52 +00:00
maps . Copy ( nodeSelector , backendNodeSelector )
}
return nodeSelector
2022-09-05 04:01:14 +00:00
}
2024-02-08 17:39:32 +00:00
func tolerations ( backendTolerations [ ] Toleration ) [ ] v1 . Toleration {
2023-12-19 03:53:52 +00:00
var tolerations [ ] v1 . Toleration
if len ( backendTolerations ) > 0 {
2024-01-11 18:17:07 +00:00
log . Trace ( ) . Msgf ( "tolerations that will be used in the backend options: %v" , backendTolerations )
2023-12-19 03:53:52 +00:00
for _ , backendToleration := range backendTolerations {
toleration := toleration ( backendToleration )
tolerations = append ( tolerations , toleration )
}
2022-09-05 04:01:14 +00:00
}
2023-12-19 03:53:52 +00:00
return tolerations
2022-09-05 04:01:14 +00:00
}
2024-02-08 17:39:32 +00:00
func toleration ( backendToleration Toleration ) v1 . Toleration {
2023-12-19 03:53:52 +00:00
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 04:01:14 +00:00
}
}
2023-11-26 07:46:06 +00:00
2024-03-13 21:41:13 +00:00
func podSecurityContext ( sc * SecurityContext , secCtxConf SecurityContextConfig , stepPrivileged bool ) * v1 . PodSecurityContext {
2023-11-26 07:46:06 +00:00
var (
nonRoot * bool
user * int64
group * int64
fsGroup * int64
2024-01-12 22:32:24 +00:00
seccomp * v1 . SeccompProfile
2023-11-26 07:46:06 +00:00
)
2024-03-13 21:41:13 +00:00
if secCtxConf . RunAsNonRoot {
nonRoot = newBool ( true )
2023-11-26 07:46:06 +00:00
}
if sc != nil {
2024-03-13 21:41:13 +00:00
// only allow to set user if its not root or step is privileged
if sc . RunAsUser != nil && ( * sc . RunAsUser != 0 || stepPrivileged ) {
user = sc . RunAsUser
}
// only allow to set group if its not root or step is privileged
if sc . RunAsGroup != nil && ( * sc . RunAsGroup != 0 || stepPrivileged ) {
group = sc . RunAsGroup
}
// only allow to set fsGroup if its not root or step is privileged
if sc . FSGroup != nil && ( * sc . FSGroup != 0 || stepPrivileged ) {
fsGroup = sc . FSGroup
}
// only allow to set nonRoot if it's not set globally already
if nonRoot == nil && sc . RunAsNonRoot != nil {
nonRoot = sc . RunAsNonRoot
}
2023-11-26 07:46:06 +00:00
2024-01-12 22:32:24 +00:00
seccomp = seccompProfile ( sc . SeccompProfile )
}
if nonRoot == nil && user == nil && group == nil && fsGroup == nil && seccomp == nil {
2023-11-26 07:46:06 +00:00
return nil
}
2023-12-19 03:53:52 +00:00
securityContext := & v1 . PodSecurityContext {
2024-01-12 22:32:24 +00:00
RunAsNonRoot : nonRoot ,
RunAsUser : user ,
RunAsGroup : group ,
FSGroup : fsGroup ,
SeccompProfile : seccomp ,
2023-11-26 07:46:06 +00:00
}
2024-01-11 18:17:07 +00:00
log . Trace ( ) . Msgf ( "pod security context that will be used: %v" , securityContext )
2023-12-19 03:53:52 +00:00
return securityContext
2023-11-26 07:46:06 +00:00
}
2024-02-08 17:39:32 +00:00
func seccompProfile ( scp * SecProfile ) * v1 . SeccompProfile {
2024-01-12 22:32:24 +00:00
if scp == nil || len ( scp . Type ) == 0 {
return nil
}
log . Trace ( ) . Msgf ( "using seccomp profile: %v" , scp )
seccompProfile := & v1 . SeccompProfile {
Type : v1 . SeccompProfileType ( scp . Type ) ,
}
if len ( scp . LocalhostProfile ) > 0 {
seccompProfile . LocalhostProfile = & scp . LocalhostProfile
}
return seccompProfile
}
2024-02-08 17:39:32 +00:00
func containerSecurityContext ( sc * SecurityContext , stepPrivileged bool ) * v1 . SecurityContext {
2024-03-13 21:41:13 +00:00
if ! stepPrivileged {
2023-11-26 07:46:06 +00:00
return nil
}
2024-03-13 21:41:13 +00:00
if sc != nil && sc . Privileged != nil && * sc . Privileged {
securityContext := & v1 . SecurityContext {
Privileged : newBool ( true ) ,
}
log . Trace ( ) . Msgf ( "container security context that will be used: %v" , securityContext )
return securityContext
2023-11-26 07:46:06 +00:00
}
2024-03-13 21:41:13 +00:00
return nil
2023-12-19 03:53:52 +00:00
}
2024-02-08 17:39:32 +00:00
func apparmorAnnotation ( containerName string , scp * SecProfile ) ( * string , * string ) {
2024-01-12 22:32:24 +00:00
if scp == nil {
return nil , nil
}
log . Trace ( ) . Msgf ( "using AppArmor profile: %v" , scp )
var (
profileType string
profilePath string
)
2024-02-08 17:39:32 +00:00
if scp . Type == SecProfileTypeRuntimeDefault {
2024-01-12 22:32:24 +00:00
profileType = "runtime"
profilePath = "default"
}
2024-02-08 17:39:32 +00:00
if scp . Type == SecProfileTypeLocalhost {
2024-01-12 22:32:24 +00:00
profileType = "localhost"
profilePath = scp . LocalhostProfile
}
if len ( profileType ) == 0 {
return nil , nil
}
key := v1 . AppArmorBetaContainerAnnotationKeyPrefix + containerName
value := profileType + "/" + profilePath
return & key , & value
}
2023-12-19 03:53:52 +00:00
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
}
2024-02-08 17:39:32 +00:00
func startPod ( ctx context . Context , engine * kube , step * types . Step , options BackendOptions ) ( * v1 . Pod , error ) {
2024-01-21 02:56:37 +00:00
podName , err := stepToPodName ( step )
2023-12-19 03:53:52 +00:00
if err != nil {
return nil , err
}
2024-01-15 02:59:08 +00:00
engineConfig := engine . getConfig ( )
2024-02-08 17:39:32 +00:00
pod , err := mkPod ( step , engineConfig , podName , engine . goos , options )
2023-12-19 03:53:52 +00:00
if err != nil {
return nil , err
}
2024-01-11 18:17:07 +00:00
log . Trace ( ) . Msgf ( "creating pod: %s" , pod . Name )
2024-01-15 02:59:08 +00:00
return engine . client . CoreV1 ( ) . Pods ( engineConfig . Namespace ) . Create ( ctx , pod , metav1 . CreateOptions { } )
2023-12-19 03:53:52 +00:00
}
func stopPod ( ctx context . Context , engine * kube , step * types . Step , deleteOpts metav1 . DeleteOptions ) error {
2024-01-21 02:56:37 +00:00
podName , err := stepToPodName ( step )
2023-12-19 03:53:52 +00:00
if err != nil {
return err
}
2024-01-11 18:17:07 +00:00
log . Trace ( ) . Str ( "name" , podName ) . Msg ( "deleting pod" )
2023-12-19 03:53:52 +00:00
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 07:46:06 +00:00
}