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

bootstrap script should be executed on the host agent #41

Merged
merged 13 commits into from
Jul 26, 2021
84 changes: 75 additions & 9 deletions agent/cloudinit/cloudinit.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
package cloudinit

import (
"bytes"
"compress/gzip"
"encoding/base64"
"fmt"
"io"
"path/filepath"
"strings"

"github.com/pkg/errors"
"sigs.k8s.io/yaml"
Expand All @@ -14,17 +19,17 @@ type ScriptExecutor struct {
}

type bootstrapConfig struct {
FilesToWrite []files `json:"write_files"`
FilesToWrite []Files `json:"write_files"`
CommandsToExecute []string `json:"runCmd"`
}

type files struct {
Path string `json:"path,"`
// Encoding string `json:"encoding,omitempty"`
// Owner string `json:"owner,omitempty"`
// Permissions string `json:"permissions,omitempty"`
Content string `json:"content"`
//Append bool `json:"append,"`
type Files struct {
Path string `json:"path,"`
Encoding string `json:"encoding,omitempty"`
Owner string `json:"owner,omitempty"`
Permissions string `json:"permissions,omitempty"`
Content string `json:"content"`
Append bool `json:"append,omitempty"`
}

func (se ScriptExecutor) Execute(bootstrapScript string) error {
Expand All @@ -40,7 +45,12 @@ func (se ScriptExecutor) Execute(bootstrapScript string) error {
return errors.Wrap(err, fmt.Sprintf("Error creating the directory %s", directoryToCreate))
}

err = se.WriteFilesExecutor.WriteToFile(file.Path, file.Content)
encodings := parseEncodingScheme(file.Encoding)
file.Content, err = decodeContent(file.Content, encodings)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("error decoding content for %s", file.Path))
}
err = se.WriteFilesExecutor.WriteToFile(file)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("Error writing the file %s", file.Path))
}
Expand All @@ -54,3 +64,59 @@ func (se ScriptExecutor) Execute(bootstrapScript string) error {
}
return nil
}

func parseEncodingScheme(e string) []string {
e = strings.ToLower(e)
e = strings.TrimSpace(e)

switch e {
case "gz+base64", "gzip+base64", "gz+b64", "gzip+b64":
return []string{"application/base64", "application/x-gzip"}
case "base64", "b64":
return []string{"application/base64"}
}

return []string{"text/plain"}
}

func decodeContent(content string, encodings []string) (string, error) {
for _, e := range encodings {
switch e {
case "application/base64":
rByte, err := base64.StdEncoding.DecodeString(content)
if err != nil {
return content, errors.WithStack(err)
}
content = string(rByte)
case "application/x-gzip":
rByte, err := gUnzipData([]byte(content))
if err != nil {
return content, err
}
content = string(rByte)
case "text/plain":
continue
default:
return content, errors.Errorf("Unknown bootstrap data encoding: %q", content)
}
}
return content, nil
}

func gUnzipData(data []byte) ([]byte, error) {
var r io.Reader
var err error
b := bytes.NewBuffer(data)
r, err = gzip.NewReader(b)
if err != nil {
return nil, errors.WithStack(err)
}

var resB bytes.Buffer
_, err = resB.ReadFrom(r)
if err != nil {
return nil, errors.WithStack(err)
}

return resB.Bytes(), nil
}
99 changes: 68 additions & 31 deletions agent/cloudinit/cloudinit_test.go
Original file line number Diff line number Diff line change
@@ -1,30 +1,32 @@
package cloudinit_test

import (
"encoding/base64"
"errors"
"fmt"
"io/ioutil"
"os"
"path"
"strings"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"

"github.com/vmware-tanzu/cluster-api-provider-byoh/agent/cloudinit"
"github.com/vmware-tanzu/cluster-api-provider-byoh/agent/cloudinit/cloudinitfakes"
)

var someBootstrapSecret = `
write_files:
- path: /tmp/file1.txt
content: some-content
runCmd:
- echo 'some run command'
`

