Skip to content

Commit 0d14857

Browse files
authored
Unattended Flux installation for GitHub repos (automatic addition of deploy key) (#2274)
Unattended Flux installation (automatic addition of deploy key) 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 18aef15 commit 0d14857

File tree

15 files changed

+316
-12
lines changed

15 files changed

+316
-12
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: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,9 @@
816816
},
817817
"Operator": {
818818
"properties": {
819+
"commitOperatorManifests": {
820+
"type": "boolean"
821+
},
819822
"label": {
820823
"type": "string"
821824
},
@@ -824,6 +827,9 @@
824827
},
825828
"withHelm": {
826829
"type": "boolean"
830+
},
831+
"readOnly": {
832+
"type": "boolean"
827833
}
828834
},
829835
"additionalProperties": false,

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,10 @@ func SetDefaultGitSettings(c *ClusterConfig) {
248248
return
249249
}
250250

251+
if c.Git.Operator.CommitOperatorManifests == nil {
252+
c.Git.Operator.CommitOperatorManifests = Enabled()
253+
}
254+
251255
if c.Git.Operator.Label == "" {
252256
c.Git.Operator.Label = "flux"
253257
}

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: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -743,12 +743,16 @@ type Repo struct {
743743
// Operator groups all configuration options related to the operator used to
744744
// keep the cluster and the Git repository in sync.
745745
type Operator struct {
746+
// +optional
747+
CommitOperatorManifests *bool `json:"commitOperatorManifests,omitempty"` // Commit and push Flux manifests to the Git Repo on install
746748
// +optional
747749
Label string `json:"label,omitempty"` // e.g. flux
748750
// +optional
749751
Namespace string `json:"namespace,omitempty"` // e.g. flux
750752
// +optional
751753
WithHelm *bool `json:"withHelm,omitempty"` // whether to install the Flux Helm Operator or not
754+
// +optional
755+
ReadOnly bool `json:"readOnly,omitempty"` // Instruct Flux to read-only mode and create the deploy key as read-only
752756
}
753757

754758
// Profile groups all details on a quickstart profile to enable on the cluster

pkg/apis/eksctl.io/v1alpha5/zz_generated.deepcopy.go

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

pkg/ctl/cmdutils/gitops.go

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

29+
commitOperatorManifests = "commit-operator-manifests"
30+
2831
profileName = "profile-source"
2932
profileRevision = "profile-revision"
3033
)
@@ -51,6 +54,10 @@ func AddCommonFlagsForFlux(fs *pflag.FlagSet, opts *api.Git) {
5154
"Directory within the Git repository where to commit the Flux manifests")
5255
fs.StringVar(&opts.Operator.Namespace, namespace, "flux",
5356
"Cluster namespace where to install Flux, the Helm Operator and Tiller")
57+
fs.BoolVar(&opts.Operator.ReadOnly, readOnly, false,
58+
"Instruct Flux to read-only mode and create the deploy key as read-only")
59+
opts.Operator.CommitOperatorManifests = fs.Bool(commitOperatorManifests, true,
60+
"Commit and push Flux manifests to the Git Repo on install")
5461
opts.Operator.WithHelm = fs.Bool(withHelm, true, "Install the Helm Operator and Tiller")
5562
}
5663

@@ -65,7 +72,8 @@ func AddCommonFlagsForGit(fs *pflag.FlagSet, repo *api.Repo) {
6572
"Username to use as Git committer")
6673
fs.StringVar(&repo.Email, gitEmail, "",
6774
"Email to use as Git committer")
68-
fs.StringVar(&repo.PrivateSSHKeyPath, gitPrivateSSHKeyPath, "",
75+
fs.StringVar(&repo.PrivateSSHKeyPath,
76+
gitPrivateSSHKeyPath, "",
6977
"Optional path to the private SSH key to use with Git, e.g. ~/.ssh/id_rsa")
7078
}
7179

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.Delete(context.Background(), cfg); err != nil {
190+
return err
191+
}
192+
}
193+
187194
return nil
188195
}
189196

pkg/ctl/enable/repo.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ func doEnableRepository(cmd *cmdutils.Cmd) error {
9393
logger.Critical("unable to set up gitops repo: %s", err.Error())
9494
return err
9595
}
96+
9697
logger.Info(userInstructions)
97-
return err
98+
99+
return nil
98100
}

