Replace SSH shell-outs with Go implementation
This commit is contained in:
@@ -45,9 +45,6 @@ func runCheckCmd(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
logAction("checking prerequisites")
|
logAction("checking prerequisites")
|
||||||
checkFailed := false
|
checkFailed := false
|
||||||
if !sshCheck() {
|
|
||||||
checkFailed = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if !kubectlCheck(ctx, ">=1.18.0") {
|
if !kubectlCheck(ctx, ">=1.18.0") {
|
||||||
checkFailed = true
|
checkFailed = true
|
||||||
@@ -76,21 +73,6 @@ func runCheckCmd(cmd *cobra.Command, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func sshCheck() bool {
|
|
||||||
ok := true
|
|
||||||
for _, cmd := range []string{"ssh-keygen", "ssh-keyscan"} {
|
|
||||||
_, err := exec.LookPath(cmd)
|
|
||||||
if err != nil {
|
|
||||||
logFailure("%s not found", cmd)
|
|
||||||
ok = false
|
|
||||||
} else {
|
|
||||||
logSuccess("%s found", cmd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func kubectlCheck(ctx context.Context, version string) bool {
|
func kubectlCheck(ctx context.Context, version string) bool {
|
||||||
_, err := exec.LookPath("kubectl")
|
_, err := exec.LookPath("kubectl")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -2,20 +2,24 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/elliptic"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1"
|
sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1"
|
||||||
"github.com/manifoldco/promptui"
|
"github.com/manifoldco/promptui"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"io/ioutil"
|
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
"k8s.io/apimachinery/pkg/api/errors"
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
"k8s.io/apimachinery/pkg/util/wait"
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
"strings"
|
|
||||||
|
"github.com/fluxcd/toolkit/internal/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
var createSourceGitCmd = &cobra.Command{
|
var createSourceGitCmd = &cobra.Command{
|
||||||
@@ -55,12 +59,14 @@ For private Git repositories, the basic authentication credentials are stored in
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
sourceGitURL string
|
sourceGitURL string
|
||||||
sourceGitBranch string
|
sourceGitBranch string
|
||||||
sourceGitTag string
|
sourceGitTag string
|
||||||
sourceGitSemver string
|
sourceGitSemver string
|
||||||
sourceGitUsername string
|
sourceGitUsername string
|
||||||
sourceGitPassword string
|
sourceGitPassword string
|
||||||
|
sourceGitKeyAlgorithm string
|
||||||
|
sourceGitRSABits int
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -70,6 +76,8 @@ func init() {
|
|||||||
createSourceGitCmd.Flags().StringVar(&sourceGitSemver, "tag-semver", "", "git tag semver range")
|
createSourceGitCmd.Flags().StringVar(&sourceGitSemver, "tag-semver", "", "git tag semver range")
|
||||||
createSourceGitCmd.Flags().StringVarP(&sourceGitUsername, "username", "u", "", "basic authentication username")
|
createSourceGitCmd.Flags().StringVarP(&sourceGitUsername, "username", "u", "", "basic authentication username")
|
||||||
createSourceGitCmd.Flags().StringVarP(&sourceGitPassword, "password", "p", "", "basic authentication password")
|
createSourceGitCmd.Flags().StringVarP(&sourceGitPassword, "password", "p", "", "basic authentication password")
|
||||||
|
createSourceGitCmd.Flags().StringVarP(&sourceGitKeyAlgorithm, "ssh-algorithm", "", "rsa", "SSH public key algorithm")
|
||||||
|
createSourceGitCmd.Flags().IntVarP(&sourceGitRSABits, "ssh-rsa-bits", "", 2048, "SSH RSA public key bit size")
|
||||||
|
|
||||||
createSourceCmd.AddCommand(createSourceGitCmd)
|
createSourceCmd.AddCommand(createSourceGitCmd)
|
||||||
}
|
}
|
||||||
@@ -99,8 +107,20 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
withAuth := false
|
withAuth := false
|
||||||
if strings.HasPrefix(sourceGitURL, "ssh") {
|
if u.Scheme == "ssh" {
|
||||||
if err := generateSSH(ctx, name, u.Host, tmpDir); err != nil {
|
var keyGen ssh.KeyPairGenerator
|
||||||
|
switch strings.ToLower(sourceGitKeyAlgorithm) {
|
||||||
|
case "rsa":
|
||||||
|
keyGen = ssh.NewRSAGenerator(sourceGitRSABits)
|
||||||
|
case "ecdsa":
|
||||||
|
// TODO(hidde): make curve configurable by flag
|
||||||
|
keyGen = ssh.NewECDSAGenerator(elliptic.P521())
|
||||||
|
}
|
||||||
|
host := u.Host
|
||||||
|
if u.Port() == "" {
|
||||||
|
host = host + ":22"
|
||||||
|
}
|
||||||
|
if err := generateSSH(ctx, keyGen, name, host, tmpDir); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
withAuth = true
|
withAuth = true
|
||||||
@@ -193,27 +213,13 @@ func generateBasicAuth(ctx context.Context, name string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateSSH(ctx context.Context, name, host, tmpDir string) error {
|
func generateSSH(ctx context.Context, generator ssh.KeyPairGenerator, name, host, user string) error {
|
||||||
logGenerate("generating host key for %s", host)
|
|
||||||
|
|
||||||
command := fmt.Sprintf("ssh-keyscan %s > %s/known_hosts", host, tmpDir)
|
|
||||||
if _, err := utils.execCommand(ctx, ModeStderrOS, command); err != nil {
|
|
||||||
return fmt.Errorf("ssh-keyscan failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
logGenerate("generating deploy key")
|
logGenerate("generating deploy key")
|
||||||
|
kp, err := generator.Generate()
|
||||||
command = fmt.Sprintf("ssh-keygen -b 2048 -t rsa -f %s/identity -q -N \"\"", tmpDir)
|
if err != nil {
|
||||||
if _, err := utils.execCommand(ctx, ModeStderrOS, command); err != nil {
|
return fmt.Errorf("SSH key pair generation failed: %w", err)
|
||||||
return fmt.Errorf("ssh-keygen failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
command = fmt.Sprintf("cat %s/identity.pub", tmpDir)
|
|
||||||
if deployKey, err := utils.execCommand(ctx, ModeCapture, command); err != nil {
|
|
||||||
return fmt.Errorf("unable to read identity.pub: %w", err)
|
|
||||||
} else {
|
|
||||||
fmt.Print(deployKey)
|
|
||||||
}
|
}
|
||||||
|
fmt.Printf("%s", kp.PublicKey)
|
||||||
|
|
||||||
prompt := promptui.Prompt{
|
prompt := promptui.Prompt{
|
||||||
Label: "Have you added the deploy key to your repository",
|
Label: "Have you added the deploy key to your repository",
|
||||||
@@ -223,9 +229,16 @@ func generateSSH(ctx context.Context, name, host, tmpDir string) error {
|
|||||||
return fmt.Errorf("aborting")
|
return fmt.Errorf("aborting")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logAction("collecting SSH server public key for generated public key algorithm")
|
||||||
|
serverKey, err := ssh.ScanHostKey(host, user, kp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logSuccess("collected public key from SSH server")
|
||||||
|
|
||||||
logAction("saving keys")
|
logAction("saving keys")
|
||||||
files := fmt.Sprintf("--from-file=%s/identity --from-file=%s/identity.pub --from-file=%s/known_hosts",
|
files := fmt.Sprintf("--from-literal=identity=\"%s\" --from-literal=identity.pub=\"%s\" --from-literal=known_hosts=\"%s\"",
|
||||||
tmpDir, tmpDir, tmpDir)
|
kp.PublicKey, kp.PrivateKey, serverKey)
|
||||||
secret := fmt.Sprintf("kubectl -n %s create secret generic %s %s --dry-run=client -oyaml | kubectl apply -f-",
|
secret := fmt.Sprintf("kubectl -n %s create secret generic %s %s --dry-run=client -oyaml | kubectl apply -f-",
|
||||||
namespace, name, files)
|
namespace, name, files)
|
||||||
if _, err := utils.execCommand(ctx, ModeOS, secret); err != nil {
|
if _, err := utils.execCommand(ctx, ModeOS, secret); err != nil {
|
||||||
|
|||||||
1
go.mod
1
go.mod
@@ -8,6 +8,7 @@ require (
|
|||||||
github.com/fluxcd/source-controller v0.0.1-beta.1
|
github.com/fluxcd/source-controller v0.0.1-beta.1
|
||||||
github.com/manifoldco/promptui v0.7.0
|
github.com/manifoldco/promptui v0.7.0
|
||||||
github.com/spf13/cobra v1.0.0
|
github.com/spf13/cobra v1.0.0
|
||||||
|
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073
|
||||||
k8s.io/api v0.18.2
|
k8s.io/api v0.18.2
|
||||||
k8s.io/apimachinery v0.18.2
|
k8s.io/apimachinery v0.18.2
|
||||||
k8s.io/client-go v0.18.2
|
k8s.io/client-go v0.18.2
|
||||||
|
|||||||
47
internal/ssh/host_scan.go
Normal file
47
internal/ssh/host_scan.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
"golang.org/x/crypto/ssh/knownhosts"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ScanHostKey(host string, user string, pair *KeyPair) ([]byte, error) {
|
||||||
|
signer, err := ssh.ParsePrivateKey(pair.PrivateKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
col := &collector{}
|
||||||
|
config := &ssh.ClientConfig{
|
||||||
|
User: user,
|
||||||
|
Auth: []ssh.AuthMethod{
|
||||||
|
ssh.PublicKeys(signer),
|
||||||
|
},
|
||||||
|
HostKeyCallback: col.StoreKey(),
|
||||||
|
}
|
||||||
|
client, err := ssh.Dial("tcp", host, config)
|
||||||
|
if err == nil {
|
||||||
|
defer client.Close()
|
||||||
|
}
|
||||||
|
if len(col.knownKeys) > 0 {
|
||||||
|
return col.knownKeys, nil
|
||||||
|
}
|
||||||
|
return col.knownKeys, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type collector struct {
|
||||||
|
knownKeys []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *collector) StoreKey() ssh.HostKeyCallback {
|
||||||
|
return func(hostname string, remote net.Addr, key ssh.PublicKey) error {
|
||||||
|
c.knownKeys = append(
|
||||||
|
c.knownKeys,
|
||||||
|
fmt.Sprintf("%s %s %s\n", knownhosts.Normalize(hostname), key.Type(), base64.StdEncoding.EncodeToString(key.Marshal()))...,
|
||||||
|
)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
89
internal/ssh/key_pair.go
Normal file
89
internal/ssh/key_pair.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
type KeyPair struct {
|
||||||
|
PublicKey []byte
|
||||||
|
PrivateKey []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type KeyPairGenerator interface {
|
||||||
|
Generate() (*KeyPair, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type RSAGenerator struct {
|
||||||
|
bits int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRSAGenerator(bits int) KeyPairGenerator {
|
||||||
|
return &RSAGenerator{bits}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *RSAGenerator) Generate() (*KeyPair, error) {
|
||||||
|
pk, err := rsa.GenerateKey(rand.Reader, g.bits)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = pk.Validate()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pub, err := generatePublicKey(&pk.PublicKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &KeyPair{
|
||||||
|
PublicKey: pub,
|
||||||
|
PrivateKey: encodePrivateKeyToPEM(pk),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ECDSAGenerator struct {
|
||||||
|
c elliptic.Curve
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewECDSAGenerator(c elliptic.Curve) KeyPairGenerator {
|
||||||
|
return &ECDSAGenerator{c}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *ECDSAGenerator) Generate() (*KeyPair, error) {
|
||||||
|
pk, err := ecdsa.GenerateKey(g.c, rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pub, err := generatePublicKey(&pk.PublicKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &KeyPair{
|
||||||
|
PublicKey: pub,
|
||||||
|
PrivateKey: encodePrivateKeyToPEM(pk),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func generatePublicKey(pk interface{}) ([]byte, error) {
|
||||||
|
b, err := ssh.NewPublicKey(pk)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
k := ssh.MarshalAuthorizedKey(b)
|
||||||
|
return k, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodePrivateKeyToPEM(pk interface{}) []byte {
|
||||||
|
b, _ := x509.MarshalPKCS8PrivateKey(pk)
|
||||||
|
block := pem.Block{
|
||||||
|
Type: "PRIVATE KEY",
|
||||||
|
Bytes: b,
|
||||||
|
}
|
||||||
|
return pem.EncodeToMemory(&block)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user