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

make k8s the default builder #541

Merged
merged 12 commits into from
Sep 5, 2024
1 change: 0 additions & 1 deletion e2e/system/build_from_git_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ func (s *Suite) TestBuildFromGit() {
s.T().Logf("Error cleaning up knuu: %v", err)
}
})

s.Require().NoError(target.Build().Commit(ctx))

s.T().Logf("Starting instance")
Expand Down
34 changes: 34 additions & 0 deletions e2e/system/file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,40 @@ func (s *Suite) TestDownloadFileFromRunningInstance() {

s.Assert().Equal(fileContent, string(gotContent))
}
func (s *Suite) TestDownloadFileFromBuilder() {
const namePrefix = "download-file-builder"
s.T().Parallel()
// Setup

target, err := s.Knuu.NewInstance(namePrefix + "-target")
s.Require().NoError(err)

ctx := context.Background()
s.Require().NoError(target.Build().SetImage(ctx, "alpine:latest"))

s.T().Cleanup(func() {
if err := target.Execution().Destroy(ctx); err != nil {
s.T().Logf("error destroying instance: %v", err)
}
})

// Test logic
const (
fileContent = "Hello World!"
filePath = "/hello.txt"
)

s.Require().NoError(target.Storage().AddFileBytes([]byte(fileContent), filePath, "0:0"))

// The commit is required to make the changes persistent to the image
s.Require().NoError(target.Build().Commit(ctx))

// Now test if the file can be downloaded correctly from the built image
gotContent, err := target.Storage().GetFileBytes(ctx, filePath)
s.Require().NoError(err, "Error getting file bytes")

s.Assert().Equal(fileContent, string(gotContent))
}

