Skip to content

Commit a8a330a

Browse files
committed
feat: Unattended Flux installation
You don't need to manually add GitHub deploy keys anymore. This feature enables you to install Flux via eksctl in an unattended way, by automatically creating GitHub deply key on cluster creation and on `eksctl enable repo`, and by automatically deleting the deploy key on cluster deletion. All you need to use this feature is providing `GITHUB_TOKEN` that has access to your repository's deploy keys, and a standard cluster.yaml that contains a `git` configuration for installing Flux. Usage: ``` $ eksctl create cluster -f cluster.yaml eksctl automatically creates a deploy key named `eksctl-REGION-NAME` from the public ssh key generated by Flux ``` ``` $ eksctl delete cluster -f cluster.yaml eksctl automatically deletes the deploy key named `eksctl-REGION-NAME` by calling GitHub API ``` ``` $ eksctl enable repo -f cluster.yaml eksctl automatically creates a deploy key named `eksctl-REGION-NAME` from the public ssh key generated by Flux ``` Please also note that this feature has an extra ability to make the deploy key "read-only". With the read-only deploy key, If you prefer that, you can effectively block Flux from ever pushing commits to the repository. This can be enabled by setting `git.readOnly` to `true` or passing `--readonly` to `eksctl enable repo`. Resolves #2273
1 parent 6674195 commit a8a330a

File tree

12 files changed

+211
-23
lines changed

12 files changed

+211
-23
lines changed

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ require (
2222
github.com/gobwas/glob v0.2.3
2323
github.com/gofrs/flock v0.7.1
2424
github.com/golangci/golangci-lint v1.27.0
25+
github.com/google/go-github/v31 v31.0.0
2526
github.com/goreleaser/goreleaser v0.136.0
2627
github.com/instrumenta/kubeval v0.0.0-20190918223246-8d013ec9fc56
2728
github.com/justinbarrick/go-k8s-portforward v1.0.3
@@ -48,6 +49,7 @@ require (
4849
github.com/weaveworks/github-release v0.6.3-0.20161024133933-73deea6af1e8
4950
github.com/weaveworks/launcher v0.0.0-20180711153254-f1b2830d4f2d
5051
github.com/whilp/git-urls v0.0.0-20160530060445-31bac0d230fa
52+
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
5153
golang.org/x/sys v0.0.0-20200428200454-593003d681fa // indirect
5254
golang.org/x/tools v0.0.0-20200502202811-ed308ab3e770
5355
k8s.io/api v0.16.8

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,8 +449,12 @@ github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
449449
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
450450
github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
451451
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
452+
github.com/google/go-github/v25 v25.0.1 h1:s405kPD52lKa1MVxiEumod/E6/+0pvQ8Ed/sT65DjKc=
453+
github.com/google/go-github/v25 v25.0.1/go.mod h1:6z5pC69qHtrPJ0sXPsj4BLnd82b+r6sLB7qcBoRZqpw=
452454
github.com/google/go-github/v28 v28.1.1 h1:kORf5ekX5qwXO2mGzXXOjMe/g6ap8ahVe0sBEulhSxo=
453455
github.com/google/go-github/v28 v28.1.1/go.mod h1:bsqJWQX05omyWVmc00nEUql9mhQyv38lDZ8kPZcQVoM=
456+
github.com/google/go-github/v31 v31.0.0 h1:JJUxlP9lFK+ziXKimTCprajMApV1ecWD4NB6CCb0plo=
457+
github.com/google/go-github/v31 v31.0.0/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gAWfhbweiEed3pM=
454458
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
455459
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
456460
github.com/google/go-replayers/grpcreplay v0.1.0 h1:eNb1y9rZFmY4ax45uEEECSa8fsxGRU+8Bil52ASAwic=

pkg/apis/eksctl.io/v1alpha5/assets/schema.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,9 @@
410410
"bootstrapProfile": {
411411
"$schema": "http://json-schema.org/draft-04/schema#",
412412
"$ref": "#/definitions/Profile"
413+
},
414+
"readOnly": {
415+
"type": "boolean"
413416
}
414417
},
415418
"additionalProperties": false,

pkg/apis/eksctl.io/v1alpha5/schema.go

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/apis/eksctl.io/v1alpha5/types.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -712,6 +712,8 @@ type Git struct {
712712
Operator Operator `json:"operator,omitempty"`
713713
// +optional
714714
BootstrapProfile *Profile `json:"bootstrapProfile,omitempty"` // one or many profiles to enable on this cluster once it is created
715+
// +optional
716+
ReadOnly bool `json:"readOnly,omitempty"` // Instruct Flux to read-only mode and create the deploy key as read-only
715717
}
716718

717719
// NewGit returns a new empty Git configuration

pkg/ctl/cmdutils/gitops.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const (
2323
gitFluxPath = "git-flux-subdir"
2424
gitLabel = "git-label"
2525
namespace = "namespace"
26+
readOnly = "read-only"
2627
withHelm = "with-helm"
2728

2829
profileName = "profile-source"
@@ -51,6 +52,8 @@ func AddCommonFlagsForFlux(fs *pflag.FlagSet, opts *api.Git) {
5152
"Directory within the Git repository where to commit the Flux manifests")
5253
fs.StringVar(&opts.Operator.Namespace, namespace, "flux",
5354
"Cluster namespace where to install Flux, the Helm Operator and Tiller")
55+
fs.BoolVar(&opts.ReadOnly, readOnly, false,
56+
"Instruct Flux to read-only mode and create the deploy key as read-only")
5457
opts.Operator.WithHelm = fs.Bool(withHelm, true, "Install the Helm Operator and Tiller")
5558
}
5659

@@ -65,7 +68,8 @@ func AddCommonFlagsForGit(fs *pflag.FlagSet, repo *api.Repo) {
6568
"Username to use as Git committer")
6669
fs.StringVar(&repo.Email, gitEmail, "",
6770
"Email to use as Git committer")
68-
fs.StringVar(&repo.PrivateSSHKeyPath, gitPrivateSSHKeyPath, "",
71+
fs.StringVar(&repo.PrivateSSHKeyPath,
72+
gitPrivateSSHKeyPath, "",
6973
"Optional path to the private SSH key to use with Git, e.g. ~/.ssh/id_rsa")
7074
}
7175

pkg/ctl/delete/cluster.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/weaveworks/eksctl/pkg/cfn/manager"
1717
"github.com/weaveworks/eksctl/pkg/ctl/cmdutils"
1818
"github.com/weaveworks/eksctl/pkg/elb"
19+
"github.com/weaveworks/eksctl/pkg/gitops/deploykey"
1920
iamoidc "github.com/weaveworks/eksctl/pkg/iam/oidc"
2021
"github.com/weaveworks/eksctl/pkg/kubernetes"
2122
"github.com/weaveworks/eksctl/pkg/printers"
@@ -184,6 +185,12 @@ func doDeleteCluster(cmd *cmdutils.Cmd) error {
184185
logger.Success("all cluster resources were deleted")
185186
}
186187

188+
{
189+
if err := deploykey.ForCluster(cfg).Delete(context.Background()); err != nil {
190+
return err
191+
}
192+
}
193+
187194
return nil
188195
}
189196