pkg/gitops/deploykey/deploykey.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package deploykey
2+
3+
import (
4+
"context"
5+
"os"
6+
7+
"github.com/kris-nova/logger"
8+
"github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5"
9+
)
10+
11+
type GitProvider interface {
12+
Put(ctx context.Context, fluxSSHKey PublicKey) error
13+
Delete(ctx context.Context) error
14+
}
15+
16+
func ForCluster(cluster *v1alpha5.ClusterConfig) GitProvider {
17+
var (
18+
repoURL string
19+
readOnly bool
20+
)
21+
22+
if git := cluster.Git; git != nil {
23+
if repo := git.Repo; repo != nil {
24+
repoURL = repo.URL
25+
}
26+
27+
readOnly = git.Operator.ReadOnly
28+
}
29+
30+
if repoURL == "" {
31+
return nil
32+
}
33+
34+
if owner, repo, ok := getGitHubOwnerRepoFromRepoURL(repoURL); !ok {
35+
logger.Info("skipped managing GitHub deploy key for URL %s: Only `[email protected]:OWNER/REPO.git` is accepted for automatic deploy key creation", repoURL)
36+
} else if githubToken := os.Getenv(EnvVarGitHubToken); githubToken == "" {
37+
logger.Info("GITHUB_TOKEN is not set. Please set it so that eksctl is able to create and delete GitHub deploy key from Flux SSH public key")
38+
} else {
39+
return &GitHubProvider{
40+
cluster: cluster.Metadata,
41+
githubToken: githubToken,
42+
readOnly: readOnly,
43+
owner: owner,
44+
repo: repo,
45+
}
46+
}
47+
48+
return nil
49+
}
50+
51+
func Put(ctx context.Context, cluster *v1alpha5.ClusterConfig, fluxSSHKey PublicKey) (bool, error) {
52+
p := ForCluster(cluster)
53+
54+
if p == nil {
55+
return false, nil
56+
}
57+
58+
return true, p.Put(ctx, fluxSSHKey)
59+
}
60+
61+
func Delete(ctx context.Context, cluster *v1alpha5.ClusterConfig) error {
62+
p := ForCluster(cluster)
63+
64+
if p == nil {
65+
return nil
66+
}
67+
68+
return p.Delete(ctx)
69+
}
70+
71+
// PublicKey represents a public SSH key as it is returned by flux
72+
type PublicKey struct {
73+
Key string
74+
}

pkg/gitops/deploykey/github.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package deploykey
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"regexp"
7+
8+
"github.com/google/go-github/v31/github"
9+
"github.com/kris-nova/logger"
10+
api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5"
11+
"golang.org/x/oauth2"
12+
)
13+
14+
const (
15+
EnvVarGitHubToken = "GITHUB_TOKEN"
16+
)
17+
18+
type GitHubProvider struct {
19+
cluster *api.ClusterMeta
20+
owner, repo string
21+
readOnly bool
22+
githubToken string
23+
}
24+
25+
func (p *GitHubProvider) Put(ctx context.Context, fluxSSHKey PublicKey) error {
26+
gh := p.getGitHubAPIClient(ctx)
27+
28+
logger.Info("creating GitHub deploy key from Flux SSH public key")
29+
30+
title := p.getDeployKeyTitle()
31+
32+
key, _, err := gh.Repositories.CreateKey(ctx, p.owner, p.repo, &github.Key{
33+
Key: &fluxSSHKey.Key,
34+
Title: &title,
35+
ReadOnly: &p.readOnly,
36+
})
37+
38+
if err != nil {
39+
return err
40+
}
41+
42+
logger.Info("%s configured with Flux SSH public key\n%s", *key.Title, fluxSSHKey.Key)
43+
44+
return nil
45+
}
46+
47+
func (p *GitHubProvider) Delete(ctx context.Context) error {
48+
gh := p.getGitHubAPIClient(ctx)
49+
50+
logger.Info("deleting GitHub deploy key")
51+
52+
title := p.getDeployKeyTitle()
53+
54+
keys, _, err := gh.Repositories.ListKeys(ctx, p.owner, p.repo, &github.ListOptions{})
55+
if err != nil {
56+
return err
57+
}
58+
59+
var keyID int64
60+
61+
for _, key := range keys {
62+
if key.GetTitle() == title {
63+
keyID = key.GetID()
64+
65+
break
66+
}
67+
}
68+
69+
if keyID == 0 {
70+
logger.Info("skipped deleting GitHub deploy key %q: The key does not exist. Probably you've already deleted it?")
71+
72+
return nil
73+
}
74+
75+
if _, err := gh.Repositories.DeleteKey(ctx, p.owner, p.repo, keyID); err != nil {
76+
return err
77+
}
78+
79+
logger.Info("deleted GitHub deploy key %s", title)
80+
81+
return nil
82+
}
83+
84+
func (p *GitHubProvider) getGitHubAPIClient(ctx context.Context) *github.Client {
85+
ts := oauth2.StaticTokenSource(
86+
&oauth2.Token{AccessToken: p.githubToken},
87+
)
88+
tc := oauth2.NewClient(ctx, ts)
89+
gh := github.NewClient(tc)
90+
91+
return gh
92+
}
93+
94+
func getGitHubOwnerRepoFromRepoURL(repoURL string) (string, string, bool) {
95+
if repoURL == "" {
96+
return "", "", false
97+
}
98+
99+
sshFull := regexp.MustCompile(`ssh://[email protected]/([^/]+)/([^.]+).git`)
100+
sshShort := regexp.MustCompile(`[email protected]:([^/]+)/([^.]+).git`)
101+
102+
patterns := []*regexp.Regexp{
103+
sshFull,
104+
sshShort,
105+
}
106+
107+
for _, p := range patterns {
108+
m := p.FindStringSubmatch(repoURL)
109+
if len(m) == 3 {
110+
return m[1], m[2], true
111+
}
112+
}
113+
114+
return "", "", false
115+
}
116+
117+
func (p *GitHubProvider) getDeployKeyTitle() string {
118+
return fmt.Sprintf("eksctl-flux-%s-%s", p.cluster.Region, p.cluster.Name)
119+
}

0 commit comments

Comments
 (0)