Merge "[#45] iso generation pulls network data from ephemeral host"

This commit is contained in:
Zuul 2020-03-11 17:24:28 +00:00 committed by Gerrit Code Review
commit dc9de0114b
20 changed files with 598 additions and 128 deletions

View File

@ -7,72 +7,137 @@ import (
)
const (
// TODO (dukov) This should depend on cluster api version once it is
// fully available for Metal3. In other words:
// - Secret for v1alpha1
// - KubeAdmConfig for v1alpha2
EphemeralClusterConfKind = "Secret"
UserDataKind = "Secret"
NetworkDataKind = "Secret"
BareMetalHostKind = "BareMetalHost"
EphemeralHostLabel = "airshipit.org/ephemeral-node=true"
EphemeralUserDataLabel = "airshipit.org/ephemeral-user-data=true"
networkDataKey = "networkData"
userDataKey = "userData"
)
func decodeData(cfg document.Document, key string) ([]byte, error) {
data, err := cfg.GetStringMap("data")
// GetCloudData reads YAML document input and generates cloud-init data for
// ephemeral node.
func GetCloudData(docBundle document.Bundle) (userData []byte, netConf []byte, err error) {
userData, err = getUserData(docBundle)
if err != nil {
return nil, ErrDataNotSupplied{DocName: cfg.GetName(), Key: key}
return nil, nil, err
}
res, ok := data[key]
if !ok {
return nil, ErrDataNotSupplied{DocName: cfg.GetName(), Key: key}
netConf, err = getNetworkData(docBundle)
if err != nil {
return nil, nil, err
}
return b64.StdEncoding.DecodeString(res)
return userData, netConf, err
}
// getDataFromSecret extracts data from Secret with respect to overrides
func getDataFromSecret(cfg document.Document, key string) ([]byte, error) {
data, err := cfg.GetStringMap("stringData")
func getUserData(docBundle document.Bundle) ([]byte, error) {
// find the user-data document
selector := document.NewSelector().ByKind(UserDataKind).ByLabel(EphemeralUserDataLabel)
docs, err := docBundle.Select(selector)
if err != nil {
return decodeData(cfg, key)
return nil, err
}
var userDataDoc document.Document = &document.Factory{}
switch numDocsFound := len(docs); {
case numDocsFound == 0:
return nil, document.ErrDocNotFound{Selector: selector}
case numDocsFound > 1:
return nil, document.ErrMultipleDocsFound{Selector: selector}
case numDocsFound == 1:
userDataDoc = docs[0]
}
// finally, try and retrieve the data we want from the document
userData, err := decodeData(userDataDoc, userDataKey)
if err != nil {
return nil, err
}
return userData, nil
}
func getNetworkData(docBundle document.Bundle) ([]byte, error) {
// find the baremetal host indicated as the ephemeral node
selector := document.NewSelector().ByKind(BareMetalHostKind).ByLabel(EphemeralHostLabel)
docs, err := docBundle.Select(selector)
if err != nil {
return nil, err
}
var bmhDoc document.Document = &document.Factory{}
switch numDocsFound := len(docs); {
case numDocsFound == 0:
return nil, document.ErrDocNotFound{Selector: selector}
case numDocsFound > 1:
return nil, document.ErrMultipleDocsFound{Selector: selector}
case numDocsFound == 1:
bmhDoc = docs[0]
}
// extract the network data document pointer from the bmh document
netConfDocName, err := bmhDoc.GetString("spec.networkData.name")
if err != nil {
return nil, err
}
netConfDocNamespace, err := bmhDoc.GetString("spec.networkData.namespace")
if err != nil {
return nil, err
}
// try and find these documents in our bundle
selector = document.NewSelector().ByKind(NetworkDataKind).ByNamespace(netConfDocNamespace).ByName(netConfDocName)
docs, err = docBundle.Select(selector)
if err != nil {
return nil, err
}
var networkDataDoc document.Document = &document.Factory{}
switch numDocsFound := len(docs); {
case numDocsFound == 0:
return nil, document.ErrDocNotFound{Selector: selector}
case numDocsFound > 1:
return nil, document.ErrMultipleDocsFound{Selector: selector}
case numDocsFound == 1:
networkDataDoc = docs[0]
}
// finally, try and retrieve the data we want from the document
netData, err := decodeData(networkDataDoc, networkDataKey)
if err != nil {
return nil, err
}
return netData, nil
}
func decodeData(cfg document.Document, key string) ([]byte, error) {
var needsBase64Decode = false
// TODO(alanmeadows): distinguish between missing net-data key
// and missing data/stringData keys in the Secret
data, err := cfg.GetStringMap("data")
if err == nil {
needsBase64Decode = true
} else {
// we'll catch any error below
data, err = cfg.GetStringMap("stringData")
if err != nil {
return nil, ErrDataNotSupplied{DocName: cfg.GetName(), Key: "data or stringData"}
}
}
res, ok := data[key]
if !ok {
return decodeData(cfg, key)
return nil, ErrDataNotSupplied{DocName: cfg.GetName(), Key: key}
}
if needsBase64Decode {
return b64.StdEncoding.DecodeString(res)
}
return []byte(res), nil
}
// GetCloudData reads YAML document input and generates cloud-init data for
// node (i.e. Cluster API Machine) with bootstrap label.
func GetCloudData(docBundle document.Bundle, bsSelector string) ([]byte, []byte, error) {
var userData []byte
var netConf []byte
docs, err := docBundle.GetByLabel(bsSelector)
if err != nil {
return nil, nil, err
}
var ephemeralCfg document.Document
for _, doc := range docs {
if doc.GetKind() == EphemeralClusterConfKind {
ephemeralCfg = doc
break
}
}
if ephemeralCfg == nil {
return nil, nil, document.ErrDocNotFound{
Selector: bsSelector,
Kind: EphemeralClusterConfKind,
}
}
netConf, err = getDataFromSecret(ephemeralCfg, "netconfig")
if err != nil {
return nil, nil, err
}
userData, err = getDataFromSecret(ephemeralCfg, "userdata")
if err != nil {
return nil, nil, err
}
return userData, netConf, nil
}

View File

@ -5,6 +5,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"sigs.k8s.io/kustomize/v3/pkg/types"
"opendev.org/airship/airshipctl/pkg/document"
"opendev.org/airship/airshipctl/testutil"
@ -16,54 +17,90 @@ func TestGetCloudData(t *testing.T) {
require.NoError(t, err, "Building Bundle Failed")
tests := []struct {
selector string
labelFilter string
expectedUserData []byte
expectedNetData []byte
expectedErr error
}{
{
selector: "test=test",
labelFilter: "test=validdocset",
expectedUserData: []byte("cloud-init"),
expectedNetData: []byte("net-config"),
expectedErr: nil,
},
{
labelFilter: "test=ephemeralmissing",
expectedUserData: nil,
expectedNetData: nil,
expectedErr: document.ErrDocNotFound{
Selector: "test=test",
Kind: "Secret",
Selector: document.NewSelector().
ByLabel("airshipit.org/ephemeral-node=true").
ByKind("BareMetalHost"),
},
},
{
selector: "airshipit.org/ephemeral=false",
labelFilter: "test=ephemeralduplicate",
expectedUserData: nil,
expectedNetData: nil,
expectedErr: ErrDataNotSupplied{
DocName: "node1-bmc-secret1",
Key: "netconfig",
expectedErr: document.ErrMultipleDocsFound{
Selector: document.NewSelector().
ByLabel("airshipit.org/ephemeral-node=true").
ByKind("BareMetalHost"),
},
},
{
selector: "test=nodataforcfg",
labelFilter: "test=networkdatabadpointer",
expectedUserData: nil,
expectedNetData: nil,
expectedErr: ErrDataNotSupplied{
DocName: "node1-bmc-secret2",
Key: "netconfig",
expectedErr: document.ErrDocNotFound{
Selector: document.NewSelector().
ByKind("Secret").
ByNamespace("networkdatabadpointer-missing").
ByName("networkdatabadpointer-missing"),
},
},
{
selector: "airshipit.org/ephemeral=true",
expectedUserData: []byte("cloud-init"),
expectedNetData: []byte("netconfig\n"),
expectedErr: nil,
labelFilter: "test=networkdatamalformed",
expectedUserData: nil,
expectedNetData: nil,
expectedErr: ErrDataNotSupplied{DocName: "networkdatamalformed-malformed", Key: networkDataKey},
},
{
selector: "some-data in (true, True)",
expectedUserData: []byte("cloud-init"),
expectedNetData: []byte("netconfig\n"),
expectedErr: nil,
labelFilter: "test=networkdatamissing",
expectedUserData: nil,
expectedNetData: nil,
expectedErr: types.NoFieldError{Field: "spec.networkData.name"},
},
{
labelFilter: "test=userdatamalformed",
expectedUserData: nil,
expectedNetData: nil,
expectedErr: ErrDataNotSupplied{DocName: "userdatamalformed-somesecret", Key: userDataKey},
},
{
labelFilter: "test=userdatamissing",
expectedUserData: nil,
expectedNetData: nil,
expectedErr: document.ErrDocNotFound{
Selector: document.NewSelector().
ByKind("Secret").
ByLabel("airshipit.org/ephemeral-user-data=true"),
},
},
}
for _, tt := range tests {
actualUserData, actualNetData, actualErr := GetCloudData(bundle, tt.selector)
// prune the bundle down using the label filter for the specific test
selector := document.NewSelector().ByLabel(tt.labelFilter)
filteredBundle, err := bundle.SelectBundle(selector)
require.NoError(t, err, "Building filtered bundle for %s failed", tt.labelFilter)
// ensure each test case filter has at least one document
docs, err := filteredBundle.GetAllDocuments()
require.NoError(t, err, "GetAllDocuments failed")
require.NotZero(t, docs)
actualUserData, actualNetData, actualErr := GetCloudData(filteredBundle)
assert.Equal(t, tt.expectedUserData, actualUserData)
assert.Equal(t, tt.expectedNetData, actualNetData)

View File

@ -11,6 +11,17 @@ type ErrDataNotSupplied struct {
Key string
}
// ErrDuplicateNetworkDataDocuments error returned if multiple network documents
// were found with the same name in the same namespace
type ErrDuplicateNetworkDataDocuments struct {
DocName string
Namespace string
}
func (e ErrDataNotSupplied) Error() string {
return fmt.Sprintf("Document %s has no key %s", e.DocName, e.Key)
}
func (e ErrDuplicateNetworkDataDocuments) Error() string {
return fmt.Sprintf("Found more than one document with the name %s in namespace %s", e.DocName, e.Namespace)
}

View File

@ -0,0 +1,36 @@
# in this document set, we have no ephemerally labeled node
# which should cause an error
apiVersion: v1
kind: Secret
metadata:
labels:
test: ephemeralduplicate
name: ephemeralduplicate
type: Opaque
---
apiVersion: metal3.io/v1alpha1
kind: BareMetalHost
metadata:
labels:
test: ephemeralduplicate
airshipit.org/ephemeral-node: 'true'
name: ephemeralduplicate-master-1
---
apiVersion: metal3.io/v1alpha1
kind: BareMetalHost
metadata:
labels:
test: ephemeralduplicate
airshipit.org/ephemeral-node: 'true'
name: ephemeralduplicate-master-2
---
apiVersion: v1
kind: Secret
metadata:
labels:
airshipit.org/ephemeral-user-data: 'true'
test: ephemeralduplicate
name: ephemeralduplicate-airship-isogen-userdata
type: Opaque
stringData:
userData: cloudinit

View File

@ -0,0 +1,27 @@
# in this document set, we have no ephemerally labeled node
# which should cause an error
apiVersion: v1
kind: Secret
metadata:
labels:
test: ephemeralmissing
name: ephemeralmissing
type: Opaque
---
apiVersion: metal3.io/v1alpha1
kind: BareMetalHost
metadata:
labels:
test: ephemeralmissing
name: ephemeralmissing-master-1
---
apiVersion: v1
kind: Secret
metadata:
labels:
airshipit.org/ephemeral-user-data: 'true'
test: ephemeralmissing
name: ephemeralmissing-airship-isogen-userdata
type: Opaque
stringData:
userData: cloud-init

View File

@ -1,2 +1,9 @@
resources:
- secret.yaml
- ephemeralduplicate.yaml
- ephemeralmissing.yaml
- networkdatabadpointer.yaml
- networkdatamalformed.yaml
- networkdatamissing.yaml
- userdatamalformed.yaml
- userdatamissing.yaml
- validdocset.yaml

View File

@ -0,0 +1,38 @@
# in this document set, we have an ephemeral node however
# it lacks a networkData clause
apiVersion: v1
kind: Secret
metadata:
labels:
test: networkdatabadpointer
name: networkdatabadpointer-master-1-bmc
type: Opaque
stringData:
username: foobar
password: goober
---
apiVersion: metal3.io/v1alpha1
kind: BareMetalHost
metadata:
labels:
airshipit.org/ephemeral-node: 'true'
test: networkdatabadpointer
name: networkdatabadpointer-master-1
spec:
bmc:
address: ipmi://127.0.0.1
credentialsName: networkdatabadpointer-master-1-bmc
networkData:
name: networkdatabadpointer-missing
namespace: networkdatabadpointer-missing
---
apiVersion: v1
kind: Secret
metadata:
labels:
airshipit.org/ephemeral-user-data: 'true'
test: networkdatabadpointer
name: networkdatabadpointer-airship-isogen-userdata
type: Opaque
stringData:
userData: cloud-init

View File

@ -0,0 +1,50 @@
# in this document set, we have an ephemeral node with
# resolvable network data, but it is malformed lacking
# the proper field
apiVersion: v1
kind: Secret
metadata:
labels:
test: networkdatamalformed
name: networkdatamalformed-master-1-bmc
type: Opaque
stringData:
username: foobar
password: goober
---
apiVersion: v1
kind: Secret
namespace: malformed
metadata:
labels:
test: networkdatamalformed
name: networkdatamalformed-malformed
type: Opaque
stringData:
no-net-data-key: the required 'net-data' key is missing
---
apiVersion: metal3.io/v1alpha1
kind: BareMetalHost
metadata:
labels:
airshipit.org/ephemeral-node: 'true'
test: networkdatamalformed
name: networkdatamalformed-master-1
spec:
bmc:
address: ipmi://127.0.0.1
credentialsName: networkdatamalformed-master-1-bmc
networkData:
name: networkdatamalformed-malformed
namespace: malformed
---
apiVersion: v1
kind: Secret
metadata:
labels:
airshipit.org/ephemeral-user-data: 'true'
test: networkdatamalformed
name: networkdatamalformed-airship-isogen-userdata
type: Opaque
stringData:
userData: cloud-init

View File

@ -0,0 +1,46 @@
# in this document set, we have an ephemeral node with
# but it lacks a networkData clause
apiVersion: v1
kind: Secret
metadata:
labels:
test: networkdatamissing
name: networkdatamissing-master-1-bmc
type: Opaque
stringData:
username: foobar
password: goober
---
apiVersion: v1
kind: Secret
namespace: missing
metadata:
labels:
test: missing
name: networkdatamissing-missing
type: Opaque
stringData:
networkData: there is network data here, but we have no reference to it
---
apiVersion: metal3.io/v1alpha1
kind: BareMetalHost
metadata:
labels:
airshipit.org/ephemeral-node: 'true'
test: networkdatamissing
name: networkdatamissing-master-1
spec:
bmc:
address: ipmi://127.0.0.1
credentialsName: networkdatamissing-master-1-bmc
---
apiVersion: v1
kind: Secret
metadata:
labels:
airshipit.org/ephemeral-user-data: 'true'
test: networkdatamissing
name: networkdatamissing-airship-isogen-userdata
type: Opaque
stringData:
userData: cloud-init

View File

@ -1,41 +0,0 @@
apiVersion: v1
kind: Secret
metadata:
labels:
airshipit.org/ephemeral: "true"
name: node1-bmc-secret
type: Opaque
data:
netconfig: bmV0Y29uZmlnCg==
stringData:
userdata: cloud-init
---
apiVersion: v1
kind: Secret
metadata:
labels:
airshipit.org/ephemeral: "false"
name: node1-bmc-secret1
type: Opaque
---
apiVersion: v1
kind: Secret
metadata:
labels:
test: nodataforcfg
name: node1-bmc-secret2
type: Opaque
data:
foo: bmV0Y29uZmlnCg==
---
apiVersion: v1
kind: Secret
metadata:
labels:
some-data: "True"
name: node1-bmc-in-secret2
type: Opaque
data:
netconfig: bmV0Y29uZmlnCg==
stringData:
userdata: cloud-init

View File

@ -0,0 +1,12 @@
# in this document set, we have a secret that contains a label for our
# iso generation userdata, but it is malformed lacking a user-data key
apiVersion: v1
kind: Secret
metadata:
labels:
airshipit.org/ephemeral-user-data: 'true'
test: userdatamalformed
name: userdatamalformed-somesecret
type: Opaque
stringData:
no-user-data: this secret has the right label but is missing the 'user-data' key

View File

@ -0,0 +1,11 @@
# in this document set, we lack a document that contains our ephemeral
# iso generation userdata
apiVersion: v1
kind: Secret
metadata:
labels:
test: userdatamissing
name: userdatamissing-somesecret
type: Opaque
stringData:
userData: "this secret lacks the label airshipit.org/ephemeral-user-data: true"

View File

@ -0,0 +1,66 @@
# in this document set, we have an ephemeral node with
# the right label, resolvable/valid network data and
# a user-data secret with the right label
#
# we also introduce a second baremetal host that is not
# labeled as the ephemeral node to facilitate testing
apiVersion: v1
kind: Secret
metadata:
labels:
test: validdocset
name: master-1-bmc
type: Opaque
stringData:
username: foobar
password: goober
---
apiVersion: v1
kind: Secret
metadata:
labels:
airshipit.org/ephemeral-user-data: 'true'
test: validdocset
name: airship-isogen-userdata
type: Opaque
stringData:
userData: cloud-init
---
apiVersion: v1
kind: Secret
namespace: metal3
metadata:
labels:
test: validdocset
name: master-1-networkdata
type: Opaque
stringData:
networkData: net-config
---
apiVersion: metal3.io/v1alpha1
kind: BareMetalHost
metadata:
labels:
test: validdocset
name: master-2
bmc:
address: ipmi://127.0.0.1
credentialsName: master-2-bmc
networkData:
name: master-2-networkdata
namespace: metal3
---
apiVersion: metal3.io/v1alpha1
kind: BareMetalHost
metadata:
labels:
airshipit.org/ephemeral-node: 'true'
test: validdocset
name: master-1
spec:
bmc:
address: ipmi://127.0.0.1
credentialsName: master-1-bmc
networkData:
name: master-1-networkdata
namespace: metal3

View File

@ -119,8 +119,7 @@ func generateBootstrapIso(
) error {
cntVol := strings.Split(cfg.Container.Volume, ":")[1]
log.Print("Creating cloud-init for ephemeral K8s")
label := document.EphemeralClusterSelector
userData, netConf, err := cloudinit.GetCloudData(docBundle, label)
userData, netConf, err := cloudinit.GetCloudData(docBundle)
if err != nil {
return err
}

View File

@ -1,11 +1,66 @@
# in this document set, we have an ephemeral node with
# the right label, resolvable/valid network data and
# a user-data secret with the right label
#
# we also introduce a second baremetal host that is not
# labeled as the ephemeral node to facilitate testing
apiVersion: v1
kind: Secret
metadata:
labels:
airshipit.org/ephemeral: "true"
name: node1-bmc-secret
test: validdocset
name: master-1-bmc
type: Opaque
data:
netconfig: bmV0Y29uZmlnCg==
stringData:
userdata: cloud-init
username: foobar
password: goober
---
apiVersion: v1
kind: Secret
metadata:
labels:
airshipit.org/ephemeral-user-data: 'true'
test: validdocset
name: airship-isogen-userdata
type: Opaque
stringData:
userData: cloud-init
---
apiVersion: v1
kind: Secret
namespace: metal3
metadata:
labels:
test: validdocset
name: master-1-networkdata
type: Opaque
stringData:
networkData: net-config
---
apiVersion: metal3.io/v1alpha1
kind: BareMetalHost
metadata:
labels:
test: validdocset
name: master-2
bmc:
address: ipmi://127.0.0.1
credentialsName: master-2-bmc
networkData:
name: master-2-networkdata
namespace: metal3
---
apiVersion: metal3.io/v1alpha1
kind: BareMetalHost
metadata:
labels:
airshipit.org/ephemeral-node: 'true'
test: validdocset
name: master-1
spec:
bmc:
address: ipmi://127.0.0.1
credentialsName: master-1-bmc
networkData:
name: master-1-networkdata
namespace: metal3

View File

@ -77,7 +77,7 @@ func (infra *Infra) Deploy() error {
}
if len(docs) == 0 {
return document.ErrDocNotFound{
Selector: ls,
Selector: selector,
}
}

View File

@ -49,6 +49,7 @@ type Bundle interface {
SetFileSystem(FileSystem) error
GetFileSystem() FileSystem
Select(selector Selector) ([]Document, error)
SelectBundle(selector Selector) (Bundle, error)
GetByGvk(string, string, string) ([]Document, error)
GetByName(string) (Document, error)
GetByAnnotation(annotationSelector string) ([]Document, error)
@ -207,6 +208,37 @@ func (b *BundleFactory) Select(selector Selector) ([]Document, error) {
return docSet, err
}
// SelectBundle offers an interface to pass a Selector, built on top of kustomize Selector
// to the bundle returning a new Bundle that matches the criteria. This is useful
// where you want to actually prune the underlying bundle you are working with
// rather then getting back the matching documents for scenarios like
// test cases where you want to pass in custom "filtered" bundles
// specific to the test case
func (b *BundleFactory) SelectBundle(selector Selector) (Bundle, error) {
// use the kustomize select method
resources, err := b.ResMap.Select(selector.Selector)
if err != nil {
return nil, err
}
// create a blank resourcemap and append the found resources
// into the new resource map
resourceMap := resmap.New()
for _, res := range resources {
if err = resourceMap.Append(res); err != nil {
return nil, err
}
}
// return a new bundle with the same options and filesystem
// as this one but with a reduced resourceMap
return &BundleFactory{
KustomizeBuildOptions: b.KustomizeBuildOptions,
ResMap: resourceMap,
FileSystem: b.FileSystem,
}, nil
}
// GetByAnnotation is a convenience method to get documents for a particular annotation
func (b *BundleFactory) GetByAnnotation(annotationSelector string) ([]Document, error) {
// Construct kustomize annotation selector

View File

@ -6,10 +6,18 @@ import (
// ErrDocNotFound returned if desired document not found
type ErrDocNotFound struct {
Selector string
Kind string
Selector Selector
}
// ErrMultipleDocsFound returned if desired document not found
type ErrMultipleDocsFound struct {
Selector Selector
}
func (e ErrDocNotFound) Error() string {
return fmt.Sprintf("Document filtered by selector %s with Kind %s not found", e.Selector, e.Kind)
return fmt.Sprintf("Document filtered by selector %q found no documents", e.Selector)
}
func (e ErrMultipleDocsFound) Error() string {
return fmt.Sprintf("Document filtered by selector %q found more than one document", e.Selector)
}

View File

@ -26,12 +26,24 @@ func (s Selector) ByName(name string) Selector {
return s
}
// ByNamespace select by namepace
func (s Selector) ByNamespace(namespace string) Selector {
s.Namespace = namespace
return s
}
// ByGvk select by gvk
func (s Selector) ByGvk(group, version, kind string) Selector {
s.Gvk = gvk.Gvk{Group: group, Version: version, Kind: kind}
return s
}
// ByKind select by Kind
func (s Selector) ByKind(kind string) Selector {
s.Gvk = gvk.Gvk{Kind: kind}
return s
}
// ByLabel select by label selector
func (s Selector) ByLabel(labelSelector string) Selector {
if s.LabelSelector != "" {

View File

@ -94,8 +94,7 @@ func getRemoteDirectConfig(settings *environment.AirshipCTLSettings) (*config.Re
}
if len(docs) == 0 {
return nil, "", document.ErrDocNotFound{
Selector: ls,
Kind: AirshipHostKind,
Selector: selector,
}
}