var _ = Describe("Cloudinit", func() {
var (
workDir string
err error
)

Context("Testing write_files and runCmd directives of cloudinit", func() {
var (
fakeFileWriter *cloudinitfakes.FakeIFileWriter
fakeCmdExecutor *cloudinitfakes.FakeICmdRunner
scriptExecutor cloudinit.ScriptExecutor
err error
fakeFileWriter *cloudinitfakes.FakeIFileWriter
fakeCmdExecutor *cloudinitfakes.FakeICmdRunner
scriptExecutor cloudinit.ScriptExecutor
defaultBootstrapSecret string
)

BeforeEach(func() {
Expand All @@ -34,31 +36,67 @@ var _ = Describe("Cloudinit", func() {
WriteFilesExecutor: fakeFileWriter,
RunCmdExecutor: fakeCmdExecutor,
}

defaultBootstrapSecret = fmt.Sprintf(`write_files:
- path: %s/defaultFile.txt
content: some-content
runCmd:
- echo 'some run command'`, workDir)

workDir, err = ioutil.TempDir("", "cloudinit_ut")
Expect(err).ToNot(HaveOccurred())
})

AfterEach(func() {
huchen2021 marked this conversation as resolved.
Show resolved Hide resolved
err := os.RemoveAll(workDir)
Expect(err).ToNot(HaveOccurred())
})

It("should write files successfully", func() {
bootstrapSecretUnencoded := `write_files:
- path: /tmp/a/file1.txt
content: some-content
- path: /tmp/b/file2.txt
content: whatever`
fileDir1 := path.Join(workDir, "dir1")
fileName1 := path.Join(fileDir1, "file1.txt")
fileContent1 := "some-content-1"

fileDir2 := path.Join(workDir, "dir2")
fileName2 := path.Join(fileDir2, "file2.txt")
fileContent2 := "some-content-2"
fileBase64Content := base64.StdEncoding.EncodeToString([]byte(fileContent2))
user := "root"
group := "root"
permissions := "0777"
encoding := "base64"

bootstrapSecretUnencoded := fmt.Sprintf(`write_files:
- path: %s
content: %s
- path: %s
content: %s
owner: %s:%s
permissions: '%s'
append: true
encoding: %s`, fileName1, fileContent1, fileName2, fileBase64Content, user, group, permissions, encoding)

err = scriptExecutor.Execute(bootstrapSecretUnencoded)
Expect(err).ToNot(HaveOccurred())

Expect(fakeFileWriter.MkdirIfNotExistsCallCount()).To(Equal(2))
Expect(fakeFileWriter.WriteToFileCallCount()).To(Equal(2))

dirNameForFirstFile := fakeFileWriter.MkdirIfNotExistsArgsForCall(0)
Expect(dirNameForFirstFile).To(Equal("/tmp/a"))
firstFileName, firstFileContents := fakeFileWriter.WriteToFileArgsForCall(0)
Expect(firstFileName).To(Equal("/tmp/a/file1.txt"))
Expect(firstFileContents).To(Equal("some-content"))
Expect(dirNameForFirstFile).To(Equal(fileDir1))
firstFile := fakeFileWriter.WriteToFileArgsForCall(0)
Expect(firstFile.Path).To(Equal(fileName1))
Expect(firstFile.Content).To(Equal(fileContent1))

dirNameForSecondFile := fakeFileWriter.MkdirIfNotExistsArgsForCall(1)
Expect(dirNameForSecondFile).To(Equal("/tmp/b"))
secondFileName, secondFileContents := fakeFileWriter.WriteToFileArgsForCall(1)
Expect(secondFileName).To(Equal("/tmp/b/file2.txt"))
Expect(secondFileContents).To(Equal("whatever"))
Expect(dirNameForSecondFile).To(Equal(fileDir2))
secondFile := fakeFileWriter.WriteToFileArgsForCall(1)
Expect(secondFile.Path).To(Equal(fileName2))
Expect(secondFile.Content).To(Equal(fileContent2))
Expect(secondFile.Permissions).To(Equal(permissions))
Expect(secondFile.Owner).To(Equal(strings.Join([]string{user, group}, ":")))
Expect(secondFile.Append).To(BeTrue())

})

It("should error out when an invalid yaml is passed", func() {
Expand All @@ -71,7 +109,7 @@ var _ = Describe("Cloudinit", func() {
It("should error out when there is not enough permission to mkdir", func() {
fakeFileWriter.MkdirIfNotExistsReturns(errors.New("not enough permissions"))

err := scriptExecutor.Execute(someBootstrapSecret)
err := scriptExecutor.Execute(defaultBootstrapSecret)

Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("not enough permissions"))
Expand All @@ -82,14 +120,14 @@ var _ = Describe("Cloudinit", func() {
It("should error out write to file failes", func() {
fakeFileWriter.WriteToFileReturns(errors.New("cannot write to file"))

err := scriptExecutor.Execute(someBootstrapSecret)
err := scriptExecutor.Execute(defaultBootstrapSecret)

Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("cannot write to file"))
})

It("run the command given in the runCmd directive", func() {
err := scriptExecutor.Execute(someBootstrapSecret)
err := scriptExecutor.Execute(defaultBootstrapSecret)
Expect(err).ToNot(HaveOccurred())

Expect(fakeCmdExecutor.RunCmdCallCount()).To(Equal(1))
Expand All @@ -108,9 +146,8 @@ var _ = Describe("Cloudinit", func() {
})

It("should error out when command execution fails", func() {

fakeCmdExecutor.RunCmdReturns(errors.New("command execution failed"))
err := scriptExecutor.Execute(someBootstrapSecret)
err := scriptExecutor.Execute(defaultBootstrapSecret)
Expect(err).To(HaveOccurred())

Expect(fakeCmdExecutor.RunCmdCallCount()).To(Equal(1))
Expand Down
22 changes: 10 additions & 12 deletions agent/cloudinit/cloudinitfakes/fake_ifile_writer.go

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

11 changes: 8 additions & 3 deletions agent/cloudinit/cmd_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,18 @@ type CmdRunner struct {
}

func (r CmdRunner) RunCmd(cmd string) error {
subStrs := []string{"kubeadm init", "kubeadm join"}

if strings.Contains(cmd, "kubeadm") {
cmd = "kubeadm join --config /run/kubeadm/kubeadm-join-config.yaml --ignore-preflight-errors=all && echo success > /run/cluster-api/bootstrap-success.complete"
for _, subStr := range subStrs {
huchen2021 marked this conversation as resolved.
Show resolved Hide resolved
if strings.Contains(cmd, subStr) {
index := strings.Index(cmd, subStr)
index += len(subStr)
newCmd := cmd[:index] + " --ignore-preflight-errors=all " + cmd[index:]
jamiemonserrate marked this conversation as resolved.
Show resolved Hide resolved
cmd = newCmd
}
}
command := exec.Command("/bin/sh", "-c", cmd)
output, err := command.Output()
fmt.Println(string(output))
return err

}
Loading