Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce network_data for host #348

Merged
merged 9 commits into from
Feb 28, 2020
Merged
13 changes: 13 additions & 0 deletions deploy/crds/metal3.io_baremetalhosts_crd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,19 @@ spec:
- checksum
- url
type: object
networkData:
description: NetworkData holds the reference to the Secret containing
content of network_data.json which is passed to Config Drive
properties:
name:
description: Name is unique within a namespace to reference a secret
resource.
type: string
namespace:
description: Namespace defines the space within which the secret
name must be unique.
type: string
type: object
online:
description: Should the server be online?
type: boolean
Expand Down
4 changes: 4 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ mainly, but not only, provisioning details.
and its namespace, so it can be attached to the host before it boots
for configuring different aspects of the OS (like networking, storage, ...).

* *networkData* -- A reference to the Secret containing the network
configuration data (e.g. network\_data.json) and its namespace, so it can be
attached to the host before it boots to set network up

* *description* -- A human-provided string to help identify the host.

### BareMetalHost status
Expand Down
4 changes: 4 additions & 0 deletions pkg/apis/metal3/v1alpha1/baremetalhost_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,10 @@ type BareMetalHostSpec struct {
// data to be passed to the host before it boots.
UserData *corev1.SecretReference `json:"userData,omitempty"`

// NetworkData holds the reference to the Secret containing content
// of network_data.json which is passed to Config Drive
NetworkData *corev1.SecretReference `json:"networkData,omitempty"`

// Description is a human-entered text used to help identify the host
Description string `json:"description,omitempty"`

Expand Down
5 changes: 5 additions & 0 deletions pkg/apis/metal3/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 5 additions & 26 deletions pkg/controller/baremetalhost/baremetalhost_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import (
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"

"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
Expand Down Expand Up @@ -529,34 +528,14 @@ func (r *ReconcileBareMetalHost) actionMatchProfile(prov provisioner.Provisioner

// Start/continue provisioning if we need to.
func (r *ReconcileBareMetalHost) actionProvisioning(prov provisioner.Provisioner, info *reconcileInfo) actionResult {
getUserData := func() (string, error) {
if info.host.Spec.UserData == nil {
info.log.Info("no user data for host")
return "", nil
}
info.log.Info("fetching user data before provisioning")
userDataSecret := &corev1.Secret{}
key := types.NamespacedName{
Name: info.host.Spec.UserData.Name,
Namespace: info.host.Spec.UserData.Namespace,
}
err := r.client.Get(context.TODO(), key, userDataSecret)
if err != nil {
return "", errors.Wrap(err,
"failed to fetch user data from secret reference")
}
if content, ok := userDataSecret.Data["userData"]; ok {
return string(content), nil
} else if content, ok := userDataSecret.Data["value"]; ok {
return string(content), nil
} else {
return "", errors.New("userData or value key not found in secret")
}
hostConf := &hostConfigData{
host: info.host,
log: info.log,
client: r.client,
}

info.log.Info("provisioning")

provResult, err := prov.Provision(getUserData)
provResult, err := prov.Provision(hostConf)
if err != nil {
return actionError{errors.Wrap(err, "failed to provision")}
}
Expand Down
40 changes: 23 additions & 17 deletions pkg/controller/baremetalhost/baremetalhost_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,11 @@ func init() {
metal3apis.AddToScheme(scheme.Scheme)
}

func newSecret(name, username, password string) *corev1.Secret {
data := make(map[string][]byte)
data["username"] = []byte(base64.StdEncoding.EncodeToString([]byte(username)))
data["password"] = []byte(base64.StdEncoding.EncodeToString([]byte(password)))
func newSecret(name string, data map[string]string) *corev1.Secret {
secretData := make(map[string][]byte)
for k, v := range data {
secretData[k] = []byte(base64.StdEncoding.EncodeToString([]byte(v)))
}

secret := &corev1.Secret{
TypeMeta: metav1.TypeMeta{
Expand All @@ -50,12 +51,16 @@ func newSecret(name, username, password string) *corev1.Secret {
Namespace: namespace,
ResourceVersion: "1",
},
Data: data,
Data: secretData,
}

return secret
}

func newBMCCredsSecret(name, username, password string) *corev1.Secret {
return newSecret(name, map[string]string{"username": username, "password": password})
}

func newHost(name string, spec *metal3v1alpha1.BareMetalHostSpec) *metal3v1alpha1.BareMetalHost {
return &metal3v1alpha1.BareMetalHost{
TypeMeta: metav1.TypeMeta{
Expand Down Expand Up @@ -90,7 +95,8 @@ func newTestReconciler(initObjs ...runtime.Object) *ReconcileBareMetalHost {
c := fakeclient.NewFakeClient(initObjs...)

// Add a default secret that can be used by most hosts.
c.Create(goctx.TODO(), newSecret(defaultSecretName, "User", "Pass"))
bmcSecret := newBMCCredsSecret(defaultSecretName, "User", "Pass")
c.Create(goctx.TODO(), bmcSecret)

return &ReconcileBareMetalHost{
client: c,
Expand Down Expand Up @@ -276,7 +282,7 @@ func TestUpdateGoodCredentialsOnNewSecret(t *testing.T) {
)

// Define a second valid secret and update the host to use it.
secret2 := newSecret("bmc-creds-valid2", "User", "Pass")
secret2 := newBMCCredsSecret("bmc-creds-valid2", "User", "Pass")
err := r.client.Create(goctx.TODO(), secret2)
if err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -305,7 +311,7 @@ func TestUpdateGoodCredentialsOnNewSecret(t *testing.T) {
// to one that is missing data.
func TestUpdateGoodCredentialsOnBadSecret(t *testing.T) {
host := newDefaultHost(t)
badSecret := newSecret("bmc-creds-no-user", "", "Pass")
badSecret := newBMCCredsSecret("bmc-creds-no-user", "", "Pass")
r := newTestReconciler(host, badSecret)

tryReconcile(t, r, host,
Expand Down Expand Up @@ -376,7 +382,7 @@ func TestMissingBMCParameters(t *testing.T) {
}{
{
Scenario: "secret without username",
Secret: newSecret("bmc-creds-no-user", "", "Pass"),
Secret: newBMCCredsSecret("bmc-creds-no-user", "", "Pass"),
Host: newHost("missing-bmc-username",
&metal3v1alpha1.BareMetalHostSpec{
BMC: metal3v1alpha1.BMCDetails{
Expand All @@ -388,7 +394,7 @@ func TestMissingBMCParameters(t *testing.T) {

{
Scenario: "secret without password",
Secret: newSecret("bmc-creds-no-pass", "User", ""),
Secret: newBMCCredsSecret("bmc-creds-no-pass", "User", ""),
Host: newHost("missing-bmc-password",
&metal3v1alpha1.BareMetalHostSpec{
BMC: metal3v1alpha1.BMCDetails{
Expand All @@ -400,7 +406,7 @@ func TestMissingBMCParameters(t *testing.T) {

{
Scenario: "malformed address",
Secret: newSecret("bmc-creds-ok", "User", "Pass"),
Secret: newBMCCredsSecret("bmc-creds-ok", "User", "Pass"),
Host: newHost("invalid-bmc-address",
&metal3v1alpha1.BareMetalHostSpec{
BMC: metal3v1alpha1.BMCDetails{
Expand All @@ -412,7 +418,7 @@ func TestMissingBMCParameters(t *testing.T) {

{
Scenario: "missing address",
Secret: newSecret("bmc-creds-ok", "User", "Pass"),
Secret: newBMCCredsSecret("bmc-creds-ok", "User", "Pass"),
Host: newHost("missing-bmc-address",
&metal3v1alpha1.BareMetalHostSpec{
BMC: metal3v1alpha1.BMCDetails{
Expand All @@ -424,7 +430,7 @@ func TestMissingBMCParameters(t *testing.T) {

{
Scenario: "missing secret",
Secret: newSecret("bmc-creds-ok", "User", "Pass"),
Secret: newBMCCredsSecret("bmc-creds-ok", "User", "Pass"),
Host: newHost("missing-bmc-credentials-ref",
&metal3v1alpha1.BareMetalHostSpec{
BMC: metal3v1alpha1.BMCDetails{
Expand All @@ -436,7 +442,7 @@ func TestMissingBMCParameters(t *testing.T) {

{
Scenario: "no such secret",
Secret: newSecret("bmc-creds-ok", "User", "Pass"),
Secret: newBMCCredsSecret("bmc-creds-ok", "User", "Pass"),
Host: newHost("non-existent-bmc-secret-ref",
&metal3v1alpha1.BareMetalHostSpec{
BMC: metal3v1alpha1.BMCDetails{
Expand All @@ -459,7 +465,7 @@ func TestMissingBMCParameters(t *testing.T) {
// be correct the status of the host moves out of the error state.
func TestFixSecret(t *testing.T) {

secret := newSecret("bmc-creds-no-user", "", "Pass")
secret := newBMCCredsSecret("bmc-creds-no-user", "", "Pass")
host := newHost("fix-secret",
&metal3v1alpha1.BareMetalHostSpec{
BMC: metal3v1alpha1.BMCDetails{
Expand Down Expand Up @@ -496,7 +502,7 @@ func TestBreakThenFixSecret(t *testing.T) {

// Create the host without any errors and wait for it to be
// registered and get a provisioning ID.
secret := newSecret("bmc-creds-toggle-user", "User", "Pass")
secret := newBMCCredsSecret("bmc-creds-toggle-user", "User", "Pass")
host := newHost("break-then-fix-secret",
&metal3v1alpha1.BareMetalHostSpec{
BMC: metal3v1alpha1.BMCDetails{
Expand Down Expand Up @@ -778,7 +784,7 @@ func TestDeleteHost(t *testing.T) {
t.Run(host.Name, func(t *testing.T) {
host.DeletionTimestamp = &now
host.Status.Provisioning.ID = "made-up-id"
badSecret := newSecret("bmc-creds-no-user", "", "Pass")
badSecret := newBMCCredsSecret("bmc-creds-no-user", "", "Pass")
r := newTestReconciler(host, badSecret)

tryReconcile(t, r, host,
Expand Down
3 changes: 2 additions & 1 deletion pkg/controller/baremetalhost/demo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ func newDemoReconciler(initObjs ...runtime.Object) *ReconcileBareMetalHost {
c := fakeclient.NewFakeClient(initObjs...)

// Add a default secret that can be used by most hosts.
c.Create(goctx.TODO(), newSecret(defaultSecretName, "User", "Pass"))
bmcSecret := newSecret(defaultSecretName, map[string]string{"username": "User", "password": "Pass"})
c.Create(goctx.TODO(), bmcSecret)

return &ReconcileBareMetalHost{
client: c,
Expand Down
11 changes: 11 additions & 0 deletions pkg/controller/baremetalhost/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,14 @@ func (e SaveBMCSecretOwnerError) Error() string {
return fmt.Sprintf("Failed to set owner of BMC secret %s",
e.message)
}

// NoDataInSecretError is returned when host configuration
// data were not found in referenced secret
type NoDataInSecretError struct {
secret string
key string
}

func (e NoDataInSecretError) Error() string {
return fmt.Sprintf("Secret %s does not contain key %s", e.secret, e.key)
}
81 changes: 81 additions & 0 deletions pkg/controller/baremetalhost/host_config_data.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package baremetalhost

import (
"context"
"fmt"

"github.com/pkg/errors"

metal3v1alpha1 "github.com/metal3-io/baremetal-operator/pkg/apis/metal3/v1alpha1"

"github.com/go-logr/logr"

corev1 "k8s.io/api/core/v1"

"k8s.io/apimachinery/pkg/types"

"sigs.k8s.io/controller-runtime/pkg/client"
)

// hostConfigData is an implementation of host configuration data interface.
// Object is able to retrive data from secrets referenced in a host spec
type hostConfigData struct {
host *metal3v1alpha1.BareMetalHost
log logr.Logger
client client.Client
}

// Generic method for data extraction from a Secret. Function uses dataKey
// parameter to detirmine which data to return in case secret contins multiple
// keys
func (hcd *hostConfigData) getSecretData(name, namespace, dataKey string) (string, error) {
secret := &corev1.Secret{}
key := types.NamespacedName{
Name: name,
Namespace: namespace,
}
if err := hcd.client.Get(context.TODO(), key, secret); err != nil {
errMsg := fmt.Sprintf("failed to fetch user data from secret %s defined in namespace %s", name, namespace)
return "", errors.Wrap(err, errMsg)
}

data, ok := secret.Data[dataKey]
if ok {
return string(data), nil
}
// There is no data under dataKey (userData or networkData).
// Tring to falback to 'value' key
if data, ok = secret.Data["value"]; !ok {
hostConfigDataError.WithLabelValues(dataKey).Inc()
return "", NoDataInSecretError{secret: name, key: dataKey}
}

return string(data), nil
}

// UserData get Operating System configuration data
func (hcd *hostConfigData) UserData() (string, error) {
if hcd.host.Spec.UserData == nil {
hcd.log.Info("UserData is not set return empty string")
return "", nil
}
return hcd.getSecretData(
hcd.host.Spec.UserData.Name,
hcd.host.Spec.UserData.Namespace,
"userData",
)

}

// NetworkData get network configuration
func (hcd *hostConfigData) NetworkData() (string, error) {
if hcd.host.Spec.NetworkData == nil {
hcd.log.Info("NetworkData is not set returning epmty(nil) data")
return "", nil
}
return hcd.getSecretData(
hcd.host.Spec.NetworkData.Name,
hcd.host.Spec.NetworkData.Namespace,
"networkData",
)
}
Loading