pkg/ctl/enable/repo.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package enable
22

33
import (
44
"context"
5+
"github.com/weaveworks/eksctl/pkg/gitops/deploykey"
56
"time"
67

78
"github.com/kris-nova/logger"
@@ -88,11 +89,17 @@ func doEnableRepository(cmd *cmdutils.Cmd) error {
8889
return nil
8990
}
9091

91-
userInstructions, err := installer.Run(context.Background())
92+
userInstructions, fluxSSHKey, err := installer.Run(context.Background())
9293
if err != nil {
9394
logger.Critical("unable to set up gitops repo: %s", err.Error())
9495
return err
9596
}
97+
98+
if fluxSSHKey != nil {
99+
return deploykey.ForCluster(cmd.ClusterConfig).Put(context.Background(), *fluxSSHKey)
100+
}
101+
96102
logger.Info(userInstructions)
97-
return err
103+
104+
return nil
98105
}

pkg/gitops/deploykey/deploykey.go

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package deploykey
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"github.com/google/go-github/v31/github"
7+
"github.com/kris-nova/logger"
8+
api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5"
9+
"github.com/weaveworks/eksctl/pkg/gitops/flux"
10+
"golang.org/x/oauth2"
11+
"os"
12+
"regexp"
13+
)
14+
15+
type Manager struct {
16+
cluster *api.ClusterMeta
17+
repoURL string
18+
readOnly bool
19+
}
20+
21+
func ForCluster(cluster *api.ClusterConfig) *Manager {
22+
km := &Manager{
23+
cluster: cluster.Metadata,
24+
}
25+
26+
if git := cluster.Git; git != nil {
27+
if repo := git.Repo; repo != nil {
28+
km.repoURL = repo.URL
29+
}
30+
31+
km.readOnly = git.ReadOnly
32+
}
33+
34+
return km
35+
}
36+
37+
func (km *Manager) Put(ctx context.Context, fluxSSHKey flux.PublicKey) error {
38+
owner, repo, ok := km.getOwnerRepoFromRepoURL()
39+
if !ok {
40+
logger.Info("Skipped creating GitHub deploy key from Flux SSH public key for URL %s: Only `[email protected]:OWNER/REPO.git` is accepted for automatic deploy key creation", km.repoURL)
41+
42+
return nil
43+
}
44+
45+
gh, ok := km.getGitHubAPIClient(ctx)
46+
if !ok {
47+
logger.Info("Skipped creating GitHub deploy key from Flux SSH public key due to missing GITHUB_TOKEN")
48+
49+
return nil
50+
}
51+
52+
logger.Info("Creating GitHub deploy key from Flux SSH public key")
53+
54+
title := km.getDeployKeyTitle(km.cluster)
55+
56+
key, _, err := gh.Repositories.CreateKey(ctx, owner, repo, &github.Key{
57+
Key: &fluxSSHKey.Key,
58+
Title: &title,
59+
ReadOnly: &km.readOnly,
60+
})
61+
62+
if err != nil {
63+
return err
64+
}
65+
66+
logger.Info("%s configured with Flux SSH public key\n%s", *key.Title, fluxSSHKey.Key)
67+
68+
return nil
69+
}
70+
71+
func (km *Manager) Delete(ctx context.Context) error {
72+
owner, repo, ok := km.getOwnerRepoFromRepoURL()
73+
if !ok {
74+
logger.Info("Skipped deleting GitHub deploy key for URL %s: Only `[email protected]:OWNER/REPO.git` is accepted for automatic deploy key creation", km.repoURL)
75+
76+
return nil
77+
}
78+
79+
gh, ok := km.getGitHubAPIClient(ctx)
80+
if !ok {
81+
logger.Info("Skipped deleting GitHub deploy key due to missing GITHUB_TOKEN")
82+
83+
return nil
84+
}
85+
86+
logger.Info("Deleting GitHub deploy key")
87+
88+
title := km.getDeployKeyTitle(km.cluster)
89+
90+
keys, _, err := gh.Repositories.ListKeys(ctx, owner, repo, &github.ListOptions{})
91+
if err != nil {
92+
return err
93+
}
94+
95+
var keyID int64
96+
97+
for _, key := range keys {
98+
if key.GetTitle() == title {
99+
keyID = key.GetID()
100+
101+
break
102+
}
103+
}
104+
105+
if keyID == 0 {
106+
logger.Info("Skipped deleting GitHub deploy key %q: The key does not exist. Probably you've already deleted it?")
107+
108+
return nil
109+
}
110+
111+
if _, err := gh.Repositories.DeleteKey(ctx, owner, repo, keyID); err != nil {
112+
return err
113+
}
114+
115+
logger.Info("Deleted GitHub deploy key %s", title)
116+
117+
return nil
118+
}
119+
120+
func (km *Manager) getGitHubAPIClient(ctx context.Context) (*github.Client, bool) {
121+
githubToken := os.Getenv("GITHUB_TOKEN")
122+
123+
if githubToken == "" {
124+
return nil, false
125+
}
126+
127+
ts := oauth2.StaticTokenSource(
128+
&oauth2.Token{AccessToken: githubToken},
129+
)
130+
tc := oauth2.NewClient(ctx, ts)
131+
gh := github.NewClient(tc)
132+
133+
return gh, true
134+
}
135+
136+
func (km *Manager) getOwnerRepoFromRepoURL() (string, string, bool) {
137+
if km.repoURL == "" {
138+
return "", "", false
139+
}
140+
141+
r := regexp.MustCompile(`[email protected]:([^/]+)/([^.]+).git`)
142+
143+
m := r.FindStringSubmatch(km.repoURL)
144+
145+
if len(m) != 3 {
146+
return "", "", false
147+
}
148+
149+
return m[1], m[2], true
150+
}
151+
152+
func (km *Manager) getDeployKeyTitle(cluster *api.ClusterMeta) string {
153+
return fmt.Sprintf("eksctl-flux-%s-%s", cluster.Region, cluster.Name)
154+
}

