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
ctx context . Context
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 {
log . Error ( ) . Msgf ( "could not unmarshal pod labels '%s': %s" , c . String ( "backend-k8s-pod-labels" ) , err )
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 {
log . Error ( ) . Msgf ( "could not unmarshal pod annotations '%s': %s" , c . String ( "backend-k8s-pod-annotations" ) , err )
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.
func New ( ctx context . Context ) types . Backend {
2021-11-27 01:29:14 +00:00
return & kube {
2022-09-05 04:01:14 +00:00
ctx : ctx ,
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
}
2023-12-14 18:20:47 +00:00
func ( e * kube ) Load ( context . Context ) ( * types . BackendInfo , error ) {
2022-09-05 04:01:14 +00:00
config , err := configFromCliContext ( e . ctx )
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 {
if stage . Alias == "services" {
for _ , step := range stage . Steps {
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
}
}
}
2023-12-22 23:42:30 +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 {
2023-12-19 03:53:52 +00:00
log . Trace ( ) . Str ( "taskUUID" , taskUUID ) . Msgf ( "Starting step: %s" , step . Name )
_ , 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 ) {
2023-03-21 19:00:45 +00:00
podName , err := dnsName ( step . Name )
if err != nil {
return nil , err
}
2022-09-05 04:01:14 +00:00
2023-07-20 18:39:20 +00:00
log . Trace ( ) . Str ( "taskUUID" , taskUUID ) . Msgf ( "Waiting for pod: %s" , podName )
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 ) {
2022-09-05 04:01:14 +00:00
pod := new . ( * v1 . Pod )
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 ) {
return nil , fmt . Errorf ( "Could not pull image for pod %s" , pod . Name )
}
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 ) {
2023-03-21 19:00:45 +00:00
podName , err := dnsName ( step . Name )
if err != nil {
return nil , err
}
2022-09-05 04:01:14 +00:00
2023-07-20 18:39:20 +00:00
log . Trace ( ) . Str ( "taskUUID" , taskUUID ) . Msgf ( "Tail logs of pod: %s" , podName )
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 ) {
2022-09-05 04:01:14 +00:00
pod := new . ( * v1 . Pod )
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
// rc := io.NopCloser(bytes.NewReader(e.logs.Bytes()))
// e.logs.Reset()
// return rc, nil
2019-04-06 13:44:04 +00:00
}
2023-12-19 03:53:52 +00:00
func ( e * kube ) DestroyStep ( _ context . Context , step * types . Step , taskUUID string ) error {
log . Trace ( ) . Str ( "taskUUID" , taskUUID ) . Msgf ( "Stopping step: %s" , step . Name )
err := stopPod ( e . ctx , e , step , defaultDeleteOptions )
return err
2023-11-01 08:35:11 +00:00
}
2019-04-06 13:44:04 +00:00
// Destroy the pipeline environment.
2023-07-20 18:39:20 +00:00
func ( e * kube ) DestroyWorkflow ( _ context . Context , conf * types . Config , taskUUID string ) error {
log . Trace ( ) . Str ( "taskUUID" , taskUUID ) . Msg ( "Deleting Kubernetes primitives" )
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 {
2023-12-19 03:53:52 +00:00
err := stopPod ( e . 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 {
err := stopService ( e . ctx , e , step , defaultDeleteOptions )
2022-09-05 04:01:14 +00:00
if err != nil {
return err
}
}
}
}
for _ , vol := range conf . Volumes {
2023-12-19 03:53:52 +00:00
err := stopVolume ( e . 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
}