Skip to content

Commit da56041

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 da56041

File tree

12 files changed

+214
-23
lines changed

12 files changed

+214
-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: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"context"
55
"time"
66

7+
"github.com/weaveworks/eksctl/pkg/gitops/deploykey"
8+
79
"github.com/kris-nova/logger"
810
"github.com/spf13/cobra"
911
"github.com/spf13/pflag"
@@ -88,11 +90,17 @@ func doEnableRepository(cmd *cmdutils.Cmd) error {
8890
return nil
8991
}
9092

91-
userInstructions, err := installer.Run(context.Background())
93+
userInstructions, fluxSSHKey, err := installer.Run(context.Background())
9294
if err != nil {
9395
logger.Critical("unable to set up gitops repo: %s", err.Error())
9496
return err
9597
}
98+
99+
if fluxSSHKey != nil {
100+
return deploykey.ForCluster(cmd.ClusterConfig).Put(context.Background(), *fluxSSHKey)
101+
}
102+
96103
logger.Info(userInstructions)
97-
return err
104+
105+
return nil
98106
}

pkg/gitops/deploykey/deploykey.go

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

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)