func (s *Suite) TestMinio() {
const (
Expand Down
119 changes: 8 additions & 111 deletions pkg/container/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,13 @@
package container

import (
"archive/tar"
"bytes"
"context"
"crypto/sha256"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"time"

"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"github.com/sirupsen/logrus"

"github.com/celestiaorg/knuu/pkg/builder"
Expand All @@ -26,24 +19,18 @@ type BuilderFactory struct {
imageNameFrom string
imageNameTo string
imageBuilder builder.Builder
cli *client.Client
dockerFileInstructions []string
buildContext string
}

// NewBuilderFactory creates a new instance of BuilderFactory.
func NewBuilderFactory(imageName, buildContext string, imageBuilder builder.Builder) (*BuilderFactory, error) {
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return nil, ErrCreatingDockerClient.Wrap(err)
}
err = os.MkdirAll(buildContext, 0755)
if err != nil {
if err := os.MkdirAll(buildContext, 0755); err != nil {
return nil, ErrFailedToCreateContextDir.Wrap(err)
}

return &BuilderFactory{
imageNameFrom: imageName,
cli: cli,
dockerFileInstructions: []string{"FROM " + imageName},
buildContext: buildContext,
imageBuilder: imageBuilder,
Expand All @@ -55,104 +42,24 @@ func (f *BuilderFactory) ImageNameFrom() string {
return f.imageNameFrom
}

// ExecuteCmdInBuilder runs the provided command in the context of the given builder.
// It returns the command's output or any error encountered.
func (f *BuilderFactory) ExecuteCmdInBuilder(command []string) (string, error) {
// AddCmdToBuilder runs the provided command in the context of the given builder.
mojtaba-esk marked this conversation as resolved.
Show resolved Hide resolved
func (f *BuilderFactory) AddCmdToBuilder(command []string) {
f.dockerFileInstructions = append(f.dockerFileInstructions, "RUN "+strings.Join(command, " "))
// FIXME: does not return expected output
return "", nil
}

// AddToBuilder adds a file from the source path to the destination path in the image, with the specified ownership.
func (f *BuilderFactory) AddToBuilder(srcPath, destPath, chown string) error {
func (f *BuilderFactory) AddToBuilder(srcPath, destPath, chown string) {
f.dockerFileInstructions = append(f.dockerFileInstructions, "ADD --chown="+chown+" "+srcPath+" "+destPath)
return nil
}

// ReadFileFromBuilder reads a file from the given builder's mount point.
// It returns the file's content or any error encountered.
func (f *BuilderFactory) ReadFileFromBuilder(filePath string) ([]byte, error) {
if f.imageNameTo == "" {
return nil, ErrNoImageNameProvided
}
containerConfig := &container.Config{
Image: f.imageNameTo,
Cmd: []string{"tail", "-f", "/dev/null"}, // This keeps the container running
}
resp, err := f.cli.ContainerCreate(
context.Background(),
containerConfig,
nil,
nil,
nil,
"",
)
if err != nil {
return nil, ErrFailedToCreateContainer.Wrap(err)
}

defer func() {
// Stop the container
timeout := int(time.Duration(10) * time.Second)
stopOptions := container.StopOptions{
Timeout: &timeout,
}

if err := f.cli.ContainerStop(context.Background(), resp.ID, stopOptions); err != nil {
logrus.Warn(ErrFailedToStopContainer.Wrap(err))
}

// Remove the container
if err := f.cli.ContainerRemove(context.Background(), resp.ID, container.RemoveOptions{}); err != nil {
logrus.Warn(ErrFailedToRemoveContainer.Wrap(err))
}
}()

if err := f.cli.ContainerStart(context.Background(), resp.ID, container.StartOptions{}); err != nil {
return nil, ErrFailedToStartContainer.Wrap(err)
}

// Now you can copy the file
reader, _, err := f.cli.CopyFromContainer(context.Background(), resp.ID, filePath)
if err != nil {
return nil, ErrFailedToCopyFileFromContainer.Wrap(err)
}
defer reader.Close()

tarReader := tar.NewReader(reader)

for {
header, err := tarReader.Next()

if err == io.EOF {
break // End of archive
}
if err != nil {
return nil, ErrFailedToReadFromTar.Wrap(err)
}

if header.Typeflag == tar.TypeReg { // if it's a file then extract it
data, err := io.ReadAll(tarReader)
if err != nil {
return nil, ErrFailedToReadFileFromTar.Wrap(err)
}
return data, nil
}
}

return nil, ErrFileNotFoundInTar
}

// SetEnvVar sets the value of an environment variable in the builder.
func (f *BuilderFactory) SetEnvVar(name, value string) error {
func (f *BuilderFactory) SetEnvVar(name, value string) {
f.dockerFileInstructions = append(f.dockerFileInstructions, "ENV "+name+"="+value)
return nil
}

// SetUser sets the user in the builder.
func (f *BuilderFactory) SetUser(user string) error {
func (f *BuilderFactory) SetUser(user string) {
f.dockerFileInstructions = append(f.dockerFileInstructions, "USER "+user)
return nil
}

// Changed returns true if the builder has been modified, false otherwise.
Expand All @@ -178,6 +85,7 @@ func (f *BuilderFactory) PushBuilderImage(ctx context.Context, imageName string)
return ErrFailedToCreateContextDir.Wrap(err)
}
}

dockerFile := strings.Join(f.dockerFileInstructions, "\n")
err := os.WriteFile(dockerFilePath, []byte(dockerFile), 0644)
if err != nil {
Expand Down Expand Up @@ -240,17 +148,6 @@ func (f *BuilderFactory) BuildImageFromGitRepo(ctx context.Context, gitCtx build
return err
}

func runCommand(cmd *exec.Cmd) error { // nolint: unused
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
return fmt.Errorf("command failed: %w\nstdout: %s\nstderr: %s", err, stdout.String(), stderr.String())
}
return nil
}

// GenerateImageHash creates a hash value based on the contents of the Dockerfile instructions and all files in the build context.
func (f *BuilderFactory) GenerateImageHash() (string, error) {
hasher := sha256.New()
Expand Down
22 changes: 7 additions & 15 deletions pkg/instance/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,7 @@ func (b *build) ExecuteCommand(command ...string) error {
return ErrAddingCommandNotAllowed.WithParams(b.instance.state.String())
}

_, err := b.builderFactory.ExecuteCmdInBuilder(command)
if err != nil {
return ErrExecutingCommandInInstance.WithParams(command, b.instance.name).Wrap(err)
}
b.builderFactory.AddCmdToBuilder(command)
mojtaba-esk marked this conversation as resolved.
Show resolved Hide resolved
return nil
}

Expand All @@ -122,9 +119,7 @@ func (b *build) SetUser(user string) error {
return ErrSettingUserNotAllowed.WithParams(b.instance.state.String())
}

if err := b.builderFactory.SetUser(user); err != nil {
return ErrSettingUser.WithParams(user, b.instance.name).Wrap(err)
}
b.builderFactory.SetUser(user)
mojtaba-esk marked this conversation as resolved.
Show resolved Hide resolved
b.instance.Logger.Debugf("Set user '%s' for instance '%s'", user, b.instance.name)
return nil
}
Expand Down Expand Up @@ -196,14 +191,10 @@ func (b *build) getBuildDir() string {
}

// addFileToBuilder adds a file to the builder
func (b *build) addFileToBuilder(src, dest, chown string) error {
_ = src
func (b *build) addFileToBuilder(src, dest, chown string) {
// dest is the same as src here, as we copy the file to the build dir with the subfolder structure of dest
err := b.builderFactory.AddToBuilder(dest, dest, chown)
if err != nil {
return ErrAddingFileToInstance.WithParams(dest, b.instance.name).Wrap(err)
}
return nil
_ = src
b.builderFactory.AddToBuilder(dest, dest, chown)
mojtaba-esk marked this conversation as resolved.
Show resolved Hide resolved
}

// SetEnvironmentVariable sets the given environment variable in the instance
Expand All @@ -214,7 +205,8 @@ func (b *build) SetEnvironmentVariable(key, value string) error {
}
b.instance.Logger.Debugf("Setting environment variable '%s' in instance '%s'", key, b.instance.name)
if b.instance.state == StatePreparing {
return b.builderFactory.SetEnvVar(key, value)
b.builderFactory.SetEnvVar(key, value)
return nil
mojtaba-esk marked this conversation as resolved.
Show resolved Hide resolved
}

b.env[key] = value
Expand Down
48 changes: 46 additions & 2 deletions pkg/instance/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"strings"

"github.com/celestiaorg/knuu/pkg/k8s"
"github.com/celestiaorg/knuu/pkg/names"

"k8s.io/apimachinery/pkg/api/resource"
)
Expand Down Expand Up @@ -46,7 +47,8 @@ func (s *storage) AddFile(src string, dest string, chown string) error {

switch s.instance.state {
case StatePreparing:
return s.instance.build.addFileToBuilder(src, dest, chown)
s.instance.build.addFileToBuilder(src, dest, chown)
return nil
mojtaba-esk marked this conversation as resolved.
Show resolved Hide resolved
case StateCommitted:
return s.addFileToInstance(dstPath, dest, chown)
}
Expand Down Expand Up @@ -163,7 +165,7 @@ func (s *storage) GetFileBytes(ctx context.Context, file string) ([]byte, error)
}

if s.instance.state != StateStarted {
bytes, err := s.instance.build.builderFactory.ReadFileFromBuilder(file)
bytes, err := s.readFileFromImage(ctx, file)
if err != nil {
return nil, ErrGettingFile.WithParams(file, s.instance.name).Wrap(err)
}
Expand Down Expand Up @@ -345,6 +347,48 @@ func (s *storage) destroyFiles(ctx context.Context) error {
return nil
}

func (s *storage) readFileFromImage(ctx context.Context, filePath string) ([]byte, error) {
// Another way to implement this is to download all the layers of the image and then
// extract the file from them, but it seems hacky and will run on the user's machine.
// Therefore, we will use the tmp instance to get the file from the image

tmpName, err := names.NewRandomK8("tmp-dl")
if err != nil {
return nil, err
}

ti, err := New(tmpName, s.instance.SystemDependencies)
if err != nil {
return nil, err
}
if err := ti.build.SetImage(ctx, s.instance.build.ImageName()); err != nil {
return nil, err
}

if err := ti.build.SetStartCommand("sleep", "infinity"); err != nil {
return nil, err
}

if err := ti.build.Commit(ctx); err != nil {
return nil, err
}

if err := ti.execution.Start(ctx); err != nil {
return nil, err
}
defer func() {
if err := ti.execution.Destroy(ctx); err != nil {
ti.Logger.Errorf("failed to destroy tmp instance %s: %v", ti.k8sName, err)
}
}()

output, err := ti.execution.ExecuteCommand(ctx, "cat", filePath)
if err != nil {
return nil, err
}
return []byte(output), nil
}

func (s *storage) clone() *storage {
if s == nil {
return nil
Expand Down
Loading