Merge pull request #1001 from fluxcd/manifestgen-deploysecret-kustomization

Add `sourcesecret` and `kustomization` manifestgen
pull/1018/head
Hidde Beydals 4 years ago committed by GitHub
commit bb3562427b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -19,13 +19,11 @@ package main
import ( import (
"context" "context"
"fmt" "fmt"
"net/url"
"path/filepath" "path/filepath"
"time" "time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/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"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
@ -36,6 +34,7 @@ import (
"github.com/fluxcd/flux2/internal/flags" "github.com/fluxcd/flux2/internal/flags"
"github.com/fluxcd/flux2/internal/utils" "github.com/fluxcd/flux2/internal/utils"
"github.com/fluxcd/flux2/pkg/manifestgen/install" "github.com/fluxcd/flux2/pkg/manifestgen/install"
kus "github.com/fluxcd/flux2/pkg/manifestgen/kustomization"
"github.com/fluxcd/flux2/pkg/manifestgen/sync" "github.com/fluxcd/flux2/pkg/manifestgen/sync"
) )
@ -200,6 +199,7 @@ func generateSyncManifests(url, branch, name, namespace, targetPath, tmpDir stri
URL: url, URL: url,
Branch: branch, Branch: branch,
Interval: interval, Interval: interval,
Secret: namespace,
TargetPath: targetPath, TargetPath: targetPath,
ManifestFile: sync.MakeDefaultOptions().ManifestFile, ManifestFile: sync.MakeDefaultOptions().ManifestFile,
} }
@ -214,9 +214,19 @@ func generateSyncManifests(url, branch, name, namespace, targetPath, tmpDir stri
return "", err return "", err
} }
outputDir := filepath.Dir(output) outputDir := filepath.Dir(output)
if err := utils.GenerateKustomizationYaml(outputDir); err != nil {
kusOpts := kus.MakeDefaultOptions()
kusOpts.BaseDir = tmpDir
kusOpts.TargetPath = filepath.Dir(manifest.Path)
kustomization, err := kus.Generate(kusOpts)
if err != nil {
return "", err return "", err
} }
if _, err = kustomization.WriteFile(tmpDir); err != nil {
return "", err
}
return outputDir, nil return outputDir, nil
} }
@ -269,35 +279,6 @@ func shouldCreateDeployKey(ctx context.Context, kubeClient client.Client, namesp
return false return false
} }
func generateDeployKey(ctx context.Context, kubeClient client.Client, url *url.URL, namespace string) (string, error) {
pair, err := generateKeyPair(ctx, sourceGitArgs.keyAlgorithm, sourceGitArgs.keyRSABits, sourceGitArgs.keyECDSACurve)
if err != nil {
return "", err
}
hostKey, err := scanHostKey(ctx, url)
if err != nil {
return "", err
}
secret := corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: namespace,
Namespace: namespace,
},
StringData: map[string]string{
"identity": string(pair.PrivateKey),
"identity.pub": string(pair.PublicKey),
"known_hosts": string(hostKey),
},
}
if err := upsertSecret(ctx, kubeClient, secret); err != nil {
return "", err
}
return string(pair.PublicKey), nil
}
func checkIfBootstrapPathDiffers(ctx context.Context, kubeClient client.Client, namespace string, path string) (string, bool) { func checkIfBootstrapPathDiffers(ctx context.Context, kubeClient client.Client, namespace string, path string) (string, bool) {
namespacedName := types.NamespacedName{ namespacedName := types.NamespacedName{
Name: namespace, Name: namespace,

@ -26,14 +26,14 @@ import (
"path/filepath" "path/filepath"
"time" "time"
"github.com/fluxcd/pkg/git"
"github.com/spf13/cobra" "github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/yaml"
"github.com/fluxcd/pkg/git"
"github.com/fluxcd/flux2/internal/flags" "github.com/fluxcd/flux2/internal/flags"
"github.com/fluxcd/flux2/internal/utils" "github.com/fluxcd/flux2/internal/utils"
"github.com/fluxcd/flux2/pkg/manifestgen/sourcesecret"
) )
var bootstrapGitHubCmd = &cobra.Command{ var bootstrapGitHubCmd = &cobra.Command{
@ -244,44 +244,48 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error {
logger.Successf("install completed") logger.Successf("install completed")
} }
repoURL := repository.GetURL() repoURL := repository.GetSSH()
secretOpts := sourcesecret.Options{
if bootstrapArgs.tokenAuth {
// setup HTTPS token auth
secret := corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: rootArgs.namespace, Name: rootArgs.namespace,
Namespace: rootArgs.namespace, Namespace: rootArgs.namespace,
},
StringData: map[string]string{
"username": "git",
"password": ghToken,
},
} }
if err := upsertSecret(ctx, kubeClient, secret); err != nil { if bootstrapArgs.tokenAuth {
return err // Setup HTTPS token auth
} repoURL = repository.GetURL()
} else { secretOpts.Username = "git"
// setup SSH deploy key secretOpts.Password = ghToken
repoURL = repository.GetSSH() } else if shouldCreateDeployKey(ctx, kubeClient, rootArgs.namespace) {
if shouldCreateDeployKey(ctx, kubeClient, rootArgs.namespace) { // Setup SSH auth
logger.Actionf("configuring deploy key") u, err := url.Parse(repoURL)
u, err := url.Parse(repository.GetSSH())
if err != nil { if err != nil {
return fmt.Errorf("git URL parse failed: %w", err) return fmt.Errorf("git URL parse failed: %w", err)
} }
secretOpts.SSHHostname = u.Hostname()
secretOpts.PrivateKeyAlgorithm = sourcesecret.RSAPrivateKeyAlgorithm
secretOpts.RSAKeyBits = 2048
}
key, err := generateDeployKey(ctx, kubeClient, u, rootArgs.namespace) secret, err := sourcesecret.Generate(secretOpts)
if err != nil { if err != nil {
return fmt.Errorf("generating deploy key failed: %w", err) return err
}
var s corev1.Secret
if err := yaml.Unmarshal([]byte(secret.Content), &s); err != nil {
return err
}
if len(s.StringData) > 0 {
logger.Actionf("configuring deploy key")
if err := upsertSecret(ctx, kubeClient, s); err != nil {
return err
} }
if ppk, ok := s.StringData[sourcesecret.PublicKeySecretKey]; ok {
keyName := "flux" keyName := "flux"
if githubArgs.path != "" { if githubArgs.path != "" {
keyName = fmt.Sprintf("flux-%s", githubArgs.path) keyName = fmt.Sprintf("flux-%s", githubArgs.path)
} }
if changed, err := provider.AddDeployKey(ctx, repository, key, keyName); err != nil { if changed, err := provider.AddDeployKey(ctx, repository, ppk, keyName); err != nil {
return err return err
} else if changed { } else if changed {
logger.Successf("deploy key configured") logger.Successf("deploy key configured")

@ -29,12 +29,13 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/yaml"
"github.com/fluxcd/pkg/git" "github.com/fluxcd/pkg/git"
"github.com/fluxcd/flux2/internal/flags" "github.com/fluxcd/flux2/internal/flags"
"github.com/fluxcd/flux2/internal/utils" "github.com/fluxcd/flux2/internal/utils"
"github.com/fluxcd/flux2/pkg/manifestgen/sourcesecret"
) )
var bootstrapGitLabCmd = &cobra.Command{ var bootstrapGitLabCmd = &cobra.Command{
@ -218,44 +219,48 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error {
logger.Successf("install completed") logger.Successf("install completed")
} }
repoURL := repository.GetURL() repoURL := repository.GetSSH()
secretOpts := sourcesecret.Options{
if bootstrapArgs.tokenAuth {
// setup HTTPS token auth
secret := corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: rootArgs.namespace, Name: rootArgs.namespace,
Namespace: rootArgs.namespace, Namespace: rootArgs.namespace,
},
StringData: map[string]string{
"username": "git",
"password": glToken,
},
}
if err := upsertSecret(ctx, kubeClient, secret); err != nil {
return err
} }
} else { if bootstrapArgs.tokenAuth {
// setup SSH deploy key // Setup HTTPS token auth
repoURL = repository.GetSSH() repoURL = repository.GetURL()
if shouldCreateDeployKey(ctx, kubeClient, rootArgs.namespace) { secretOpts.Username = "git"
logger.Actionf("configuring deploy key") secretOpts.Password = glToken
} else if shouldCreateDeployKey(ctx, kubeClient, rootArgs.namespace) {
// Setup SSH auth
u, err := url.Parse(repoURL) u, err := url.Parse(repoURL)
if err != nil { if err != nil {
return fmt.Errorf("git URL parse failed: %w", err) return fmt.Errorf("git URL parse failed: %w", err)
} }
secretOpts.SSHHostname = u.Hostname()
secretOpts.PrivateKeyAlgorithm = sourcesecret.RSAPrivateKeyAlgorithm
secretOpts.RSAKeyBits = 2048
}
key, err := generateDeployKey(ctx, kubeClient, u, rootArgs.namespace) secret, err := sourcesecret.Generate(secretOpts)
if err != nil { if err != nil {
return fmt.Errorf("generating deploy key failed: %w", err) return err
}
var s corev1.Secret
if err := yaml.Unmarshal([]byte(secret.Content), &s); err != nil {
return err
}
if len(s.StringData) > 0 {
logger.Actionf("configuring deploy key")
if err := upsertSecret(ctx, kubeClient, s); err != nil {
return err
} }
if ppk, ok := s.StringData[sourcesecret.PublicKeySecretKey]; ok {
keyName := "flux" keyName := "flux"
if gitlabArgs.path != "" { if gitlabArgs.path != "" {
keyName = fmt.Sprintf("flux-%s", gitlabArgs.path) keyName = fmt.Sprintf("flux-%s", gitlabArgs.path)
} }
if changed, err := provider.AddDeployKey(ctx, repository, key, keyName); err != nil { if changed, err := provider.AddDeployKey(ctx, repository, ppk, keyName); err != nil {
return err return err
} else if changed { } else if changed {
logger.Successf("deploy key configured") logger.Successf("deploy key configured")

@ -18,15 +18,12 @@ package main
import ( import (
"context" "context"
"fmt"
"github.com/spf13/cobra" "github.com/spf13/cobra"
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"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/yaml"
) )
var createSecretCmd = &cobra.Command{ var createSecretCmd = &cobra.Command{
@ -39,23 +36,6 @@ func init() {
createCmd.AddCommand(createSecretCmd) createCmd.AddCommand(createSecretCmd)
} }
func makeSecret(name string) (corev1.Secret, error) {
secretLabels, err := parseLabels()
if err != nil {
return corev1.Secret{}, err
}
return corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: rootArgs.namespace,
Labels: secretLabels,
},
StringData: map[string]string{},
Data: nil,
}, nil
}
func upsertSecret(ctx context.Context, kubeClient client.Client, secret corev1.Secret) error { func upsertSecret(ctx context.Context, kubeClient client.Client, secret corev1.Secret) error {
namespacedName := types.NamespacedName{ namespacedName := types.NamespacedName{
Namespace: secret.GetNamespace(), Namespace: secret.GetNamespace(),
@ -81,19 +61,3 @@ func upsertSecret(ctx context.Context, kubeClient client.Client, secret corev1.S
} }
return nil return nil
} }
func exportSecret(secret corev1.Secret) error {
secret.TypeMeta = metav1.TypeMeta{
APIVersion: "v1",
Kind: "Secret",
}
data, err := yaml.Marshal(secret)
if err != nil {
return err
}
fmt.Println("---")
fmt.Println(resourceToString(data))
return nil
}

@ -20,15 +20,15 @@ import (
"context" "context"
"crypto/elliptic" "crypto/elliptic"
"fmt" "fmt"
"io/ioutil"
"net/url" "net/url"
"time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/yaml"
"github.com/fluxcd/flux2/internal/flags" "github.com/fluxcd/flux2/internal/flags"
"github.com/fluxcd/flux2/internal/utils" "github.com/fluxcd/flux2/internal/utils"
"github.com/fluxcd/pkg/ssh" "github.com/fluxcd/flux2/pkg/manifestgen/sourcesecret"
) )
var createSecretGitCmd = &cobra.Command{ var createSecretGitCmd = &cobra.Command{
@ -107,11 +107,6 @@ func createSecretGitCmdRun(cmd *cobra.Command, args []string) error {
return fmt.Errorf("secret name is required") return fmt.Errorf("secret name is required")
} }
name := args[0] name := args[0]
secret, err := makeSecret(name)
if err != nil {
return err
}
if secretGitArgs.url == "" { if secretGitArgs.url == "" {
return fmt.Errorf("url is required") return fmt.Errorf("url is required")
} }
@ -121,96 +116,63 @@ func createSecretGitCmdRun(cmd *cobra.Command, args []string) error {
return fmt.Errorf("git URL parse failed: %w", err) return fmt.Errorf("git URL parse failed: %w", err)
} }
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) labels, err := parseLabels()
defer cancel()
switch u.Scheme {
case "ssh":
pair, err := generateKeyPair(ctx, secretGitArgs.keyAlgorithm, secretGitArgs.rsaBits, secretGitArgs.ecdsaCurve)
if err != nil { if err != nil {
return err return err
} }
hostKey, err := scanHostKey(ctx, u) opts := sourcesecret.Options{
if err != nil { Name: name,
return err Namespace: rootArgs.namespace,
} Labels: labels,
ManifestFile: sourcesecret.MakeDefaultOptions().ManifestFile,
secret.StringData = map[string]string{
"identity": string(pair.PrivateKey),
"identity.pub": string(pair.PublicKey),
"known_hosts": string(hostKey),
}
if !createArgs.export {
logger.Generatef("deploy key: %s", string(pair.PublicKey))
} }
switch u.Scheme {
case "ssh":
opts.SSHHostname = u.Hostname()
opts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(secretGitArgs.keyAlgorithm)
opts.RSAKeyBits = int(secretGitArgs.rsaBits)
opts.ECDSACurve = secretGitArgs.ecdsaCurve.Curve
case "http", "https": case "http", "https":
if secretGitArgs.username == "" || secretGitArgs.password == "" { if secretGitArgs.username == "" || secretGitArgs.password == "" {
return fmt.Errorf("for Git over HTTP/S the username and password are required") return fmt.Errorf("for Git over HTTP/S the username and password are required")
} }
opts.Username = secretGitArgs.username
secret.StringData = map[string]string{ opts.Password = secretGitArgs.password
"username": secretGitArgs.username, opts.CAFilePath = secretGitArgs.caFile
"password": secretGitArgs.password, default:
return fmt.Errorf("git URL scheme '%s' not supported, can be: ssh, http and https", u.Scheme)
} }
if secretGitArgs.caFile != "" { secret, err := sourcesecret.Generate(opts)
ca, err := ioutil.ReadFile(secretGitArgs.caFile)
if err != nil { if err != nil {
return fmt.Errorf("failed to read CA file '%s': %w", secretGitArgs.caFile, err) return err
} }
secret.StringData["caFile"] = string(ca)
if createArgs.export {
fmt.Println(secret.Content)
return nil
} }
default: var s corev1.Secret
return fmt.Errorf("git URL scheme '%s' not supported, can be: ssh, http and https", u.Scheme) if err := yaml.Unmarshal([]byte(secret.Content), &s); err != nil {
return err
} }
if createArgs.export { if ppk, ok := s.StringData[sourcesecret.PublicKeySecretKey]; ok {
return exportSecret(secret) logger.Generatef("deploy key: %s", ppk)
} }
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
defer cancel()
kubeClient, err := utils.KubeClient(rootArgs.kubeconfig, rootArgs.kubecontext) kubeClient, err := utils.KubeClient(rootArgs.kubeconfig, rootArgs.kubecontext)
if err != nil { if err != nil {
return err return err
} }
if err := upsertSecret(ctx, kubeClient, s); err != nil {
if err := upsertSecret(ctx, kubeClient, secret); err != nil {
return err return err
} }
logger.Actionf("secret '%s' created in '%s' namespace", name, rootArgs.namespace) logger.Actionf("secret '%s' created in '%s' namespace", name, rootArgs.namespace)
return nil return nil
} }
func generateKeyPair(ctx context.Context, alg flags.PublicKeyAlgorithm, rsa flags.RSAKeyBits, ecdsa flags.ECDSACurve) (*ssh.KeyPair, error) {
var keyGen ssh.KeyPairGenerator
switch algorithm := alg.String(); algorithm {
case "rsa":
keyGen = ssh.NewRSAGenerator(int(rsa))
case "ecdsa":
keyGen = ssh.NewECDSAGenerator(ecdsa.Curve)
case "ed25519":
keyGen = ssh.NewEd25519Generator()
default:
return nil, fmt.Errorf("unsupported public key algorithm: %s", algorithm)
}
pair, err := keyGen.Generate()
if err != nil {
return nil, fmt.Errorf("key pair generation failed, error: %w", err)
}
return pair, nil
}
func scanHostKey(ctx context.Context, url *url.URL) ([]byte, error) {
host := url.Host
if url.Port() == "" {
host = host + ":22"
}
hostKey, err := ssh.ScanHostKey(host, 30*time.Second)
if err != nil {
return nil, fmt.Errorf("SSH key scan for host %s failed, error: %w", host, err)
}
return hostKey, nil
}

@ -21,8 +21,11 @@ import (
"fmt" "fmt"
"github.com/spf13/cobra" "github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/yaml"
"github.com/fluxcd/flux2/internal/utils" "github.com/fluxcd/flux2/internal/utils"
"github.com/fluxcd/flux2/pkg/manifestgen/sourcesecret"
) )
var createSecretHelmCmd = &cobra.Command{ var createSecretHelmCmd = &cobra.Command{
@ -30,8 +33,8 @@ var createSecretHelmCmd = &cobra.Command{
Short: "Create or update a Kubernetes secret for Helm repository authentication", Short: "Create or update a Kubernetes secret for Helm repository authentication",
Long: ` Long: `
The create secret helm command generates a Kubernetes secret with basic authentication credentials.`, The create secret helm command generates a Kubernetes secret with basic authentication credentials.`,
Example: ` # Create a Helm authentication secret on disk and encrypt it with Mozilla SOPS Example: `
# Create a Helm authentication secret on disk and encrypt it with Mozilla SOPS
flux create secret helm repo-auth \ flux create secret helm repo-auth \
--namespace=my-namespace \ --namespace=my-namespace \
--username=my-username \ --username=my-username \
@ -72,36 +75,45 @@ func createSecretHelmCmdRun(cmd *cobra.Command, args []string) error {
return fmt.Errorf("secret name is required") return fmt.Errorf("secret name is required")
} }
name := args[0] name := args[0]
secret, err := makeSecret(name)
labels, err := parseLabels()
if err != nil { if err != nil {
return err return err
} }
if secretHelmArgs.username != "" && secretHelmArgs.password != "" { opts := sourcesecret.Options{
secret.StringData["username"] = secretHelmArgs.username Name: name,
secret.StringData["password"] = secretHelmArgs.password Namespace: rootArgs.namespace,
Labels: labels,
Username: secretHelmArgs.username,
Password: secretHelmArgs.password,
CAFilePath: secretHelmArgs.caFile,
CertFilePath: secretHelmArgs.certFile,
KeyFilePath: secretHelmArgs.keyFile,
} }
secret, err := sourcesecret.Generate(opts)
if err = populateSecretTLS(&secret, secretHelmArgs.secretTLSFlags); err != nil { if err != nil {
return err return err
} }
if createArgs.export { if createArgs.export {
return exportSecret(secret) fmt.Println(secret.Content)
return nil
} }
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
defer cancel() defer cancel()
kubeClient, err := utils.KubeClient(rootArgs.kubeconfig, rootArgs.kubecontext) kubeClient, err := utils.KubeClient(rootArgs.kubeconfig, rootArgs.kubecontext)
if err != nil { if err != nil {
return err return err
} }
var s corev1.Secret
if err := upsertSecret(ctx, kubeClient, secret); err != nil { if err := yaml.Unmarshal([]byte(secret.Content), &s); err != nil {
return err
}
if err := upsertSecret(ctx, kubeClient, s); err != nil {
return err return err
} }
logger.Actionf("secret '%s' created in '%s' namespace", name, rootArgs.namespace)
return nil return nil
} }

@ -19,13 +19,14 @@ package main
import ( import (
"context" "context"
"fmt" "fmt"
"io/ioutil"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/yaml"
"github.com/fluxcd/flux2/internal/utils" "github.com/fluxcd/flux2/internal/utils"
"github.com/fluxcd/flux2/pkg/manifestgen/sourcesecret"
) )
var createSecretTLSCmd = &cobra.Command{ var createSecretTLSCmd = &cobra.Command{
@ -68,61 +69,48 @@ func init() {
createSecretCmd.AddCommand(createSecretTLSCmd) createSecretCmd.AddCommand(createSecretTLSCmd)
} }
func populateSecretTLS(secret *corev1.Secret, args secretTLSFlags) error {
if args.certFile != "" && args.keyFile != "" {
cert, err := ioutil.ReadFile(args.certFile)
if err != nil {
return fmt.Errorf("failed to read repository cert file '%s': %w", args.certFile, err)
}
secret.StringData["certFile"] = string(cert)
key, err := ioutil.ReadFile(args.keyFile)
if err != nil {
return fmt.Errorf("failed to read repository key file '%s': %w", args.keyFile, err)
}
secret.StringData["keyFile"] = string(key)
}
if args.caFile != "" {
ca, err := ioutil.ReadFile(args.caFile)
if err != nil {
return fmt.Errorf("failed to read repository CA file '%s': %w", args.caFile, err)
}
secret.StringData["caFile"] = string(ca)
}
return nil
}
func createSecretTLSCmdRun(cmd *cobra.Command, args []string) error { func createSecretTLSCmdRun(cmd *cobra.Command, args []string) error {
if len(args) < 1 { if len(args) < 1 {
return fmt.Errorf("secret name is required") return fmt.Errorf("secret name is required")
} }
name := args[0] name := args[0]
secret, err := makeSecret(name)
labels, err := parseLabels()
if err != nil { if err != nil {
return err return err
} }
if err = populateSecretTLS(&secret, secretTLSArgs); err != nil { opts := sourcesecret.Options{
Name: name,
Namespace: rootArgs.namespace,
Labels: labels,
CAFilePath: secretTLSArgs.caFile,
CertFilePath: secretTLSArgs.certFile,
KeyFilePath: secretTLSArgs.keyFile,
}
secret, err := sourcesecret.Generate(opts)
if err != nil {
return err return err
} }
if createArgs.export { if createArgs.export {
return exportSecret(secret) fmt.Println(secret.Content)
return nil
} }
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
defer cancel() defer cancel()
kubeClient, err := utils.KubeClient(rootArgs.kubeconfig, rootArgs.kubecontext) kubeClient, err := utils.KubeClient(rootArgs.kubeconfig, rootArgs.kubecontext)
if err != nil { if err != nil {
return err return err
} }
var s corev1.Secret
if err := upsertSecret(ctx, kubeClient, secret); err != nil { if err := yaml.Unmarshal([]byte(secret.Content), &s); err != nil {
return err
}
if err := upsertSecret(ctx, kubeClient, s); err != nil {
return err return err
} }
logger.Actionf("secret '%s' created in '%s' namespace", name, rootArgs.namespace)
return nil return nil
} }

@ -24,6 +24,8 @@ import (
"net/url" "net/url"
"os" "os"
"github.com/fluxcd/pkg/apis/meta"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta1"
"github.com/manifoldco/promptui" "github.com/manifoldco/promptui"
"github.com/spf13/cobra" "github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
@ -33,12 +35,11 @@ import (
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/yaml"
"github.com/fluxcd/pkg/apis/meta"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta1"
"github.com/fluxcd/flux2/internal/flags" "github.com/fluxcd/flux2/internal/flags"
"github.com/fluxcd/flux2/internal/utils" "github.com/fluxcd/flux2/internal/utils"
"github.com/fluxcd/flux2/pkg/manifestgen/sourcesecret"
) )
type sourceGitFlags struct { type sourceGitFlags struct {
@ -121,7 +122,6 @@ func init() {
func newSourceGitFlags() sourceGitFlags { func newSourceGitFlags() sourceGitFlags {
return sourceGitFlags{ return sourceGitFlags{
keyAlgorithm: "rsa",
keyRSABits: 2048, keyRSABits: 2048,
keyECDSACurve: flags.ECDSACurve{Curve: elliptic.P384()}, keyECDSACurve: flags.ECDSACurve{Curve: elliptic.P384()},
} }
@ -151,6 +151,9 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error {
if err != nil { if err != nil {
return fmt.Errorf("git URL parse failed: %w", err) return fmt.Errorf("git URL parse failed: %w", err)
} }
if u.Scheme != "ssh" && u.Scheme != "http" && u.Scheme != "https" {
return fmt.Errorf("git URL scheme '%s' not supported, can be: ssh, http and https", u.Scheme)
}
sourceLabels, err := parseLabels() sourceLabels, err := parseLabels()
if err != nil { if err != nil {
@ -184,12 +187,13 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error {
gitRepository.Spec.Reference.Branch = sourceGitArgs.branch gitRepository.Spec.Reference.Branch = sourceGitArgs.branch
} }
if createArgs.export {
if sourceGitArgs.secretRef != "" { if sourceGitArgs.secretRef != "" {
gitRepository.Spec.SecretRef = &meta.LocalObjectReference{ gitRepository.Spec.SecretRef = &meta.LocalObjectReference{
Name: sourceGitArgs.secretRef, Name: sourceGitArgs.secretRef,
} }
} }
if createArgs.export {
return exportGit(gitRepository) return exportGit(gitRepository)
} }
@ -201,18 +205,38 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error {
return err return err
} }
withAuth := false logger.Generatef("generating GitRepository source")
// TODO(hidde): move all auth prep to separate func? if sourceGitArgs.secretRef == "" {
if sourceGitArgs.secretRef != "" { secretOpts := sourcesecret.Options{
withAuth = true Name: name,
} else if u.Scheme == "ssh" { Namespace: rootArgs.namespace,
logger.Generatef("generating deploy key pair") ManifestFile: sourcesecret.MakeDefaultOptions().ManifestFile,
pair, err := generateKeyPair(ctx, sourceGitArgs.keyAlgorithm, sourceGitArgs.keyRSABits, sourceGitArgs.keyECDSACurve) }
switch u.Scheme {
case "ssh":
secretOpts.SSHHostname = u.Hostname()
secretOpts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(sourceGitArgs.keyAlgorithm)
secretOpts.RSAKeyBits = int(sourceGitArgs.keyRSABits)
secretOpts.ECDSACurve = sourceGitArgs.keyECDSACurve.Curve
case "https":
secretOpts.Username = sourceGitArgs.username
secretOpts.Password = sourceGitArgs.password
secretOpts.CAFilePath = sourceGitArgs.caFile
}
secret, err := sourcesecret.Generate(secretOpts)
if err != nil { if err != nil {
return err return err
} }
var s corev1.Secret
logger.Successf("deploy key: %s", pair.PublicKey) if err = yaml.Unmarshal([]byte(secret.Content), &s); err != nil {
return err
}
if len(s.StringData) > 0 {
if hk, ok := s.StringData[sourcesecret.KnownHostsSecretKey]; ok {
logger.Successf("collected public key from SSH server:\n%s", hk)
}
if ppk, ok := s.StringData[sourcesecret.PublicKeySecretKey]; ok {
logger.Generatef("deploy key: %s", ppk)
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",
IsConfirm: true, IsConfirm: true,
@ -220,73 +244,16 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error {
if _, err := prompt.Run(); err != nil { if _, err := prompt.Run(); err != nil {
return fmt.Errorf("aborting") return fmt.Errorf("aborting")
} }
logger.Actionf("collecting preferred public key from SSH server")
hostKey, err := scanHostKey(ctx, u)
if err != nil {
return err
} }
logger.Successf("collected public key from SSH server:\n%s", hostKey) logger.Actionf("applying secret with repository credentials")
if err := upsertSecret(ctx, kubeClient, s); err != nil {
logger.Actionf("applying secret with keys")
secret := corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: rootArgs.namespace,
Labels: sourceLabels,
},
StringData: map[string]string{
"identity": string(pair.PrivateKey),
"identity.pub": string(pair.PublicKey),
"known_hosts": string(hostKey),
},
}
if err := upsertSecret(ctx, kubeClient, secret); err != nil {
return err return err
} }
withAuth = true gitRepository.Spec.SecretRef = &meta.LocalObjectReference{
} else if sourceGitArgs.username != "" && sourceGitArgs.password != "" { Name: s.Name,
logger.Actionf("applying secret with basic auth credentials")
secret := corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: rootArgs.namespace,
Labels: sourceLabels,
},
StringData: map[string]string{
"username": sourceGitArgs.username,
"password": sourceGitArgs.password,
},
}
if sourceGitArgs.caFile != "" {
ca, err := ioutil.ReadFile(sourceGitArgs.caFile)
if err != nil {
return fmt.Errorf("failed to read CA file '%s': %w", sourceGitArgs.caFile, err)
}
secret.StringData["caFile"] = string(ca)
}
if err := upsertSecret(ctx, kubeClient, secret); err != nil {
return err
}
withAuth = true
} }
if withAuth {
logger.Successf("authentication configured") logger.Successf("authentication configured")
} }
logger.Generatef("generating GitRepository source")
if withAuth {
secretName := name
if sourceGitArgs.secretRef != "" {
secretName = sourceGitArgs.secretRef
}
gitRepository.Spec.SecretRef = &meta.LocalObjectReference{
Name: secretName,
}
} }
logger.Actionf("applying GitRepository source") logger.Actionf("applying GitRepository source")

@ -32,10 +32,12 @@ import (
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/yaml"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" sourcev1 "github.com/fluxcd/source-controller/api/v1beta1"
"github.com/fluxcd/flux2/internal/utils" "github.com/fluxcd/flux2/internal/utils"
"github.com/fluxcd/flux2/pkg/manifestgen/sourcesecret"
) )
var createSourceHelmCmd = &cobra.Command{ var createSourceHelmCmd = &cobra.Command{
@ -149,46 +151,27 @@ func createSourceHelmCmdRun(cmd *cobra.Command, args []string) error {
logger.Generatef("generating HelmRepository source") logger.Generatef("generating HelmRepository source")
if sourceHelmArgs.secretRef == "" { if sourceHelmArgs.secretRef == "" {
secretName := fmt.Sprintf("helm-%s", name) secretName := fmt.Sprintf("helm-%s", name)
secretOpts := sourcesecret.Options{
secret := corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName, Name: secretName,
Namespace: rootArgs.namespace, Namespace: rootArgs.namespace,
Labels: sourceLabels, Username: sourceHelmArgs.username,
}, Password: sourceHelmArgs.password,
StringData: map[string]string{}, CertFilePath: sourceHelmArgs.certFile,
} KeyFilePath: sourceHelmArgs.keyFile,
CAFilePath: sourceHelmArgs.caFile,
if sourceHelmArgs.username != "" && sourceHelmArgs.password != "" { ManifestFile: sourcesecret.MakeDefaultOptions().ManifestFile,
secret.StringData["username"] = sourceHelmArgs.username }
secret.StringData["password"] = sourceHelmArgs.password secret, err := sourcesecret.Generate(secretOpts)
}
if sourceHelmArgs.certFile != "" && sourceHelmArgs.keyFile != "" {
cert, err := ioutil.ReadFile(sourceHelmArgs.certFile)
if err != nil {
return fmt.Errorf("failed to read repository cert file '%s': %w", sourceHelmArgs.certFile, err)
}
secret.StringData["certFile"] = string(cert)
key, err := ioutil.ReadFile(sourceHelmArgs.keyFile)
if err != nil { if err != nil {
return fmt.Errorf("failed to read repository key file '%s': %w", sourceHelmArgs.keyFile, err) return err
}
secret.StringData["keyFile"] = string(key)
}
if sourceHelmArgs.caFile != "" {
ca, err := ioutil.ReadFile(sourceHelmArgs.caFile)
if err != nil {
return fmt.Errorf("failed to read repository CA file '%s': %w", sourceHelmArgs.caFile, err)
} }
secret.StringData["caFile"] = string(ca) var s corev1.Secret
if err = yaml.Unmarshal([]byte(secret.Content), &s); err != nil {
return err
} }
if len(s.StringData) > 0 {
if len(secret.StringData) > 0 {
logger.Actionf("applying secret with repository credentials") logger.Actionf("applying secret with repository credentials")
if err := upsertSecret(ctx, kubeClient, secret); err != nil { if err := upsertSecret(ctx, kubeClient, s); err != nil {
return err return err
} }
helmRepository.Spec.SecretRef = &meta.LocalObjectReference{ helmRepository.Spec.SecretRef = &meta.LocalObjectReference{

@ -14,8 +14,8 @@ flux create secret helm [name] [flags]
### Examples ### Examples
``` ```
# Create a Helm authentication secret on disk and encrypt it with Mozilla SOPS
# Create a Helm authentication secret on disk and encrypt it with Mozilla SOPS
flux create secret helm repo-auth \ flux create secret helm repo-auth \
--namespace=my-namespace \ --namespace=my-namespace \
--username=my-username \ --username=my-username \

@ -62,7 +62,7 @@ flux create source git [name] [flags]
-p, --password string basic authentication password -p, --password string basic authentication password
--secret-ref string the name of an existing secret containing SSH or basic credentials --secret-ref string the name of an existing secret containing SSH or basic credentials
--ssh-ecdsa-curve ecdsaCurve SSH ECDSA public key curve (p256, p384, p521) (default p384) --ssh-ecdsa-curve ecdsaCurve SSH ECDSA public key curve (p256, p384, p521) (default p384)
--ssh-key-algorithm publicKeyAlgorithm SSH public key algorithm (rsa, ecdsa, ed25519) (default rsa) --ssh-key-algorithm publicKeyAlgorithm SSH public key algorithm (rsa, ecdsa, ed25519)
--ssh-rsa-bits rsaKeyBits SSH RSA public key bit size (multiplies of 8) (default 2048) --ssh-rsa-bits rsaKeyBits SSH RSA public key bit size (multiplies of 8) (default 2048)
--tag string git tag --tag string git tag
--tag-semver string git tag semver range --tag-semver string git tag semver range

@ -22,6 +22,7 @@ require (
github.com/olekukonko/tablewriter v0.0.4 github.com/olekukonko/tablewriter v0.0.4
github.com/spf13/cobra v1.1.1 github.com/spf13/cobra v1.1.1
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0
k8s.io/api v0.20.2 k8s.io/api v0.20.2
k8s.io/apiextensions-apiserver v0.20.2 k8s.io/apiextensions-apiserver v0.20.2
k8s.io/apimachinery v0.20.2 k8s.io/apimachinery v0.20.2

@ -22,7 +22,6 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@ -30,6 +29,14 @@ import (
"strings" "strings"
"text/template" "text/template"
helmv2 "github.com/fluxcd/helm-controller/api/v2beta1"
imageautov1 "github.com/fluxcd/image-automation-controller/api/v1alpha1"
imagereflectv1 "github.com/fluxcd/image-reflector-controller/api/v1alpha1"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta1"
notificationv1 "github.com/fluxcd/notification-controller/api/v1beta1"
"github.com/fluxcd/pkg/runtime/dependency"
"github.com/fluxcd/pkg/version"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta1"
"github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter"
appsv1 "k8s.io/api/apps/v1" appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
@ -40,20 +47,6 @@ import (
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/kustomize/api/filesys"
"sigs.k8s.io/kustomize/api/k8sdeps/kunstruct"
"sigs.k8s.io/kustomize/api/konfig"
kustypes "sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/yaml"
helmv2 "github.com/fluxcd/helm-controller/api/v2beta1"
imageautov1 "github.com/fluxcd/image-automation-controller/api/v1alpha1"
imagereflectv1 "github.com/fluxcd/image-reflector-controller/api/v1alpha1"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta1"
notificationv1 "github.com/fluxcd/notification-controller/api/v1beta1"
"github.com/fluxcd/pkg/runtime/dependency"
"github.com/fluxcd/pkg/version"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta1"
"github.com/fluxcd/flux2/pkg/manifestgen/install" "github.com/fluxcd/flux2/pkg/manifestgen/install"
) )
@ -253,90 +246,6 @@ func MakeDependsOn(deps []string) []dependency.CrossNamespaceDependencyReference
return refs return refs
} }
// GenerateKustomizationYaml is the equivalent of running
// 'kustomize create --autodetect' in the specified dir
func GenerateKustomizationYaml(dirPath string) error {
fs := filesys.MakeFsOnDisk()
kfile := filepath.Join(dirPath, "kustomization.yaml")
scan := func(base string) ([]string, error) {
var paths []string
uf := kunstruct.NewKunstructuredFactoryImpl()
err := fs.Walk(base, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if path == base {
return nil
}
if info.IsDir() {
// If a sub-directory contains an existing kustomization file add the
// directory as a resource and do not decend into it.
for _, kfilename := range konfig.RecognizedKustomizationFileNames() {
if fs.Exists(filepath.Join(path, kfilename)) {
paths = append(paths, path)
return filepath.SkipDir
}
}
return nil
}
fContents, err := fs.ReadFile(path)
if err != nil {
return err
}
if _, err := uf.SliceFromBytes(fContents); err != nil {
return nil
}
paths = append(paths, path)
return nil
})
return paths, err
}
if _, err := os.Stat(kfile); err != nil {
abs, err := filepath.Abs(dirPath)
if err != nil {
return err
}
files, err := scan(abs)
if err != nil {
return err
}
f, err := fs.Create(kfile)
if err != nil {
return err
}
f.Close()
kus := kustypes.Kustomization{
TypeMeta: kustypes.TypeMeta{
APIVersion: kustypes.KustomizationVersion,
Kind: kustypes.KustomizationKind,
},
}
var resources []string
for _, file := range files {
relP, err := filepath.Rel(abs, file)
if err != nil {
return err
}
resources = append(resources, relP)
}
kus.Resources = resources
kd, err := yaml.Marshal(kus)
if err != nil {
return err
}
return ioutil.WriteFile(kfile, kd, os.ModePerm)
}
return nil
}
func PrintTable(writer io.Writer, header []string, rows [][]string) { func PrintTable(writer io.Writer, header []string, rows [][]string) {
table := tablewriter.NewWriter(writer) table := tablewriter.NewWriter(writer)
table.SetHeader(header) table.SetHeader(header)

@ -0,0 +1,123 @@
/*
Copyright 2021 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package kustomization
import (
"io/ioutil"
"os"
"path/filepath"
"sigs.k8s.io/kustomize/api/k8sdeps/kunstruct"
"sigs.k8s.io/kustomize/api/konfig"
kustypes "sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/yaml"
"github.com/fluxcd/flux2/pkg/manifestgen"
)
func Generate(options Options) (*manifestgen.Manifest, error) {
kfile := filepath.Join(options.TargetPath, konfig.DefaultKustomizationFileName())
abskfile := filepath.Join(options.BaseDir, kfile)
scan := func(base string) ([]string, error) {
var paths []string
uf := kunstruct.NewKunstructuredFactoryImpl()
err := options.FileSystem.Walk(base, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if path == base {
return nil
}
if info.IsDir() {
// If a sub-directory contains an existing Kustomization file add the
// directory as a resource and do not decent into it.
for _, kfilename := range konfig.RecognizedKustomizationFileNames() {
if options.FileSystem.Exists(filepath.Join(path, kfilename)) {
paths = append(paths, path)
return filepath.SkipDir
}
}
return nil
}
fContents, err := options.FileSystem.ReadFile(path)
if err != nil {
return err
}
if _, err := uf.SliceFromBytes(fContents); err != nil {
return nil
}
paths = append(paths, path)
return nil
})
return paths, err
}
if _, err := os.Stat(abskfile); err != nil {
abs, err := filepath.Abs(filepath.Dir(abskfile))
if err != nil {
return nil, err
}
files, err := scan(abs)
if err != nil {
return nil, err
}
f, err := options.FileSystem.Create(abskfile)
if err != nil {
return nil, err
}
f.Close()
kus := kustypes.Kustomization{
TypeMeta: kustypes.TypeMeta{
APIVersion: kustypes.KustomizationVersion,
Kind: kustypes.KustomizationKind,
},
}
var resources []string
for _, file := range files {
relP, err := filepath.Rel(abs, file)
if err != nil {
return nil, err
}
resources = append(resources, relP)
}
kus.Resources = resources
kd, err := yaml.Marshal(kus)
if err != nil {
return nil, err
}
return &manifestgen.Manifest{
Path: kfile,
Content: string(kd),
}, nil
}
kd, err := ioutil.ReadFile(abskfile)
if err != nil {
return nil, err
}
return &manifestgen.Manifest{
Path: kfile,
Content: string(kd),
}, nil
}

@ -0,0 +1,33 @@
/*
Copyright 2021 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package kustomization
import "sigs.k8s.io/kustomize/api/filesys"
type Options struct {
FileSystem filesys.FileSystem
BaseDir string
TargetPath string
}
func MakeDefaultOptions() Options {
return Options{
FileSystem: filesys.MakeFsOnDisk(),
BaseDir: "",
TargetPath: "",
}
}

@ -0,0 +1,74 @@
/*
Copyright 2021 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package sourcesecret
import (
"crypto/elliptic"
)
type PrivateKeyAlgorithm string
const (
RSAPrivateKeyAlgorithm PrivateKeyAlgorithm = "rsa"
ECDSAPrivateKeyAlgorithm PrivateKeyAlgorithm = "ecdsa"
Ed25519PrivateKeyAlgorithm PrivateKeyAlgorithm = "ed25519"
)
const (
UsernameSecretKey = "username"
PasswordSecretKey = "password"
CAFileSecretKey = "caFile"
CertFileSecretKey = "certFile"
KeyFileSecretKey = "keyFile"
PrivateKeySecretKey = "identity"
PublicKeySecretKey = "identity.pub"
KnownHostsSecretKey = "known_hosts"
)
type Options struct {
Name string
Namespace string
Labels map[string]string
SSHHostname string
PrivateKeyAlgorithm PrivateKeyAlgorithm
RSAKeyBits int
ECDSACurve elliptic.Curve
PrivateKeyPath string
Username string
Password string
CAFilePath string
CertFilePath string
KeyFilePath string
TargetPath string
ManifestFile string
}
func MakeDefaultOptions() Options {
return Options{
Name: "flux-system",
Namespace: "flux-system",
Labels: map[string]string{},
PrivateKeyAlgorithm: RSAPrivateKeyAlgorithm,
PrivateKeyPath: "",
Username: "",
Password: "",
CAFilePath: "",
CertFilePath: "",
KeyFilePath: "",
ManifestFile: "secret.yaml",
}
}

@ -0,0 +1,187 @@
/*
Copyright 2021 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package sourcesecret
import (
"bytes"
"encoding/pem"
"fmt"
"io/ioutil"
"net"
"path"
"time"
cryptssh "golang.org/x/crypto/ssh"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/yaml"
"github.com/fluxcd/pkg/ssh"
"github.com/fluxcd/flux2/pkg/manifestgen"
)
const defaultSSHPort = 22
func Generate(options Options) (*manifestgen.Manifest, error) {
var err error
var keypair *ssh.KeyPair
switch {
case options.Username != "", options.Password != "":
// noop
case len(options.PrivateKeyPath) > 0:
if keypair, err = loadKeyPair(options.PrivateKeyPath); err != nil {
return nil, err
}
case len(options.PrivateKeyAlgorithm) > 0:
if keypair, err = generateKeyPair(options); err != nil {
return nil, err
}
}
var hostKey []byte
if keypair != nil {
if hostKey, err = scanHostKey(options.SSHHostname); err != nil {
return nil, err
}
}
var caFile []byte
if options.CAFilePath != "" {
if caFile, err = ioutil.ReadFile(options.CAFilePath); err != nil {
return nil, fmt.Errorf("failed to read CA file: %w", err)
}
}
var certFile, keyFile []byte
if options.CertFilePath != "" && options.KeyFilePath != "" {
if certFile, err = ioutil.ReadFile(options.CertFilePath); err != nil {
return nil, fmt.Errorf("failed to read cert file: %w", err)
}
if keyFile, err = ioutil.ReadFile(options.KeyFilePath); err != nil {
return nil, fmt.Errorf("failed to read key file: %w", err)
}
}
secret := buildSecret(keypair, hostKey, caFile, certFile, keyFile, options)
b, err := yaml.Marshal(secret)
if err != nil {
return nil, err
}
return &manifestgen.Manifest{
Path: path.Join(options.TargetPath, options.Namespace, options.ManifestFile),
Content: fmt.Sprintf("---\n%s", resourceToString(b)),
}, nil
}
func buildSecret(keypair *ssh.KeyPair, hostKey, caFile, certFile, keyFile []byte, options Options) (secret corev1.Secret) {
secret.TypeMeta = metav1.TypeMeta{
APIVersion: "v1",
Kind: "Secret",
}
secret.ObjectMeta = metav1.ObjectMeta{
Name: options.Name,
Namespace: options.Namespace,
}
secret.Labels = options.Labels
secret.StringData = map[string]string{}
if options.Username != "" || options.Password != "" {
secret.StringData[UsernameSecretKey] = options.Username
secret.StringData[PasswordSecretKey] = options.Password
}
if caFile != nil {
secret.StringData[CAFileSecretKey] = string(caFile)
}
if certFile != nil && keyFile != nil {
secret.StringData[CertFileSecretKey] = string(certFile)
secret.StringData[KeyFileSecretKey] = string(keyFile)
}
if keypair != nil && hostKey != nil {
secret.StringData[PrivateKeySecretKey] = string(keypair.PrivateKey)
secret.StringData[PublicKeySecretKey] = string(keypair.PublicKey)
secret.StringData[KnownHostsSecretKey] = string(hostKey)
}
return
}
func loadKeyPair(path string) (*ssh.KeyPair, error) {
b, err := ioutil.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to open private key file: %w", err)
}
block, _ := pem.Decode(b)
if block == nil {
return nil, fmt.Errorf("failed to decode PEM block")
}
ppk, err := cryptssh.ParsePrivateKey(block.Bytes)
if err != nil {
return nil, err
}
return &ssh.KeyPair{
PublicKey: cryptssh.MarshalAuthorizedKey(ppk.PublicKey()),
PrivateKey: b,
}, nil
}
func generateKeyPair(options Options) (*ssh.KeyPair, error) {
var keyGen ssh.KeyPairGenerator
switch options.PrivateKeyAlgorithm {
case RSAPrivateKeyAlgorithm:
keyGen = ssh.NewRSAGenerator(options.RSAKeyBits)
case ECDSAPrivateKeyAlgorithm:
keyGen = ssh.NewECDSAGenerator(options.ECDSACurve)
case Ed25519PrivateKeyAlgorithm:
keyGen = ssh.NewEd25519Generator()
default:
return nil, fmt.Errorf("unsupported public key algorithm: %s", options.PrivateKeyAlgorithm)
}
pair, err := keyGen.Generate()
if err != nil {
return nil, fmt.Errorf("key pair generation failed, error: %w", err)
}
return pair, nil
}
func scanHostKey(host string) ([]byte, error) {
if _, _, err := net.SplitHostPort(host); err != nil {
// Assume we are dealing with a hostname without a port,
// append the default SSH port as this is required for
// host key scanning to work.
host = fmt.Sprintf("%s:%d", host, defaultSSHPort)
}
hostKey, err := ssh.ScanHostKey(host, 30*time.Second)
if err != nil {
return nil, fmt.Errorf("SSH key scan for host %s failed, error: %w", host, err)
}
return bytes.TrimSpace(hostKey), nil
}
func resourceToString(data []byte) string {
data = bytes.Replace(data, []byte(" creationTimestamp: null\n"), []byte(""), 1)
data = bytes.Replace(data, []byte("status: {}\n"), []byte(""), 1)
return string(data)
}

@ -26,6 +26,7 @@ type Options struct {
Name string Name string
Namespace string Namespace string
Branch string Branch string
Secret string
TargetPath string TargetPath string
ManifestFile string ManifestFile string
GitImplementation string GitImplementation string
@ -38,6 +39,7 @@ func MakeDefaultOptions() Options {
Name: "flux-system", Name: "flux-system",
Namespace: "flux-system", Namespace: "flux-system",
Branch: "main", Branch: "main",
Secret: "flux-system",
ManifestFile: "gotk-sync.yaml", ManifestFile: "gotk-sync.yaml",
TargetPath: "", TargetPath: "",
GitImplementation: "", GitImplementation: "",

@ -53,7 +53,7 @@ func Generate(options Options) (*manifestgen.Manifest, error) {
Branch: options.Branch, Branch: options.Branch,
}, },
SecretRef: &meta.LocalObjectReference{ SecretRef: &meta.LocalObjectReference{
Name: options.Name, Name: options.Secret,
}, },
GitImplementation: options.GitImplementation, GitImplementation: options.GitImplementation,
}, },

Loading…
Cancel
Save