Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement registration token secret #151

Merged
merged 12 commits into from
Jan 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/v1alpha1/githubactionrunner_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ type GithubActionRunnerSpec struct {
}

// IsValid validates conditions not covered by basic OpenAPI constraints
func (r GithubActionRunnerSpec) IsValid() (bool, error){
func (r GithubActionRunnerSpec) IsValid() (bool, error) {
if r.MaxRunners < r.MinRunners {
return false, errors.New("MaxRunners must be greater or equal to minRunners")
}
Expand Down
11 changes: 10 additions & 1 deletion codecov.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
coverage:
round: up
range: "40..100"
range: "40..60"
status:
project:
default:
target: 50%
threshold: 10%
patch:
default:
target: 50%
threshold: 10%
2 changes: 1 addition & 1 deletion config/samples/garo_v1alpha1_githubactionrunner.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ spec:
# value: theRepoName
envFrom:
- secretRef:
name: actions-runner
name: runner-pool-regtoken
image: quay.io/evryfs/github-actions-runner:latest
imagePullPolicy: Always
resources: {}
Expand Down
141 changes: 117 additions & 24 deletions controllers/githubactionrunner_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,16 @@ import (
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"strconv"
"strings"
"time"
)

const poolLabel = "garo.tietoevry.com/pool"
const finalizer = "garo.tietoevry.com/runner-registration"
const registrationTokenKey = "RUNNER_TOKEN"
const registrationTokenExpiresAtAnnotation = "garo.tietoevry.com/expiryTimestamp"
const regTokenPostfix = "regtoken"

// GithubActionRunnerReconciler reconciles a GithubActionRunner object
type GithubActionRunnerReconciler struct {
Expand All @@ -59,12 +64,12 @@ func (r *GithubActionRunnerReconciler) IsValid(obj metav1.Object) (bool, error)
return instance.Spec.IsValid()
}

// +kubebuilder:rbac:groups=garo.tietoevry.com,resources=githubactionrunners,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=garo.tietoevry.com,resources=githubactionrunners/*,verbs=get;update;patch
// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch
// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch
// Reconcile is the main loop implementing the controller action
// +kubebuilder:rbac:groups=garo.tietoevry.com,resources=githubactionrunners,verbs=*
// +kubebuilder:rbac:groups=garo.tietoevry.com,resources=githubactionrunners/*,verbs=*
// +kubebuilder:rbac:groups=core,resources=pods,verbs=*
// +kubebuilder:rbac:groups=core,resources=secrets,verbs=*
// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch
func (r *GithubActionRunnerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
reqLogger := r.Log.WithValues("githubactionrunner", req.NamespacedName)
ctx = logr.NewContext(ctx, reqLogger)
Expand Down Expand Up @@ -100,8 +105,7 @@ func (r *GithubActionRunnerReconciler) handleScaling(ctx context.Context, instan
}

// safety guard - always look for finalizers in order to unregister runners for pods about to delete
err = r.unregisterRunners(ctx, instance, podRunnerPairs)
if err != nil {
if err = r.unregisterRunners(ctx, instance, podRunnerPairs); err != nil {
return r.manageOutcome(ctx, instance, err)
}

Expand All @@ -114,9 +118,15 @@ func (r *GithubActionRunnerReconciler) handleScaling(ctx context.Context, instan
instance.Status.CurrentSize = podRunnerPairs.numPods()
scale := funk.MaxInt([]int{instance.Spec.MinRunners - podRunnerPairs.numRunners(), 1}).(int)
logger.Info("Scaling up", "numInstances", scale)

if err := r.createOrUpdateRegistrationTokenSecret(ctx, instance); err != nil {
return r.manageOutcome(ctx, instance, err)
}

if err := r.scaleUp(ctx, scale, instance); err != nil {
return r.manageOutcome(ctx, instance, err)
}

instance.Status.CurrentSize += scale
err = r.GetClient().Status().Update(ctx, instance)

Expand All @@ -129,8 +139,7 @@ func (r *GithubActionRunnerReconciler) handleScaling(ctx context.Context, instan
if err == nil {
r.GetRecorder().Event(instance, corev1.EventTypeNormal, "Scaling", fmt.Sprintf("Deleted pod %s/%s", pod.Namespace, pod.Name))
instance.Status.CurrentSize--
err := r.GetClient().Status().Update(ctx, instance)
if err != nil {
if err := r.GetClient().Status().Update(ctx, instance); err != nil {
return r.manageOutcome(ctx, instance, err)
}
}
Expand Down Expand Up @@ -166,6 +175,7 @@ func (r *GithubActionRunnerReconciler) SetupWithManager(mgr ctrl.Manager) error
return ctrl.NewControllerManagedBy(mgr).
For(&garov1alpha1.GithubActionRunner{}).
Owns(&corev1.Pod{}).
Owns(&corev1.Secret{}).
WithOptions(controller.Options{MaxConcurrentReconciles: 1}).
WithEventFilter(predicate.Funcs{
// ignore updates to status: https://stuartleeks.com/posts/kubebuilder-event-filters-part-2-update/
Expand All @@ -176,31 +186,115 @@ func (r *GithubActionRunnerReconciler) SetupWithManager(mgr ctrl.Manager) error
Complete(r)
}

func (r *GithubActionRunnerReconciler) createOrUpdateRegistrationTokenSecret(ctx context.Context, instance *garov1alpha1.GithubActionRunner) error {
logger := logr.FromContext(ctx)
secret := &corev1.Secret{
TypeMeta: metav1.TypeMeta{},
ObjectMeta: metav1.ObjectMeta{},
}
err := r.GetClient().Get(ctx, client.ObjectKeyFromObject(instance), secret)

// not found - create
if apierrors.IsNotFound(err) {
logger.Info("Registration secret not found, creating")
return r.updateRegistrationToken(ctx, instance, secret)
}

// else a problem - then return the err
if err != nil {
return err
}

// else found and check validity
epoch, err := strconv.ParseInt(secret.Annotations[registrationTokenExpiresAtAnnotation], 10, 64)
if err != nil {
return err
}

expired := time.Unix(epoch, 0).Before(time.Now().Add(-5 * time.Minute))
if expired {
logger.Info("Registration token expired, updating")
return r.updateRegistrationToken(ctx, instance, secret)
}

return err
}

func (r *GithubActionRunnerReconciler) updateRegistrationToken(ctx context.Context, instance *garov1alpha1.GithubActionRunner, secret *corev1.Secret) error {
secret.GetObjectMeta().SetName(fmt.Sprintf("%s-%s", instance.GetName(), regTokenPostfix))
secret.GetObjectMeta().SetNamespace(instance.GetNamespace())
apiToken, err := r.tokenForRef(ctx, instance)
if err != nil {
return err
}

regToken, err := r.GithubAPI.CreateRegistrationToken(ctx, instance.Spec.Organization, instance.Spec.Repository, apiToken)
if err != nil {
return err
}

_, err = controllerutil.CreateOrUpdate(ctx, r.GetClient(), secret, func() error {
objectMeta := secret.GetObjectMeta()
if err := r.addMetaData(instance, &objectMeta); err != nil {
return err
}

secret.StringData = make(map[string]string)
secret.StringData[registrationTokenKey] = *regToken.Token
if secret.GetAnnotations() == nil {
secret.SetAnnotations(make(map[string]string))
}
secret.Annotations[registrationTokenExpiresAtAnnotation] = strconv.FormatInt(regToken.ExpiresAt.Unix(), 10)

return err
})

return err
}

func (r *GithubActionRunnerReconciler) addMetaData(instance *garov1alpha1.GithubActionRunner, object *metav1.Object) error {
labels := (*object).GetLabels()
if labels == nil {
labels = make(map[string]string)
(*object).SetLabels(labels)
}
err := mergo.Merge(&labels, instance.ObjectMeta.Labels)
if err != nil {
return err
}

labels[poolLabel] = instance.Name

err = controllerutil.SetControllerReference(instance, *object, r.GetScheme())

return err
}

func (r *GithubActionRunnerReconciler) scaleUp(ctx context.Context, amount int, instance *garov1alpha1.GithubActionRunner) error {
for i := 0; i < amount; i++ {
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
GenerateName: fmt.Sprintf("%s-pod-", instance.Name),
Namespace: instance.Namespace,
Labels: map[string]string{
poolLabel: instance.Name,
},
},
}
result, err := controllerutil.CreateOrUpdate(ctx, r.GetClient(), pod, func() error {
pod.Spec = *instance.Spec.PodTemplateSpec.Spec.DeepCopy()
pod.Annotations = instance.Spec.PodTemplateSpec.Annotations
if err := mergo.Merge(&pod.Labels, instance.Spec.PodTemplateSpec.ObjectMeta.Labels, mergo.WithAppendSlice); err != nil {

meta := pod.GetObjectMeta()
if err := r.addMetaData(instance, &meta); err != nil {
return err
}

util.AddFinalizer(pod, finalizer)

return controllerutil.SetControllerReference(instance, pod, r.GetScheme())
return nil
})
logr.FromContext(ctx).Info("Creating a new Pod", "Pod.Namespace", pod.Namespace, "Pod.Name", pod.Name, "result", result)
if err != nil {
return err
}

r.GetRecorder().Event(instance, corev1.EventTypeNormal, "Scaling", fmt.Sprintf("Created pod %s/%s", pod.Namespace, pod.Name))
}

Expand All @@ -214,14 +308,16 @@ func (r *GithubActionRunnerReconciler) listRelatedPods(ctx context.Context, cr *
client.InNamespace(cr.Namespace),
client.MatchingLabels{poolLabel: cr.Name},
}
err := r.GetClient().List(ctx, podList, opts...)
if err := r.GetClient().List(ctx, podList, opts...); err != nil {
return nil, err
}

// filter result by owner-ref since it cannot be done server-side
podList.Items = funk.Filter(podList.Items, func(pod corev1.Pod) bool {
return util.IsOwner(cr, &pod)
}).([]corev1.Pod)

return podList, err
return podList, nil
}

// unregisterRunners will remove runner from github based on presence of finalizer
Expand All @@ -233,13 +329,11 @@ func (r *GithubActionRunnerReconciler) unregisterRunners(ctx context.Context, cr
if err != nil {
return err
}
err = r.GithubAPI.UnregisterRunner(ctx, cr.Spec.Organization, cr.Spec.Repository, token, *item.runner.ID)
if err != nil {
if err = r.GithubAPI.UnregisterRunner(ctx, cr.Spec.Organization, cr.Spec.Repository, token, *item.runner.ID); err != nil {
return err
}
util.RemoveFinalizer(&item.pod, finalizer)
err = r.GetClient().Update(ctx, &item.pod)
if err != nil {
if err = r.GetClient().Update(ctx, &item.pod); err != nil {
return err
}
}
Expand All @@ -251,11 +345,10 @@ func (r *GithubActionRunnerReconciler) unregisterRunners(ctx context.Context, cr
// tokenForRef returns the token referenced from the GithubActionRunner Spec.TokenRef
func (r *GithubActionRunnerReconciler) tokenForRef(ctx context.Context, cr *garov1alpha1.GithubActionRunner) (string, error) {
var secret corev1.Secret
err := r.GetClient().Get(ctx, client.ObjectKey{Name: cr.Spec.TokenRef.Name, Namespace: cr.Namespace}, &secret)

if err != nil {
if err := r.GetClient().Get(ctx, client.ObjectKey{Name: cr.Spec.TokenRef.Name, Namespace: cr.Namespace}, &secret); err != nil {
return "", err
}

return string(secret.Data[cr.Spec.TokenRef.Key]), nil
}

Expand Down
7 changes: 7 additions & 0 deletions controllers/githubactionrunner_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ func (r *mockAPI) UnregisterRunner(ctx context.Context, organization string, rep
return nil
}

func (r *mockAPI) CreateRegistrationToken(ctx context.Context, organization string, repository string, token string) (*github.RegistrationToken, error) {
return &github.RegistrationToken{
Token: github.String("sometoken"),
ExpiresAt: &github.Timestamp{},
}, nil
}

type mockAPI struct {
mock.Mock
}
Expand Down
12 changes: 12 additions & 0 deletions controllers/githubapi/runnerapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
type IRunnerAPI interface {
GetRunners(ctx context.Context, organization string, repository string, token string) ([]*github.Runner, error)
UnregisterRunner(ctx context.Context, organization string, repository string, token string, runnerID int64) error
CreateRegistrationToken(ctx context.Context, organization string, repository string, token string) (*github.RegistrationToken, error)
}

type runnerAPI struct {
Expand Down Expand Up @@ -69,3 +70,14 @@ func (r runnerAPI) UnregisterRunner(ctx context.Context, organization string, re

return err
}

func (r runnerAPI) CreateRegistrationToken(ctx context.Context, organization string, repository string, token string) (*github.RegistrationToken, error) {
client := getClient(ctx, token)
if repository != "" {
regToken, _, err := client.Actions.CreateRegistrationToken(ctx, organization, repository)
return regToken, err
}

regToken, _, err := client.Actions.CreateOrganizationRegistrationToken(ctx, organization)
return regToken, err
}