2020-02-26 21:23:23 +09:00
/ *
Copyright 2020 The actions - runner - controller 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 .
* /
package controllers
import (
"context"
"fmt"
2020-03-15 18:08:11 +09:00
"hash/fnv"
2020-04-02 09:51:40 +09:00
"k8s.io/apimachinery/pkg/types"
2020-03-15 18:08:11 +09:00
"sort"
2020-04-02 09:51:40 +09:00
"time"
2020-03-15 18:08:11 +09:00
2020-02-26 21:23:23 +09:00
"github.com/davecgh/go-spew/spew"
"github.com/go-logr/logr"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/rand"
"k8s.io/client-go/tools/record"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/summerwind/actions-runner-controller/api/v1alpha1"
)
const (
LabelKeyRunnerTemplateHash = "runner-template-hash"
2020-03-03 10:45:39 +09:00
runnerSetOwnerKey = ".metadata.controller"
2020-02-26 21:23:23 +09:00
)
// RunnerDeploymentReconciler reconciles a Runner object
type RunnerDeploymentReconciler struct {
client . Client
Log logr . Logger
Recorder record . EventRecorder
Scheme * runtime . Scheme
}
// +kubebuilder:rbac:groups=actions.summerwind.dev,resources=runnerdeployments,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=actions.summerwind.dev,resources=runnerdeployments/status,verbs=get;update;patch
2020-03-15 18:08:11 +09:00
// +kubebuilder:rbac:groups=actions.summerwind.dev,resources=runnerreplicasets,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=actions.summerwind.dev,resources=runnerreplicasets/status,verbs=get;update;patch
2020-03-27 23:25:37 +09:00
// +kubebuilder:rbac:groups=core,resources=events,verbs=create;patch
2020-02-26 21:23:23 +09:00
func ( r * RunnerDeploymentReconciler ) Reconcile ( req ctrl . Request ) ( ctrl . Result , error ) {
ctx := context . Background ( )
2020-04-02 09:51:40 +09:00
log := r . Log . WithValues ( "runnerdeployment" , req . NamespacedName )
2020-02-26 21:23:23 +09:00
var rd v1alpha1 . RunnerDeployment
if err := r . Get ( ctx , req . NamespacedName , & rd ) ; err != nil {
return ctrl . Result { } , client . IgnoreNotFound ( err )
}
if ! rd . ObjectMeta . DeletionTimestamp . IsZero ( ) {
return ctrl . Result { } , nil
}
2020-03-10 09:14:11 +09:00
var myRunnerReplicaSetList v1alpha1 . RunnerReplicaSetList
if err := r . List ( ctx , & myRunnerReplicaSetList , client . InNamespace ( req . Namespace ) , client . MatchingFields { runnerSetOwnerKey : req . Name } ) ; err != nil {
2020-03-03 10:50:52 +09:00
return ctrl . Result { } , err
2020-02-26 21:23:23 +09:00
}
2020-03-10 09:14:11 +09:00
myRunnerReplicaSets := myRunnerReplicaSetList . Items
2020-02-26 21:23:23 +09:00
2020-03-10 09:14:11 +09:00
sort . Slice ( myRunnerReplicaSets , func ( i , j int ) bool {
return myRunnerReplicaSets [ i ] . GetCreationTimestamp ( ) . After ( myRunnerReplicaSets [ j ] . GetCreationTimestamp ( ) . Time )
2020-02-26 21:23:23 +09:00
} )
2020-03-10 09:14:11 +09:00
var newestSet * v1alpha1 . RunnerReplicaSet
2020-02-26 21:23:23 +09:00
2020-03-10 09:14:11 +09:00
var oldSets [ ] v1alpha1 . RunnerReplicaSet
2020-02-26 21:23:23 +09:00
2020-03-10 09:14:11 +09:00
if len ( myRunnerReplicaSets ) > 0 {
newestSet = & myRunnerReplicaSets [ 0 ]
2020-02-26 21:23:23 +09:00
}
2020-03-10 09:14:11 +09:00
if len ( myRunnerReplicaSets ) > 1 {
oldSets = myRunnerReplicaSets [ 1 : ]
2020-02-26 21:23:23 +09:00
}
2020-03-10 09:14:11 +09:00
desiredRS , err := r . newRunnerReplicaSet ( rd )
2020-02-26 21:23:23 +09:00
if err != nil {
2020-03-15 18:08:11 +09:00
log . Error ( err , "Could not create runnerreplicaset" )
2020-02-26 21:23:23 +09:00
return ctrl . Result { } , err
}
if newestSet == nil {
if err := r . Client . Create ( ctx , & desiredRS ) ; err != nil {
2020-03-15 18:08:11 +09:00
log . Error ( err , "Failed to create runnerreplicaset resource" )
2020-02-26 21:23:23 +09:00
return ctrl . Result { } , err
}
return ctrl . Result { } , nil
}
newestTemplateHash , ok := getTemplateHash ( newestSet )
if ! ok {
2020-03-15 18:08:11 +09:00
log . Info ( "Failed to get template hash of newest runnerreplicaset resource. It must be in an invalid state. Please manually delete the runnerreplicaset so that it is recreated" )
2020-02-26 21:23:23 +09:00
return ctrl . Result { } , nil
}
desiredTemplateHash , ok := getTemplateHash ( & desiredRS )
if ! ok {
2020-03-15 18:08:11 +09:00
log . Info ( "Failed to get template hash of desired runnerreplicaset resource. It must be in an invalid state. Please manually delete the runnerreplicaset so that it is recreated" )
2020-02-26 21:23:23 +09:00
return ctrl . Result { } , nil
}
if newestTemplateHash != desiredTemplateHash {
if err := r . Client . Create ( ctx , & desiredRS ) ; err != nil {
2020-03-15 18:08:11 +09:00
log . Error ( err , "Failed to create runnerreplicaset resource" )
2020-02-26 21:23:23 +09:00
return ctrl . Result { } , err
}
2020-04-02 09:51:40 +09:00
// We requeue in order to clean up old runner replica sets later.
// Otherwise, they aren't cleaned up until the next re-sync interval.
return ctrl . Result { RequeueAfter : 5 * time . Second } , nil
2020-02-26 21:23:23 +09:00
}
2020-04-02 09:51:40 +09:00
const defaultReplicas = 1
currentDesiredReplicas := getIntOrDefault ( newestSet . Spec . Replicas , defaultReplicas )
newDesiredReplicas := getIntOrDefault ( desiredRS . Spec . Replicas , defaultReplicas )
2020-03-15 18:08:11 +09:00
// Please add more conditions that we can in-place update the newest runnerreplicaset without disruption
2020-04-02 09:51:40 +09:00
if currentDesiredReplicas != newDesiredReplicas {
newestSet . Spec . Replicas = & newDesiredReplicas
2020-02-26 21:23:23 +09:00
if err := r . Client . Update ( ctx , newestSet ) ; err != nil {
2020-03-15 18:08:11 +09:00
log . Error ( err , "Failed to update runnerreplicaset resource" )
2020-02-26 21:23:23 +09:00
return ctrl . Result { } , err
}
2020-04-02 09:51:40 +09:00
return ctrl . Result { } , err
2020-02-26 21:23:23 +09:00
}
2020-04-02 09:51:40 +09:00
// Do we old runner replica sets that should eventually deleted?
if len ( oldSets ) > 0 {
readyReplicas := newestSet . Status . ReadyReplicas
2020-02-26 21:23:23 +09:00
2020-04-02 09:51:40 +09:00
if readyReplicas < currentDesiredReplicas {
log . WithValues ( "runnerreplicaset" , types . NamespacedName {
Namespace : newestSet . Namespace ,
Name : newestSet . Name ,
} ) .
Info ( "Waiting until the newest runner replica set to be 100% available" )
2020-02-26 21:23:23 +09:00
2020-04-02 09:51:40 +09:00
return ctrl . Result { RequeueAfter : 10 * time . Second } , nil
2020-02-26 21:23:23 +09:00
}
2020-04-02 09:51:40 +09:00
for i := range oldSets {
rs := oldSets [ i ]
if err := r . Client . Delete ( ctx , & rs ) ; err != nil {
log . Error ( err , "Failed to delete runner resource" )
return ctrl . Result { } , err
}
r . Recorder . Event ( & rd , corev1 . EventTypeNormal , "RunnerReplicaSetDeleted" , fmt . Sprintf ( "Deleted runnerreplicaset '%s'" , rs . Name ) )
log . Info ( "Deleted runnerreplicaset" , "runnerdeployment" , rd . ObjectMeta . Name , "runnerreplicaset" , rs . Name )
}
2020-02-26 21:23:23 +09:00
}
return ctrl . Result { } , nil
}
2020-04-02 09:51:40 +09:00
func getIntOrDefault ( p * int , d int ) int {
if p == nil {
return d
}
return * p
}
2020-03-10 09:14:11 +09:00
func getTemplateHash ( rs * v1alpha1 . RunnerReplicaSet ) ( string , bool ) {
2020-02-26 21:23:23 +09:00
hash , ok := rs . Labels [ LabelKeyRunnerTemplateHash ]
return hash , ok
}
// ComputeHash returns a hash value calculated from pod template and
// a collisionCount to avoid hash collision. The hash will be safe encoded to
// avoid bad words.
//
// Proudly modified and adopted from k8s.io/kubernetes/pkg/util/hash.DeepHashObject and
// k8s.io/kubernetes/pkg/controller.ComputeHash.
func ComputeHash ( template interface { } ) string {
hasher := fnv . New32a ( )
hasher . Reset ( )
printer := spew . ConfigState {
Indent : " " ,
SortKeys : true ,
DisableMethods : true ,
SpewKeys : true ,
}
printer . Fprintf ( hasher , "%#v" , template )
return rand . SafeEncodeString ( fmt . Sprint ( hasher . Sum32 ( ) ) )
}
// Clones the given map and returns a new map with the given key and value added.
// Returns the given map, if labelKey is empty.
//
// Proudly copied from k8s.io/kubernetes/pkg/util/labels.CloneAndAddLabel
func CloneAndAddLabel ( labels map [ string ] string , labelKey , labelValue string ) map [ string ] string {
if labelKey == "" {
// Don't need to add a label.
return labels
}
// Clone.
newLabels := map [ string ] string { }
for key , value := range labels {
newLabels [ key ] = value
}
newLabels [ labelKey ] = labelValue
return newLabels
}
2020-03-10 09:14:11 +09:00
func ( r * RunnerDeploymentReconciler ) newRunnerReplicaSet ( rd v1alpha1 . RunnerDeployment ) ( v1alpha1 . RunnerReplicaSet , error ) {
2020-02-26 21:23:23 +09:00
newRSTemplate := * rd . Spec . Template . DeepCopy ( )
templateHash := ComputeHash ( & newRSTemplate )
// Add template hash label to selector.
labels := CloneAndAddLabel ( rd . Spec . Template . Labels , LabelKeyRunnerTemplateHash , templateHash )
newRSTemplate . Labels = labels
2020-03-10 09:14:11 +09:00
rs := v1alpha1 . RunnerReplicaSet {
2020-02-26 21:23:23 +09:00
TypeMeta : metav1 . TypeMeta { } ,
ObjectMeta : metav1 . ObjectMeta {
2020-03-15 21:50:45 +09:00
GenerateName : rd . ObjectMeta . Name + "-" ,
2020-02-26 21:23:23 +09:00
Namespace : rd . ObjectMeta . Namespace ,
Labels : labels ,
} ,
2020-03-10 09:14:11 +09:00
Spec : v1alpha1 . RunnerReplicaSetSpec {
2020-02-26 21:23:23 +09:00
Replicas : rd . Spec . Replicas ,
Template : newRSTemplate ,
} ,
}
if err := ctrl . SetControllerReference ( & rd , & rs , r . Scheme ) ; err != nil {
return rs , err
}
return rs , nil
}
func ( r * RunnerDeploymentReconciler ) SetupWithManager ( mgr ctrl . Manager ) error {
r . Recorder = mgr . GetEventRecorderFor ( "runnerdeployment-controller" )
2020-03-10 09:14:11 +09:00
if err := mgr . GetFieldIndexer ( ) . IndexField ( & v1alpha1 . RunnerReplicaSet { } , runnerSetOwnerKey , func ( rawObj runtime . Object ) [ ] string {
runnerSet := rawObj . ( * v1alpha1 . RunnerReplicaSet )
2020-03-03 10:45:39 +09:00
owner := metav1 . GetControllerOf ( runnerSet )
if owner == nil {
return nil
}
2020-03-06 08:53:28 +09:00
if owner . APIVersion != v1alpha1 . GroupVersion . String ( ) || owner . Kind != "RunnerDeployment" {
2020-03-03 10:45:39 +09:00
return nil
}
return [ ] string { owner . Name }
} ) ; err != nil {
return err
}
2020-02-26 21:23:23 +09:00
return ctrl . NewControllerManagedBy ( mgr ) .
For ( & v1alpha1 . RunnerDeployment { } ) .
2020-03-10 09:14:11 +09:00
Owns ( & v1alpha1 . RunnerReplicaSet { } ) .
2020-02-26 21:23:23 +09:00
Complete ( r )
}