Merge "Implement genCAEx and genSignedCertEx with Subj support"
This commit is contained in:
commit
e77bac1571
365
pkg/document/plugin/templater/extlib/crypto.go
Normal file
365
pkg/document/plugin/templater/extlib/crypto.go
Normal file
@ -0,0 +1,365 @@
|
||||
/*
|
||||
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
|
||||
|
||||
https://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 extlib
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
"time"
|
||||
)
|
||||
|
||||
func toUint32(i int) uint32 { return uint32(i) }
|
||||
|
||||
type dnParser struct {
|
||||
in string
|
||||
i int
|
||||
cur bytes.Buffer
|
||||
state int
|
||||
dn []string
|
||||
}
|
||||
|
||||
func (p *dnParser) startOver() {
|
||||
p.dn = append(p.dn, p.cur.String())
|
||||
p.cur = bytes.Buffer{}
|
||||
p.state = 0
|
||||
}
|
||||
|
||||
func (p *dnParser) parseParam(r rune) error {
|
||||
switch r {
|
||||
case '\\':
|
||||
p.state = 2
|
||||
case '+', '/':
|
||||
return fmt.Errorf("string %s has separator '%c', but didn't have value on position %d", p.in, r, p.i+1)
|
||||
case '=':
|
||||
p.state = 1
|
||||
p.cur.WriteRune(r)
|
||||
case '"', ',', '<', '>', ';':
|
||||
return fmt.Errorf("string %s position %d: having %c without '\\'", p.in, p.i+1, r)
|
||||
default:
|
||||
p.cur.WriteRune(r)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *dnParser) parseValue(r rune) error {
|
||||
switch r {
|
||||
case '\\':
|
||||
p.state = 3
|
||||
case '+', '/':
|
||||
p.startOver()
|
||||
case '=':
|
||||
return fmt.Errorf("string %s has extra '=' on position %d", p.in, p.i+1)
|
||||
case '"', ',', '<', '>', ';':
|
||||
return fmt.Errorf("string %s position %d: having %c without '\\'", p.in, p.i+1, r)
|
||||
default:
|
||||
p.cur.WriteRune(r)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *dnParser) parseParamEscape(r rune) error {
|
||||
switch r {
|
||||
case '=', '+', '/', '"', ',', '<', '>', ';':
|
||||
p.cur.WriteRune(r)
|
||||
p.state = 0
|
||||
default:
|
||||
return fmt.Errorf("string %s pos %d: %c shouldn't follow after '\\'", p.in, p.i+1, r)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *dnParser) parseValueEscape(r rune) error {
|
||||
switch r {
|
||||
case '=', '+', '/', '"', ',', '<', '>', ';':
|
||||
p.cur.WriteRune(r)
|
||||
p.state = 1
|
||||
default:
|
||||
return fmt.Errorf("string %s pos %d: %c shouldn't follow after '\\'", p.in, p.i+1, r)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *dnParser) Parse(in string) error {
|
||||
p.cur = bytes.Buffer{}
|
||||
p.state = 0
|
||||
p.dn = nil
|
||||
|
||||
p.in = in
|
||||
var err error
|
||||
for p.i = 0; p.i < len(p.in); {
|
||||
r, size := utf8.DecodeRuneInString(p.in[p.i:])
|
||||
switch p.state {
|
||||
case 0: // initial state
|
||||
err = p.parseParam(r)
|
||||
case 1: // the same, but after =
|
||||
err = p.parseValue(r)
|
||||
case 2: // state inside \
|
||||
err = p.parseParamEscape(r)
|
||||
case 3: // state inside \ after =
|
||||
err = p.parseValueEscape(r)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.i += size
|
||||
}
|
||||
|
||||
if p.state != 1 {
|
||||
return fmt.Errorf("string %s terminates incorrectly", p.in)
|
||||
}
|
||||
p.startOver()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Converts RFC 2253 Distinguished Names syntax back to
|
||||
// Name similar to what openssl parse_name function [1]
|
||||
// does, except that if it doesn't have / as a first simbol
|
||||
// it assumes that the whole string is CN.
|
||||
// we don't support MultiRdn - + is treated the same way as /.
|
||||
// [1] https://github.com/openssl/openssl/blob/d8ab30be9cc4d4e77008d4037e696bc41ce293f8/apps/lib/apps.c#L1624
|
||||
func nameFromString(in string) (*pkix.Name, error) {
|
||||
if len(in) > 0 && in[0] != '/' {
|
||||
return &pkix.Name{
|
||||
CommonName: in,
|
||||
}, nil
|
||||
}
|
||||
|
||||
in = in[1:]
|
||||
if len(in) == 0 {
|
||||
return &pkix.Name{
|
||||
CommonName: in,
|
||||
}, nil
|
||||
}
|
||||
|
||||
p := &dnParser{}
|
||||
err := p.Parse(in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nameFromDn(p.dn)
|
||||
}
|
||||
|
||||
func nameFromDn(dn []string) (*pkix.Name, error) {
|
||||
name := pkix.Name{}
|
||||
|
||||
for _, v := range dn {
|
||||
sv := strings.Split(v, "=")
|
||||
if len(sv) != 2 {
|
||||
return nil, fmt.Errorf("%s must have a form key=value", v)
|
||||
}
|
||||
switch sv[0] {
|
||||
case "CN":
|
||||
if name.CommonName != "" {
|
||||
return nil, fmt.Errorf("CN is already set")
|
||||
}
|
||||
name.CommonName = sv[1]
|
||||
case "SERIALNUMBER":
|
||||
if name.SerialNumber != "" {
|
||||
return nil, fmt.Errorf("SERIALNUMBER is already set")
|
||||
}
|
||||
name.SerialNumber = sv[1]
|
||||
case "C":
|
||||
name.Country = append(name.Country, sv[1])
|
||||
case "O":
|
||||
name.Organization = append(name.Organization, sv[1])
|
||||
case "OU":
|
||||
name.OrganizationalUnit = append(name.OrganizationalUnit, sv[1])
|
||||
case "L":
|
||||
name.Locality = append(name.Locality, sv[1])
|
||||
case "ST":
|
||||
name.Province = append(name.Province, sv[1])
|
||||
case "STREET":
|
||||
name.StreetAddress = append(name.StreetAddress, sv[1])
|
||||
case "POSTALCODE":
|
||||
name.PostalCode = append(name.PostalCode, sv[1])
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported property %s", sv[0])
|
||||
}
|
||||
}
|
||||
|
||||
return &name, nil
|
||||
}
|
||||
|
||||
func generateCertificateAuthorityEx(
|
||||
subj string,
|
||||
daysValid int,
|
||||
) (certificate, error) {
|
||||
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return certificate{}, fmt.Errorf("error generating rsa key: %s", err)
|
||||
}
|
||||
|
||||
return generateCertificateAuthorityWithKeyInternalEx(subj, daysValid, priv)
|
||||
}
|
||||
|
||||
func generateCertificateAuthorityWithPEMKeyEx(
|
||||
subj string,
|
||||
daysValid int,
|
||||
privPEM string,
|
||||
) (certificate, error) {
|
||||
priv, err := parsePrivateKeyPEM(privPEM)
|
||||
if err != nil {
|
||||
return certificate{}, fmt.Errorf("parsing private key: %s", err)
|
||||
}
|
||||
return generateCertificateAuthorityWithKeyInternalEx(subj, daysValid, priv)
|
||||
}
|
||||
|
||||
func generateCertificateAuthorityWithKeyInternalEx(
|
||||
subj string,
|
||||
daysValid int,
|
||||
priv crypto.PrivateKey,
|
||||
) (certificate, error) {
|
||||
ca := certificate{}
|
||||
|
||||
template, err := getBaseCertTemplateEx(subj, nil, nil, daysValid)
|
||||
if err != nil {
|
||||
return ca, err
|
||||
}
|
||||
// Override KeyUsage and IsCA
|
||||
template.KeyUsage = x509.KeyUsageKeyEncipherment |
|
||||
x509.KeyUsageDigitalSignature |
|
||||
x509.KeyUsageCertSign
|
||||
template.IsCA = true
|
||||
|
||||
ca.Cert, ca.Key, err = getCertAndKey(template, priv, template, priv)
|
||||
|
||||
return ca, err
|
||||
}
|
||||
|
||||
func generateSignedCertificateEx(
|
||||
subj string,
|
||||
ips []interface{},
|
||||
alternateDNS []interface{},
|
||||
daysValid int,
|
||||
ca certificate,
|
||||
) (certificate, error) {
|
||||
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return certificate{}, fmt.Errorf("error generating rsa key: %s", err)
|
||||
}
|
||||
return generateSignedCertificateWithKeyInternalEx(subj, ips, alternateDNS, daysValid, ca, priv)
|
||||
}
|
||||
|
||||
func generateSignedCertificateWithPEMKeyEx(
|
||||
subj string,
|
||||
ips []interface{},
|
||||
alternateDNS []interface{},
|
||||
daysValid int,
|
||||
ca certificate,
|
||||
privPEM string,
|
||||
) (certificate, error) {
|
||||
priv, err := parsePrivateKeyPEM(privPEM)
|
||||
if err != nil {
|
||||
return certificate{}, fmt.Errorf("parsing private key: %s", err)
|
||||
}
|
||||
return generateSignedCertificateWithKeyInternalEx(subj, ips, alternateDNS, daysValid, ca, priv)
|
||||
}
|
||||
|
||||
func generateSignedCertificateWithKeyInternalEx(
|
||||
subj string,
|
||||
ips []interface{},
|
||||
alternateDNS []interface{},
|
||||
daysValid int,
|
||||
ca certificate,
|
||||
priv crypto.PrivateKey,
|
||||
) (certificate, error) {
|
||||
cert := certificate{}
|
||||
|
||||
decodedSignerCert, _ := pem.Decode([]byte(ca.Cert))
|
||||
if decodedSignerCert == nil {
|
||||
return cert, errors.New("unable to decode certificate")
|
||||
}
|
||||
signerCert, err := x509.ParseCertificate(decodedSignerCert.Bytes)
|
||||
if err != nil {
|
||||
return cert, fmt.Errorf(
|
||||
"error parsing certificate: decodedSignerCert.Bytes: %s",
|
||||
err,
|
||||
)
|
||||
}
|
||||
signerKey, err := parsePrivateKeyPEM(ca.Key)
|
||||
if err != nil {
|
||||
return cert, fmt.Errorf(
|
||||
"error parsing private key: %s",
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
template, err := getBaseCertTemplateEx(subj, ips, alternateDNS, daysValid)
|
||||
if err != nil {
|
||||
return cert, err
|
||||
}
|
||||
|
||||
cert.Cert, cert.Key, err = getCertAndKey(
|
||||
template,
|
||||
priv,
|
||||
signerCert,
|
||||
signerKey,
|
||||
)
|
||||
|
||||
return cert, err
|
||||
}
|
||||
|
||||
func getBaseCertTemplateEx(
|
||||
subj string,
|
||||
ips []interface{},
|
||||
alternateDNS []interface{},
|
||||
daysValid int,
|
||||
) (*x509.Certificate, error) {
|
||||
ipAddresses, err := getNetIPs(ips)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dnsNames, err := getAlternateDNSStrs(alternateDNS)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
serialNumberUpperBound := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||
serialNumber, err := rand.Int(rand.Reader, serialNumberUpperBound)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
name, err := nameFromString(subj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &x509.Certificate{
|
||||
SerialNumber: serialNumber,
|
||||
Subject: *name,
|
||||
IPAddresses: ipAddresses,
|
||||
DNSNames: dnsNames,
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(time.Hour * 24 * time.Duration(daysValid)),
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{
|
||||
x509.ExtKeyUsageServerAuth,
|
||||
x509.ExtKeyUsageClientAuth,
|
||||
},
|
||||
BasicConstraintsValid: true,
|
||||
}, nil
|
||||
}
|
170
pkg/document/plugin/templater/extlib/crypto_test.go
Normal file
170
pkg/document/plugin/templater/extlib/crypto_test.go
Normal file
@ -0,0 +1,170 @@
|
||||
/*
|
||||
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
|
||||
|
||||
https://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 extlib
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"crypto/x509/pkix"
|
||||
)
|
||||
|
||||
func TestToUint32(t *testing.T) {
|
||||
assert.Equal(t, uint32(1), toUint32(1))
|
||||
assert.Equal(t, uint32(0xffffffff), toUint32(-1))
|
||||
}
|
||||
|
||||
func TestNameFromString(t *testing.T) {
|
||||
testCases := []struct {
|
||||
in string
|
||||
expectedOut pkix.Name
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
in: `Kubernetes API`,
|
||||
expectedOut: pkix.Name{
|
||||
CommonName: `Kubernetes API`,
|
||||
},
|
||||
},
|
||||
{
|
||||
in: `/CN=Kubernetes API`,
|
||||
expectedOut: pkix.Name{
|
||||
CommonName: `Kubernetes API`,
|
||||
},
|
||||
},
|
||||
{
|
||||
in: `/CN=James \"Jim\" Smith\, III+O=example`,
|
||||
expectedOut: pkix.Name{
|
||||
CommonName: `James "Jim" Smith, III`,
|
||||
Organization: []string{
|
||||
`example`,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
in: `/CN=admin/O=system:masters`,
|
||||
expectedOut: pkix.Name{
|
||||
CommonName: `admin`,
|
||||
Organization: []string{
|
||||
`system:masters`,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
in: `/C=AU/ST=Some-State/O=Internet Widgits Pty Ltd/CN=leaf`,
|
||||
expectedOut: pkix.Name{
|
||||
CommonName: `leaf`,
|
||||
Country: []string{
|
||||
`AU`,
|
||||
},
|
||||
Province: []string{
|
||||
`Some-State`,
|
||||
},
|
||||
Organization: []string{
|
||||
`Internet Widgits Pty Ltd`,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
in: `/C=AU/ST=QLD/CN=SSLeay\/rsa test cert`,
|
||||
expectedOut: pkix.Name{
|
||||
CommonName: `SSLeay/rsa test cert`,
|
||||
Country: []string{
|
||||
`AU`,
|
||||
},
|
||||
Province: []string{
|
||||
`QLD`,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
in: `/CN=CN/SERIALNUMBER=SN` +
|
||||
`/C=C1/C=C2` +
|
||||
`/O=O1/O=O2` +
|
||||
`/OU=OU1/OU=OU2` +
|
||||
`/L=L1/L=L2` +
|
||||
`/ST=ST1/ST=ST2` +
|
||||
`/STREET=S1/STREET=S2` +
|
||||
`/POSTALCODE=PC1/POSTALCODE=PC2`,
|
||||
expectedOut: pkix.Name{
|
||||
CommonName: `CN`,
|
||||
SerialNumber: `SN`,
|
||||
Country: []string{`C1`, `C2`},
|
||||
Organization: []string{`O1`, `O2`},
|
||||
OrganizationalUnit: []string{`OU1`, `OU2`},
|
||||
Locality: []string{`L1`, `L2`},
|
||||
Province: []string{`ST1`, `ST2`},
|
||||
StreetAddress: []string{`S1`, `S2`},
|
||||
PostalCode: []string{`PC1`, `PC2`},
|
||||
},
|
||||
},
|
||||
{
|
||||
in: `/C=AU/ST=QLD/CN=SSLeay\/rsa test cert\`,
|
||||
expectedErr: `string C=AU/ST=QLD/CN=SSLeay\/rsa test cert\ terminates incorrectly`,
|
||||
},
|
||||
{
|
||||
in: `/C=A\U/ST=QLD/CN=SSLeay\/rsa test cert`,
|
||||
expectedErr: `string C=A\U/ST=QLD/CN=SSLeay\/rsa test cert pos 5: U shouldn't follow after '\'`,
|
||||
},
|
||||
{
|
||||
in: `/C\N=AU/ST=QLD/CN=SSLeay\/rsa test cert`,
|
||||
expectedErr: `string C\N=AU/ST=QLD/CN=SSLeay\/rsa test cert pos 3: N shouldn't follow after '\'`,
|
||||
},
|
||||
{
|
||||
in: `/CN=AU/ST=QLD/CN=SSLeay\/rsa <>",test cert`,
|
||||
expectedErr: `string CN=AU/ST=QLD/CN=SSLeay\/rsa <>",test cert position 29: having < without '\'`,
|
||||
},
|
||||
{
|
||||
in: `/CN=AU=AU/ST=QLD/CN=SSLeay\/rsa test cert`,
|
||||
expectedErr: `string CN=AU=AU/ST=QLD/CN=SSLeay\/rsa test cert has extra '=' on position 6`,
|
||||
},
|
||||
{
|
||||
in: `/CN=AU/ST=QLD/CN<>",t=SSLeay\/rsa test cert`,
|
||||
expectedErr: `string CN=AU/ST=QLD/CN<>",t=SSLeay\/rsa test cert position 16: having < without '\'`,
|
||||
},
|
||||
{
|
||||
in: `/CN=AU/ST/CN=SSLeay\/rsa <>",test cert`,
|
||||
expectedErr: `string CN=AU/ST/CN=SSLeay\/rsa <>",test cert has separator '/', but didn't have value on position 9`,
|
||||
},
|
||||
{
|
||||
in: `/CN=AU/CN\<=SSLeay test cert`,
|
||||
expectedErr: `unsupported property CN<`,
|
||||
},
|
||||
{
|
||||
in: `/CN=/SP=xxx/CN=SSLeay\/rsa test cert`,
|
||||
expectedErr: `unsupported property SP`,
|
||||
},
|
||||
{
|
||||
in: `/CN=1/CN=SSLeay\/rsa test cert`,
|
||||
expectedErr: `CN is already set`,
|
||||
},
|
||||
{
|
||||
in: `/CN=1/SERIALNUMBER=1/SERIALNUMBER=2`,
|
||||
expectedErr: `SERIALNUMBER is already set`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
r, err := nameFromString(tc.in)
|
||||
if tc.expectedErr != "" {
|
||||
assert.EqualError(t, err, tc.expectedErr)
|
||||
continue
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.expectedOut, *r)
|
||||
}
|
||||
}
|
24
pkg/document/plugin/templater/extlib/fs.go
Normal file
24
pkg/document/plugin/templater/extlib/fs.go
Normal file
@ -0,0 +1,24 @@
|
||||
/*
|
||||
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
|
||||
|
||||
https://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 extlib
|
||||
|
||||
import (
|
||||
"opendev.org/airship/airshipctl/pkg/fs"
|
||||
)
|
||||
|
||||
func fileExists(path string) bool {
|
||||
docfs := fs.NewDocumentFs()
|
||||
return docfs.Exists(path)
|
||||
}
|
26
pkg/document/plugin/templater/extlib/fs_test.go
Normal file
26
pkg/document/plugin/templater/extlib/fs_test.go
Normal file
@ -0,0 +1,26 @@
|
||||
/*
|
||||
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
|
||||
|
||||
https://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 extlib
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFileExists(t *testing.T) {
|
||||
assert.Equal(t, true, fileExists("fs_test.go"))
|
||||
assert.Equal(t, false, fileExists("fs_test_nonexistent.go"))
|
||||
}
|
39
pkg/document/plugin/templater/extlib/funcmap.go
Normal file
39
pkg/document/plugin/templater/extlib/funcmap.go
Normal file
@ -0,0 +1,39 @@
|
||||
/*
|
||||
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
|
||||
|
||||
https://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 extlib
|
||||
|
||||
import (
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// GenericFuncMap returns a copy of the function map
|
||||
func GenericFuncMap() template.FuncMap {
|
||||
gfm := make(template.FuncMap, len(genericMap))
|
||||
for k, v := range genericMap {
|
||||
gfm[k] = v
|
||||
}
|
||||
return gfm
|
||||
}
|
||||
|
||||
var genericMap = map[string]interface{}{
|
||||
"genCAEx": generateCertificateAuthorityEx,
|
||||
"genCAWithKeyEx": generateCertificateAuthorityWithPEMKeyEx,
|
||||
"genSignedCertEx": generateSignedCertificateEx,
|
||||
"genSignedCertWithKeyEx": generateSignedCertificateWithPEMKeyEx,
|
||||
"fileExists": fileExists,
|
||||
"regexGen": regexGen,
|
||||
"toYaml": toYaml,
|
||||
"toUint32": toUint32,
|
||||
}
|
31
pkg/document/plugin/templater/extlib/regexgen.go
Normal file
31
pkg/document/plugin/templater/extlib/regexgen.go
Normal file
@ -0,0 +1,31 @@
|
||||
/*
|
||||
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
|
||||
|
||||
https://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 extlib
|
||||
|
||||
import (
|
||||
"github.com/lucasjones/reggen"
|
||||
)
|
||||
|
||||
// Generate Regex
|
||||
func regexGen(regex string, limit int) string {
|
||||
if limit <= 0 {
|
||||
panic("Limit cannot be less than or equal to 0")
|
||||
}
|
||||
str, err := reggen.Generate(regex, limit)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return str
|
||||
}
|
57
pkg/document/plugin/templater/extlib/regexgen_test.go
Normal file
57
pkg/document/plugin/templater/extlib/regexgen_test.go
Normal file
@ -0,0 +1,57 @@
|
||||
/*
|
||||
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
|
||||
|
||||
https://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 extlib
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRegexGen(t *testing.T) {
|
||||
tpl := `{{- $regex := "^[a-z]{5,10}$" }}
|
||||
{{- $nomatchregex := "^[a-z]{0,4}$" }}
|
||||
true={{- regexMatch $regex (regexGen $regex 10) }},
|
||||
false={{- regexMatch $nomatchregex (regexGen $regex 10) }}
|
||||
`
|
||||
out, err := runRaw(tpl, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.Equal(t, `
|
||||
true=true,
|
||||
false=false
|
||||
`, out)
|
||||
}
|
||||
|
||||
func TestRegexPanicOnPattern(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Errorf("The code did not panic")
|
||||
}
|
||||
}()
|
||||
|
||||
regexGen("[a-z", 1)
|
||||
}
|
||||
|
||||
func TestRegexPanicOnLimit(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Errorf("The code did not panic")
|
||||
}
|
||||
}()
|
||||
|
||||
regexGen("[a-z]{0,4}", 0)
|
||||
}
|
220
pkg/document/plugin/templater/extlib/sprig_crypto.go
Normal file
220
pkg/document/plugin/templater/extlib/sprig_crypto.go
Normal file
@ -0,0 +1,220 @@
|
||||
/*
|
||||
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
|
||||
|
||||
https://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 extlib
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"crypto"
|
||||
"crypto/dsa"
|
||||
"crypto/ecdsa"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
"net"
|
||||
)
|
||||
|
||||
// that pieces of code were copied from
|
||||
// https://github.com/Masterminds/sprig/blob/868e7517d046cb7540e10345b09c0d70da584c8e/crypto.go#L405
|
||||
type certificate struct {
|
||||
Cert string
|
||||
Key string
|
||||
}
|
||||
|
||||
// DSAKeyFormat stores the format for DSA keys.
|
||||
// Used by pemBlockForKey
|
||||
type DSAKeyFormat struct {
|
||||
Version int
|
||||
P, Q, G, Y, X *big.Int
|
||||
}
|
||||
|
||||
func getCertAndKey(
|
||||
template *x509.Certificate,
|
||||
signeeKey crypto.PrivateKey,
|
||||
parent *x509.Certificate,
|
||||
signingKey crypto.PrivateKey,
|
||||
) (string, string, error) {
|
||||
signeePubKey, err := getPublicKey(signeeKey)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("error retrieving public key from signee key: %s", err)
|
||||
}
|
||||
derBytes, err := x509.CreateCertificate(
|
||||
rand.Reader,
|
||||
template,
|
||||
parent,
|
||||
signeePubKey,
|
||||
signingKey,
|
||||
)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("error creating certificate: %s", err)
|
||||
}
|
||||
|
||||
certBuffer := bytes.Buffer{}
|
||||
if err := pem.Encode(
|
||||
&certBuffer,
|
||||
&pem.Block{Type: "CERTIFICATE", Bytes: derBytes},
|
||||
); err != nil {
|
||||
return "", "", fmt.Errorf("error pem-encoding certificate: %s", err)
|
||||
}
|
||||
|
||||
keyBuffer := bytes.Buffer{}
|
||||
if err := pem.Encode(
|
||||
&keyBuffer,
|
||||
pemBlockForKey(signeeKey),
|
||||
); err != nil {
|
||||
return "", "", fmt.Errorf("error pem-encoding key: %s", err)
|
||||
}
|
||||
|
||||
return certBuffer.String(), keyBuffer.String(), nil
|
||||
}
|
||||
|
||||
func pemBlockForKey(priv interface{}) *pem.Block {
|
||||
switch k := priv.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
return &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k)}
|
||||
case *dsa.PrivateKey:
|
||||
val := DSAKeyFormat{
|
||||
P: k.P, Q: k.Q, G: k.G,
|
||||
Y: k.Y, X: k.X,
|
||||
}
|
||||
bytes, err := asn1.Marshal(val)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return &pem.Block{Type: "DSA PRIVATE KEY", Bytes: bytes}
|
||||
case *ecdsa.PrivateKey:
|
||||
b, err := x509.MarshalECPrivateKey(k)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return &pem.Block{Type: "EC PRIVATE KEY", Bytes: b}
|
||||
default:
|
||||
// attempt PKCS#8 format for all other keys
|
||||
b, err := x509.MarshalPKCS8PrivateKey(k)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return &pem.Block{Type: "PRIVATE KEY", Bytes: b}
|
||||
}
|
||||
}
|
||||
|
||||
func parsePrivateKeyPEM(pemBlock string) (crypto.PrivateKey, error) {
|
||||
block, _ := pem.Decode([]byte(pemBlock))
|
||||
if block == nil {
|
||||
return nil, errors.New("no PEM data in input")
|
||||
}
|
||||
|
||||
if block.Type == "PRIVATE KEY" {
|
||||
priv, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decoding PEM as PKCS#8: %s", err)
|
||||
}
|
||||
return priv, nil
|
||||
} else if !strings.HasSuffix(block.Type, " PRIVATE KEY") {
|
||||
return nil, fmt.Errorf("no private key data in PEM block of type %s", block.Type)
|
||||
}
|
||||
|
||||
switch block.Type[:len(block.Type)-12] { // strip " PRIVATE KEY"
|
||||
case "RSA":
|
||||
priv, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing RSA private key from PEM: %s", err)
|
||||
}
|
||||
return priv, nil
|
||||
case "EC":
|
||||
priv, err := x509.ParseECPrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing EC private key from PEM: %s", err)
|
||||
}
|
||||
return priv, nil
|
||||
case "DSA":
|
||||
var k DSAKeyFormat
|
||||
_, err := asn1.Unmarshal(block.Bytes, &k)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing DSA private key from PEM: %s", err)
|
||||
}
|
||||
priv := &dsa.PrivateKey{
|
||||
PublicKey: dsa.PublicKey{
|
||||
Parameters: dsa.Parameters{
|
||||
P: k.P, Q: k.Q, G: k.G,
|
||||
},
|
||||
Y: k.Y,
|
||||
},
|
||||
X: k.X,
|
||||
}
|
||||
return priv, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid private key type %s", block.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func getPublicKey(priv crypto.PrivateKey) (crypto.PublicKey, error) {
|
||||
switch k := priv.(type) {
|
||||
case interface{ Public() crypto.PublicKey }:
|
||||
return k.Public(), nil
|
||||
case *dsa.PrivateKey:
|
||||
return &k.PublicKey, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unable to get public key for type %T", priv)
|
||||
}
|
||||
}
|
||||
|
||||
func getNetIPs(ips []interface{}) ([]net.IP, error) {
|
||||
if ips == nil {
|
||||
return []net.IP{}, nil
|
||||
}
|
||||
var ipStr string
|
||||
var ok bool
|
||||
var netIP net.IP
|
||||
netIPs := make([]net.IP, len(ips))
|
||||
for i, ip := range ips {
|
||||
ipStr, ok = ip.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("error parsing ip: %v is not a string", ip)
|
||||
}
|
||||
netIP = net.ParseIP(ipStr)
|
||||
if netIP == nil {
|
||||
return nil, fmt.Errorf("error parsing ip: %s", ipStr)
|
||||
}
|
||||
netIPs[i] = netIP
|
||||
}
|
||||
return netIPs, nil
|
||||
}
|
||||
|
||||
func getAlternateDNSStrs(alternateDNS []interface{}) ([]string, error) {
|
||||
if alternateDNS == nil {
|
||||
return []string{}, nil
|
||||
}
|
||||
var dnsStr string
|
||||
var ok bool
|
||||
alternateDNSStrs := make([]string, len(alternateDNS))
|
||||
for i, dns := range alternateDNS {
|
||||
dnsStr, ok = dns.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(
|
||||
"error processing alternate dns name: %v is not a string",
|
||||
dns,
|
||||
)
|
||||
}
|
||||
alternateDNSStrs[i] = dnsStr
|
||||
}
|
||||
return alternateDNSStrs, nil
|
||||
}
|
168
pkg/document/plugin/templater/extlib/sprig_crypto_test.go
Normal file
168
pkg/document/plugin/templater/extlib/sprig_crypto_test.go
Normal file
@ -0,0 +1,168 @@
|
||||
/*
|
||||
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
|
||||
|
||||
https://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 extlib
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"testing"
|
||||
"text/template"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
sprig "github.com/Masterminds/sprig/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
beginCertificate = "-----BEGIN CERTIFICATE-----"
|
||||
endCertificate = "-----END CERTIFICATE-----"
|
||||
)
|
||||
|
||||
var (
|
||||
// fastCertKeyAlgos is the list of private key algorithms that are supported for certificate use, and
|
||||
// are fast to generate.
|
||||
fastCertKeyAlgos = []string{
|
||||
"ecdsa",
|
||||
"ed25519",
|
||||
}
|
||||
)
|
||||
|
||||
// copy needed tests from https://github.com/Masterminds/sprig/blob/master/crypto_test.go
|
||||
func testGenCAEx(t *testing.T, keyAlgo *string, subj, expCN string) {
|
||||
var genCAExpr string
|
||||
if keyAlgo == nil {
|
||||
genCAExpr = "genCAEx"
|
||||
} else {
|
||||
genCAExpr = fmt.Sprintf(`genPrivateKey "%s" | genCAWithKeyEx`, *keyAlgo)
|
||||
}
|
||||
|
||||
tpl := fmt.Sprintf(
|
||||
`{{- $ca := %s "%s" 365 }}
|
||||
{{ $ca.Cert }}
|
||||
`,
|
||||
genCAExpr,
|
||||
subj,
|
||||
)
|
||||
out, err := runRaw(tpl, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.Contains(t, out, beginCertificate)
|
||||
assert.Contains(t, out, endCertificate)
|
||||
|
||||
decodedCert, _ := pem.Decode([]byte(out))
|
||||
assert.Nil(t, err)
|
||||
cert, err := x509.ParseCertificate(decodedCert.Bytes)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, expCN, cert.Subject.CommonName)
|
||||
assert.True(t, cert.IsCA)
|
||||
}
|
||||
|
||||
func TestGenCAEx(t *testing.T) {
|
||||
testGenCAEx(t, nil, "foo ca", "foo ca")
|
||||
testGenCAEx(t, nil, "/CN=bar ca", "bar ca")
|
||||
for i, keyAlgo := range fastCertKeyAlgos {
|
||||
t.Run(keyAlgo, func(t *testing.T) {
|
||||
testGenCAEx(t, &fastCertKeyAlgos[i], "foo ca", "foo ca")
|
||||
testGenCAEx(t, &fastCertKeyAlgos[i], "/CN=bar ca", "bar ca")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testGenSignedCertEx(t *testing.T, caKeyAlgo, certKeyAlgo *string, subj, expCn string) {
|
||||
const (
|
||||
ip1 = "10.0.0.1"
|
||||
ip2 = "10.0.0.2"
|
||||
dns1 = "bar.com"
|
||||
dns2 = "bat.com"
|
||||
)
|
||||
|
||||
var genCAExpr, genSignedCertExpr string
|
||||
if caKeyAlgo == nil {
|
||||
genCAExpr = "genCAEx"
|
||||
} else {
|
||||
genCAExpr = fmt.Sprintf(`genPrivateKey "%s" | genCAWithKeyEx`, *caKeyAlgo)
|
||||
}
|
||||
if certKeyAlgo == nil {
|
||||
genSignedCertExpr = "genSignedCertEx"
|
||||
} else {
|
||||
genSignedCertExpr = fmt.Sprintf(`genPrivateKey "%s" | genSignedCertWithKeyEx`, *certKeyAlgo)
|
||||
}
|
||||
|
||||
tpl := fmt.Sprintf(
|
||||
`{{- $ca := %s "foo" 3650 }}
|
||||
{{- $cert := %s "%s" (list "%s" "%s") (list "%s" "%s") 365 $ca }}
|
||||
{{ $cert.Cert }}`,
|
||||
genCAExpr,
|
||||
genSignedCertExpr,
|
||||
subj,
|
||||
ip1,
|
||||
ip2,
|
||||
dns1,
|
||||
dns2,
|
||||
)
|
||||
|
||||
out, err := runRaw(tpl, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.Contains(t, out, beginCertificate)
|
||||
assert.Contains(t, out, endCertificate)
|
||||
|
||||
decodedCert, _ := pem.Decode([]byte(out))
|
||||
assert.Nil(t, err)
|
||||
cert, err := x509.ParseCertificate(decodedCert.Bytes)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, expCn, cert.Subject.CommonName)
|
||||
assert.Equal(t, 1, cert.SerialNumber.Sign())
|
||||
assert.Equal(t, 2, len(cert.IPAddresses))
|
||||
assert.Equal(t, ip1, cert.IPAddresses[0].String())
|
||||
assert.Equal(t, ip2, cert.IPAddresses[1].String())
|
||||
assert.Contains(t, cert.DNSNames, dns1)
|
||||
assert.Contains(t, cert.DNSNames, dns2)
|
||||
assert.False(t, cert.IsCA)
|
||||
}
|
||||
|
||||
func TestGenSignedCertEx(t *testing.T) {
|
||||
testGenSignedCertEx(t, nil, nil, "foo ca", "foo ca")
|
||||
testGenSignedCertEx(t, nil, nil, "/CN=bar ca", "bar ca")
|
||||
for i, caKeyAlgo := range fastCertKeyAlgos {
|
||||
for j, certKeyAlgo := range fastCertKeyAlgos {
|
||||
t.Run(fmt.Sprintf("%s-%s", caKeyAlgo, certKeyAlgo), func(t *testing.T) {
|
||||
testGenSignedCertEx(t, &fastCertKeyAlgos[i], &fastCertKeyAlgos[j], "foo ca", "foo ca")
|
||||
testGenSignedCertEx(t, &fastCertKeyAlgos[i], &fastCertKeyAlgos[j], "/CN=bar ca", "bar ca")
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// runRaw runs a template with the given variables and returns the result.
|
||||
func runRaw(tpl string, vars interface{}) (string, error) {
|
||||
funcMap := sprig.TxtFuncMap()
|
||||
for i, v := range GenericFuncMap() {
|
||||
funcMap[i] = v
|
||||
}
|
||||
t := template.Must(template.New("test").Funcs(funcMap).Parse(tpl))
|
||||
var b bytes.Buffer
|
||||
err := t.Execute(&b, vars)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return b.String(), nil
|
||||
}
|
32
pkg/document/plugin/templater/extlib/yaml.go
Normal file
32
pkg/document/plugin/templater/extlib/yaml.go
Normal file
@ -0,0 +1,32 @@
|
||||
/*
|
||||
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
|
||||
|
||||
https://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 extlib
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
// Render input yaml as output yaml
|
||||
// This function is from the Helm project:
|
||||
// https://github.com/helm/helm
|
||||
// Copyright The Helm Authors
|
||||
func toYaml(v interface{}) string {
|
||||
data, err := yaml.Marshal(v)
|
||||
if err != nil {
|
||||
// Swallow errors inside of a template.
|
||||
return ""
|
||||
}
|
||||
return string(data)
|
||||
}
|
44
pkg/document/plugin/templater/extlib/yaml_test.go
Normal file
44
pkg/document/plugin/templater/extlib/yaml_test.go
Normal file
@ -0,0 +1,44 @@
|
||||
/*
|
||||
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
|
||||
|
||||
https://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 extlib
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestToYaml(t *testing.T) {
|
||||
data := []struct {
|
||||
A string
|
||||
B int
|
||||
}{
|
||||
{
|
||||
A: "test1",
|
||||
B: 1,
|
||||
},
|
||||
{
|
||||
A: "test2",
|
||||
B: 2,
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, `
|
||||
- a: test1
|
||||
b: 1
|
||||
- a: test2
|
||||
b: 2
|
||||
`[1:], toYaml(&data))
|
||||
}
|
@ -19,15 +19,15 @@ import (
|
||||
"fmt"
|
||||
"text/template"
|
||||
|
||||
sprig "github.com/Masterminds/sprig/v3"
|
||||
"github.com/lucasjones/reggen"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"sigs.k8s.io/kustomize/kyaml/kio"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
|
||||
airshipv1 "opendev.org/airship/airshipctl/pkg/api/v1alpha1"
|
||||
"opendev.org/airship/airshipctl/pkg/fs"
|
||||
|
||||
sprig "github.com/Masterminds/sprig/v3"
|
||||
|
||||
extlib "opendev.org/airship/airshipctl/pkg/document/plugin/templater/extlib"
|
||||
)
|
||||
|
||||
var _ kio.Filter = &plugin{}
|
||||
@ -48,14 +48,24 @@ func New(obj map[string]interface{}) (kio.Filter, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func funcMapAppend(fma, fmb template.FuncMap) template.FuncMap {
|
||||
for k, v := range fmb {
|
||||
_, ok := fma[k]
|
||||
if ok {
|
||||
panic(fmt.Errorf("Trying to redefine function %s that already exists", k))
|
||||
}
|
||||
fma[k] = v
|
||||
}
|
||||
return fma
|
||||
}
|
||||
|
||||
func (t *plugin) Filter(items []*yaml.RNode) ([]*yaml.RNode, error) {
|
||||
docfs := fs.NewDocumentFs()
|
||||
out := &bytes.Buffer{}
|
||||
funcMap := sprig.TxtFuncMap()
|
||||
funcMap["toUint32"] = func(i int) uint32 { return uint32(i) }
|
||||
funcMap["toYaml"] = toYaml
|
||||
funcMap["regexGen"] = regexGen
|
||||
funcMap["fileExists"] = docfs.Exists
|
||||
|
||||
funcMap := template.FuncMap{}
|
||||
funcMap = funcMapAppend(funcMap, sprig.TxtFuncMap())
|
||||
funcMap = funcMapAppend(funcMap, extlib.GenericFuncMap())
|
||||
|
||||
tmpl, err := template.New("tmpl").Funcs(funcMap).Parse(t.Template)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -79,28 +89,3 @@ func (t *plugin) Filter(items []*yaml.RNode) ([]*yaml.RNode, error) {
|
||||
}
|
||||
return append(items, res.Nodes...), nil
|
||||
}
|
||||
|
||||
// Generate Regex
|
||||
func regexGen(regex string, limit int) string {
|
||||
if limit <= 0 {
|
||||
panic("Limit cannot be less than or equal to 0")
|
||||
}
|
||||
str, err := reggen.Generate(regex, limit)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
// Render input yaml as output yaml
|
||||
// This function is from the Helm project:
|
||||
// https://github.com/helm/helm
|
||||
// Copyright The Helm Authors
|
||||
func toYaml(v interface{}) string {
|
||||
data, err := yaml.Marshal(v)
|
||||
if err != nil {
|
||||
// Swallow errors inside of a template.
|
||||
return ""
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
@ -24,6 +24,11 @@ import (
|
||||
"sigs.k8s.io/kustomize/kyaml/kio"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
|
||||
"opendev.org/airship/airshipctl/pkg/document/plugin/templater"
|
||||
)
|
||||
|
||||
@ -213,3 +218,67 @@ NoFileExists: false
|
||||
assert.Equal(t, tc.expectedOut, buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenSignedCertEx(t *testing.T) {
|
||||
testCases := []struct {
|
||||
cfg string
|
||||
expectedSubject pkix.Name
|
||||
}{
|
||||
{
|
||||
cfg: `
|
||||
apiVersion: airshipit.org/v1alpha1
|
||||
kind: Templater
|
||||
metadata:
|
||||
name: notImportantHere
|
||||
values:
|
||||
name: test
|
||||
regex: "^[a-z]{5,10}$"
|
||||
limit: 0
|
||||
template: |
|
||||
{{- $targetClusterCa:=genCAEx "Kubernetes API" 3650 }}
|
||||
{{- $targetKubeconfigCert:= genSignedCertEx "/CN=admin/O=system:masters" nil nil 365 $targetClusterCa }}
|
||||
cert: {{ $targetKubeconfigCert.Cert|b64enc|quote }}
|
||||
`,
|
||||
expectedSubject: pkix.Name{
|
||||
CommonName: `admin`,
|
||||
Organization: []string{
|
||||
`system:masters`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
cfg := make(map[string]interface{})
|
||||
err := yaml.Unmarshal([]byte(tc.cfg), &cfg)
|
||||
require.NoError(t, err)
|
||||
plugin, err := templater.New(cfg)
|
||||
require.NoError(t, err)
|
||||
buf := &bytes.Buffer{}
|
||||
nodes, err := plugin.Filter(nil)
|
||||
require.NoError(t, err)
|
||||
err = kio.ByteWriter{Writer: buf}.Write(nodes)
|
||||
require.NoError(t, err)
|
||||
|
||||
res := make(map[string]string)
|
||||
err = yaml.Unmarshal(buf.Bytes(), &res)
|
||||
require.NoError(t, err)
|
||||
|
||||
key, err := base64.StdEncoding.DecodeString(res["cert"])
|
||||
require.NoError(t, err)
|
||||
|
||||
der, _ := pem.Decode(key)
|
||||
if der == nil {
|
||||
t.Errorf("failed to find PEM block")
|
||||
return
|
||||
}
|
||||
|
||||
cert, err := x509.ParseCertificate(der.Bytes)
|
||||
if err != nil {
|
||||
t.Errorf("failed to parse: %s", err)
|
||||
return
|
||||
}
|
||||
cert.Subject.Names = nil
|
||||
assert.Equal(t, tc.expectedSubject, cert.Subject)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user