From aaf6b0bcae5d345bdbfd9dd2e881580b394a8d8c Mon Sep 17 00:00:00 2001 From: Moto Ishizawa Date: Tue, 28 Jan 2020 21:58:01 +0900 Subject: [PATCH] Implement runner controller --- config/rbac/role.yaml | 28 +++ config/samples/actions_v1alpha1_runner.yaml | 5 +- config/webhook/manifests.yaml | 0 controllers/runner_controller.go | 178 +++++++++++++++++++- go.mod | 9 + go.sum | 19 +++ main.go | 29 +++- 7 files changed, 253 insertions(+), 15 deletions(-) create mode 100644 config/rbac/role.yaml create mode 100644 config/webhook/manifests.yaml diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml new file mode 100644 index 00000000..378d00b7 --- /dev/null +++ b/config/rbac/role.yaml @@ -0,0 +1,28 @@ + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: manager-role +rules: +- apiGroups: + - actions.summerwind.dev + resources: + - runners + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - actions.summerwind.dev + resources: + - runners/status + verbs: + - get + - patch + - update diff --git a/config/samples/actions_v1alpha1_runner.yaml b/config/samples/actions_v1alpha1_runner.yaml index bc5858da..97c95dcb 100644 --- a/config/samples/actions_v1alpha1_runner.yaml +++ b/config/samples/actions_v1alpha1_runner.yaml @@ -1,7 +1,6 @@ apiVersion: actions.summerwind.dev/v1alpha1 kind: Runner metadata: - name: runner-sample + name: summerwind-actions-runner-controller spec: - # Add fields here - foo: bar + repository: summerwind/actions-runner-controller diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml new file mode 100644 index 00000000..e69de29b diff --git a/controllers/runner_controller.go b/controllers/runner_controller.go index 9a11b0b0..2c301691 100644 --- a/controllers/runner_controller.go +++ b/controllers/runner_controller.go @@ -18,36 +18,200 @@ package controllers import ( "context" + "fmt" + "reflect" + "time" "github.com/go-logr/logr" + "github.com/google/go-github/v29/github" + "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - actionsv1alpha1 "github.com/summerwind/actions-runner-controller/api/v1alpha1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/summerwind/actions-runner-controller/api/v1alpha1" ) +const ( + defaultImage = "summerwind/actions-runner:latest" +) + +type RegistrationToken struct { + Token string `json:"token"` + ExpiresAt string `json:"expires_at"` +} + // RunnerReconciler reconciles a Runner object type RunnerReconciler struct { client.Client - Log logr.Logger - Scheme *runtime.Scheme + Log logr.Logger + Scheme *runtime.Scheme + GitHubClient *github.Client } // +kubebuilder:rbac:groups=actions.summerwind.dev,resources=runners,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=actions.summerwind.dev,resources=runners/status,verbs=get;update;patch func (r *RunnerReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { - _ = context.Background() - _ = r.Log.WithValues("runner", req.NamespacedName) + ctx := context.Background() + log := r.Log.WithValues("runner", req.NamespacedName) - // your logic here + var runner v1alpha1.Runner + if err := r.Get(ctx, req.NamespacedName, &runner); err != nil { + log.Error(err, "unable to fetch Runner") + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + var pod corev1.Pod + if err := r.Get(ctx, req.NamespacedName, &pod); err != nil { + if !errors.IsNotFound(err) { + return ctrl.Result{}, err + } + + if !runner.IsRegisterable() { + reg, err := r.newRegistration(ctx, runner.Spec.Repository) + if err != nil { + log.Error(err, "failed to get new registration") + return ctrl.Result{}, err + } + + updated := runner.DeepCopy() + updated.Status.Registration = reg + + if err := r.Status().Update(ctx, updated); err != nil { + log.Error(err, "unable to update Runner status") + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil + } + + pod := r.newPod(runner) + err = r.Create(ctx, &pod) + if err != nil { + log.Error(err, "failed to create a new pod") + return ctrl.Result{}, err + } + } else { + newPod := r.newPod(runner) + if reflect.DeepEqual(pod.Spec, newPod.Spec) { + return ctrl.Result{}, nil + } + + if !runner.IsRegisterable() { + reg, err := r.newRegistration(ctx, runner.Spec.Repository) + if err != nil { + log.Error(err, "failed to get new registration") + return ctrl.Result{}, err + } + + updated := runner.DeepCopy() + updated.Status.Registration = reg + + if err := r.Status().Update(ctx, updated); err != nil { + log.Error(err, "unable to update Runner status") + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil + } + + // TODO: Do not update. + updatedPod := pod.DeepCopy() + updatedPod.Spec = newPod.Spec + + if err := r.Update(ctx, updatedPod); err != nil { + log.Error(err, "unable to update pod") + return ctrl.Result{}, err + } + } return ctrl.Result{}, nil } +func (r *RunnerReconciler) newRegistration(ctx context.Context, repo string) (v1alpha1.RunnerStatusRegistration, error) { + var reg v1alpha1.RunnerStatusRegistration + + rt, err := r.getRegistrationToken(ctx, repo) + if err != nil { + return reg, err + } + + expiresAt, err := time.Parse(time.RFC3339, rt.ExpiresAt) + if err != nil { + return reg, err + } + + reg.Repository = repo + reg.Token = rt.Token + reg.ExpiresAt = metav1.NewTime(expiresAt) + + return reg, err +} + +func (r *RunnerReconciler) getRegistrationToken(ctx context.Context, repo string) (RegistrationToken, error) { + var regToken RegistrationToken + + req, err := r.GitHubClient.NewRequest("POST", fmt.Sprintf("/repos/%s/actions/runners/registration-token", repo), nil) + if err != nil { + return regToken, err + } + + res, err := r.GitHubClient.Do(ctx, req, ®Token) + if err != nil { + return regToken, err + } + + if res.StatusCode != 201 { + return regToken, fmt.Errorf("unexpected status: %d", res.StatusCode) + } + + return regToken, nil +} + +func (r *RunnerReconciler) newPod(runner v1alpha1.Runner) corev1.Pod { + image := runner.Spec.Image + if image == "" { + image = defaultImage + } + + return corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: runner.Name, + Namespace: runner.Namespace, + }, + Spec: corev1.PodSpec{ + RestartPolicy: "Never", + Containers: []corev1.Container{ + { + Name: "runner", + Image: image, + ImagePullPolicy: "Always", + Env: []corev1.EnvVar{ + corev1.EnvVar{ + Name: "RUNNER_NAME", + Value: runner.Name, + }, + corev1.EnvVar{ + Name: "RUNNER_REPO", + Value: runner.Spec.Repository, + }, + corev1.EnvVar{ + Name: "RUNNER_TOKEN", + Value: runner.Status.Registration.Token, + }, + }, + }, + }, + }, + } +} + func (r *RunnerReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). - For(&actionsv1alpha1.Runner{}). + For(&v1alpha1.Runner{}). Complete(r) } diff --git a/go.mod b/go.mod index 8e2521da..3b247669 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,18 @@ module github.com/summerwind/actions-runner-controller go 1.13 require ( + github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect + github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect + github.com/bradleyfalzon/ghinstallation v1.1.1 github.com/go-logr/logr v0.1.0 + github.com/google/go-github v17.0.0+incompatible + github.com/google/go-github/v29 v29.0.2 github.com/onsi/ginkgo v1.8.0 github.com/onsi/gomega v1.5.0 + github.com/prometheus/common v0.0.0-20181126121408-4724e9255275 + golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 + gopkg.in/alecthomas/kingpin.v2 v2.2.6 // indirect + k8s.io/api v0.0.0-20190918155943-95b840bb6a1f k8s.io/apimachinery v0.0.0-20190913080033-27d36303b655 k8s.io/client-go v0.0.0-20190918160344-1fbdaa4c8d90 sigs.k8s.io/controller-runtime v0.4.0 diff --git a/go.sum b/go.sum index 9e3159d0..3e1437f9 100644 --- a/go.sum +++ b/go.sum @@ -18,12 +18,18 @@ github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/bradleyfalzon/ghinstallation v1.1.1 h1:pmBXkxgM1WeF8QYvDLT5kuQiHMcmf+X015GI0KM/E3I= +github.com/bradleyfalzon/ghinstallation v1.1.1/go.mod h1:vyCmHTciHx/uuyN82Zc3rXN3X2KTK8nUTCrTMwAhcug= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/bbolt v1.3.1-coreos.6/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= @@ -39,6 +45,7 @@ github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= @@ -111,6 +118,14 @@ github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Z github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-github/v29 v29.0.2 h1:opYN6Wc7DOz7Ku3Oh4l7prmkOMwEcQxpFtxdU8N8Pts= +github.com/google/go-github/v29 v29.0.2/go.mod h1:CHKiKKPHJ0REzfwc14QMklvtHwCveD0PxlMjLlzAM5E= +github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -201,6 +216,7 @@ github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a h1:9a8MnZMP0X2nL github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/soheilhy/cmux v0.1.3/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= @@ -219,6 +235,7 @@ github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRci github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= @@ -324,6 +341,8 @@ google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRn google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= diff --git a/main.go b/main.go index 62bb6ea9..ba311d4b 100644 --- a/main.go +++ b/main.go @@ -17,11 +17,15 @@ limitations under the License. package main import ( + "context" + "errors" "flag" "os" + "github.com/google/go-github/v29/github" actionsv1alpha1 "github.com/summerwind/actions-runner-controller/api/v1alpha1" "github.com/summerwind/actions-runner-controller/controllers" + "golang.org/x/oauth2" "k8s.io/apimachinery/pkg/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" @@ -50,6 +54,18 @@ func main() { "Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager.") flag.Parse() + ghToken := os.Getenv("GITHUB_TOKEN") + if ghToken == "" { + err := errors.New("github token is not specified") + setupLog.Error(err, "environment variable 'GITHUB_TOKEN' must be set") + os.Exit(1) + } + + tc := oauth2.NewClient(context.Background(), oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: ghToken}, + )) + ghClient := github.NewClient(tc) + ctrl.SetLogger(zap.New(func(o *zap.Options) { o.Development = true })) @@ -65,11 +81,14 @@ func main() { os.Exit(1) } - if err = (&controllers.RunnerReconciler{ - Client: mgr.GetClient(), - Log: ctrl.Log.WithName("controllers").WithName("Runner"), - Scheme: mgr.GetScheme(), - }).SetupWithManager(mgr); err != nil { + runnerReconciler := &controllers.RunnerReconciler{ + Client: mgr.GetClient(), + Log: ctrl.Log.WithName("controllers").WithName("Runner"), + Scheme: mgr.GetScheme(), + GitHubClient: ghClient, + } + + if err = runnerReconciler.SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Runner") os.Exit(1) }