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
//
2023-08-10 09:06:00 +00:00
// http://www.apache.org/licenses/LICENSE-2.0
2023-08-07 19:13:26 +00:00
//
// 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.
2019-04-06 13:44:04 +00:00
package kubernetes
import (
"context"
2022-09-05 04:01:14 +00:00
"fmt"
2019-04-06 13:44:04 +00:00
"io"
2021-11-26 02:34:48 +00:00
"os"
2023-11-01 14:38:37 +00:00
"runtime"
2022-09-05 04:01:14 +00:00
"time"
2019-04-06 13:44:04 +00:00
2022-09-05 04:01:14 +00:00
"github.com/rs/zerolog/log"
2022-12-31 00:37:09 +00:00
"gopkg.in/yaml.v3"
2022-09-05 04:01:14 +00:00
2023-12-14 18:20:47 +00:00
"go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/types"
2022-09-05 04:01:14 +00:00
"github.com/urfave/cli/v2"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/cache"
// To authenticate to GCP K8s clusters
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
)
2023-11-01 23:53:47 +00:00
const (
EngineName = "kubernetes"
)
2023-12-19 03:53:52 +00:00
var defaultDeleteOptions = newDefaultDeleteOptions ( )
2019-04-06 13:44:04 +00:00
2021-11-27 01:29:14 +00:00
type kube struct {
2022-09-05 04:01:14 +00:00
client kubernetes . Interface
2023-12-19 03:53:52 +00:00
config * config
2023-11-01 14:38:37 +00:00
goos string
2019-04-06 13:44:04 +00:00
}
2023-12-19 03:53:52 +00:00
type config struct {
2024-01-05 07:33:56 +00:00
Namespace string
StorageClass string
VolumeSize string
StorageRwx bool
PodLabels map [ string ] string
PodAnnotations map [ string ] string
ImagePullSecretNames [ ] string
SecurityContext SecurityContextConfig
2023-11-26 07:46:06 +00:00
}
type SecurityContextConfig struct {
RunAsNonRoot bool
2022-09-05 04:01:14 +00:00
}
2023-12-19 03:53:52 +00:00
func newDefaultDeleteOptions ( ) metav1 . DeleteOptions {
gracePeriodSeconds := int64 ( 0 ) // immediately
propagationPolicy := metav1 . DeletePropagationBackground
return metav1 . DeleteOptions {
GracePeriodSeconds : & gracePeriodSeconds ,
PropagationPolicy : & propagationPolicy ,
}
}
func configFromCliContext ( ctx context . Context ) ( * config , error ) {
2022-09-05 04:01:14 +00:00
if ctx != nil {
if c , ok := ctx . Value ( types . CliContext ) . ( * cli . Context ) ; ok {
2023-12-19 03:53:52 +00:00
config := config {
2024-01-05 07:33:56 +00:00
Namespace : c . String ( "backend-k8s-namespace" ) ,
StorageClass : c . String ( "backend-k8s-storage-class" ) ,
VolumeSize : c . String ( "backend-k8s-volume-size" ) ,
StorageRwx : c . Bool ( "backend-k8s-storage-rwx" ) ,
PodLabels : make ( map [ string ] string ) , // just init empty map to prevent nil panic
PodAnnotations : make ( map [ string ] string ) , // just init empty map to prevent nil panic
ImagePullSecretNames : c . StringSlice ( "backend-k8s-pod-image-pull-secret-names" ) ,
2023-11-26 07:46:06 +00:00
SecurityContext : SecurityContextConfig {
RunAsNonRoot : c . Bool ( "backend-k8s-secctx-nonroot" ) ,
} ,
2022-12-31 00:37:09 +00:00
}
2024-01-05 07:33:56 +00:00
// TODO: remove in next major
if len ( config . ImagePullSecretNames ) == 1 && config . ImagePullSecretNames [ 0 ] == "regcred" {
log . Warn ( ) . Msg ( "WOODPECKER_BACKEND_K8S_PULL_SECRET_NAMES is set to the default ('regcred'). It will default to empty in Woodpecker 3.0. Set it explicitly before then." )
}
2022-12-31 00:37:09 +00:00
// Unmarshal label and annotation settings here to ensure they're valid on startup
2023-01-04 17:51:21 +00:00
if labels := c . String ( "backend-k8s-pod-labels" ) ; labels != "" {
if err := yaml . Unmarshal ( [ ] byte ( labels ) , & config . PodLabels ) ; err != nil {
2024-01-10 19:57:12 +00:00
log . Error ( ) . Err ( err ) . Msgf ( "could not unmarshal pod labels '%s'" , c . String ( "backend-k8s-pod-labels" ) )
2023-01-04 17:51:21 +00:00
return nil , err
}
2022-12-31 00:37:09 +00:00
}
2023-01-04 17:51:21 +00:00
if annotations := c . String ( "backend-k8s-pod-annotations" ) ; annotations != "" {
if err := yaml . Unmarshal ( [ ] byte ( c . String ( "backend-k8s-pod-annotations" ) ) , & config . PodAnnotations ) ; err != nil {
2024-01-10 19:57:12 +00:00
log . Error ( ) . Err ( err ) . Msgf ( "could not unmarshal pod annotations '%s'" , c . String ( "backend-k8s-pod-annotations" ) )
2023-01-04 17:51:21 +00:00
return nil , err
}
2022-12-31 00:37:09 +00:00
}
return & config , nil
2022-09-05 04:01:14 +00:00
}
}
2023-03-19 19:24:43 +00:00
return nil , types . ErrNoCliContextFound
2022-09-05 04:01:14 +00:00
}
2021-11-27 01:29:14 +00:00
2023-12-14 18:20:47 +00:00
// New returns a new Kubernetes Backend.
2024-01-11 21:15:15 +00:00
func New ( ) types . Backend {
return & kube { }
2019-04-06 13:44:04 +00:00
}
2021-11-27 01:29:14 +00:00
func ( e * kube ) Name ( ) string {
2023-11-01 23:53:47 +00:00
return EngineName
2021-11-26 02:34:48 +00:00
}
2023-03-19 19:24:43 +00:00
func ( e * kube ) IsAvailable ( context . Context ) bool {
2021-11-26 02:34:48 +00:00
host := os . Getenv ( "KUBERNETES_SERVICE_HOST" )
return len ( host ) > 0
}
2024-01-11 21:15:15 +00:00
func ( e * kube ) Load ( ctx context . Context ) ( * types . BackendInfo , error ) {
config , err := configFromCliContext ( ctx )
2022-09-05 04:01:14 +00:00
if err != nil {
2023-11-01 14:38:37 +00:00
return nil , err
2022-09-05 04:01:14 +00:00
}
e . config = config
var kubeClient kubernetes . Interface
_ , err = rest . InClusterConfig ( )
if err != nil {
kubeClient , err = getClientOutOfCluster ( )
} else {
kubeClient , err = getClientInsideOfCluster ( )
}
if err != nil {
2023-11-01 14:38:37 +00:00
return nil , err
2022-09-05 04:01:14 +00:00
}
e . client = kubeClient
2023-11-01 14:38:37 +00:00
// TODO(2693): use info resp of kubeClient to define platform var
e . goos = runtime . GOOS
2023-12-14 18:20:47 +00:00
return & types . BackendInfo {
2023-11-01 14:38:37 +00:00
Platform : runtime . GOOS + "/" + runtime . GOARCH ,
} , nil
2021-11-26 02:34:48 +00:00
}
2019-04-06 13:44:04 +00:00
// Setup the pipeline environment.
2023-07-20 18:39:20 +00:00
func ( e * kube ) SetupWorkflow ( ctx context . Context , conf * types . Config , taskUUID string ) error {
log . Trace ( ) . Str ( "taskUUID" , taskUUID ) . Msgf ( "Setting up Kubernetes primitives" )
2022-09-05 04:01:14 +00:00
for _ , vol := range conf . Volumes {
2023-12-19 03:53:52 +00:00
_ , err := startVolume ( ctx , e , vol . Name )
2022-09-05 04:01:14 +00:00
if err != nil {
return err
}
}
2023-12-22 23:42:30 +00:00
extraHosts := [ ] types . HostAlias { }
2022-09-05 04:01:14 +00:00
for _ , stage := range conf . Stages {
2024-01-09 04:42:36 +00:00
for _ , step := range stage . Steps {
if step . Type == types . StepTypeService {
2023-12-19 03:53:52 +00:00
svc , err := startService ( ctx , e , step )
2023-03-21 19:00:45 +00:00
if err != nil {
return err
}
2023-12-22 23:42:30 +00:00
hostAlias := types . HostAlias { Name : step . Networks [ 0 ] . Aliases [ 0 ] , IP : svc . Spec . ClusterIP }
extraHosts = append ( extraHosts , hostAlias )
2022-09-05 04:01:14 +00:00
}
}
}
2024-01-11 18:17:07 +00:00
log . Trace ( ) . Msgf ( "adding extra hosts: %v" , extraHosts )
2022-09-05 04:01:14 +00:00
for _ , stage := range conf . Stages {
for _ , step := range stage . Steps {
step . ExtraHosts = extraHosts
}
}
2019-04-06 13:44:04 +00:00
return nil
}
2022-09-05 04:01:14 +00:00
// Start the pipeline step.
2023-07-20 18:39:20 +00:00
func ( e * kube ) StartStep ( ctx context . Context , step * types . Step , taskUUID string ) error {
2024-01-09 04:42:36 +00:00
if step . Type == types . StepTypeService {
// a service should be started by SetupWorkflow so we can ignore it
log . Trace ( ) . Msgf ( "StartStep got service '%s', ignoring it." , step . Name )
return nil
}
2024-01-11 18:17:07 +00:00
log . Trace ( ) . Str ( "taskUUID" , taskUUID ) . Msgf ( "starting step: %s" , step . Name )
2023-12-19 03:53:52 +00:00
_ , err := startPod ( ctx , e , step )
2022-09-05 04:01:14 +00:00
return err
2019-04-06 13:44:04 +00:00
}
// Wait for the pipeline step to complete and returns
// the completion results.
2023-07-20 18:39:20 +00:00
func ( e * kube ) WaitStep ( ctx context . Context , step * types . Step , taskUUID string ) ( * types . State , error ) {
2024-01-09 04:42:36 +00:00
podName , err := stepToPodName ( step )
2023-03-21 19:00:45 +00:00
if err != nil {
return nil , err
}
2022-09-05 04:01:14 +00:00
2024-01-11 18:17:07 +00:00
log . Trace ( ) . Str ( "taskUUID" , taskUUID ) . Msgf ( "waiting for pod: %s" , podName )
2023-07-20 18:39:20 +00:00
2022-09-05 04:01:14 +00:00
finished := make ( chan bool )
2023-11-12 17:23:48 +00:00
podUpdated := func ( old , new any ) {
2024-01-12 01:01:02 +00:00
pod , ok := new . ( * v1 . Pod )
if ! ok {
log . Error ( ) . Msgf ( "could not parse pod: %v" , new )
return
}
2022-09-05 04:01:14 +00:00
if pod . Name == podName {
if isImagePullBackOffState ( pod ) {
finished <- true
}
switch pod . Status . Phase {
case v1 . PodSucceeded , v1 . PodFailed , v1 . PodUnknown :
finished <- true
}
}
}
// TODO 5 seconds is against best practice, k3s didn't work otherwise
si := informers . NewSharedInformerFactoryWithOptions ( e . client , 5 * time . Second , informers . WithNamespace ( e . config . Namespace ) )
2023-03-20 23:48:15 +00:00
if _ , err := si . Core ( ) . V1 ( ) . Pods ( ) . Informer ( ) . AddEventHandler (
2022-09-05 04:01:14 +00:00
cache . ResourceEventHandlerFuncs {
UpdateFunc : podUpdated ,
} ,
2023-03-20 23:48:15 +00:00
) ; err != nil {
return nil , err
}
2023-02-15 23:54:33 +00:00
stop := make ( chan struct { } )
si . Start ( stop )
defer close ( stop )
2022-09-05 04:01:14 +00:00
// TODO Cancel on ctx.Done
<- finished
pod , err := e . client . CoreV1 ( ) . Pods ( e . config . Namespace ) . Get ( ctx , podName , metav1 . GetOptions { } )
if err != nil {
return nil , err
}
if isImagePullBackOffState ( pod ) {
2024-01-10 21:56:42 +00:00
return nil , fmt . Errorf ( "could not pull image for pod %s" , pod . Name )
2022-09-05 04:01:14 +00:00
}
bs := & types . State {
ExitCode : int ( pod . Status . ContainerStatuses [ 0 ] . State . Terminated . ExitCode ) ,
Exited : true ,
OOMKilled : false ,
}
return bs , nil
2019-04-06 13:44:04 +00:00
}
// Tail the pipeline step logs.
2023-07-20 18:39:20 +00:00
func ( e * kube ) TailStep ( ctx context . Context , step * types . Step , taskUUID string ) ( io . ReadCloser , error ) {
2024-01-09 04:42:36 +00:00
podName , err := stepToPodName ( step )
2023-03-21 19:00:45 +00:00
if err != nil {
return nil , err
}
2022-09-05 04:01:14 +00:00
2024-01-11 18:17:07 +00:00
log . Trace ( ) . Str ( "taskUUID" , taskUUID ) . Msgf ( "tail logs of pod: %s" , podName )
2023-07-20 18:39:20 +00:00
2022-09-05 04:01:14 +00:00
up := make ( chan bool )
2023-11-12 17:23:48 +00:00
podUpdated := func ( old , new any ) {
2024-01-12 01:01:02 +00:00
pod , ok := new . ( * v1 . Pod )
if ! ok {
log . Error ( ) . Msgf ( "could not parse pod: %v" , new )
return
}
2022-09-05 04:01:14 +00:00
if pod . Name == podName {
switch pod . Status . Phase {
case v1 . PodRunning , v1 . PodSucceeded , v1 . PodFailed :
up <- true
}
}
}
// TODO 5 seconds is against best practice, k3s didn't work otherwise
si := informers . NewSharedInformerFactoryWithOptions ( e . client , 5 * time . Second , informers . WithNamespace ( e . config . Namespace ) )
2023-03-20 23:48:15 +00:00
if _ , err := si . Core ( ) . V1 ( ) . Pods ( ) . Informer ( ) . AddEventHandler (
2022-09-05 04:01:14 +00:00
cache . ResourceEventHandlerFuncs {
UpdateFunc : podUpdated ,
} ,
2023-03-20 23:48:15 +00:00
) ; err != nil {
return nil , err
}
2023-02-15 23:54:33 +00:00
stop := make ( chan struct { } )
si . Start ( stop )
defer close ( stop )
2022-09-05 04:01:14 +00:00
<- up
opts := & v1 . PodLogOptions {
2022-12-31 00:37:09 +00:00
Follow : true ,
Container : podName ,
2022-09-05 04:01:14 +00:00
}
logs , err := e . client . CoreV1 ( ) . RESTClient ( ) . Get ( ) .
Namespace ( e . config . Namespace ) .
Name ( podName ) .
Resource ( "pods" ) .
SubResource ( "log" ) .
VersionedParams ( opts , scheme . ParameterCodec ) .
Stream ( ctx )
if err != nil {
return nil , err
}
rc , wc := io . Pipe ( )
go func ( ) {
defer logs . Close ( )
defer wc . Close ( )
defer rc . Close ( )
_ , err = io . Copy ( wc , logs )
if err != nil {
return
}
} ( )
return rc , nil
2019-04-06 13:44:04 +00:00
}
2024-01-11 21:15:15 +00:00
func ( e * kube ) DestroyStep ( ctx context . Context , step * types . Step , taskUUID string ) error {
2024-01-09 04:42:36 +00:00
if step . Type == types . StepTypeService {
// a service should be stopped by DestroyWorkflow so we can ignore it
2024-01-11 21:15:15 +00:00
log . Trace ( ) . Msgf ( "destroyStep got service '%s', ignoring it." , step . Name )
2024-01-09 04:42:36 +00:00
return nil
}
2024-01-11 21:15:15 +00:00
log . Trace ( ) . Str ( "taskUUID" , taskUUID ) . Msgf ( "Stopping step: %s" , step . Name )
err := stopPod ( ctx , e , step , defaultDeleteOptions )
2023-12-19 03:53:52 +00:00
return err
2023-11-01 08:35:11 +00:00
}
2019-04-06 13:44:04 +00:00
// Destroy the pipeline environment.
2024-01-11 21:15:15 +00:00
func ( e * kube ) DestroyWorkflow ( ctx context . Context , conf * types . Config , taskUUID string ) error {
2024-01-11 18:17:07 +00:00
log . Trace ( ) . Str ( "taskUUID" , taskUUID ) . Msg ( "deleting Kubernetes primitives" )
2023-07-20 18:39:20 +00:00
2022-09-05 04:01:14 +00:00
// Use noContext because the ctx sent to this function will be canceled/done in case of error or canceled by user.
for _ , stage := range conf . Stages {
for _ , step := range stage . Steps {
2024-01-11 21:15:15 +00:00
err := stopPod ( ctx , e , step , defaultDeleteOptions )
2023-03-21 19:00:45 +00:00
if err != nil {
return err
}
2022-09-05 04:01:14 +00:00
2023-12-19 03:53:52 +00:00
if step . Type == types . StepTypeService {
2024-01-11 21:15:15 +00:00
err := stopService ( ctx , e , step , defaultDeleteOptions )
2022-09-05 04:01:14 +00:00
if err != nil {
return err
}
}
}
}
for _ , vol := range conf . Volumes {
2024-01-11 21:15:15 +00:00
err := stopVolume ( ctx , e , vol . Name , defaultDeleteOptions )
2023-03-21 19:00:45 +00:00
if err != nil {
return err
}
2022-09-05 04:01:14 +00:00
}
2019-04-06 13:44:04 +00:00
return nil
}