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

This commit updates the iso generation process to pull the
network data from the ephemeral host network data from the
baremetalhost spec of the host with the right label, namely:

airshipit.org/ephemeral-node=true

It will back into the secret name and namespace specified
for the given host and extract the network data from this
secret which should be identical to the network configuration
the host would receive during normal provisioning.

It also pulls the user-data for the iso generation process
which is specific to the iso generation process from a secret
with a similar special label:

airshipit.org/ephemeral-user-data=true

Change-Id: Iae6edeb231d9dbae0b316aa73009f145d57ac316
This commit is contained in:
Alan Meadows 2020-02-19 12:10:38 +04:00 committed by Dmitry Ukov
parent d155be2145
commit 28db50e6d6
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

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