pkg/gitops/flux/installer.go

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ const (
3232

3333
// Installer installs Flux
3434
type Installer struct {
35-
cluster *api.ClusterMeta
3635
opts *api.Git
3736
timeout time.Duration
3837
k8sRestConfig *rest.Config
@@ -62,12 +61,12 @@ func NewInstaller(k8sRestConfig *rest.Config, k8sClientSet kubeclient.Interface,
6261
}
6362

6463
// Run runs the Flux installer
65-
func (fi *Installer) Run(ctx context.Context) (string, error) {
64+
func (fi *Installer) Run(ctx context.Context) (string, *PublicKey, error) {
6665

6766
logger.Info("Generating manifests")
6867
manifests, err := fi.getManifests()
6968
if err != nil {
70-
return "", err
69+
return "", nil, err
7170
}
7271

7372
logger.Info("Cloning %s", fi.opts.Repo.URL)
@@ -78,7 +77,7 @@ func (fi *Installer) Run(ctx context.Context) (string, error) {
7877
}
7978
cloneDir, err := fi.gitClient.CloneRepoInTmpDir("eksctl-install-flux-clone-", options)
8079
if err != nil {
81-
return "", errors.Wrapf(err, "cannot clone repository %s", fi.opts.Repo.URL)
80+
return "", nil, errors.Wrapf(err, "cannot clone repository %s", fi.opts.Repo.URL)
8281
}
8382
cleanCloneDir := false
8483
defer func() {
@@ -93,22 +92,22 @@ func (fi *Installer) Run(ctx context.Context) (string, error) {
9392
logger.Info("Writing Flux manifests")
9493
fluxManifestDir := filepath.Join(cloneDir, fi.opts.Repo.FluxPath)
9594
if err := writeFluxManifests(fluxManifestDir, manifests); err != nil {
96-
return "", err
95+
return "", nil, err
9796
}
9897

9998
if err := fi.createFluxNamespaceIfMissing(manifests); err != nil {
100-
return "", err
99+
return "", nil, err
101100
}
102101

103102
logger.Info("Applying manifests")
104103
if err := fi.applyManifests(manifests); err != nil {
105-
return "", err
104+
return "", nil, err
106105
}
107106

108107
if api.IsEnabled(fi.opts.Operator.WithHelm) {
109108
logger.Info("Waiting for Helm Operator to start")
110109
if err := waitForHelmOpToStart(fi.opts.Operator.Namespace, fi.timeout, fi.k8sClientSet); err != nil {
111-
return "", err
110+
return "", nil, err
112111
}
113112
logger.Info("Helm Operator started successfully")
114113
logger.Info("see https://docs.fluxcd.io/projects/helm-operator for details on how to use the Helm Operator")
@@ -117,27 +116,29 @@ func (fi *Installer) Run(ctx context.Context) (string, error) {
117116
logger.Info("Waiting for Flux to start")
118117
err = waitForFluxToStart(fi.opts.Operator.Namespace, fi.timeout, fi.k8sClientSet)
119118
if err != nil {
120-
return "", err
119+
return "", nil, err
121120
}
122121
logger.Info("fetching public SSH key from Flux")
123122
fluxSSHKey, err := getPublicKeyFromFlux(ctx, fi.opts.Operator.Namespace, fi.timeout, fi.k8sRestConfig, fi.k8sClientSet)
124123
if err != nil {
125-
return "", err
124+
return "", nil, err
126125
}
127126

128127
logger.Info("Flux started successfully")
129128
logger.Info("see https://docs.fluxcd.io/projects/flux for details on how to use Flux")
130129

131-
logger.Info("Committing and pushing manifests to %s", fi.opts.Repo.URL)
132-
if err = fi.addFilesToRepo(); err != nil {
133-
return "", err
130+
if !fi.opts.ReadOnly {
131+
logger.Info("Committing and pushing manifests to %s", fi.opts.Repo.URL)
132+
if err = fi.addFilesToRepo(); err != nil {
133+
return "", nil, err
134+
}
134135
}
135136
cleanCloneDir = true
136137

137138
logger.Info("Flux will only operate properly once it has write-access to the Git repository")
138139
instruction := fmt.Sprintf("please configure %s so that the following Flux SSH public key has write access to it\n%s",
139140
fi.opts.Repo.URL, fluxSSHKey.Key)
140-
return instruction, nil
141+
return instruction, &fluxSSHKey, nil
141142
}
142143

143144
// IsFluxInstalled returns an error if Flux is not installed in the cluster. To determine that it looks for the flux
@@ -283,7 +284,7 @@ func getFluxManifests(opts *api.Git, cs kubeclient.Interface) (map[string][]byte
283284
GitLabel: opts.Operator.Label,
284285
GitUser: opts.Repo.User,
285286
GitEmail: opts.Repo.Email,
286-
GitReadOnly: false,
287+
GitReadOnly: opts.ReadOnly,
287288
Namespace: opts.Operator.Namespace,
288289
ManifestGeneration: true,
289290
AdditionalFluxArgs: []string{"--sync-garbage-collection"},

pkg/gitops/flux/installer_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ var _ = Describe("Installer", func() {
2929
},
3030
}
3131
mockInstaller := &Installer{
32-
cluster: &api.ClusterMeta{Name: "cluster-1", Region: "us-west-2"},
3332
opts: mockOpts,
3433
k8sClientSet: fake.NewSimpleClientset(),
3534
}

0 commit comments

Comments
 (0)