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"
)
2022-10-30 23:26:49 +00:00
2023-12-19 03:53:52 +00:00
func mkPod ( namespace , name , image , workDir , goos , serviceAccountName string ,
pool , privileged bool ,
2023-12-22 23:42:30 +00:00
commands , vols [ ] string ,
2023-12-19 03:53:52 +00:00
labels , annotations , env , nodeSelector map [ string ] string ,
2023-12-22 23:42:30 +00:00
extraHosts [ ] types . HostAlias , tolerations [ ] types . Toleration , resources types . Resources ,
2023-12-19 03:53:52 +00:00
securityContext * types . SecurityContext , securityContextConfig SecurityContextConfig ,
) ( * v1 . Pod , error ) {
var err error
meta := podMeta ( name , namespace , labels , annotations )
2023-12-22 23:42:30 +00:00
spec , err := podSpec ( serviceAccountName , vols , env , nodeSelector , extraHosts , tolerations , securityContext , securityContextConfig )
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
container , err := podContainer ( name , image , workDir , goos , pool , privileged , commands , vols , env ,
resources , securityContext )
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
}
func podName ( step * types . Step ) ( string , error ) {
return dnsName ( step . Name )
}
func podMeta ( name , namespace string , labels , annotations map [ string ] string ) metav1 . ObjectMeta {
meta := metav1 . ObjectMeta {
Name : name ,
Namespace : namespace ,
Annotations : annotations ,
}
if labels == nil {
labels = make ( map [ string ] string , 1 )
}
labels [ StepLabel ] = name
meta . Labels = labels
return meta
}
2023-12-22 23:42:30 +00:00
func podSpec ( serviceAccountName string , vols [ ] string , env , backendNodeSelector map [ string ] string ,
extraHosts [ ] types . HostAlias , backendTolerations [ ] types . Toleration ,
2023-12-19 03:53:52 +00:00
securityContext * types . SecurityContext , securityContextConfig SecurityContextConfig ,
) ( v1 . PodSpec , error ) {
var err error
spec := v1 . PodSpec {
RestartPolicy : v1 . RestartPolicyNever ,
ServiceAccountName : serviceAccountName ,
ImagePullSecrets : [ ] v1 . LocalObjectReference { { Name : "regcred" } } ,
}
spec . HostAliases = hostAliases ( extraHosts )
spec . NodeSelector = nodeSelector ( backendNodeSelector , env [ "CI_SYSTEM_PLATFORM" ] )
spec . Tolerations = tolerations ( backendTolerations )
spec . SecurityContext = podSecurityContext ( securityContext , securityContextConfig )
spec . Volumes , err = volumes ( vols )
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
}
func podContainer ( name , image , workDir , goos string , pull , privileged bool , commands , volumes [ ] string , env map [ string ] string , resources types . Resources ,
securityContext * types . SecurityContext ,
) ( 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 {
Name : name ,
Image : image ,
WorkingDir : workDir ,
}
if pull {
container . ImagePullPolicy = v1 . PullAlways
}
if len ( commands ) != 0 {
scriptEnv , command , args := common . GenerateContainerConf ( commands , goos )
container . Command = command
container . Args = args
maps . Copy ( env , scriptEnv )
}
container . Env = mapToEnvVars ( env )
container . SecurityContext = containerSecurityContext ( securityContext , privileged )
container . Resources , err = resourceRequirements ( resources )
if err != nil {
return container , err
}
container . VolumeMounts , err = volumeMounts ( volumes )
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
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 {
2023-12-19 03:53:52 +00:00
hostAliases := [ ] v1 . HostAlias { }
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
2023-12-19 03:53:52 +00:00
func resourceRequirements ( resources types . Resources ) ( v1 . ResourceRequirements , error ) {
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 {
return nil , fmt . Errorf ( "resource request '%v' quantity '%v': %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
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 {
log . Trace ( ) . Msgf ( "Appending labels to the node selector from the backend options: %v" , backendNodeSelector )
maps . Copy ( nodeSelector , backendNodeSelector )
}
return nodeSelector
2022-09-05 04:01:14 +00:00
}
2023-12-19 03:53:52 +00: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 04:01:14 +00:00
}
2023-12-19 03:53:52 +00:00
return tolerations
2022-09-05 04:01:14 +00:00
}
2023-12-19 03:53:52 +00: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 04:01:14 +00:00
}
}
2023-11-26 07:46:06 +00: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 03:53:52 +00:00
securityContext := & v1 . PodSecurityContext {
2023-11-26 07:46:06 +00:00
RunAsNonRoot : nonRoot ,
RunAsUser : user ,
RunAsGroup : group ,
FSGroup : fsGroup ,
}
2023-12-19 03:53:52 +00:00
log . Trace ( ) . Msgf ( "Pod security context that will be used: %v" , securityContext )
return securityContext
2023-11-26 07:46:06 +00: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 03:53:52 +00:00
securityContext := & v1 . SecurityContext {
2023-11-26 07:46:06 +00:00
Privileged : privileged ,
}
2023-12-19 03:53:52 +00: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
}
pod , err := mkPod ( engine . config . Namespace , podName , step . Image , step . WorkingDir , engine . goos , step . BackendOptions . Kubernetes . ServiceAccountName ,
step . Pull , step . Privileged ,
2023-12-22 23:42:30 +00:00
step . Commands , step . Volumes ,
2023-12-19 03:53:52 +00:00
engine . config . PodLabels , engine . config . PodAnnotations , step . Environment , step . BackendOptions . Kubernetes . NodeSelector ,
2023-12-22 23:42:30 +00:00
step . ExtraHosts , step . BackendOptions . Kubernetes . Tolerations , step . BackendOptions . Kubernetes . Resources , step . BackendOptions . Kubernetes . SecurityContext , engine . config . SecurityContext )
2023-12-19 03:53:52 +00: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 07:46:06 +00:00
}