diff --git a/README.md b/README.md index d2519cba6b..37ef523062 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,15 @@ This controller operates self-hosted runners for GitHub Actions on your Kubernet [GitHub Actions](https://github.com/features/actions) is a very useful tool for automating development. GitHub Actions jobs are run in the cloud by default, but you may want to run your jobs in your environment. [Self-hosted runner](https://github.com/actions/runner) can be used for such use cases, but requires the provisioning and configuration of a virtual machine instance. Instead if you already have a Kubernetes cluster, it makes more sense to run the self-hosted runner on top of it. -*actions-runner-controller* makes that possible. Just create a *Runner* resource on your Kubernetes, and it will run and operate the self-hosted runner for the specified repository. Combined with Kubernetes RBAC, you can also build simple Self-hosted runners as a Service. +**actions-runner-controller** makes that possible. Just create a *Runner* resource on your Kubernetes, and it will run and operate the self-hosted runner for the specified repository. Combined with Kubernetes RBAC, you can also build simple Self-hosted runners as a Service. ## Installation -First, install *actions-runner-controller* with a manifest file. This will create *actions-runner-system* namespace in your Kubernetes and deploy the required resources. +actions-runner-controller uses [cert-manager](https://cert-manager.io/docs/installation/kubernetes/) for certificate management of Admission Webhook. Make sure you have already installed cert-manager before you install. The installation instructions for cert-manager can be found below. + +- [Installing cert-manager on Kubernetes](https://cert-manager.io/docs/installation/kubernetes/) + +Install the custom resource and actions-runner-controller itself. This will create actions-runner-system namespace in your Kubernetes and deploy the required resources. ``` $ kubectl apply -f https://github.com/summerwind/actions-runner-controller/releases/latest/download/actions-runner-controller.yaml @@ -18,7 +22,7 @@ $ kubectl apply -f https://github.com/summerwind/actions-runner-controller/relea ## Setting up authentication with GitHub API -There are two ways for _actions-runner-controller_ to authenticate with the the GitHub API: +There are two ways for actions-runner-controller to authenticate with the the GitHub API: 1. Using GitHub App. 2. Using Personal Access Token. diff --git a/api/v1alpha1/runner_types.go b/api/v1alpha1/runner_types.go index 252b5c3956..285eac5104 100644 --- a/api/v1alpha1/runner_types.go +++ b/api/v1alpha1/runner_types.go @@ -17,6 +17,8 @@ limitations under the License. package v1alpha1 import ( + "errors" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -75,6 +77,19 @@ type RunnerSpec struct { TerminationGracePeriodSeconds *int64 `json:"terminationGracePeriodSeconds,omitempty"` } +// ValidateRepository validates repository field. +func (rs *RunnerSpec) ValidateRepository() error { + // Organization and repository are both exclusive. + if len(rs.Organization) == 0 && len(rs.Repository) == 0 { + return errors.New("Spec needs organization or repository") + } + if len(rs.Organization) > 0 && len(rs.Repository) > 0 { + return errors.New("Spec cannot have both organization and repository") + } + + return nil +} + // RunnerStatus defines the observed state of Runner type RunnerStatus struct { Registration RunnerStatusRegistration `json:"registration"` diff --git a/api/v1alpha1/runner_webhook.go b/api/v1alpha1/runner_webhook.go new file mode 100644 index 0000000000..59fe0e8ed4 --- /dev/null +++ b/api/v1alpha1/runner_webhook.go @@ -0,0 +1,84 @@ +/* +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 v1alpha1 + +import ( + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" +) + +// log is for logging in this package. +var runnerLog = logf.Log.WithName("runner-resource") + +func (r *Runner) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} + +// +kubebuilder:webhook:path=/mutate-actions-summerwind-dev-v1alpha1-runner,verbs=create;update,mutating=true,failurePolicy=fail,groups=actions.summerwind.dev,resources=runners,versions=v1alpha1,name=mutate.runner.actions.summerwind.dev + +var _ webhook.Defaulter = &Runner{} + +// Default implements webhook.Defaulter so a webhook will be registered for the type +func (r *Runner) Default() { + // Nothing to do. +} + +// +kubebuilder:webhook:path=/validate-actions-summerwind-dev-v1alpha1-runner,verbs=create;update,mutating=false,failurePolicy=fail,groups=actions.summerwind.dev,resources=runners,versions=v1alpha1,name=validate.runner.actions.summerwind.dev + +var _ webhook.Validator = &Runner{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *Runner) ValidateCreate() error { + runnerLog.Info("validate resource to be created", "name", r.Name) + return r.Validate() +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *Runner) ValidateUpdate(old runtime.Object) error { + runnerLog.Info("validate resource to be updated", "name", r.Name) + return r.Validate() +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (r *Runner) ValidateDelete() error { + return nil +} + +// Validate validates resource spec. +func (r *Runner) Validate() error { + var ( + errList field.ErrorList + err error + ) + + err = r.Spec.ValidateRepository() + if err != nil { + errList = append(errList, field.Invalid(field.NewPath("spec", "repository"), r.Spec.Repository, err.Error())) + } + + if len(errList) > 0 { + return apierrors.NewInvalid(r.GroupVersionKind().GroupKind(), r.Name, errList) + } + + return nil +} diff --git a/api/v1alpha1/runnerdeployment_webhook.go b/api/v1alpha1/runnerdeployment_webhook.go new file mode 100644 index 0000000000..d64e6acb04 --- /dev/null +++ b/api/v1alpha1/runnerdeployment_webhook.go @@ -0,0 +1,84 @@ +/* +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 v1alpha1 + +import ( + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" +) + +// log is for logging in this package. +var runenrDeploymentLog = logf.Log.WithName("runnerdeployment-resource") + +func (r *RunnerDeployment) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} + +// +kubebuilder:webhook:path=/mutate-actions-summerwind-dev-v1alpha1-runnerdeployment,verbs=create;update,mutating=true,failurePolicy=fail,groups=actions.summerwind.dev,resources=runnerdeployments,versions=v1alpha1,name=mutate.runnerdeployment.actions.summerwind.dev + +var _ webhook.Defaulter = &RunnerDeployment{} + +// Default implements webhook.Defaulter so a webhook will be registered for the type +func (r *RunnerDeployment) Default() { + // Nothing to do. +} + +// +kubebuilder:webhook:path=/validate-actions-summerwind-dev-v1alpha1-runnerdeployment,verbs=create;update,mutating=false,failurePolicy=fail,groups=actions.summerwind.dev,resources=runnerdeployments,versions=v1alpha1,name=validate.runnerdeployment.actions.summerwind.dev + +var _ webhook.Validator = &RunnerDeployment{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *RunnerDeployment) ValidateCreate() error { + runenrDeploymentLog.Info("validate resource to be created", "name", r.Name) + return r.Validate() +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *RunnerDeployment) ValidateUpdate(old runtime.Object) error { + runenrDeploymentLog.Info("validate resource to be updated", "name", r.Name) + return r.Validate() +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (r *RunnerDeployment) ValidateDelete() error { + return nil +} + +// Validate validates resource spec. +func (r *RunnerDeployment) Validate() error { + var ( + errList field.ErrorList + err error + ) + + err = r.Spec.Template.Spec.ValidateRepository() + if err != nil { + errList = append(errList, field.Invalid(field.NewPath("spec", "template", "spec", "repository"), r.Spec.Template.Spec.Repository, err.Error())) + } + + if len(errList) > 0 { + return apierrors.NewInvalid(r.GroupVersionKind().GroupKind(), r.Name, errList) + } + + return nil +} diff --git a/api/v1alpha1/runnerreplicaset_webhook.go b/api/v1alpha1/runnerreplicaset_webhook.go new file mode 100644 index 0000000000..b026ff6d47 --- /dev/null +++ b/api/v1alpha1/runnerreplicaset_webhook.go @@ -0,0 +1,84 @@ +/* +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 v1alpha1 + +import ( + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" +) + +// log is for logging in this package. +var runnerReplicaSetLog = logf.Log.WithName("runnerreplicaset-resource") + +func (r *RunnerReplicaSet) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} + +// +kubebuilder:webhook:path=/mutate-actions-summerwind-dev-v1alpha1-runnerreplicaset,verbs=create;update,mutating=true,failurePolicy=fail,groups=actions.summerwind.dev,resources=runnerreplicasets,versions=v1alpha1,name=mutate.runnerreplicaset.actions.summerwind.dev + +var _ webhook.Defaulter = &RunnerReplicaSet{} + +// Default implements webhook.Defaulter so a webhook will be registered for the type +func (r *RunnerReplicaSet) Default() { + // Nothing to do. +} + +// +kubebuilder:webhook:path=/validate-actions-summerwind-dev-v1alpha1-runnerreplicaset,verbs=create;update,mutating=false,failurePolicy=fail,groups=actions.summerwind.dev,resources=runnerreplicasets,versions=v1alpha1,name=validate.runnerreplicaset.actions.summerwind.dev + +var _ webhook.Validator = &RunnerReplicaSet{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *RunnerReplicaSet) ValidateCreate() error { + runnerReplicaSetLog.Info("validate resource to be created", "name", r.Name) + return r.Validate() +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *RunnerReplicaSet) ValidateUpdate(old runtime.Object) error { + runnerReplicaSetLog.Info("validate resource to be updated", "name", r.Name) + return r.Validate() +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (r *RunnerReplicaSet) ValidateDelete() error { + return nil +} + +// Validate validates resource spec. +func (r *RunnerReplicaSet) Validate() error { + var ( + errList field.ErrorList + err error + ) + + err = r.Spec.Template.Spec.ValidateRepository() + if err != nil { + errList = append(errList, field.Invalid(field.NewPath("spec", "template", "spec", "repository"), r.Spec.Template.Spec.Repository, err.Error())) + } + + if len(errList) > 0 { + return apierrors.NewInvalid(r.GroupVersionKind().GroupKind(), r.Name, errList) + } + + return nil +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 029f721978..7989c3da5d 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -22,7 +22,7 @@ package v1alpha1 import ( "k8s.io/api/core/v1" - runtime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index 711219a787..d1be385a5e 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -17,9 +17,9 @@ bases: - ../rbac - ../manager # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in crd/kustomization.yaml -#- ../webhook +- ../webhook # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. -#- ../certmanager +- ../certmanager # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. #- ../prometheus @@ -36,39 +36,39 @@ patchesStrategicMerge: #- manager_prometheus_metrics_patch.yaml # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in crd/kustomization.yaml -#- manager_webhook_patch.yaml +- manager_webhook_patch.yaml # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. # 'CERTMANAGER' needs to be enabled to use ca injection -#- webhookcainjection_patch.yaml +- webhookcainjection_patch.yaml # the following config is for teaching kustomize how to do var substitution vars: # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. -#- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR -# objref: -# kind: Certificate -# group: cert-manager.io -# version: v1alpha2 -# name: serving-cert # this name should match the one in certificate.yaml -# fieldref: -# fieldpath: metadata.namespace -#- name: CERTIFICATE_NAME -# objref: -# kind: Certificate -# group: cert-manager.io -# version: v1alpha2 -# name: serving-cert # this name should match the one in certificate.yaml -#- name: SERVICE_NAMESPACE # namespace of the service -# objref: -# kind: Service -# version: v1 -# name: webhook-service -# fieldref: -# fieldpath: metadata.namespace -#- name: SERVICE_NAME -# objref: -# kind: Service -# version: v1 -# name: webhook-service +- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR + objref: + kind: Certificate + group: cert-manager.io + version: v1alpha2 + name: serving-cert # this name should match the one in certificate.yaml + fieldref: + fieldpath: metadata.namespace +- name: CERTIFICATE_NAME + objref: + kind: Certificate + group: cert-manager.io + version: v1alpha2 + name: serving-cert # this name should match the one in certificate.yaml +- name: SERVICE_NAMESPACE # namespace of the service + objref: + kind: Service + version: v1 + name: webhook-service + fieldref: + fieldpath: metadata.namespace +- name: SERVICE_NAME + objref: + kind: Service + version: v1 + name: webhook-service diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index e69de29bb2..4964e062ff 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -0,0 +1,124 @@ + +--- +apiVersion: admissionregistration.k8s.io/v1beta1 +kind: MutatingWebhookConfiguration +metadata: + creationTimestamp: null + name: mutating-webhook-configuration +webhooks: +- clientConfig: + caBundle: Cg== + service: + name: webhook-service + namespace: system + path: /mutate-actions-summerwind-dev-v1alpha1-runner + failurePolicy: Fail + name: mutate.runner.actions.summerwind.dev + rules: + - apiGroups: + - actions.summerwind.dev + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - runners +- clientConfig: + caBundle: Cg== + service: + name: webhook-service + namespace: system + path: /mutate-actions-summerwind-dev-v1alpha1-runnerdeployment + failurePolicy: Fail + name: mutate.runnerdeployment.actions.summerwind.dev + rules: + - apiGroups: + - actions.summerwind.dev + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - runnerdeployments +- clientConfig: + caBundle: Cg== + service: + name: webhook-service + namespace: system + path: /mutate-actions-summerwind-dev-v1alpha1-runnerreplicaset + failurePolicy: Fail + name: mutate.runnerreplicaset.actions.summerwind.dev + rules: + - apiGroups: + - actions.summerwind.dev + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - runnerreplicasets + +--- +apiVersion: admissionregistration.k8s.io/v1beta1 +kind: ValidatingWebhookConfiguration +metadata: + creationTimestamp: null + name: validating-webhook-configuration +webhooks: +- clientConfig: + caBundle: Cg== + service: + name: webhook-service + namespace: system + path: /validate-actions-summerwind-dev-v1alpha1-runner + failurePolicy: Fail + name: validate.runner.actions.summerwind.dev + rules: + - apiGroups: + - actions.summerwind.dev + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - runners +- clientConfig: + caBundle: Cg== + service: + name: webhook-service + namespace: system + path: /validate-actions-summerwind-dev-v1alpha1-runnerdeployment + failurePolicy: Fail + name: validate.runnerdeployment.actions.summerwind.dev + rules: + - apiGroups: + - actions.summerwind.dev + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - runnerdeployments +- clientConfig: + caBundle: Cg== + service: + name: webhook-service + namespace: system + path: /validate-actions-summerwind-dev-v1alpha1-runnerreplicaset + failurePolicy: Fail + name: validate.runnerreplicaset.actions.summerwind.dev + rules: + - apiGroups: + - actions.summerwind.dev + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - runnerreplicasets diff --git a/controllers/runner_controller.go b/controllers/runner_controller.go index 8a01168c33..52f9ab3f79 100644 --- a/controllers/runner_controller.go +++ b/controllers/runner_controller.go @@ -66,7 +66,7 @@ func (r *RunnerReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { return ctrl.Result{}, client.IgnoreNotFound(err) } - err := validateRunnerSpec(&runner.Spec) + err := runner.Validate() if err != nil { log.Info("Failed to validate runner spec", "error", err.Error()) return ctrl.Result{}, nil @@ -449,15 +449,3 @@ func removeFinalizer(finalizers []string) ([]string, bool) { return result, removed } - -// organization & repository are both exclusive - however this cannot be checked with kubebuilder -// therefore have an additional check here to log an error in case spec is invalid -func validateRunnerSpec(spec *v1alpha1.RunnerSpec) error { - if len(spec.Organization) == 0 && len(spec.Repository) == 0 { - return fmt.Errorf("RunnerSpec needs organization or repository") - } - if len(spec.Organization) > 0 && len(spec.Repository) > 0 { - return fmt.Errorf("RunnerSpec cannot have both organization and repository") - } - return nil -} diff --git a/main.go b/main.go index 8e7d132774..295601a365 100644 --- a/main.go +++ b/main.go @@ -174,6 +174,19 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "RunnerDeployment") os.Exit(1) } + + if err = (&actionsv1alpha1.Runner{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Runner") + os.Exit(1) + } + if err = (&actionsv1alpha1.RunnerDeployment{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "RunnerDeployment") + os.Exit(1) + } + if err = (&actionsv1alpha1.RunnerReplicaSet{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "RunnerReplicaSet") + os.Exit(1) + } // +kubebuilder:scaffold:builder setupLog.Info("starting manager")