Skip to content

Commit

Permalink
feat: build job with dockerfile (#42)
Browse files Browse the repository at this point in the history
* feat: add support to deploy job with Dockerfile

* test: add support to deploy job with Dockerfile

* chore: change deprecated method
  • Loading branch information
crgisch authored Sep 16, 2024
1 parent 6831043 commit 7fa4d9b
Show file tree
Hide file tree
Showing 9 changed files with 398 additions and 73 deletions.
27 changes: 13 additions & 14 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ require (
github.com/google/go-containerregistry v0.12.0
github.com/moby/buildkit v0.11.3
github.com/stretchr/testify v1.8.0
golang.org/x/sync v0.1.0
google.golang.org/grpc v1.50.1
google.golang.org/protobuf v1.28.1
golang.org/x/sync v0.7.0
google.golang.org/grpc v1.65.0
google.golang.org/protobuf v1.34.1
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.22.5
k8s.io/apimachinery v0.22.5
Expand All @@ -38,8 +38,8 @@ require (
github.com/gofrs/flock v0.8.1 // indirect
github.com/gogo/googleapis v1.4.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/googleapis/gnostic v0.5.5 // indirect
Expand Down Expand Up @@ -71,16 +71,15 @@ require (
go.opentelemetry.io/otel/sdk v1.4.1 // indirect
go.opentelemetry.io/otel/trace v1.4.1 // indirect
go.opentelemetry.io/proto/otlp v0.12.0 // indirect
golang.org/x/crypto v0.2.0 // indirect
golang.org/x/mod v0.6.0 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/oauth2 v0.1.0 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/term v0.5.0 // indirect
golang.org/x/text v0.7.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/mod v0.8.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/oauth2 v0.20.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/term v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
golang.org/x/time v0.1.0 // indirect
golang.org/x/tools v0.1.12 // indirect
google.golang.org/appengine v1.6.7 // indirect
golang.org/x/tools v0.6.0 // indirect
google.golang.org/genproto v0.0.0-20220706185917-7780775163c4 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
Expand Down
56 changes: 29 additions & 27 deletions go.sum

Large diffs are not rendered by default.

12 changes: 11 additions & 1 deletion pkg/build/buildkit/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ func (b *BuildKit) Build(ctx context.Context, r *pb.BuildRequest, w io.Writer) (
case "BUILD_KIND_APP_BUILD_WITH_CONTAINER_FILE":
return b.buildFromContainerFile(ctx, c, r, ow)

case "BUILD_KIND_JOB_DEPLOY_WITH_CONTAINER_FILE":
return b.buildFromContainerFile(ctx, c, r, ow)

case "BUILD_KIND_PLATFORM_WITH_CONTAINER_FILE":
return nil, b.buildPlatform(ctx, c, r, ow)
}
Expand Down Expand Up @@ -382,7 +385,14 @@ func (b *BuildKit) buildFromContainerFile(ctx context.Context, c *client.Client,
files = bytes.NewReader(r.Data)
}

tmpDir, cleanFunc, err := generateBuildLocalDir(ctx, b.opts.TempDir, r.Containerfile, nil, r.App.EnvVars, files)
envVars := map[string]string{}
if r.App != nil {
envVars = r.App.EnvVars
} else if r.Job != nil {
envVars = r.Job.EnvVars
}

tmpDir, cleanFunc, err := generateBuildLocalDir(ctx, b.opts.TempDir, r.Containerfile, nil, envVars, files)
if err != nil {
return nil, err
}
Expand Down
240 changes: 240 additions & 0 deletions pkg/build/buildkit/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,246 @@ EXPOSE 8080/tcp 80/tcp 8000/tcp 9090 8888
})
}

func TestBuildKit_BuildJob_FromContainerFile(t *testing.T) {
bc := newBuildKitClient(t)
defer bc.Close()

t.Run("Dockerfile mounting the job's env vars", func(t *testing.T) {
destImage := baseRegistry(t, "my-job", "")

dockerfile := `FROM busybox:latest
RUN --mount=type=secret,id=tsuru-job-envvars,target=/var/run/secrets/envs.sh \
. /var/run/secrets/envs.sh \
&& echo ${MY_ENV_VAR} > /tmp/envs \
&& echo ${DATABASE_PASSWORD} >> /tmp/envs
ENV MY_ANOTHER_VAR="another var"
`

req := &pb.BuildRequest{
Kind: pb.BuildKind_BUILD_KIND_JOB_DEPLOY_WITH_CONTAINER_FILE,
Job: &pb.TsuruJob{
Name: "my-job",
EnvVars: map[string]string{
"MY_ENV_VAR": "hello world",
"DATABASE_PASSWORD": "aw3some`p4ss!",
},
},
DestinationImages: []string{destImage},
Containerfile: string(dockerfile),
PushOptions: &pb.PushOptions{
InsecureRegistry: registryHTTP,
},
}

jobFiles, err := NewBuildKit(bc, BuildKitOptions{TempDir: t.TempDir()}).Build(context.TODO(), req, os.Stdout)
require.NoError(t, err)
assert.Equal(t, &pb.TsuruConfig{
ImageConfig: &pb.ContainerImageConfig{
Cmd: []string{"sh"},
},
}, jobFiles)

dc := newDockerClient(t)
defer dc.Close()

r, err := dc.ImagePull(context.TODO(), destImage, dockertypes.ImagePullOptions{})
require.NoError(t, err)
defer r.Close()

fmt.Println("Pulling container image", destImage)
_, err = io.Copy(os.Stdout, r)
require.NoError(t, err)

defer func() {
fmt.Printf("Removing container image %s\n", destImage)
_, nerr := dc.ImageRemove(context.TODO(), destImage, dockertypes.ImageRemoveOptions{Force: true})
require.NoError(t, nerr)
}()

t.Run("should not store the env vars in the container image manifest", func(t *testing.T) {
is, _, err := dc.ImageInspectWithRaw(context.TODO(), destImage)
require.NoError(t, err)
require.NotNil(t, is.Config)
assert.NotEmpty(t, is.Config.Env)
for _, env := range is.Config.Env {
assert.False(t, strings.HasPrefix(env, "MY_ENV_VAR="), "Env MY_ENV_VAR should not be exported to image manifest")
assert.False(t, strings.HasPrefix(env, "DATABASE_PASSWORD="), "Env DATABASE_PASSWORD shold not be exported to image manifest")

if strings.HasPrefix(env, "MY_ANOTHER_VAR=") {
assert.Equal(t, "MY_ANOTHER_VAR=another var", env)
}
}
})
t.Run("should be able to see env vars during the build", func(t *testing.T) {
containerCreateResp, err := dc.ContainerCreate(context.TODO(), &dockertypescontainer.Config{
Image: destImage,
Cmd: dockerstrslice.StrSlice{"sleep", "Inf"},
}, nil, nil, nil, "")
require.NoError(t, err)
require.NotEmpty(t, containerCreateResp.ID, "container ID cannot be empty")

containerID := containerCreateResp.ID
fmt.Printf("Container created (ID=%s)\n", containerID)

defer func() {
fmt.Printf("Removing container (ID=%s)\n", containerID)
require.NoError(t, dc.ContainerRemove(context.TODO(), containerID, dockertypes.ContainerRemoveOptions{Force: true, RemoveVolumes: true}))
}()

err = dc.ContainerStart(context.TODO(), containerID, dockertypes.ContainerStartOptions{})
require.NoError(t, err)
fmt.Printf("Starting container (ID=%s)\n", containerID)

execCreateResp, err := dc.ContainerExecCreate(context.TODO(), containerID, dockertypes.ExecConfig{
Tty: true,
AttachStderr: true,
AttachStdout: true,
Cmd: []string{"cat", "/tmp/envs"},
})
require.NoError(t, err)
require.NotEmpty(t, execCreateResp.ID, "exec ID cannot be empty")

execID := execCreateResp.ID

hijackedResp, err := dc.ContainerExecAttach(context.TODO(), execID, dockertypes.ExecStartCheck{})
require.NoError(t, err)
require.NotNil(t, hijackedResp.Reader)
defer hijackedResp.Close()

var stderr, stdout bytes.Buffer
_, err = dockerstdcopy.StdCopy(&stdout, &stderr, hijackedResp.Reader)
require.NoError(t, err)
assert.Equal(t, "hello world\r\naw3some`p4ss!\r\n", stdout.String())
assert.Empty(t, stderr.String())
})

t.Run("job build with Dockerfile", func(t *testing.T) {
destImage := baseRegistry(t, "my-job", "")

dockerfile := `FROM busybox:latest
RUN --mount=type=secret,id=tsuru-job-envvars,target=/var/run/secrets/envs.sh \
. /var/run/secrets/envs.sh \
&& echo ${MY_ENV_VAR} > /tmp/envs \
&& echo ${DATABASE_PASSWORD} >> /tmp/envs
ENV MY_ANOTHER_VAR="another var"
`

req := &pb.BuildRequest{
Kind: pb.BuildKind_BUILD_KIND_APP_BUILD_WITH_CONTAINER_FILE,
Job: &pb.TsuruJob{
Name: "my-job",
EnvVars: map[string]string{
"MY_ENV_VAR": "hello world",
"DATABASE_PASSWORD": "aw3some`p4ss!",
},
},
DestinationImages: []string{destImage},
Containerfile: string(dockerfile),
PushOptions: &pb.PushOptions{
InsecureRegistry: registryHTTP,
},
}

b := NewBuildKit(bc, BuildKitOptions{TempDir: t.TempDir()})
jobFiles, err := b.Build(context.TODO(), req, os.Stdout)

//jobFiles, err := NewBuildKit(bc, BuildKitOptions{TempDir: t.TempDir()}).Build(context.TODO(), req, os.Stdout)
require.NoError(t, err)
assert.Equal(t, &pb.TsuruConfig{
ImageConfig: &pb.ContainerImageConfig{
Cmd: []string{"sh"},
},
}, jobFiles)

dc := newDockerClient(t)
defer dc.Close()

r, err := dc.ImagePull(context.TODO(), destImage, dockertypes.ImagePullOptions{})
require.NoError(t, err)
defer r.Close()

fmt.Println("Pulling container image", destImage)
_, err = io.Copy(os.Stdout, r)
require.NoError(t, err)

defer func() {
fmt.Printf("Removing container image %s\n", destImage)
_, nerr := dc.ImageRemove(context.TODO(), destImage, dockertypes.ImageRemoveOptions{Force: true})
require.NoError(t, nerr)
}()
})
})

t.Run("neither Procfile nor tsuru.yaml, should use command from image manifest", func(t *testing.T) {
destImage := baseRegistry(t, "my-job", "")

dockerfile := `FROM busybox
EXPOSE 8080/tcp
ENTRYPOINT ["/path/to/my/server.sh"]
CMD ["--port", "8080"]
`

req := &pb.BuildRequest{
Kind: pb.BuildKind_BUILD_KIND_JOB_DEPLOY_WITH_CONTAINER_FILE,
Job: &pb.TsuruJob{
Name: "my-job",
},
DestinationImages: []string{destImage},
Containerfile: string(dockerfile),
PushOptions: &pb.PushOptions{
InsecureRegistry: registryHTTP,
},
}

jobFiles, err := NewBuildKit(bc, BuildKitOptions{TempDir: t.TempDir()}).Build(context.TODO(), req, os.Stdout)
require.NoError(t, err)
assert.Equal(t, &pb.TsuruConfig{
ImageConfig: &pb.ContainerImageConfig{
Entrypoint: []string{"/path/to/my/server.sh"},
Cmd: []string{"--port", "8080"},
ExposedPorts: []string{"8080/tcp"},
},
}, jobFiles)
})

t.Run("multiple exposed ports, should ensure the ascending order of ports", func(t *testing.T) {
destImage := baseRegistry(t, "my-job", "")

dockerfile := `FROM busybox
EXPOSE 100/udp 53/udp 443/udp
EXPOSE 8080/tcp 80/tcp 8000/tcp 9090 8888
`
req := &pb.BuildRequest{
Kind: pb.BuildKind_BUILD_KIND_JOB_DEPLOY_WITH_CONTAINER_FILE,
Job: &pb.TsuruJob{
Name: "my-job",
},
DestinationImages: []string{destImage},
Containerfile: string(dockerfile),
PushOptions: &pb.PushOptions{
InsecureRegistry: registryHTTP,
},
}

jobFiles, err := NewBuildKit(bc, BuildKitOptions{TempDir: t.TempDir()}).Build(context.TODO(), req, os.Stdout)
require.NoError(t, err)
assert.Equal(t, &pb.TsuruConfig{
ImageConfig: &pb.ContainerImageConfig{
Cmd: []string{"sh"},
ExposedPorts: []string{"53/udp", "80/tcp", "100/udp", "443/udp", "8000/tcp", "8080/tcp", "8888/tcp", "9090/tcp"},
},
}, jobFiles)
})
}

func compressGZIP(t *testing.T, path string) []byte {
t.Helper()
var data bytes.Buffer
Expand Down
Loading

0 comments on commit 7fa4d9b

Please sign in to comment.