From 21b5b0765b9a4c68d3fd1834159c3a3780f97267 Mon Sep 17 00:00:00 2001 From: Christian Hernandez Date: Wed, 3 Nov 2021 16:30:08 -0700 Subject: [PATCH 1/3] Switched from using token to using deploy key for repo access --- cmd/github/github.go | 151 +++++++++++++++++++++++++++++++++++-- cmd/templates/templates.go | 32 ++++++-- cmd/utils/utils.go | 26 +++++++ go.mod | 2 +- 4 files changed, 197 insertions(+), 14 deletions(-) diff --git a/cmd/github/github.go b/cmd/github/github.go index ee0484c..6d1fdab 100644 --- a/cmd/github/github.go +++ b/cmd/github/github.go @@ -2,14 +2,21 @@ package github import ( "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "io/ioutil" "os" "time" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing/object" "github.com/go-git/go-git/v5/plumbing/transport/http" + plumbingssh "github.com/go-git/go-git/v5/plumbing/transport/ssh" "github.com/google/go-github/v39/github" log "github.com/sirupsen/logrus" + "golang.org/x/crypto/ssh" "golang.org/x/oauth2" ) @@ -38,28 +45,49 @@ func CreateRepo(name *string, token string, private *bool, workdir string) (bool r := &github.Repository{Name: name, Private: private, Description: description, AutoInit: &autoInit} repo, _, err := client.Repositories.Create(ctx, "", r) if err != nil { - log.Fatal(err) + return false, "", err + } + + // Create an SSHKeypair for the repo. + publicKeyBytes, err := generateSSHKeypair(*name, workdir) + if err != nil { + return false, "", err + } + + // upload public sshkey as a deploy key + err = uploadDeployKey(publicKeyBytes, repo.GetOwner().GetLogin(), *name, client) + if err != nil { return false, "", err } // Get the remote URL and set the name of the local copy - repoUrl := repo.GetCloneURL() + //repoUrl := repo.GetCloneURL() + repoUrl := repo.GetSSHURL() localRepo := workdir + "/" + *name // Maksure the localRepo is there os.MkdirAll(localRepo, 0755) + // Read sshkey to do the clone + privateKeyFile := workdir + "/" + *name + "_rsa" + authKey, err := plumbingssh.NewPublicKeysFromFile("git", privateKeyFile, "") + if err != nil { + return false, "", err + } + // Clone the repo locally in the working dir (as localRepo) _, err = git.PlainClone(localRepo, false, &git.CloneOptions{ - URL: repoUrl, - Auth: &http.BasicAuth{ - Username: "unused", - Password: token, - }, + URL: repoUrl, + Auth: authKey, + /* + Auth: &http.BasicAuth{ + Username: "unused", + Password: token, + }, + */ }) if err != nil { - log.Fatal(err) return false, "", err } @@ -67,6 +95,7 @@ func CreateRepo(name *string, token string, private *bool, workdir string) (bool return true, repoUrl, nil } +// CommitAndPush commits and pushes changes to a github repo that has been changed locally func CommitAndPush(dir string, token string, msg string) (bool, error) { // Open the dir for commiting repo, err := git.PlainOpen(dir) @@ -122,3 +151,109 @@ func CommitAndPush(dir string, token string, msg string) (bool, error) { return true, nil } + +// generateSSHKeypair generates an sshkeypair to use as a deploykey on Github +func generateSSHKeypair(clustername string, workdir string) ([]byte, error) { + key := workdir + "/" + clustername + "_rsa" + savePrivateFileTo := key + savePublicFileTo := key + ".pub" + bitSize := 4096 + + privateKey, err := generatePrivateKey(bitSize) + if err != nil { + return nil, err + } + + publicKeyBytes, err := generatePublicKey(&privateKey.PublicKey) + if err != nil { + return nil, err + } + + privateKeyBytes := encodePrivateKeyToPEM(privateKey) + + err = writeKeyToFile(privateKeyBytes, savePrivateFileTo) + if err != nil { + return nil, err + } + + err = writeKeyToFile([]byte(publicKeyBytes), savePublicFileTo) + if err != nil { + return nil, err + } + return publicKeyBytes, nil +} + +// generatePrivateKey creates a RSA Private Key of specified byte size +func generatePrivateKey(bitSize int) (*rsa.PrivateKey, error) { + // Private Key generation + privateKey, err := rsa.GenerateKey(rand.Reader, bitSize) + if err != nil { + return nil, err + } + + // Validate Private Key + err = privateKey.Validate() + if err != nil { + return nil, err + } + + return privateKey, nil +} + +// encodePrivateKeyToPEM encodes Private Key from RSA to PEM format +func encodePrivateKeyToPEM(privateKey *rsa.PrivateKey) []byte { + // Get ASN.1 DER format + privDER := x509.MarshalPKCS1PrivateKey(privateKey) + + // pem.Block + privBlock := pem.Block{ + Type: "RSA PRIVATE KEY", + Headers: nil, + Bytes: privDER, + } + + // Private key in PEM format + privatePEM := pem.EncodeToMemory(&privBlock) + + return privatePEM +} + +// generatePublicKey take a rsa.PublicKey and return bytes suitable for writing to .pub file. Returns in the format "ssh-rsa ..." +func generatePublicKey(privatekey *rsa.PublicKey) ([]byte, error) { + publicRsaKey, err := ssh.NewPublicKey(privatekey) + if err != nil { + return nil, err + } + + pubKeyBytes := ssh.MarshalAuthorizedKey(publicRsaKey) + + return pubKeyBytes, nil +} + +// writePemToFile writes keys to a file +func writeKeyToFile(keyBytes []byte, saveFileTo string) error { + err := ioutil.WriteFile(saveFileTo, keyBytes, 0600) + if err != nil { + return err + } + + return nil +} + +// uploadDeployKey uploads deploykey to GitHub +func uploadDeployKey(publicKeyBytes []byte, repoOwner string, name string, client *github.Client) error { + // Set up the github key object based on the key given to use as a []byte + mykey := string(publicKeyBytes) + key := &github.Key{ + Key: &mykey, + } + + // upload the deploykey to the repo + _, _, err := client.Repositories.CreateKey(context.TODO(), repoOwner, name, key) + if err != nil { + return err + } + + // if we're here we should be okay + return nil +} diff --git a/cmd/templates/templates.go b/cmd/templates/templates.go index 5d7745a..72f9e0a 100644 --- a/cmd/templates/templates.go +++ b/cmd/templates/templates.go @@ -1,6 +1,7 @@ package templates import ( + "encoding/base64" "os" "strings" @@ -61,6 +62,7 @@ data: - /spec/allocations ` +/* var ArgoCdOverlayDefaultRepoSecret string = `apiVersion: v1 kind: Secret metadata: @@ -75,6 +77,21 @@ stringData: password: {{.GitHubToken}} {{ end }} ` +*/ + +var ArgoCdOverlayDefaultRepoSecret string = `apiVersion: v1 +kind: Secret +metadata: + name: cluster-repo + namespace: argocd + labels: + argocd.argoproj.io/secret-type: repository +type: Opaque +data: + sshPrivateKey: {{.SSHPrivateKey}} + type: Z2l0 + url: {{.ClusterGitOpsRepo}} +` var ArgoCdComponetnsApplicationSetKustomize string = `resources: - cluster-components.yaml @@ -325,14 +342,19 @@ func CreateRepoSkel(name *string, workdir string, ghtoken string, gitopsrepo str } // Write out the argocd secret of the repo based on the vars and template + sshKeyFile, err := utils.B64EncodeFile(workdir + "/" + *name + "_rsa") + if err != nil { + return false, err + } githubInfo := struct { ClusterGitOpsRepo string - GitHubToken string - IsPrivate bool + SSHPrivateKey string + //IsPrivate bool }{ - ClusterGitOpsRepo: gitopsrepo, - GitHubToken: ghtoken, - IsPrivate: *private, + ClusterGitOpsRepo: base64.StdEncoding.EncodeToString([]byte(gitopsrepo)), + SSHPrivateKey: sshKeyFile, + //GitHubToken: ghtoken, + //IsPrivate: *private, } _, err = utils.WriteTemplate(ArgoCdOverlayDefaultRepoSecret, dir+"/"+"repo-secret.yaml", githubInfo) if err != nil { diff --git a/cmd/utils/utils.go b/cmd/utils/utils.go index 109d75f..e4a3a00 100644 --- a/cmd/utils/utils.go +++ b/cmd/utils/utils.go @@ -1,6 +1,8 @@ package utils import ( + "bufio" + "encoding/base64" "fmt" "io" "io/ioutil" @@ -220,3 +222,27 @@ func CopyDir(source string, dest string) error { } return err } + +// B64EncodeFile returns the base64 encoding of a file as a string. The file must be a full path +func B64EncodeFile(file string) (string, error) { + // Open file on disk. + f, err := os.Open(file) + if err != nil { + return "", err + } + // be sure to close the file + defer f.Close() + + // Read file into byte slice. + reader := bufio.NewReader(f) + content, err := ioutil.ReadAll(reader) + if err != nil { + return "", err + } + + // Encode as base64. + encoded := base64.StdEncoding.EncodeToString(content) + + // return result + return encoded, nil +} diff --git a/go.mod b/go.mod index dcc2a9c..68185a0 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,6 @@ require ( require ( github.com/google/go-querystring v1.1.0 // indirect - golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f ) @@ -120,6 +119,7 @@ require ( github.com/rwtodd/Go.Sed v0.0.0-20210816025313-55464686f9ef github.com/sanathkr/go-yaml v0.0.0-20170819195128-ed9d249f429b // indirect github.com/sanathkr/yaml v0.0.0-20170819201035-0056894fa522 // indirect + golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 k8s.io/kubectl v0.22.2 sigs.k8s.io/kustomize/api v0.10.0 sigs.k8s.io/kustomize/kyaml v0.12.0 From 047f1ca259ce6a6ac92cd6f4be59e4a542e59001 Mon Sep 17 00:00:00 2001 From: Christian Hernandez Date: Wed, 3 Nov 2021 17:07:02 -0700 Subject: [PATCH 2/3] git ssh bug fix --- cmd/createCluster_aws.go | 3 ++- cmd/createCluster_development.go | 3 ++- cmd/github/github.go | 20 ++++++++++++++------ cmd/templates/templates.go | 3 ++- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/cmd/createCluster_aws.go b/cmd/createCluster_aws.go index 5288c26..1ff7baf 100644 --- a/cmd/createCluster_aws.go +++ b/cmd/createCluster_aws.go @@ -112,7 +112,8 @@ doesn't create one for you).`, } // Git push newly exported YAML to GitOps repo - _, err = github.CommitAndPush(WorkDir+"/"+clusterName, ghToken, "exporting existing YAML") + privateKeyFile := WorkDir + "/" + clusterName + "_rsa" + _, err = github.CommitAndPush(WorkDir+"/"+clusterName, privateKeyFile, "exporting existing YAML") if err != nil { log.Fatal(err) } diff --git a/cmd/createCluster_development.go b/cmd/createCluster_development.go index c858473..69318f2 100644 --- a/cmd/createCluster_development.go +++ b/cmd/createCluster_development.go @@ -91,7 +91,8 @@ so beware. This create a local cluster for testing. PRE-PRE-ALPHA.`, } // Git push newly exported YAML to GitOps repo - _, err = github.CommitAndPush(WorkDir+"/"+clusterName, ghToken, "exporting existing YAML") + privateKeyFile := WorkDir + "/" + clusterName + "_rsa" + _, err = github.CommitAndPush(WorkDir+"/"+clusterName, privateKeyFile, "exporting existing YAML") if err != nil { log.Fatal(err) } diff --git a/cmd/github/github.go b/cmd/github/github.go index 6d1fdab..cebf46c 100644 --- a/cmd/github/github.go +++ b/cmd/github/github.go @@ -12,7 +12,6 @@ import ( "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing/object" - "github.com/go-git/go-git/v5/plumbing/transport/http" plumbingssh "github.com/go-git/go-git/v5/plumbing/transport/ssh" "github.com/google/go-github/v39/github" log "github.com/sirupsen/logrus" @@ -96,7 +95,7 @@ func CreateRepo(name *string, token string, private *bool, workdir string) (bool } // CommitAndPush commits and pushes changes to a github repo that has been changed locally -func CommitAndPush(dir string, token string, msg string) (bool, error) { +func CommitAndPush(dir string, privateKeyFile string, msg string) (bool, error) { // Open the dir for commiting repo, err := git.PlainOpen(dir) if err != nil { @@ -133,13 +132,22 @@ func CommitAndPush(dir string, token string, msg string) (bool, error) { return false, err } + // Read sshkey to do the clone + authKey, err := plumbingssh.NewPublicKeysFromFile("git", privateKeyFile, "") + if err != nil { + return false, err + } + //Push to repo err = repo.Push(&git.PushOptions{ RemoteName: "origin", - Auth: &http.BasicAuth{ - Username: "unused", - Password: token, - }, + Auth: authKey, + /* + Auth: &http.BasicAuth{ + Username: "unused", + Password: token, + }, + */ }) if err != nil { diff --git a/cmd/templates/templates.go b/cmd/templates/templates.go index 72f9e0a..7689eeb 100644 --- a/cmd/templates/templates.go +++ b/cmd/templates/templates.go @@ -473,7 +473,8 @@ func CreateRepoSkel(name *string, workdir string, ghtoken string, gitopsrepo str // Commit and push initialize skel log.Info("Pushing initial skel repo structure") - _, err := github.CommitAndPush(repoDir, ghtoken, "initializing skel repo structure") + privateKeyFile := workdir + "/" + *name + "_rsa" + _, err := github.CommitAndPush(repoDir, privateKeyFile, "initializing skel repo structure") if err != nil { return false, err } From 6df52c48304718fecc4be99298c3b6381d806408 Mon Sep 17 00:00:00 2001 From: Christian Hernandez Date: Thu, 4 Nov 2021 18:06:00 -0700 Subject: [PATCH 3/3] made Kubernetes version a consistant var --- cmd/capi/capi.go | 6 ++++-- cmd/root.go | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/cmd/capi/capi.go b/cmd/capi/capi.go index a690131..fff4ce3 100644 --- a/cmd/capi/capi.go +++ b/cmd/capi/capi.go @@ -48,6 +48,8 @@ var CNIurl string = "https://docs.projectcalico.org/v3.20/manifests/calico.yaml" var decUnstructured = yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme) +var KubernetesVersion string = "v1.22.2" + // CreateAwsK8sInstance creates a Kubernetes cluster on AWS using CAPI and CAPI-AWS func CreateAwsK8sInstance(kindkconfig string, clusterName *string, workdir string, awscreds map[string]string, capicfg string, createHaCluster bool, skipCloudFormation bool) (bool, error) { // Export AWS settings as Env vars @@ -133,7 +135,7 @@ func CreateAwsK8sInstance(kindkconfig string, clusterName *string, workdir strin ClusterName: *clusterName, ControlPlaneMachineCount: &cpMachineCount, WorkerMachineCount: &workerMachineCount, - KubernetesVersion: "v1.22.2", + KubernetesVersion: KubernetesVersion, TargetNamespace: "default", } @@ -349,7 +351,7 @@ func CreateDevelK8sInstance(kindkconfig string, clusterName *string, workdir str ClusterName: *clusterName, ControlPlaneMachineCount: &cpMachineCount, WorkerMachineCount: &workerMachineCount, - KubernetesVersion: "v1.22.1", + KubernetesVersion: KubernetesVersion, TargetNamespace: "default", ProviderRepositorySource: &capiclient.ProviderRepositorySourceOptions{Flavor: "development"}, } diff --git a/cmd/root.go b/cmd/root.go index 11e0905..4568e41 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -17,7 +17,7 @@ var CapiCfg string // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ Use: "gokp", - Version: "v0.0.3", + Version: "v0.0.4", Short: "GOKP installs a GitOps ready Kubernetes cluster", Long: `GOKP creates a Kubernetes cluster using CAPI. It is meant to be a GitOps native Kubernetes cluster ready to use.