[AIR-145] Generate cloud init settings
Settins are generated based on a secret data Change-Id: Ib4c25e720759694432e03796ae5d1b4f2f2a1a1b
This commit is contained in:
parent
915c47506b
commit
6b82a529fc
78
pkg/bootstrap/cloudinit/cloud-init.go
Normal file
78
pkg/bootstrap/cloudinit/cloud-init.go
Normal file
@ -0,0 +1,78 @@
|
||||
package cloudinit
|
||||
|
||||
import (
|
||||
b64 "encoding/base64"
|
||||
|
||||
"opendev.org/airship/airshipctl/pkg/document"
|
||||
)
|
||||
|
||||
const (
|
||||
// TODO (dukov) This should depend on cluster api version once it is
|
||||
// fully available for Metal3. In other words:
|
||||
// - Sectet for v1alpha1
|
||||
// - KubeAdmConfig for v1alpha2
|
||||
EphemeralClusterConfKind = "Secret"
|
||||
)
|
||||
|
||||
func decodeData(cfg document.Document, key string) ([]byte, error) {
|
||||
data, err := cfg.GetStringMap("data")
|
||||
if err != nil {
|
||||
return nil, ErrDataNotSupplied{DocName: cfg.GetName(), Key: key}
|
||||
}
|
||||
|
||||
res, ok := data[key]
|
||||
if !ok {
|
||||
return nil, ErrDataNotSupplied{DocName: cfg.GetName(), Key: key}
|
||||
}
|
||||
|
||||
return b64.StdEncoding.DecodeString(res)
|
||||
}
|
||||
|
||||
// getDataFromSecret extracts data from Secret with respect to overrides
|
||||
func getDataFromSecret(cfg document.Document, key string) ([]byte, error) {
|
||||
data, err := cfg.GetStringMap("stringData")
|
||||
if err != nil {
|
||||
return decodeData(cfg, key)
|
||||
}
|
||||
|
||||
res, ok := data[key]
|
||||
if !ok {
|
||||
return decodeData(cfg, key)
|
||||
}
|
||||
return []byte(res), nil
|
||||
}
|
||||
|
||||
// GetCloudData reads YAML document input and generates cloud-init data for
|
||||
// node (i.e. Cluster API Machine) with bootstrap annotation.
|
||||
func GetCloudData(docBundle document.Bundle, bsAnnotation string) ([]byte, []byte, error) {
|
||||
var userData []byte
|
||||
var netConf []byte
|
||||
docs, err := docBundle.GetByAnnotation(bsAnnotation)
|
||||
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{
|
||||
Annotation: bsAnnotation,
|
||||
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
|
||||
}
|
67
pkg/bootstrap/cloudinit/cloud-init_test.go
Normal file
67
pkg/bootstrap/cloudinit/cloud-init_test.go
Normal file
@ -0,0 +1,67 @@
|
||||
package cloudinit
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"opendev.org/airship/airshipctl/pkg/document"
|
||||
"opendev.org/airship/airshipctl/testutil"
|
||||
)
|
||||
|
||||
func TestGetCloudData(t *testing.T) {
|
||||
|
||||
fSys := testutil.SetupTestFs(t, "testdata")
|
||||
bundle, err := document.NewBundle(fSys, "/", "/")
|
||||
require.NoError(t, err, "Building Bundle Failed")
|
||||
|
||||
tests := []struct {
|
||||
ann string
|
||||
expectedUserData []byte
|
||||
expectedNetData []byte
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
ann: "test=test",
|
||||
expectedUserData: nil,
|
||||
expectedNetData: nil,
|
||||
expectedErr: document.ErrDocNotFound{
|
||||
Annotation: "test=test",
|
||||
Kind: "Secret",
|
||||
},
|
||||
},
|
||||
{
|
||||
ann: "airshipit.org/clustertype=nodata",
|
||||
expectedUserData: nil,
|
||||
expectedNetData: nil,
|
||||
expectedErr: ErrDataNotSupplied{
|
||||
DocName: "node1-bmc-secret1",
|
||||
Key: "netconfig",
|
||||
},
|
||||
},
|
||||
{
|
||||
ann: "test=nodataforcfg",
|
||||
expectedUserData: nil,
|
||||
expectedNetData: nil,
|
||||
expectedErr: ErrDataNotSupplied{
|
||||
DocName: "node1-bmc-secret2",
|
||||
Key: "netconfig",
|
||||
},
|
||||
},
|
||||
{
|
||||
ann: "airshipit.org/clustertype=ephemeral",
|
||||
expectedUserData: []byte("cloud-init"),
|
||||
expectedNetData: []byte("netconfig\n"),
|
||||
expectedErr: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
actualUserData, actualNetData, actualErr := GetCloudData(bundle, tt.ann)
|
||||
|
||||
assert.Equal(t, tt.expectedUserData, actualUserData)
|
||||
assert.Equal(t, tt.expectedNetData, actualNetData)
|
||||
assert.Equal(t, tt.expectedErr, actualErr)
|
||||
}
|
||||
}
|
16
pkg/bootstrap/cloudinit/errors.go
Normal file
16
pkg/bootstrap/cloudinit/errors.go
Normal file
@ -0,0 +1,16 @@
|
||||
package cloudinit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ErrDataNotSupplied error returned of no user-data or network configuration
|
||||
// in the Secret
|
||||
type ErrDataNotSupplied struct {
|
||||
DocName string
|
||||
Key string
|
||||
}
|
||||
|
||||
func (e ErrDataNotSupplied) Error() string {
|
||||
return fmt.Sprintf("Document %s has no key %s", e.DocName, e.Key)
|
||||
}
|
2
pkg/bootstrap/cloudinit/testdata/kustomization.yaml
vendored
Normal file
2
pkg/bootstrap/cloudinit/testdata/kustomization.yaml
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
resources:
|
||||
- secret.yaml
|
29
pkg/bootstrap/cloudinit/testdata/secret.yaml
vendored
Normal file
29
pkg/bootstrap/cloudinit/testdata/secret.yaml
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
annotations:
|
||||
airshipit.org/clustertype: ephemeral
|
||||
name: node1-bmc-secret
|
||||
type: Opaque
|
||||
data:
|
||||
netconfig: bmV0Y29uZmlnCg==
|
||||
stringData:
|
||||
userdata: cloud-init
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
annotations:
|
||||
airshipit.org/clustertype: nodata
|
||||
name: node1-bmc-secret1
|
||||
type: Opaque
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
annotations:
|
||||
test: nodataforcfg
|
||||
name: node1-bmc-secret2
|
||||
type: Opaque
|
||||
data:
|
||||
foo: bmV0Y29uZmlnCg==
|
@ -4,10 +4,20 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"opendev.org/airship/airshipctl/pkg/bootstrap/cloudinit"
|
||||
"opendev.org/airship/airshipctl/pkg/container"
|
||||
"opendev.org/airship/airshipctl/pkg/document"
|
||||
"opendev.org/airship/airshipctl/pkg/errors"
|
||||
"opendev.org/airship/airshipctl/pkg/util"
|
||||
|
||||
"sigs.k8s.io/kustomize/v3/pkg/fs"
|
||||
)
|
||||
|
||||
const (
|
||||
builderConfigFileName = "builder-conf.yaml"
|
||||
)
|
||||
|
||||
// GenerateBootstrapIso will generate data for cloud init and start ISO builder container
|
||||
@ -24,6 +34,15 @@ func GenerateBootstrapIso(settings *Settings, args []string, out io.Writer) erro
|
||||
return err
|
||||
}
|
||||
|
||||
if err := verifyInputs(cfg, args, out); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
docBundle, err := document.NewBundle(fs.MakeRealFS(), args[0], "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(out, "Creating ISO builder container")
|
||||
builder, err := container.NewContainer(
|
||||
&ctx, cfg.Container.ContainerRuntime,
|
||||
@ -32,17 +51,78 @@ func GenerateBootstrapIso(settings *Settings, args []string, out io.Writer) erro
|
||||
return err
|
||||
}
|
||||
|
||||
return generateBootstrapIso(builder, cfg, out, settings.Debug)
|
||||
return generateBootstrapIso(docBundle, builder, cfg, out, settings.Debug)
|
||||
}
|
||||
|
||||
func generateBootstrapIso(builder container.Container, cfg *Config, out io.Writer, debug bool) error {
|
||||
func verifyInputs(cfg *Config, args []string, out io.Writer) error {
|
||||
if len(args) == 0 {
|
||||
fmt.Fprintln(out, "Specify path to document model. Config param from global settings is not supported")
|
||||
return errors.ErrNotImplemented{}
|
||||
}
|
||||
|
||||
if cfg.Container.Volume == "" {
|
||||
fmt.Fprintln(out, "Specify volume bind for ISO builder container")
|
||||
return errors.ErrWrongConfig{}
|
||||
}
|
||||
|
||||
if (cfg.Builder.UserDataFileName == "") || (cfg.Builder.NetworkConfigFileName == "") {
|
||||
fmt.Fprintln(out, "UserDataFileName or NetworkConfigFileName are not specified in ISO builder config")
|
||||
return errors.ErrWrongConfig{}
|
||||
}
|
||||
|
||||
vols := strings.Split(cfg.Container.Volume, ":")
|
||||
switch {
|
||||
case len(vols) == 1:
|
||||
cfg.Container.Volume = fmt.Sprintf("%s:%s", vols[0], vols[0])
|
||||
case len(vols) > 2:
|
||||
fmt.Fprintln(out, "Bad container volume format. Use hostPath:contPath")
|
||||
return errors.ErrWrongConfig{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getContainerCfg(cfg *Config, userData []byte, netConf []byte) (map[string][]byte, error) {
|
||||
hostVol := strings.Split(cfg.Container.Volume, ":")[0]
|
||||
|
||||
fls := make(map[string][]byte)
|
||||
fls[filepath.Join(hostVol, cfg.Builder.UserDataFileName)] = userData
|
||||
fls[filepath.Join(hostVol, cfg.Builder.NetworkConfigFileName)] = netConf
|
||||
builderData, err := cfg.ToYAML()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fls[filepath.Join(hostVol, builderConfigFileName)] = builderData
|
||||
return fls, nil
|
||||
}
|
||||
|
||||
func generateBootstrapIso(
|
||||
docBubdle document.Bundle,
|
||||
builder container.Container,
|
||||
cfg *Config,
|
||||
out io.Writer,
|
||||
debug bool,
|
||||
) error {
|
||||
cntVol := strings.Split(cfg.Container.Volume, ":")[1]
|
||||
fmt.Fprintln(out, "Creating cloud-init for ephemeral K8s")
|
||||
userData, netConf, err := cloudinit.GetCloudData(docBubdle, EphemeralClusterAnnotation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var fls map[string][]byte
|
||||
fls, err = getContainerCfg(cfg, userData, netConf)
|
||||
if err = util.WriteFiles(fls, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vols := []string{cfg.Container.Volume}
|
||||
builderCfgLocation := filepath.Join(cntVol, builderConfigFileName)
|
||||
fmt.Fprintf(out, "Running default container command. Mounted dir: %s\n", vols)
|
||||
if err := builder.RunCommand(
|
||||
[]string{},
|
||||
nil,
|
||||
vols,
|
||||
[]string{},
|
||||
[]string{fmt.Sprintf("BUILDER_CONFIG=%s", builderCfgLocation)},
|
||||
debug,
|
||||
); err != nil {
|
||||
return err
|
||||
|
@ -7,6 +7,11 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"opendev.org/airship/airshipctl/pkg/document"
|
||||
"opendev.org/airship/airshipctl/pkg/errors"
|
||||
"opendev.org/airship/airshipctl/testutil"
|
||||
)
|
||||
|
||||
type mockContainer struct {
|
||||
@ -38,9 +43,25 @@ func (mc *mockContainer) GetId() string {
|
||||
}
|
||||
|
||||
func TestBootstrapIso(t *testing.T) {
|
||||
fSys := testutil.SetupTestFs(t, "testdata")
|
||||
bundle, err := document.NewBundle(fSys, "/", "/")
|
||||
require.NoError(t, err, "Building Bundle Failed")
|
||||
|
||||
volBind := "/tmp:/dst"
|
||||
testErr := fmt.Errorf("TestErr")
|
||||
testCfg := &Config{
|
||||
Container: Container{
|
||||
Volume: volBind,
|
||||
ContainerRuntime: "docker",
|
||||
},
|
||||
Builder: Builder{
|
||||
UserDataFileName: "user-data",
|
||||
NetworkConfigFileName: "net-conf",
|
||||
},
|
||||
}
|
||||
expOut := []string{
|
||||
"Running default container command. Mounted dir: []\n",
|
||||
"Creating cloud-init for ephemeral K8s\n",
|
||||
fmt.Sprintf("Running default container command. Mounted dir: [%s]\n", volBind),
|
||||
"ISO successfully built.\n",
|
||||
"Debug flag is set. Container TESTID stopped but not deleted.\n",
|
||||
"Removing container.\n",
|
||||
@ -57,9 +78,9 @@ func TestBootstrapIso(t *testing.T) {
|
||||
builder: &mockContainer{
|
||||
runCommand: func() error { return testErr },
|
||||
},
|
||||
cfg: &Config{},
|
||||
cfg: testCfg,
|
||||
debug: false,
|
||||
expectedOut: expOut[0],
|
||||
expectedOut: expOut[0] + expOut[1],
|
||||
expectdErr: testErr,
|
||||
},
|
||||
{
|
||||
@ -67,9 +88,9 @@ func TestBootstrapIso(t *testing.T) {
|
||||
runCommand: func() error { return nil },
|
||||
getId: func() string { return "TESTID" },
|
||||
},
|
||||
cfg: &Config{},
|
||||
cfg: testCfg,
|
||||
debug: true,
|
||||
expectedOut: expOut[0] + expOut[1] + expOut[2],
|
||||
expectedOut: expOut[0] + expOut[1] + expOut[2] + expOut[3],
|
||||
expectdErr: nil,
|
||||
},
|
||||
{
|
||||
@ -78,16 +99,16 @@ func TestBootstrapIso(t *testing.T) {
|
||||
getId: func() string { return "TESTID" },
|
||||
rmContainer: func() error { return testErr },
|
||||
},
|
||||
cfg: &Config{},
|
||||
cfg: testCfg,
|
||||
debug: false,
|
||||
expectedOut: expOut[0] + expOut[1] + expOut[3],
|
||||
expectedOut: expOut[0] + expOut[1] + expOut[2] + expOut[4],
|
||||
expectdErr: testErr,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
actualOut := bytes.NewBufferString("")
|
||||
actualErr := generateBootstrapIso(tt.builder, tt.cfg, actualOut, tt.debug)
|
||||
actualErr := generateBootstrapIso(bundle, tt.builder, tt.cfg, actualOut, tt.debug)
|
||||
|
||||
errS := fmt.Sprintf("generateBootstrapIso should have return error %s, got %s", tt.expectdErr, actualErr)
|
||||
assert.Equal(t, actualErr, tt.expectdErr, errS)
|
||||
@ -96,3 +117,63 @@ func TestBootstrapIso(t *testing.T) {
|
||||
assert.Equal(t, actualOut.String(), tt.expectedOut, errS)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyInputs(t *testing.T) {
|
||||
tests := []struct {
|
||||
cfg *Config
|
||||
args []string
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
cfg: &Config{},
|
||||
args: []string{},
|
||||
expectedErr: errors.ErrNotImplemented{},
|
||||
},
|
||||
{
|
||||
cfg: &Config{},
|
||||
args: []string{"."},
|
||||
expectedErr: errors.ErrWrongConfig{},
|
||||
},
|
||||
{
|
||||
cfg: &Config{
|
||||
Container: Container{
|
||||
Volume: "/tmp:/dst",
|
||||
},
|
||||
},
|
||||
args: []string{"."},
|
||||
expectedErr: errors.ErrWrongConfig{},
|
||||
},
|
||||
{
|
||||
cfg: &Config{
|
||||
Container: Container{
|
||||
Volume: "/tmp",
|
||||
},
|
||||
Builder: Builder{
|
||||
UserDataFileName: "user-data",
|
||||
NetworkConfigFileName: "net-conf",
|
||||
},
|
||||
},
|
||||
args: []string{"."},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
cfg: &Config{
|
||||
Container: Container{
|
||||
Volume: "/tmp:/dst:/dst1",
|
||||
},
|
||||
Builder: Builder{
|
||||
UserDataFileName: "user-data",
|
||||
NetworkConfigFileName: "net-conf",
|
||||
},
|
||||
},
|
||||
args: []string{"."},
|
||||
expectedErr: errors.ErrWrongConfig{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
actualErr := verifyInputs(tt.cfg, tt.args, bytes.NewBufferString(""))
|
||||
assert.Equal(t, tt.expectedErr, actualErr)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -8,6 +8,11 @@ import (
|
||||
"opendev.org/airship/airshipctl/pkg/environment"
|
||||
)
|
||||
|
||||
const (
|
||||
// TODO this should be part of a airshipctl config
|
||||
EphemeralClusterAnnotation = "airshipit.org/clustertype=ephemeral"
|
||||
)
|
||||
|
||||
// Settings settings for isogen command
|
||||
type Settings struct {
|
||||
*environment.AirshipCTLSettings
|
||||
|
2
pkg/bootstrap/isogen/testdata/kustomization.yaml
vendored
Normal file
2
pkg/bootstrap/isogen/testdata/kustomization.yaml
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
resources:
|
||||
- secret.yaml
|
11
pkg/bootstrap/isogen/testdata/secret.yaml
vendored
Normal file
11
pkg/bootstrap/isogen/testdata/secret.yaml
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
annotations:
|
||||
airshipit.org/clustertype: ephemeral
|
||||
name: node1-bmc-secret
|
||||
type: Opaque
|
||||
data:
|
||||
netconfig: bmV0Y29uZmlnCg==
|
||||
stringData:
|
||||
userdata: cloud-init
|
15
pkg/document/errors.go
Normal file
15
pkg/document/errors.go
Normal file
@ -0,0 +1,15 @@
|
||||
package document
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ErrDocNotFound returned if desired document not found
|
||||
type ErrDocNotFound struct {
|
||||
Annotation string
|
||||
Kind string
|
||||
}
|
||||
|
||||
func (e ErrDocNotFound) Error() string {
|
||||
return fmt.Sprintf("Document annotated by %s with Kind %s not found", e.Annotation, e.Kind)
|
||||
}
|
@ -7,3 +7,11 @@ type ErrNotImplemented struct {
|
||||
func (e ErrNotImplemented) Error() string {
|
||||
return "Error. Not implemented"
|
||||
}
|
||||
|
||||
// ErrWrongConfig returned in case of incorrect configuration
|
||||
type ErrWrongConfig struct {
|
||||
}
|
||||
|
||||
func (e ErrWrongConfig) Error() string {
|
||||
return "Error. Wrong configuration"
|
||||
}
|
||||
|
17
pkg/util/writefiles.go
Normal file
17
pkg/util/writefiles.go
Normal file
@ -0,0 +1,17 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
)
|
||||
|
||||
// WriteFiles write multiple files described in a map
|
||||
func WriteFiles(fls map[string][]byte, mode os.FileMode) error {
|
||||
for fileName, data := range fls {
|
||||
if err := ioutil.WriteFile(fileName, data, mode); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user