mirror of https://github.com/fluxcd/flux2.git
Add `sourcesecret` and `kustomization` manifestgen
This includes a change to the `sync` generator to make the deploy secret name configurable. Signed-off-by: Hidde Beydals <hello@hidde.co>pull/1001/head
parent
ff2833c4d1
commit
8a5bba80bf
@ -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)
|
||||||
|
}
|
Loading…
Reference in New Issue